carlos / platform Public

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 alone

There 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.