Coolify's own SERVICE_*/POSTGRES_* magic vars report as orphans forever — a correct box can never read clean #87
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#87
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?
Summary
A prod box with zero real drift cannot make
cast diffsay clean. Today's run, after #86 removed the last genuine finding:Every one of those is Coolify's own generated magic var —
SERVICE_FQDN_*/SERVICE_URL_*minted for a compose app's per-container domains, and the one-click umami service's bundled-Postgres credentials. cast never declares them,applynever touches them, and they will be there forever. They are not drift. Yet they are the entire content of the report, and they hold two resources permanently inchange.The asymmetry — cast already knows
draft.tshas exactly this judgment and applies it:It refuses to copy these into a draft, because they are "generated per-instance by Coolify … and mean nothing anywhere else."
But
diffEnv's remove-candidate branch consults onlyreserved.ts:isReservedEnvNamecoversSOURCE_COMMIT+COOLIFY_*only. Same judgment, two homes, applied in one of them.Why this is worth fixing, not tolerating
This issue's own predecessor said it best. #78's Impact section:
That is precisely what a permanent 16-line orphan list does, but to the whole report rather than five keys. A diff that can never say clean trains the reader to skim it — and the next time something real appears, it appears in a list that has cried wolf on every previous run. #78 sat misdiagnosed for exactly this reason.
Probe — see what would be reclassified
Copy-paste (swap the uuid; use
/services/$UUID/envsfor a service):coolify_magicis what the narrow rule silences.datastore_ishis the contested set (see below).everything_elsemust keep reporting — that bucket is where a genuine orphan hides.The design decision this needs — narrow vs wide
draft.tsdeliberately errs WIDE:isProviderGeneratedmatchesCOOLIFY_MAGICor any name carrying both a datastore word and a connection word (POSTGRES_USER,DB_HOST, …). Its comment explains why the width is safe there:That asymmetry inverts in a diff. Over-matching here does not withhold a value for review — it hides a live-only var the operator should see. A stray
DATABASE_URLleft on a resource by hand is exactly the orphan worth reporting, and the wide rule would swallow it silently.So the two call sites want the same vocabulary but different widths, and that must be explicit rather than accidental:
COOLIFY_MAGICprefix only) → silences the 13SERVICE_*lines above; leaves umami'sPOSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DBstill reported.isProviderGenerated) → silences all 16, at the cost of hiding any future hand-left datastore var.Those three
POSTGRES_*on umami are the crux: they are genuinely Coolify's (the one-click service's bundled Postgres, per D-253/D-279 in the incubator brain) but they match only the wide rule. Note umami is slated to stop being a one-click service (incubator D-279), which shrinks — but does not remove — this case.What to do
reserved.tsis the natural one: it already owns "names the platform, not the manifest, controls". Havedraft.tsanddiffEnvboth consult it instead ofdraft.tsowning the regex privately.diffEnv(over-matching hides real orphans) and keep the wide rule indraft— and say why at both sites, because the next reader will assume one rule.remove-candidate. Either drop silently, or surface once as a finding (theReservedVarprecedent) — "N Coolify-generated vars present; apply never removes them" — rather than N lines of per-var noise.POSTGRES_*-on-a-service explicitly, one way or the other. A "clean" that still prints 3 lines has not fixed the thing this issue is about.FOO_URLstill reports.Found while verifying #85/#86 against prod: with the shadow-row bug fixed, this is all that stands between a correct box and a clean diff.
Probe run against prod
core— this settles the narrow-vs-wide decision empirically.1.
coolify_magicis exactly core's orphan listAll six, one for one. The narrow rule alone makes
coreread clean. No wide rule needed for the application case.2.
datastore_ishkills the wide rule — decisivelyIt contains
DATABASE_URLandREDIS_URL: cast's own manifest-declared vars, resolved from${resource:postgres.url}/${resource:redis.url}(#60).They are harmless today only by accident of ordering — the remove-candidate branch fires solely for
!(key in desired.vars), and these are declared, so they never reach it. But that is exactly the point:A live-only
DATABASE_URLis a connection string still pointing at a box nobody declares any more. That is the precise poisondraft.tswas built to refuse ("the new box comes up WORKING, reading and writing the old box's database … you find out the day the old box is deleted"). ApplyingisProviderGenerated's width indiffEnvwould make the diff hide it, silently, forever.So: narrow (
COOLIFY_MAGICprefix) indiffEnv, wide (isProviderGenerated) indraft— and the divergence is now evidence-backed rather than a judgment call. The checklist item stands as written.3.
everything_elseconfirms no over-reachTwenty entries, every one manifest-declared. Nothing in that bucket should be silenced, and nothing would be.
Still open: umami's
POSTGRES_*This probe covered the application. The service still needs its own (
/services/<uuid>/envs), and it holds the contested three:SERVICE_URL_UMAMI,SERVICE_FQDN_UMAMI,SERVICE_URL_UMAMI_3000,SERVICE_FQDN_UMAMI_3000,SERVICE_USER_POSTGRES,SERVICE_PASSWORD_POSTGRES,SERVICE_PASSWORD_64_UMAMIPOSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DBSo narrow gets 13 of 16 box-wide, and umami keeps printing three lines — which does not finish what this issue is about.
Those three are the one-click template's bundled-Postgres credentials: genuinely Coolify's, genuinely undeclarable, and not
SERVICE_-prefixed. Options, in preference order:kind === "service"only. A Coolify service is a vendored bundle whose internals cast does not model — an undeclared datastore var there is the template's, never a hand-left one. TheDATABASE_URLhazard above is an application hazard; applications keep the narrow rule. This buys all 16 without blinding the case that matters.COOLIFY_MAGICto name the one-click Postgres trio explicitly — cheap, but a list that rots per template.(1) looks right and is narrow in the way that counts, but it deserves a second opinion before it lands.
Worth noting: incubator D-279 retires umami-as-a-one-click-service in favour of a self-managed app on our own Postgres, which deletes this case rather than solving it — so (2) is not unreasonable as a holding position either.