inventory --emit-draft: a reviewable blueprint of a live instance (#27) #33
No reviewers
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#33
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/inventory-emit-draft"
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?
Closes #27.
cast inventorycan already sweep a whole instance. Now it can emit what it sees as a draft of cast's own inputs:Every emitted text file opens with a header saying it is a proposal, machine-generated from instance X at time T, and that
applydoes not read it.The boundary
A draft is a PROPOSAL. It is never desired state, and
applynever reads it. The path is sweep → emit draft → human reviews → manifest PR →capture→apply— the same shape asterraform import→ HCL. It is emitted from the sweep path only (--emit-draftwith a repo is refused: that is the reconcile path, and a project with a manifest is one whose manifest is the truth — regenerating it from a live box would let that box's accumulated cruft overwrite the reviewed spec). It refuses a target directory that already holds a repo. Adoption is one-way.The two things that would make a draft actively dangerous
1. Copied provider-generated values. A
DATABASE_URLread off the source points at the source box's Postgres. Emit it, rebuild elsewhere, and the new box comes up working — reading and writing the old box's database. You find out the day the old box is deleted.So provider-generated names are placeheld and flagged, never copied. Decided by name, never by value, in two families: Coolify's magic vars (
^SERVICE_(FQDN|URL|USER|PASSWORD|BASE64|REALBASE64)), and any name whose segments carry both a datastore word (DATABASE,DB,POSTGRES,REDIS, …) and a connection word (URL,URI,DSN,HOST,PASSWORD, …). It errs wide on purpose: over-matching a real secret placeholds it loudly and recoverably; under-matching a generated one copies it silently and rebuilds a box that works against a dead machine's database. Placeheld names land in the manifest'sgenerated_secrets:, so a latercapturerepeats the placeholding with no flag to remember.2. Silent losses.
UNCAPTURED.mdis a first-class output, emitted on every run, listing per resource every live setting cast saw and could not express: destinations (#21), service hostnames (4.1.2 has no flatdomainson services), Basic Auth / custom Traefik labels, Include Source Commit in Build, and anything else with no manifest field. A blueprint that omits these without saying so is worse than no blueprint — in a disaster you would trust it and rebuild a different box.Three judgment calls worth your eye
--environmentis a tiebreak, not a filter. The first cut filtered the instance by env name and silently dropped La Familia — which lives alone inproduction, and is exactly the project this verb exists to bootstrap. Now a project with one populated environment is always drafted; only a project with two forces the flag, and cast refuses rather than picking.UNCAPTURED.md— but its env values are still captured. Values are irrecoverable once the box is gone; structure is not.Docs
README gains Drafting a box that was never declared;
docs/semantics.mdgains the Drafts contract. Both carry the what a blueprint cannot restore table — the GitHub App private key and the S3 access keys are not in the repo (correctly: it holds no live credentials) and cannot be regenerated from it. A DR runbook has to say so.The load-bearing test
Asserts the source's real
DATABASE_URL,REDIS_URLandSERVICE_PASSWORD_*values appear in no emitted artifact — walking every file in the tree, decrypting the age store (the one place a copied value would hide from a grep), and checking stdout — while an ordinary secret (MAILGUN_KEY) is carried through.Gates:
npm run check,npm run build,npm test(238 passing) all green.🤖 Generated with Claude Code