cast capture: adopt a hand-built Coolify instance into the age secret store #15
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#15
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?
Problem
cast is scoped to the steady state: manifest → Coolify, forever. It has no
adoption path — no way to bootstrap the age secret store from a Coolify
instance that was built by hand, before any manifest existed.
That is a real, recurring-once-per-legacy-box operation, and it is on the
critical path of the prod migration (D-193, Task 7 "secrets capture"). Today the
operator does it by hand:
Every input to that pipeline is something cast already has:
${...}refs in the manifest's envtemplates (
.infra/env/*.env.template)--statedirenvironments.yamlread:sensitiveSo a human is shuffling cast's own inputs through a terminal. The failure modes
are exactly the ones cast exists to remove: a name silently missed (the template
substitutes empty → the app boots misconfigured), or a plaintext secret leaking
through shell history, terminal scrollback, or a tmp file that never got
shredded.
Why this can't be a naive dump
The mapping is not purely mechanical. Some entries encode migration
decisions rather than facts about the source box. From the live case:
DATABASE_URL_PROD,REDIS_URL_PROD,UMAMI_DATABASE_URLmust be written asthe literal
pending-coolify-generated— the source box's real URLs point atthe source box's Postgres/Redis. Copying them would be confidently wrong in
a way that looks entirely plausible, and the target's real URLs don't exist
until Coolify creates the resources.
ADMIN_EMAILmust be the operator, not the source value —staging and prod share a Mailgun domain, so a staging box carrying the real
ADMIN_EMAILcan mail real users.A "capture everything" verb would produce a store that is wrong in ~4 of 17
entries, silently. So the verb must force disposition rather than guess.
Proposal
<env>(the
${NAME}refs — the manifest already declares exactly this set).read:sensitive).literal
pending-coolify-generated, never the live valuewrite an empty
the cases where the source value must not be carried over
confirmation.
secrets/<repo>.<env>.env.age. Plaintext never touches disk outside atmpfs, and never touches stdout.
Marking a name as generated should be a manifest property, not a flag the
operator has to remember — the manifest already knows
DATABASE_URLcomes froma Coolify-created Postgres.
Acceptance
cast capture heavy-duty/incubator --env prodproduces a store containingexactly the names the manifest requires — no more, no fewer.
box.
on exit.
Note on urgency
Deliberately not urgent. This verb writes the prod secret store; new code
whose bugs land there, on a migration's critical path, is a worse trade than one
attended manual pass. File now, build when it isn't load-bearing.