Adoption and drift disagree about Unit: a record can never adopt a unit-stamped route Issue open
Migrated from github.com/carlosframework/platform issue #354, opened 2026-08-28. GitHub issues are closed for this repo; see AGENTS.md, "Work is tracked on amadan, not GitHub".
Still outstanding: Adoption and drift disagree about Unit, so a record can never adopt a unit-stamped route.
---
The inconsistency
Two code paths in internal/provision/reconcile.go ask the same question about Route.Unit and give different answers.
Adoption — shapeMismatch (internal/provision/reconcile.go:398) compares Unit unconditionally:
case existing.Unit != want.Unit:
return fmt.Sprintf("unit %q/%q", existing.Unit, want.Unit)Drift — the converge path (internal/provision/reconcile.go:766) explicitly refuses to:
// Unit is compared only when the RECORD states one: `carlos route -unit`
// (issue #110) is the supported way to stamp the unit owning a route's
// process out of band, and a record that never spoke to Unit must not
// treat that stamp as drift — or this would fire on a healthy, correctly
// stamped route every pass, forever.The drift path's reasoning is right, and it applies verbatim to adoption.
Why a record can never state a unit
handleAPIInstanceCreate refuses unit on purpose (internal/console/apiinstances.go:507):
> What is deliberately NOT here: pool (manifest policy), hibernate, unit and environment (operator columns with their own verbs)
So an instance record's want.Unit is always "". Against any route stamped by carlos route -unit, shapeMismatch returns unit ""/"console.service" and adoption is refused — permanently, on every pass:
provision: <host> exists but is not the route this record describes (unit "…"/"") — leaving it aloneThere is no supported way for a record to say the unit, because the create door is designed not to accept one.
Impact
Not data loss — the removal phase gates on Provisioned (internal/provision/reconcile.go:858), so an unadopted route is never retired. The route keeps serving. The costs are:
- Permanent per-pass log noise on every unit-stamped route with a record.
- No adoption means no authority transfer — the record never stamps
Provisioned, so the route stays outside the provisioning lifecycle it was created to join. - Any tenant who stamps a unit out of band via
carlos route -unit(the supported verb, #110) permanently locks that route out of adoption.
Fix
Make shapeMismatch tolerate an unstated record unit, matching the rule the codebase already states 350 lines later:
case want.Unit != "" && existing.Unit != want.Unit:Converge-grade change — wants its own PR, review and roll.
Found by
Trying carlos deploy -app console on tito during the console-as-an-app rollout (#295). The console turned out not to be app-shaped for other reasons too, but this one is tenant-facing and independent of that.