carlos / platform Public

provision: reconcile mints every shape the record describes

routeFor is now the single builder for what a record materializes; the
adoption match test compares against it, so a shape the pass would mint and
one it will adopt cannot drift apart. Also wires ValidateShape into the
record-collection loop as a safety net (skip-and-protect, same style as the
existing ValidHostShape check) -- ValidateShape's only door before this was
the console create handler, which does not guarantee everything already in
the bucket.

Design: docs/superpowers/specs/2026-08-10-route-records-design.md §4
Paul Campbell 78e60d09d8c9e2a8ada0b7a0958a7a9dddac7930 parent 0d6ee3c
2 files changed, +154 −11
  • internal/provision/reconcile.go +41 −11
  • internal/provision/reconcile_test.go +113 −0
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go
index 7928ca80d..dfcf51b66 100644
--- a/internal/provision/reconcile.go
+++ b/internal/provision/reconcile.go
@@ -45,6 +45,31 @@ type ReconcileConfig struct {
// only within an account, never globally.
type acctApp struct{ acct, app string }
+// routeFor is the one answer to "what route does this record describe". The
+// mint path builds from it and the adoption match test (design §4.2)
+// compares against it — one function, so a shape the reconciler would mint
+// and a shape it will adopt can never drift apart.
+//
+// Socket and DBPath are DERIVED, never declared: DefaultSocket and
+// DefaultDBPath exist so a hand-added route and a provisioned one land on
+// identical path shapes, and their doc comment records what two names for
+// one database cost.
+func routeFor(rec Record, dataDir, seed string) registry.Route {
+ rt := registry.Route{
+ Host: rec.Host, Slug: rec.Slug, Kind: rec.KindOrDefault(),
+ Addr: rec.Addr, Unit: rec.Unit,
+ App: rec.App, Account: rec.Account, Pool: rec.Pool, Region: rec.Region,
+ Channel: rec.Channel, Environment: rec.Environment,
+ Hibernate: rec.HibernateOrDefault(), StatusNamed: rec.StatusNamed,
+ Provisioned: true, Seed: seed, Status: "ready",
+ }
+ if rec.DerivesPaths() {
+ rt.Socket = DefaultSocket(rec.Host)
+ rt.DBPath = DefaultDBPath(dataDir, rec.Host)
+ }
+ return rt
+}
+
// Reconcile materializes bucket instance records into registry routes for
// THIS agent's region and pools: mint on appearance, converge seed, remove
// on record deletion — removal only for routes this pass (or an earlier one
@@ -122,6 +147,18 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
protect[host] = true
continue
}
+ // ValidateShape is the console create door's policy check — but a
+ // door is not a guarantee about what is already in the bucket (the
+ // same reasoning as the host-shape check just above). This pass
+ // materializes whatever it finds, so a record whose Kind/Addr/Unit/
+ // Environment combination is malformed must be caught here too, or
+ // a bad value becomes a live route. Protected like a corrupted
+ // body: the record is there, it just cannot be used.
+ if err := ValidateShape(rec); err != nil {
+ log.Printf("provision: skipping record %s: invalid shape: %v", key, err)
+ protect[host] = true
+ continue
+ }
if rec.Region != cfg.Region || !poolServed(cfg.Pools, rec.Pool) {
continue // another agent's jurisdiction, not this pass's business
}
@@ -224,21 +261,14 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
existing, ok := byHost[rec.Host]
switch {
case !ok:
- // Hibernate true is the activator model — and what hands the DB
- // to the lease replicator from birth; DBPath explicit is what
- // keeps ops.FleetDBs' hibernating-exclusion correct.
- nr := registry.Route{
- Host: rec.Host, Slug: rec.Slug, Kind: "instance",
- Socket: DefaultSocket(rec.Host), DBPath: DefaultDBPath(cfg.DataDir, rec.Host),
- App: rec.App, Account: rec.Account, Pool: rec.Pool, Region: rec.Region,
- Channel: rec.Channel, Hibernate: true, Provisioned: true,
- Seed: seed, Status: "ready",
- }
+ // Hibernate and the path derivation now come from the record
+ // (routeFor); everything else about this branch is unchanged.
+ nr := routeFor(rec, cfg.DataDir, seed)
if err := reg.Upsert(nr); err != nil {
log.Printf("provision: mint %s: %v", rec.Host, err)
continue
}
- log.Printf("provision: minted instance route %s (app %q acct %q)", rec.Host, rec.App, rec.Account)
+ log.Printf("provision: minted %s route %s (app %q acct %q)", nr.Kind, rec.Host, rec.App, rec.Account)
case existing.Provisioned && existing.App == rec.App && existing.Account == rec.Account:
// seedIsKnown guards against stamping a transient manifest-read
// failure's "" fallback over a working seed — see seedFor.
diff --git a/internal/provision/reconcile_test.go b/internal/provision/reconcile_test.go
index 19f6aebba..95bcf388f 100644
--- a/internal/provision/reconcile_test.go
+++ b/internal/provision/reconcile_test.go
@@ -129,6 +129,70 @@ func TestReconcileMints(t *testing.T) {
}
}
+// Every shape the record can now describe, minted in one pass. The three
+// cases are the production routes the old vocabulary could not express:
+// a console (addr-backed, unit-owned, never hibernating), a whole-app TCP
+// tenant, and a static site.
+func TestReconcileMintsEveryShape(t *testing.T) {
+ s, sqid := setup(t)
+ off := false
+ recs := []Record{
+ {Schema: 1, Host: "console.go.tito.io", Slug: "console", App: "tito", Account: sqid,
+ Pool: "tito", Region: "ie", Channel: "stable", Kind: "instance",
+ Addr: "127.0.0.1:8090", Unit: "console.service", CreatedAt: time.Now()},
+ {Schema: 1, Host: "whole.go.tito.io", Slug: "whole", App: "tito", Account: sqid,
+ Pool: "tito", Region: "ie", Channel: "stable", Addr: "127.0.0.1:9443",
+ Hibernate: &off, CreatedAt: time.Now()},
+ {Schema: 1, Host: "site.go.tito.io", Slug: "site", App: "tito", Account: sqid,
+ Pool: "tito", Region: "ie", Channel: "stable", Kind: "static",
+ StatusNamed: true, CreatedAt: time.Now()},
+ }
+ for _, rec := range recs {
+ if err := CreateRecord(ctx, s, rec); err != nil {
+ t.Fatal(err)
+ }
+ }
+ reg := regWith(t)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+
+ console := *mustGet(t, reg, "console.go.tito.io")
+ console.CreatedAt = 0
+ wantConsole := registry.Route{
+ Host: "console.go.tito.io", Slug: "console", Kind: "instance",
+ Addr: "127.0.0.1:8090", Unit: "console.service",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable",
+ Provisioned: true, Seed: `["seed"]`, Status: "ready",
+ }
+ if console != wantConsole {
+ t.Errorf("console route = %+v, want %+v", console, wantConsole)
+ }
+
+ whole := *mustGet(t, reg, "whole.go.tito.io")
+ if whole.Socket != "" || whole.DBPath != "" {
+ t.Errorf("an addr-backed tenant derived filesystem paths: socket %q db %q", whole.Socket, whole.DBPath)
+ }
+ if whole.Hibernate {
+ t.Error("an explicitly non-hibernating record minted a hibernating route")
+ }
+
+ site := *mustGet(t, reg, "site.go.tito.io")
+ if site.Kind != "static" {
+ t.Errorf("static route Kind = %q", site.Kind)
+ }
+ if site.Socket != "" || site.DBPath != "" {
+ t.Errorf("a static route derived filesystem paths: socket %q db %q", site.Socket, site.DBPath)
+ }
+ if site.Hibernate {
+ t.Error("a static route was armed for hibernation — it has no process to wake")
+ }
+ if !site.StatusNamed {
+ t.Error("StatusNamed did not reach the row")
+ }
+}
+
// 2. Scope.
func TestReconcileScope(t *testing.T) {
s, sqid := setup(t)
@@ -436,6 +500,55 @@ func TestReconcileSkipsRecordWithInvalidHostShape(t *testing.T) {
mustGet(t, reg, bad)
}
+// ValidateShape is a safety net, not just the console door's policy check:
+// a record can reach the bucket by paths other than the console (an older
+// or future writer, or direct bucket access), and this pass must not
+// materialize one whose Kind/Addr/Unit/Environment combination is
+// malformed. Skipped loudly, and protected from removal for the same
+// reason a corrupted body or an invalid host shape is — an unusable record
+// is not a retired one.
+func TestReconcileSkipsRecordWithInvalidShape(t *testing.T) {
+ s, sqid := setup(t)
+ // kind "service" with no addr: ValidateShape refuses this combination
+ // (the platform only proxies to a service, so it must name its
+ // upstream), but it agrees with its own key path and its host shape is
+ // fine — the only thing wrong is the Kind/Addr combination itself.
+ bad := Record{Schema: 1, Host: "bad-service.go.tito.io", Slug: "bad-service", App: "tito",
+ Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", Kind: "service",
+ CreatedAt: time.Now()}
+ body, err := json.Marshal(bad)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, RecordKey(sqid, "tito", bad.Host), body, ""); err != nil {
+ t.Fatal(err)
+ }
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ reg := regWith(t)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get(bad.Host); err != nil || rt != nil {
+ t.Errorf("minted a route from a shape-invalid record: %+v %v", rt, err)
+ }
+ // The good neighbour in the same pass is untouched by the skip.
+ mustGet(t, reg, "acme.go.tito.io")
+
+ // And a route that somehow already carries that host is not treated as
+ // retired: the record is there, it just cannot be used.
+ if err := reg.Upsert(registry.Route{Host: bad.Host, Kind: "instance", App: "tito",
+ Status: "ready", Provisioned: true, Region: "ie", Pool: "tito", Account: sqid}); err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ mustGet(t, reg, bad.Host)
+}
+
// CRITICAL regression (code review finding): an existing route's record
// going unparseable — not gone, just unreadable this pass — must never read
// as "the bucket retired it". Before the fix, json.Unmarshal failure and