smoke resolves its target instance-wide: it can write to a different project's — or a different environment's — app of the same name #29
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#29
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
smokeresolves the application it writes to by name, against every application the token can see:—
src/cli.ts, thesmokecommandGET /applicationsis not scoped to a project or an environment. It returns every application in the token's team, across all projects and all environments on that instance..find()then takes the first one whose name matches and writes to it.So
smoke_target: coredoes not name an application. It names whichever application calledcoreCoolify happens to list first.Why this is live, not hypothetical
It does not need two projects. One project with two environments on one Coolify instance is enough — and that is the ordinary shape:
instance:is an optional, per-environment binding, so with none set,prodandstagingread the same.coolify.envand live on the same control plane, on different servers. Both environments have an application calledcore.cast smoke --env stagingcan therefore POST its canary vars onto prod'score. Nothing catches it:The verb whose entire job is to be a write against a live application picks its victim by a name lookup that cannot distinguish prod from staging.
What the damage actually is
Bounded, but not nil, and worth being precise about:
smokedeletes both on the happy path.smokethrows before cleaning up — and what it leaves behind (INFRA_SMOKE_KEEP, or a strayINFRA_SMOKE_PROBE) is left on the wrong application, possibly prod's.The shape
This is the same failure this repo keeps naming (#12, #14, #17, #18, #21): a coordinate that is not actually a coordinate. A bare resource name is unique only within a project and an environment, exactly as a bare
<repo>is unique only within an org (#12) — and cast has already learned that lesson twice.smokeis the one caller still resolving a name in a namespace it never established.It is also the last piece of the
smoke_targetmove in #21. That fixed where the target is declared — project-scoped,environments.<env>.projects.<repo>.smoke_target. It deliberately did not touch how the declared name is resolved, which is this.Proposal
Resolve the target inside the project and environment it was declared under, instead of instance-wide.
fetchLive(client, projectName, coolifyEnv)already does exactly this lookup, andsmokenow takes<org>/<repo>, so the project is in hand. What it still lacks are the read-side coordinates every other verb has:--project <name>— Coolify's name for the project, when it isn't the repo's--environment <name>— Coolify's name for the environment, when it isn't ours…the D-241 split, one level up from the resource. Then refuse rather than guess when the named app is not in that project + environment — the
LiveLookupdisposition: an absent target is not an empty result, it is the absence of anything to write to.One wrinkle worth deciding deliberately. The deprecated state-file-scoped
smoke_target(kept in #21 so unmigrated state files keep working) has no project to scope to —cast smoke --env prodwith no repo cannot be resolved this way at all. So either that path keeps the instance-wide lookup with the bug intact, or the<org>/<repo>positional becomes required and the deprecated key is dropped in the same pass. The second is cleaner, and the deprecation warning is already in place to lead there.#28 moved the config; the resolution bug is still here — and #28 also handed us the fix.
smoke_targetis now project-scoped (environments.<env>.projects.<slug>.smoke_target), andcast smoketakes an optional<org>/<repo>. That fixes which name smoke looks for. It does not fix where it looks: the resolution still lists/applicationsinstance-wide and picks the first name match, so an app calledcorein another project — or in another environment of the same project — can still be the one that gets written to.The declaration got scoped. The lookup did not.
The fix is already sitting in the codebase:
fetchLive(client, projectName, coolifyEnv)returns exactly the resources in one project's one environment, and every other verb already goes through it. Resolvingsmoke_targetthrough that instead of the instance-wide list makes the lookup as narrow as the declaration — and it inherits the absent-target refusal (#12) and--project/--environment(#17) for free.Note what makes this worse than a normal name collision: smoke writes. It POSTs two env vars onto the app it resolves and deletes them again. Every other verb that could hit the wrong resource merely reports the wrong thing. This one mutates it — on a live box, in a different project, quite possibly one belonging to someone else. On the instance this all came from, a single team owns three projects, two of which are third-party client sites.