carlos / platform Public

feat: derived route_aliases registry table (#151)

Gives the registry the derived table global hostname claims (Task 1,
internal/domains/claims.go) materialize into. A later reconcile pass
owns every row; the edge reloads on a table change, so Fingerprint
covers alias/host/account/app — but not the dns_status/dns_checked_at/
cert_expires_at stamp columns, which stamp every 5 minutes and would
force a pointless fleet-wide edge reload each pass if hashed (same
reasoning as the existing routes SELECT's own omission of those
columns).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell bf35e46b6808c1133b0e4614767c0fced0a93e7a parent 98fed20
2 files changed, +307 −3
  • internal/registry/registry.go +121 −3
  • internal/registry/registry_test.go +186 −0
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index a0778d0b8..124ce77a6 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -333,6 +333,22 @@ CREATE TABLE IF NOT EXISTS routes (
return err
}
}
+ // route_aliases is a derived table (a later reconcile pass owns every
+ // row, materializing domains.Claim records into it) — a plain CREATE
+ // TABLE IF NOT EXISTS, no legacy shape to carry forward.
+ if _, err := r.sql.Exec(`
+CREATE TABLE IF NOT EXISTS route_aliases (
+ alias TEXT PRIMARY KEY,
+ host TEXT NOT NULL,
+ account TEXT NOT NULL DEFAULT '',
+ app TEXT NOT NULL DEFAULT '',
+ dns_status TEXT NOT NULL DEFAULT '',
+ dns_checked_at INTEGER NOT NULL DEFAULT 0,
+ cert_expires_at INTEGER NOT NULL DEFAULT 0,
+ created_at INTEGER NOT NULL DEFAULT 0
+);`); err != nil {
+ return err
+ }
if err := r.migrateAccounts(); err != nil {
return err
}
@@ -1167,9 +1183,88 @@ WHERE kind = 'instance' AND next_due > 0 AND next_due <= ? ORDER BY host`, now)
return out, rows.Err()
}
-// Fingerprint summarizes the routes, fleets and fleet_boxes tables over a
-// fresh connection, for the edge's cheap change poll. It is content-accurate:
-// it hashes the actual VALUES of the reload-relevant columns, not a
+// RouteAlias is one derived row of the route_aliases table: an extra
+// hostname that resolves to an existing route's host, materialized from a
+// domains.Claim by a later reconcile pass (this package owns the storage
+// shape only — nothing here decides which claims win). Alias is the extra
+// hostname being attached; Host is the route it points at. DNSStatus,
+// DNSCheckedAt and CertExpiresAt are box-stamped health state, same shape as
+// the identically-named columns on routes, and are deliberately excluded
+// from Fingerprint (see there) so a stamping pass alone never forces an edge
+// reload.
+type RouteAlias struct {
+ Alias string
+ Host string
+ Account string
+ App string
+ DNSStatus string
+ DNSCheckedAt int64
+ CertExpiresAt int64
+ CreatedAt int64
+}
+
+// ListAliases returns every route alias, ordered by alias.
+func (r *Registry) ListAliases() ([]RouteAlias, error) {
+ rows, err := r.sql.Query(`SELECT alias, host, account, app, dns_status,
+dns_checked_at, cert_expires_at, created_at FROM route_aliases ORDER BY alias`)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var out []RouteAlias
+ for rows.Next() {
+ var a RouteAlias
+ if err := rows.Scan(&a.Alias, &a.Host, &a.Account, &a.App, &a.DNSStatus,
+ &a.DNSCheckedAt, &a.CertExpiresAt, &a.CreatedAt); err != nil {
+ return nil, err
+ }
+ out = append(out, a)
+ }
+ return out, rows.Err()
+}
+
+// UpsertAlias inserts or updates an alias row by its hostname. Only host,
+// account and app are overwritten on conflict — the DNS/cert stamps and
+// created_at are reconcile-pass- and box-stamp-owned state that a repoint
+// must not reset out from under the passes that maintain them (the same
+// preserved-column shape Upsert uses for routes, applied here by simply
+// leaving those columns out of the SET list rather than a CASE guard, since
+// every writer of this table already knows it must not carry stamps).
+func (r *Registry) UpsertAlias(a RouteAlias) error {
+ _, err := r.sql.Exec(`
+INSERT INTO route_aliases (alias, host, account, app, dns_status, dns_checked_at, cert_expires_at, created_at)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ON CONFLICT(alias) DO UPDATE SET host=excluded.host, account=excluded.account, app=excluded.app`,
+ a.Alias, a.Host, a.Account, a.App, a.DNSStatus, a.DNSCheckedAt, a.CertExpiresAt, a.CreatedAt)
+ return err
+}
+
+// DeleteAlias removes an alias by its hostname. Idempotent: an alias already
+// gone is a success, same contract as Delete on routes.
+func (r *Registry) DeleteAlias(alias string) error {
+ _, err := r.sql.Exec(`DELETE FROM route_aliases WHERE alias = ?`, alias)
+ return err
+}
+
+// SetAliasDNSStatus stamps an alias's DNS health verdict + check time,
+// narrowly (same rationale as SetDNSStatus on routes: it lands on its own
+// cadence and must not race a concurrent UpsertAlias repoint).
+func (r *Registry) SetAliasDNSStatus(alias, status string, at int64) error {
+ _, err := r.sql.Exec(`UPDATE route_aliases SET dns_status = ?, dns_checked_at = ? WHERE alias = ?`, status, at, alias)
+ return err
+}
+
+// SetAliasCertExpiresAt stamps an alias's cached-cert NotAfter, narrowly.
+// 0 = unknown (same rationale as SetCertExpiresAt on routes).
+func (r *Registry) SetAliasCertExpiresAt(alias string, at int64) error {
+ _, err := r.sql.Exec(`UPDATE route_aliases SET cert_expires_at = ? WHERE alias = ?`, at, alias)
+ return err
+}
+
+// Fingerprint summarizes the routes, fleets, fleet_boxes and route_aliases
+// tables over a fresh connection, for the edge's cheap change poll. It is
+// content-accurate: it hashes the actual VALUES of the reload-relevant
+// columns, not a
// length-weighted aggregate, so two DIFFERENT values of the SAME length
// (channel canary/1 -> canary/2, a git short-sha version bump, a rotated
// sha256 token hash, a failover restamp box-a -> box-b) change the result
@@ -1293,5 +1388,28 @@ FROM fleet_boxes ORDER BY fleet, label`)
return "", err
}
+ // route_aliases: only alias/host/account/app decide which hostnames the
+ // edge routes and where. dns_status/dns_checked_at/cert_expires_at are
+ // deliberately excluded here for the same reason the routes SELECT above
+ // omits its own dns_status/cert_expires_at columns — a box-side pass
+ // stamps them on a 5-minute cadence, and hashing a value that moves on
+ // that cadence would force a pointless fleet-wide edge reload every pass.
+ fmt.Fprint(h, "\x1droute_aliases\x1e")
+ arows, err := db.Query(`SELECT alias, host, account, app FROM route_aliases ORDER BY alias`)
+ if err != nil {
+ return "", err
+ }
+ defer arows.Close()
+ for arows.Next() {
+ var alias, host, account, app string
+ if err := arows.Scan(&alias, &host, &account, &app); err != nil {
+ return "", err
+ }
+ fmt.Fprintf(h, "%s\x1f%s\x1f%s\x1f%s\x1e", alias, host, account, app)
+ }
+ if err := arows.Err(); err != nil {
+ return "", err
+ }
+
return fmt.Sprintf("%d:%x", n, h.Sum(nil)), nil
}
diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go
index c537e3415..0f8717b7b 100644
--- a/internal/registry/registry_test.go
+++ b/internal/registry/registry_test.go
@@ -1834,3 +1834,189 @@ func TestSetPlacementUnknownHostErrors(t *testing.T) {
t.Fatal("SetPlacement on an unknown host: got nil error, want one")
}
}
+
+// TestRouteAliasCRUD pins UpsertAlias/ListAliases/DeleteAlias's contract: a
+// re-upsert repoints host/account/app but must NOT reset the DNS/cert stamps
+// or created_at — those are reconcile-pass-owned and box-stamped state, the
+// same shape as Upsert's preserved columns on routes.
+func TestRouteAliasCRUD(t *testing.T) {
+ reg := openTestRegistry(t)
+ if err := reg.UpsertAlias(RouteAlias{
+ Alias: "extra.example.com", Host: "app.example.com",
+ Account: "bab", App: "myapp", CreatedAt: 1000,
+ }); err != nil {
+ t.Fatalf("UpsertAlias: %v", err)
+ }
+ got, err := reg.ListAliases()
+ if err != nil {
+ t.Fatalf("ListAliases: %v", err)
+ }
+ if len(got) != 1 {
+ t.Fatalf("ListAliases = %+v, want exactly one row", got)
+ }
+ if got[0].Alias != "extra.example.com" || got[0].Host != "app.example.com" ||
+ got[0].Account != "bab" || got[0].App != "myapp" || got[0].CreatedAt != 1000 {
+ t.Fatalf("round-tripped alias = %+v, want the upserted values", got[0])
+ }
+
+ // Stamp the row the way a box-side pass would, THEN re-upsert a repoint —
+ // the stamps and created_at must survive.
+ if err := reg.SetAliasDNSStatus("extra.example.com", "pointing", 2000); err != nil {
+ t.Fatalf("SetAliasDNSStatus: %v", err)
+ }
+ if err := reg.SetAliasCertExpiresAt("extra.example.com", 3000); err != nil {
+ t.Fatalf("SetAliasCertExpiresAt: %v", err)
+ }
+ if err := reg.UpsertAlias(RouteAlias{
+ Alias: "extra.example.com", Host: "other.example.com",
+ Account: "bad", App: "otherapp", CreatedAt: 9999,
+ }); err != nil {
+ t.Fatalf("re-UpsertAlias: %v", err)
+ }
+ got, err = reg.ListAliases()
+ if err != nil {
+ t.Fatalf("ListAliases: %v", err)
+ }
+ if len(got) != 1 {
+ t.Fatalf("ListAliases after re-upsert = %+v, want exactly one row", got)
+ }
+ a := got[0]
+ if a.Host != "other.example.com" || a.Account != "bad" || a.App != "otherapp" {
+ t.Fatalf("re-upsert did not repoint host/account/app: %+v", a)
+ }
+ if a.DNSStatus != "pointing" || a.DNSCheckedAt != 2000 || a.CertExpiresAt != 3000 {
+ t.Fatalf("re-upsert clobbered stamps: %+v", a)
+ }
+ if a.CreatedAt != 1000 {
+ t.Fatalf("re-upsert clobbered created_at: got %d, want the FIRST insert's 1000", a.CreatedAt)
+ }
+
+ // Delete is idempotent: gone is success, called twice is still success.
+ if err := reg.DeleteAlias("extra.example.com"); err != nil {
+ t.Fatalf("DeleteAlias: %v", err)
+ }
+ if err := reg.DeleteAlias("extra.example.com"); err != nil {
+ t.Fatalf("DeleteAlias (already gone): %v", err)
+ }
+ got, err = reg.ListAliases()
+ if err != nil {
+ t.Fatalf("ListAliases: %v", err)
+ }
+ if len(got) != 0 {
+ t.Fatalf("ListAliases after delete = %+v, want none", got)
+ }
+}
+
+// TestRouteAliasStamps pins SetAliasDNSStatus/SetAliasCertExpiresAt as narrow
+// UPDATEs that persist independently of UpsertAlias — same rationale as
+// SetDNSStatus/SetCertExpiresAt on routes.
+func TestRouteAliasStamps(t *testing.T) {
+ reg := openTestRegistry(t)
+ if err := reg.UpsertAlias(RouteAlias{Alias: "extra.example.com", Host: "app.example.com"}); err != nil {
+ t.Fatalf("UpsertAlias: %v", err)
+ }
+ if err := reg.SetAliasDNSStatus("extra.example.com", "elsewhere", 111); err != nil {
+ t.Fatalf("SetAliasDNSStatus: %v", err)
+ }
+ if err := reg.SetAliasCertExpiresAt("extra.example.com", 222); err != nil {
+ t.Fatalf("SetAliasCertExpiresAt: %v", err)
+ }
+ got, err := reg.ListAliases()
+ if err != nil {
+ t.Fatalf("ListAliases: %v", err)
+ }
+ if len(got) != 1 {
+ t.Fatalf("ListAliases = %+v, want exactly one row", got)
+ }
+ if got[0].DNSStatus != "elsewhere" || got[0].DNSCheckedAt != 111 || got[0].CertExpiresAt != 222 {
+ t.Fatalf("stamps did not persist: %+v", got[0])
+ }
+
+ // A same-host UpsertAlias (no repoint) must not disturb the stamps either.
+ if err := reg.UpsertAlias(RouteAlias{Alias: "extra.example.com", Host: "app.example.com"}); err != nil {
+ t.Fatalf("UpsertAlias: %v", err)
+ }
+ got, err = reg.ListAliases()
+ if err != nil {
+ t.Fatalf("ListAliases: %v", err)
+ }
+ if got[0].DNSStatus != "elsewhere" || got[0].DNSCheckedAt != 111 || got[0].CertExpiresAt != 222 {
+ t.Fatalf("UpsertAlias disturbed stamps that survived a plain re-upsert: %+v", got[0])
+ }
+}
+
+// TestFingerprintCoversAliases pins the fingerprint contract from the task
+// brief: an alias write moves the fingerprint (so the edge reloads and
+// starts routing it), but a STAMP write never does — stamping runs on a
+// 5-minute cadence and hashing it would force a pointless fleet-wide edge
+// reload every pass (same reasoning as the routes section's own omission of
+// dns_status/cert_expires_at).
+func TestFingerprintCoversAliases(t *testing.T) {
+ reg := openTestRegistry(t)
+ fp1, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.UpsertAlias(RouteAlias{Alias: "extra.example.com", Host: "app.example.com", Account: "bab", App: "myapp"}); err != nil {
+ t.Fatalf("UpsertAlias: %v", err)
+ }
+ fp2, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fp2 == fp1 {
+ t.Fatal("Fingerprint blind to an alias UpsertAlias")
+ }
+
+ if err := reg.SetAliasDNSStatus("extra.example.com", "pointing", 12345); err != nil {
+ t.Fatalf("SetAliasDNSStatus: %v", err)
+ }
+ fp3, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fp3 != fp2 {
+ t.Fatalf("Fingerprint moved on an alias stamp (%s -> %s) — the edge would reload for a change that does not touch routing", fp2, fp3)
+ }
+
+ if err := reg.DeleteAlias("extra.example.com"); err != nil {
+ t.Fatalf("DeleteAlias: %v", err)
+ }
+ fp4, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fp4 == fp2 {
+ t.Fatal("Fingerprint blind to a DeleteAlias")
+ }
+}
+
+// TestMigrateIdempotentWithAliases pins migrate()'s additive contract for the
+// new table: opening an already-migrated registry a second time (CREATE
+// TABLE IF NOT EXISTS route_aliases) must not error.
+func TestMigrateIdempotentWithAliases(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "registry.db")
+ r1, err := Open(path)
+ if err != nil {
+ t.Fatalf("first Open: %v", err)
+ }
+ if err := r1.UpsertAlias(RouteAlias{Alias: "extra.example.com", Host: "app.example.com"}); err != nil {
+ t.Fatalf("UpsertAlias: %v", err)
+ }
+ if err := r1.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ r2, err := Open(path)
+ if err != nil {
+ t.Fatalf("second Open over the same path: %v", err)
+ }
+ defer r2.Close()
+ got, err := r2.ListAliases()
+ if err != nil {
+ t.Fatalf("ListAliases: %v", err)
+ }
+ if len(got) != 1 || got[0].Alias != "extra.example.com" {
+ t.Fatalf("route_aliases row lost across a second Open: %+v", got)
+ }
+}