carlos / platform Public

feat(provision): reconcile instance records into routes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell e7b2d5118bda4d1ea4433bde9cf3c889328a4805 parent 046c336
2 files changed, +621 −0
  • internal/provision/reconcile.go +281 −0
  • internal/provision/reconcile_test.go +340 −0
diff --git a/internal/provision/reconcile.go b/internal/provision/reconcile.go
new file mode 100644
index 000000000..0eb8bcca6
--- /dev/null
+++ b/internal/provision/reconcile.go
@@ -0,0 +1,281 @@
+package provision
+
+// Reconcile turns bucket instance records into registry routes, for THIS
+// agent's own region and pool set. Spec:
+// docs/superpowers/specs/2026-08-04-instance-provisioning-design.md §4.
+//
+// DNS seam (spec §10): multi-region deployments write the per-slug CNAME at
+// provision time in the console's create handler, not here — the reconcile
+// pass stays DNS-free so a box never needs Route53 credentials.
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "slices"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// ReconcileConfig is everything one pass reads its world through — the same
+// injection-seam shape domains.Config uses (tests supply fixtures, the host
+// agent supplies production values).
+type ReconcileConfig struct {
+ // Region is this agent's region code. "" is the unconstrained region —
+ // a box that hasn't been told its place serves only region-less
+ // records, never someone else's residency promise.
+ Region string
+ // Pools are the pools this box serves. Empty means the shared "" pool
+ // only — NOT "every pool" (poolServed below).
+ Pools []string
+ // DataDir feeds DefaultDBPath ("/data" in production).
+ DataDir string
+ // Now is the clock seam; nil = time.Now.
+ Now func() time.Time
+}
+
+// acctApp identifies one app within one account — the unit both the record
+// scan and the manifest/seed cache below key on, since app names are unique
+// only within an account, never globally.
+type acctApp struct{ acct, app string }
+
+// 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
+// running the same scope) minted, marked by Provisioned, the same authority
+// discipline as domains.Reconcile's canary removal. Per-item failures log
+// and skip; a failed account-manifest or record read additionally disables
+// the removal phase for the whole pass — a transient read failure must
+// never mass-retire a fleet. Minting is additive and always safe, so it
+// proceeds regardless.
+func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg ReconcileConfig) error {
+ if cfg.Now == nil {
+ cfg.Now = time.Now
+ }
+
+ known, readFailed, err := knownAccounts(ctx, s)
+ if err != nil {
+ return err
+ }
+
+ // --- collect in-scope records --------------------------------------------
+ keys, err := s.List(ctx, "apps/")
+ if err != nil {
+ return fmt.Errorf("provision: list apps: %w", err)
+ }
+ recs := map[string]Record{} // host -> record, in-scope only
+ for _, key := range keys {
+ acct, app, host, ok := ParseRecordKey(key, known)
+ if !ok {
+ continue
+ }
+ raw, _, err := s.Get(ctx, key)
+ if err != nil {
+ log.Printf("provision: skipping record %s: read failed: %v", key, err)
+ // The record's absence from recs below must not read as "the
+ // bucket retired it" — a transient Get failure disables removal
+ // for the whole pass, same as an unreadable account manifest.
+ readFailed = true
+ continue
+ }
+ var rec Record
+ if err := json.Unmarshal(raw, &rec); err != nil {
+ log.Printf("provision: skipping record %s: malformed: %v", key, err)
+ continue
+ }
+ if rec.Host != host || rec.Account != acct || rec.App != app {
+ log.Printf("provision: skipping record %s: disagrees with its own path (host=%q account=%q app=%q)",
+ key, rec.Host, rec.Account, rec.App)
+ continue
+ }
+ if rec.Region != cfg.Region || !poolServed(cfg.Pools, rec.Pool) {
+ continue // another agent's jurisdiction, not this pass's business
+ }
+ recs[rec.Host] = rec
+ }
+
+ // --- manifest seed cache, per (acct, app), lazily -------------------------
+ // known=false (a genuine read failure, not "no manifest") means the seed
+ // value returned is a mint-only fallback: safe to hand a BRAND NEW route
+ // (nothing existing to protect; the next readable pass converges it),
+ // never safe to feed the converge branch below — SetSeed-ing an existing
+ // route to "" on a transient blip would erase a working seed for a
+ // reason that has nothing to do with the manifest actually changing.
+ seedCache := map[acctApp]string{}
+ seedKnown := map[acctApp]bool{}
+ seenManifest := map[acctApp]bool{}
+ seedFor := func(acct, app string) (seed string, known bool) {
+ k := acctApp{acct, app}
+ if seenManifest[k] {
+ return seedCache[k], seedKnown[k]
+ }
+ seenManifest[k] = true
+ m, err := GetManifest(ctx, s, acct, app)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ seedCache[k], seedKnown[k] = "", true // absent manifest: confirmed no seed
+ return seedCache[k], seedKnown[k]
+ }
+ // A record already written is truth regardless — the manifest
+ // only governs the API door — but a genuine read failure must
+ // still hold back removal: this pass cannot tell a disabled
+ // feature from a transient blip, so it treats it as the latter.
+ log.Printf("provision: manifest %s/%s: read failed: %v", acct, app, err)
+ readFailed = true
+ return "", false
+ }
+ seedCache[k], seedKnown[k] = seedJSON(m), true
+ return seedCache[k], seedKnown[k]
+ }
+
+ // --- mint / converge / conflict -------------------------------------------
+ routes, err := reg.List()
+ if err != nil {
+ return fmt.Errorf("provision: list routes: %w", err)
+ }
+ byHost := map[string]registry.Route{}
+ for _, rt := range routes {
+ byHost[rt.Host] = rt
+ }
+
+ for _, rec := range recs {
+ // A mint-only fallback ("", the record-already-written-is-truth
+ // case) is fine here even when the manifest is unreadable this
+ // pass — there is no existing seed value a brand new route could
+ // clobber, and a later pass converges it once the manifest reads.
+ seed, seedIsKnown := seedFor(rec.Account, rec.App)
+ 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",
+ }
+ 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)
+ 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.
+ if seedIsKnown && existing.Seed != seed {
+ if err := reg.SetSeed(rec.Host, seed); err != nil {
+ log.Printf("provision: converge seed on %s: %v", rec.Host, err)
+ }
+ }
+ default:
+ log.Printf("provision: %s exists but was not minted by this pass (app %q acct %q) — leaving it alone",
+ rec.Host, rec.App, rec.Account)
+ }
+ }
+
+ // --- removal, only when nothing failed to read this pass ------------------
+ // Three conditions, each load-bearing: Provisioned (the flag — not the
+ // app, not the channel — grants removal authority; a hand-added route
+ // never has it), scope (this box's own region/pool jurisdiction — a
+ // foreign box's provisioned route is not this pass's business), and
+ // absence from the record set this pass just read (the bucket's own
+ // decision to retire the instance, not a guess this pass made).
+ if !readFailed {
+ for _, rt := range routes {
+ if !rt.Provisioned || rt.Region != cfg.Region || !poolServed(cfg.Pools, rt.Pool) {
+ continue
+ }
+ if _, ok := recs[rt.Host]; ok {
+ continue
+ }
+ if err := reg.Delete(rt.Host); err != nil {
+ log.Printf("provision: remove retired instance route %s: %v", rt.Host, err)
+ continue
+ }
+ log.Printf("provision: removed retired instance route %s (record gone)", rt.Host)
+ }
+ }
+ return nil
+}
+
+// knownAccounts lists accounts/ and materializes the sqid set this pass
+// trusts for ParseRecordKey's "known account" guard — the same three-way
+// sqid agreement domains.Reconcile enforces (reconcile.go:106): the key
+// path's sqid, the manifest body's own sqid, and the frozen encoding of the
+// body's id must all agree, or the manifest is a lie about identity and is
+// skipped, loudly. A tombstoned manifest (m.Deleted()) is skipped too —
+// retired accounts own no in-scope records.
+//
+// Listing failure returns an error (nothing here can proceed without it); a
+// per-manifest read or parse failure instead sets readFailed and is
+// otherwise skipped — never fatal to the pass, but disabling enough that the
+// removal phase in Reconcile refuses to run on it.
+func knownAccounts(ctx context.Context, s release.Store) (known map[string]bool, readFailed bool, err error) {
+ known = map[string]bool{domains.OpsSqid: true}
+ keys, err := s.List(ctx, "accounts/")
+ if err != nil {
+ return nil, false, fmt.Errorf("provision: list accounts: %w", err)
+ }
+ for _, key := range keys {
+ sqid, ok := domains.AccountManifestSqid(key)
+ if !ok {
+ continue
+ }
+ body, _, err := s.Get(ctx, key)
+ if err != nil {
+ log.Printf("provision: skipping %s: read failed: %v", key, err)
+ readFailed = true
+ continue
+ }
+ var m domains.AccountManifest
+ if err := json.Unmarshal(body, &m); err != nil || m.ID <= 0 || m.Sqid == "" {
+ log.Printf("provision: skipping %s: malformed account manifest: %v", key, err)
+ readFailed = true
+ continue
+ }
+ if m.Sqid != sqid || domains.Sqid(m.ID) != sqid {
+ log.Printf("provision: skipping %s: sqid %q/%q is not Sqid(%d)=%q", key, sqid, m.Sqid, m.ID, domains.Sqid(m.ID))
+ readFailed = true
+ continue
+ }
+ if m.Deleted() {
+ continue
+ }
+ known[sqid] = true
+ }
+ return known, readFailed, nil
+}
+
+// poolServed reports whether pool is one this box serves. An empty
+// cfg.Pools means the shared "" pool ONLY, never "every pool" — the same
+// convention domains.Config's AppsDomain=="" carries: unconfigured means the
+// default, not "all".
+func poolServed(pools []string, pool string) bool {
+ if len(pools) == 0 {
+ return pool == ""
+ }
+ return slices.Contains(pools, pool)
+}
+
+// seedJSON renders a manifest's seed argv as the wire text SetSeed and a
+// minted route's Seed field carry: "" for nil/empty (no seed), else the
+// JSON array of the argv strings.
+func seedJSON(m *Manifest) string {
+ if m == nil || len(m.Seed) == 0 {
+ return ""
+ }
+ b, err := json.Marshal(m.Seed) // []string always marshals; err is unreachable
+ if err != nil {
+ return ""
+ }
+ return string(b)
+}
diff --git a/internal/provision/reconcile_test.go b/internal/provision/reconcile_test.go
new file mode 100644
index 000000000..c6891234d
--- /dev/null
+++ b/internal/provision/reconcile_test.go
@@ -0,0 +1,340 @@
+package provision
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+var ctx = context.Background()
+
+// acctID is the id behind every test's account manifest. Its sqid is always
+// computed with domains.Sqid, never hardcoded — the same discipline
+// internal/domains/reconcile_test.go follows for the identical reason: a
+// literal sqid string would silently stop agreeing with domains.Sqid the
+// moment the alphabet or offset ever changed.
+const acctID int64 = 2
+
+func dirStore(t *testing.T) *release.DirStore {
+ t.Helper()
+ return release.NewDirStore(t.TempDir())
+}
+
+func regWith(t *testing.T, routes ...registry.Route) *registry.Registry {
+ t.Helper()
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, rt := range routes {
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return reg
+}
+
+func mustGet(t *testing.T, reg *registry.Registry, host string) *registry.Route {
+ t.Helper()
+ rt, err := reg.Get(host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if rt == nil {
+ t.Fatalf("route %s missing", host)
+ }
+ return rt
+}
+
+func accountManifestBody(id int64, sqid, name string) []byte {
+ return []byte(`{"id":` + fmt.Sprint(id) + `,"sqid":"` + sqid + `","name":"` + name +
+ `","created_at":"2026-08-02T09:00:00Z"}`)
+}
+
+// writeAccountManifest writes a fresh (create-only) account manifest and
+// hands back its sqid — the frozen encoding of id, computed by
+// domains.Sqid, never a literal.
+func writeAccountManifest(t *testing.T, s release.Store, id int64, name string) string {
+ t.Helper()
+ sqid := domains.Sqid(id)
+ if _, err := s.Put(ctx, "accounts/"+sqid+"/account.json", accountManifestBody(id, sqid, name), ""); err != nil {
+ t.Fatal(err)
+ }
+ return sqid
+}
+
+// putReplace overwrites an existing object (read-etag-then-put), for tests
+// that need to corrupt or repair an object already written by
+// writeAccountManifest's create-only Put.
+func putReplace(t *testing.T, s release.Store, key string, body []byte) {
+ t.Helper()
+ _, etag, err := s.Get(ctx, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := s.Put(ctx, key, body, etag); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// setup writes a valid account manifest (id acctID) and that account's
+// "tito" app instances manifest (domain go.tito.io, pool tito, seed
+// ["seed"]) — the fixture every test case below builds records on top of.
+func setup(t *testing.T) (*release.DirStore, string) {
+ t.Helper()
+ s := dirStore(t)
+ sqid := writeAccountManifest(t, s, acctID, "Tito")
+ if err := PutManifest(ctx, s, sqid, "tito", Manifest{
+ Schema: 1, App: "tito", Domain: "go.tito.io", Pool: "tito", Seed: []string{"seed"},
+ }); err != nil {
+ t.Fatal(err)
+ }
+ return s, sqid
+}
+
+func acmeRecord(sqid string) Record {
+ return Record{Schema: 1, Host: "acme.go.tito.io", Slug: "acme", App: "tito",
+ Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", CreatedAt: time.Now()}
+}
+
+// 1. Mint.
+func TestReconcileMints(t *testing.T) {
+ s, sqid := setup(t)
+ 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)
+ }
+ got := *mustGet(t, reg, "acme.go.tito.io")
+ got.CreatedAt = 0 // stamped by Upsert; not part of the contract under test
+ want := registry.Route{
+ Host: "acme.go.tito.io", Slug: "acme", Kind: "instance",
+ Socket: "/run/carlos/acme.go.tito.io.sock", DBPath: "/data/acme.go.tito.io.db",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable",
+ Hibernate: true, Provisioned: true, Seed: `["seed"]`, Status: "ready",
+ }
+ if got != want {
+ t.Errorf("minted route = %+v, want %+v", got, want)
+ }
+}
+
+// 2. Scope.
+func TestReconcileScope(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ reg := regWith(t)
+
+ // Wrong region entirely.
+ if err := Reconcile(ctx, s, reg, ReconcileConfig{Region: "us", Pools: []string{"tito"}, DataDir: "/data"}); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get("acme.go.tito.io"); err != nil || rt != nil {
+ t.Errorf("minted for a foreign region: %+v %v", rt, err)
+ }
+
+ // Right region, but this box serves no pools (nil = the shared "" pool
+ // only) — "tito" is not among them.
+ if err := Reconcile(ctx, s, reg, ReconcileConfig{Region: "ie", DataDir: "/data"}); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get("acme.go.tito.io"); err != nil || rt != nil {
+ t.Errorf("minted for an unserved pool: %+v %v", rt, err)
+ }
+
+ // A record with empty pool+region mints under a zero-value cfg (the
+ // unconstrained region, the shared pool).
+ unscoped := Record{Schema: 1, Host: "shared.go.tito.io", Slug: "shared", App: "tito",
+ Account: sqid, Channel: "stable", CreatedAt: time.Now()}
+ if err := CreateRecord(ctx, s, unscoped); err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, ReconcileConfig{}); err != nil {
+ t.Fatal(err)
+ }
+ mustGet(t, reg, "shared.go.tito.io")
+}
+
+// 3. Idempotent.
+func TestReconcileIdempotent(t *testing.T) {
+ s, sqid := setup(t)
+ 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)
+ }
+ before, err := reg.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ after, err := reg.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(before, after) {
+ t.Errorf("second pass changed the registry:\nbefore %+v\nafter %+v", before, after)
+ }
+}
+
+// 4. Converge seed.
+func TestReconcileConvergesSeed(t *testing.T) {
+ s, sqid := setup(t)
+ 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)
+ }
+ before := *mustGet(t, reg, "acme.go.tito.io")
+
+ if err := PutManifest(ctx, s, sqid, "tito", Manifest{
+ Schema: 1, App: "tito", Domain: "go.tito.io", Pool: "tito", Seed: []string{"seed", "-v"},
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ after := *mustGet(t, reg, "acme.go.tito.io")
+ if after.Seed != `["seed","-v"]` {
+ t.Errorf("seed = %q, want the converged manifest seed", after.Seed)
+ }
+ after.Seed = before.Seed
+ if after != before {
+ t.Errorf("converge touched more than Seed: before %+v after %+v", before, after)
+ }
+}
+
+// 5. Conflict.
+func TestReconcileLeavesConflictAlone(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ handAdded := registry.Route{Host: "acme.go.tito.io", Kind: "instance", App: "other",
+ Status: "ready", Addr: "127.0.0.1:9000"}
+ reg := regWith(t, handAdded)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ before := *mustGet(t, reg, "acme.go.tito.io")
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ after := *mustGet(t, reg, "acme.go.tito.io")
+ if before != after {
+ t.Errorf("conflicting hand-added route changed: before %+v after %+v", before, after)
+ }
+ if n, err := reg.List(); err != nil || len(n) != 1 {
+ t.Errorf("pass minted something alongside the conflict: %+v %v", n, err)
+ }
+}
+
+// 6. Remove.
+func TestReconcileRemoves(t *testing.T) {
+ s, sqid := setup(t)
+ if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
+ t.Fatal(err)
+ }
+ handAdded := registry.Route{Host: "neighbor.go.tito.io", Kind: "instance", App: "other",
+ Status: "ready", Addr: "127.0.0.1:9001"}
+ foreign := registry.Route{Host: "us.go.tito.io", Kind: "instance", App: "tito",
+ Status: "ready", Provisioned: true, Region: "us", Pool: "tito", Account: sqid}
+ reg := regWith(t, handAdded, foreign)
+ cfg := ReconcileConfig{Region: "ie", Pools: []string{"tito"}, DataDir: "/data"}
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ mustGet(t, reg, "acme.go.tito.io") // minted first
+
+ if err := DeleteRecord(ctx, s, sqid, "tito", "acme.go.tito.io"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get("acme.go.tito.io"); err != nil || rt != nil {
+ t.Errorf("retired route survived: %+v %v", rt, err)
+ }
+ mustGet(t, reg, "neighbor.go.tito.io") // hand-added, never provisioned: survives
+ mustGet(t, reg, "us.go.tito.io") // provisioned but out of this box's region: survives
+}
+
+// 7. Fail-safe.
+func TestReconcileFailSafeOnBrokenAccountManifest(t *testing.T) {
+ s, sqid := setup(t)
+ 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)
+ }
+ mustGet(t, reg, "acme.go.tito.io") // minted while the manifest was good
+
+ putReplace(t, s, "accounts/"+sqid+"/account.json", []byte("not json"))
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ // Mints nothing new for the now-unreadable account, but the existing
+ // provisioned route must NOT be retired: readFailed disables removal for
+ // the whole pass, even though the record has (from this pass's point of
+ // view, with the account unknown) effectively vanished from the diff set.
+ mustGet(t, reg, "acme.go.tito.io")
+
+ putReplace(t, s, "accounts/"+sqid+"/account.json", accountManifestBody(acctID, sqid, "Tito"))
+ if err := DeleteRecord(ctx, s, sqid, "tito", "acme.go.tito.io"); err != nil {
+ t.Fatal(err)
+ }
+ if err := Reconcile(ctx, s, reg, cfg); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get("acme.go.tito.io"); err != nil || rt != nil {
+ t.Errorf("route survived after the manifest was fixed and the record removed: %+v %v", rt, err)
+ }
+}
+
+// 8. Malformed record.
+func TestReconcileSkipsMalformedRecord(t *testing.T) {
+ s, sqid := setup(t)
+ body, err := json.Marshal(Record{Schema: 1, Host: "wrong-host.go.tito.io", Slug: "wrong",
+ App: "tito", Account: sqid, Pool: "tito", Region: "ie", Channel: "stable", CreatedAt: time.Now()})
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Written at the key for "acme.go.tito.io" but the body claims a
+ // different host: the record disagrees with its own path.
+ if _, err := s.Put(ctx, RecordKey(sqid, "tito", "acme.go.tito.io"), body, ""); 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("acme.go.tito.io"); err != nil || rt != nil {
+ t.Errorf("minted from a malformed record's key host: %+v %v", rt, err)
+ }
+ if rt, err := reg.Get("wrong-host.go.tito.io"); err != nil || rt != nil {
+ t.Errorf("minted from a malformed record's body host: %+v %v", rt, err)
+ }
+}