The bootstrap is two-pass and cast has no pass 2 — nothing can fill a generated secret after apply creates it #48
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#48
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The gap
The bootstrap of a project with
generated_secretsis two-pass by construction:applycreates the database; Coolify generates the realDATABASE_URL.cast has no pass 2.
capturecannot do it — a name ingenerated_secretsis placeheld with thepending-coolify-generatedliteral by design (capture.ts:130-137), and re-runningcaptureafter the apply placeholds it again, with no flag to say otherwise (semantics.md:509).draftdoes the same (draft.ts:672). Both are right to: they are pre-apply verbs, and copying a source box's connection URL into a new box's store is exactly the silent, unrecoverable failure their name-pattern rule exists to prevent (draft.ts:73-75).But nothing takes the post-apply path, so the operator does. This is what pass 2 looks like today, out of the
heavy-duty/incubatorrunbook — a handagere-encrypt, against a store holding 14 names, with the prod key in a process substitution and the plaintext kept off disk by hand:Every step of that is a step cast already knows how to do, and it is being done by a human, against production, holding a decryption key, with a
jqfilter that must not pick the third row (umami's bundled Postgres is not ours). A mis-step here is not a typo, it is prod's credentials.Why it matters beyond ergonomics
Until pass 2 runs, the store's value for
DATABASE_URLis a placeholder while the live value is real — which is precisely the state that makes the next routineapplyoverwrite a working secret (#47). So this is not a nice-to-have: the absence of pass 2 is what leaves the gun loaded. #47 stops the shot; this issue removes the gun. Neither is sufficient alone — a refusal an operator cannot act on except by hand-crafting ciphertext is a refusal they will eventually route around.And it is on the DR path: "rebuild the control plane from state" means apply-from-nothing, which means every generated secret in every store is a placeholder again, which means this hand-dance repeated per environment under pressure.
Suggested shape
Same verb, same ceremony, same store-writing code path — the flag simply inverts capture's disposition rule: names in
generated_secretsare the ones it fills, everything else is left exactly as the store has it.internal_db_urlonGET /databases, which the runbook's curl above proves the live API returns; the vendored 4.1.2 spec documents the route's body as "Content is very complex. Will be implemented later.", so as with #46 the spec's silence is not evidence of absence).GET /databasesreturns every database on the box, including other projects' and umami's bundled one; picking by name across that list is the #29 bug wearing a different hat.--force), so it cannot silently rotate a secret an operator set by hand.capture.ts:166's rule holds: the only value-shaped thing printed is the placeholder literal it is replacing.pending-coolify-generatedremain in the store for that environment, and the name count is unchanged. That assertion is currently a line in a human runbook.Worth considering, and deliberately not proposed here: making
applydo this automatically after a create. It would close the window entirely — but it makes the verb that mutates Coolify also mutate the encrypted store and hence the git repo, which is a much bigger blast radius for a verb people run on a schedule. A separate, explicit, operator-run verb is the right first step; automating it is a decision for after this has been used a few times.Related
applyoverwrites a live generated secret with the placeholder (the guard; this is the fix that makes it actionable).Superseded in approach by #60.
This issue asks for a pass 2 — a verb that fills the generated secret into the store after
applycreates it. #60 argues there should be no pass 2:DATABASE_URLis not a secret anyone authored, it is a fact about a resource cast itself created and can ask for at any time. Derive it from the declared database instead of storing a copy of it.That deletes the hand-
agedance quoted in this issue rather than automating it, and takes the two names out of the store entirely (incubator prod: 14 → 12). The DR argument here gets stronger, not weaker — a from-nothing rebuild resolves the URL live instead of needing a second attended pass per environment.Leaving this open as the problem statement; #60 is the proposed resolution.