package provision import ( "context" "errors" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "os" "path/filepath" "testing" ) func TestClaimRemovalUsesTypedHookForBothPasses(t *testing.T) { for _, sidecar := range []bool{false, true} { t.Run(map[bool]string{false: "instance", true: "sidecar"}[sidecar], func(t *testing.T) { r, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer r.Close() rt := registry.Route{Host: "shop.example", Kind: "instance", Provisioned: true, ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "route"}} if sidecar { rt.Kind = SidecarKind rt.Host = "worker.sidecar.internal" } if err = r.Upsert(rt); err != nil { t.Fatal(err) } cfg := ReconcileConfig{DataDir: t.TempDir(), ClaimEnabled: true} calls := 0 pass := Reconcile if sidecar { pass = ReconcileSidecars } store := release.NewDirStore(t.TempDir()) if err = pass(context.Background(), store, r, cfg); err != nil { t.Fatal(err) } if got, e := r.Get(rt.Host); e != nil || got == nil { t.Fatal("nil provider deleted route") } cfg.ClaimRetire = func(_ context.Context, got registry.Route) error { calls++ if got.ClaimOwnership != rt.ClaimOwnership { t.Fatal("ownership dropped") } return ErrClaimAdmission } if err = pass(context.Background(), store, r, cfg); err != nil { t.Fatal(err) } if calls != 1 { t.Fatalf("typed calls = %d", calls) } if got, e := r.Get(rt.Host); e != nil || got == nil { t.Fatal("refused retirement deleted route") } }) } } func TestClaimInstallGuardsMintAndKeepInBothPasses(t *testing.T) { for _, sidecar := range []bool{false, true} { t.Run(map[bool]string{false: "instance", true: "sidecar"}[sidecar], func(t *testing.T) { var s release.Store var reg *registry.Registry var host string cfg := ReconcileConfig{ClaimEnabled: true, DataDir: t.TempDir()} pass := Reconcile if sidecar { f := sidecarSetup(t) f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"run"}, Channel: "stable"}) f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"}) s, reg, host = f.store, f.reg, f.host("bot", "eleven") pass = ReconcileSidecars } else { store, sqid := setup(t) record := acmeRecord(sqid) if err := CreateRecord(ctx, store, record); err != nil { t.Fatal(err) } s, reg, host = store, regWith(t), record.Host cfg.Region = "ie" cfg.Pools = []string{"tito"} } defer reg.Close() if err := pass(ctx, s, reg, cfg); err != nil { t.Fatal(err) } if got, err := reg.Get(host); err != nil || got != nil { t.Fatal("missing provider minted route") } calls := 0 owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"} cfg.ClaimInstall = func(_ context.Context, rec ClaimRecord, write func(registry.ClaimOwnership) error) error { calls++ if rec.Host != host || rec.Key == "" || rec.Family == "" || rec.Value == nil { t.Fatal("selection identity missing") } return write(owner) } for i := 0; i < 2; i++ { if err := pass(ctx, s, reg, cfg); err != nil { t.Fatal(err) } } if calls != 2 { t.Fatalf("mint and keep calls = %d", calls) } got, err := reg.Get(host) if err != nil || got == nil || got.ClaimOwnership != owner { t.Fatalf("mint lost ownership: %v", err) } owner.ClaimStamp++ if err := pass(ctx, s, reg, cfg); err != nil { t.Fatal(err) } current, err := reg.Get(host) if err != nil || *current != *got { t.Fatalf("stale keep changed successor: %v", err) } }) } } // claimEffects stands in for the production ClaimInstall hook's durable // effects: activating the host guard and writing the inventory. Each call // leaves a sentinel file for both, so a pass that reached the hook cannot // look untouched, whatever the hook went on to decide. type claimEffects struct { dir string owner registry.ClaimOwnership calls int } func newClaimEffects(t *testing.T) *claimEffects { t.Helper() return &claimEffects{dir: t.TempDir(), owner: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}} } func (e *claimEffects) hook(t *testing.T) func(context.Context, ClaimRecord, func(registry.ClaimOwnership) error) error { return func(_ context.Context, rec ClaimRecord, write func(registry.ClaimOwnership) error) error { e.calls++ for _, name := range []string{"guard.active", "inventory." + rec.Host} { if err := os.WriteFile(filepath.Join(e.dir, name), []byte(rec.Key), 0o600); err != nil { t.Fatal(err) } } return write(e.owner) } } // untouched fails unless the hook never ran: no call, no guard, no inventory. func (e *claimEffects) untouched(t *testing.T, why string) { t.Helper() left, err := os.ReadDir(e.dir) if err != nil { t.Fatal(err) } if e.calls != 0 || len(left) != 0 { t.Fatalf("%s: claim hook ran before refusal: calls=%d sentinels=%d", why, e.calls, len(left)) } } // reached fails unless the hook ran exactly n times and left its sentinels, // proving the harness can observe the effect untouched() denies. func (e *claimEffects) reached(t *testing.T, n int, why string) { t.Helper() left, err := os.ReadDir(e.dir) if err != nil { t.Fatal(err) } if e.calls != n || len(left) == 0 { t.Fatalf("%s: claim hook calls=%d (want %d) sentinels=%d", why, e.calls, n, len(left)) } } func instanceClaimFixture(t *testing.T) (release.Store, *registry.Registry, string, ReconcileConfig) { t.Helper() store, sqid := setup(t) record := acmeRecord(sqid) if err := CreateRecord(ctx, store, record); err != nil { t.Fatal(err) } reg := regWith(t) t.Cleanup(func() { reg.Close() }) return store, reg, record.Host, ReconcileConfig{ClaimEnabled: true, DataDir: t.TempDir(), Region: "ie", Pools: []string{"tito"}} } func sidecarClaimFixture(t *testing.T) (*sidecarFixture, string, ReconcileConfig) { t.Helper() f := sidecarSetup(t) t.Cleanup(func() { f.reg.Close() }) f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"run"}, Channel: "stable"}) f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"}) return f, f.host("bot", "eleven"), ReconcileConfig{ClaimEnabled: true, DataDir: f.dir} } func TestClaimRefusalsPrecedeInstallHookInstanceMint(t *testing.T) { refuse := errors.New("no sealed config") cases := []struct { name string apply func(*ReconcileConfig, *int) gated bool }{ {"inbound none", func(cfg *ReconcileConfig, _ *int) { cfg.Inbound = InboundNone }, false}, {"config gate", func(cfg *ReconcileConfig, gates *int) { cfg.ConfigGate = func(context.Context, registry.Route) error { *gates++; return refuse } }, true}, {"layout unreadable", func(cfg *ReconcileConfig, _ *int) { cfg.Layout = func(context.Context, release.Store) (string, error) { return "", refuse } }, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s, reg, host, cfg := instanceClaimFixture(t) fx := newClaimEffects(t) cfg.ClaimInstall = fx.hook(t) gates := 0 tc.apply(&cfg, &gates) if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } fx.untouched(t, tc.name) if got, err := reg.Get(host); err != nil || got != nil { t.Fatalf("refused mint wrote a route: %v", err) } if tc.gated && gates != 1 { t.Fatalf("config gate asked %d times, want once", gates) } }) } t.Run("accepted mint reaches the hook once", func(t *testing.T) { s, reg, host, cfg := instanceClaimFixture(t) fx := newClaimEffects(t) cfg.ClaimInstall = fx.hook(t) cfg.ConfigGate = func(context.Context, registry.Route) error { return nil } if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } fx.reached(t, 1, "accepted") if got, err := reg.Get(host); err != nil || got == nil || got.ClaimOwnership != fx.owner { t.Fatalf("accepted mint lost ownership: %v", err) } }) } // Both refusals are mint-time rules. A kept row converges as before, and its // claim hook still runs, whatever the box's inbound posture or config gate. func TestClaimKeepStillReachesInstallHookInstance(t *testing.T) { s, reg, host, cfg := instanceClaimFixture(t) fx := newClaimEffects(t) cfg.ClaimInstall = fx.hook(t) if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } fx.reached(t, 1, "mint") minted, err := reg.Get(host) if err != nil || minted == nil { t.Fatalf("mint: %v", err) } gates := 0 cfg.Inbound = InboundNone cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return errors.New("refuse") } if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } fx.reached(t, 2, "keep") if gates != 0 { t.Fatalf("config gate asked %d times for a kept row", gates) } if got, err := reg.Get(host); err != nil || got == nil || *got != *minted { t.Fatalf("keep changed the row: %v", err) } } func TestClaimRefusalsPrecedeInstallHookSidecarMint(t *testing.T) { refuse := errors.New("no sealed config") cases := []struct { name string apply func(*testing.T, string, *ReconcileConfig, *int) }{ {"co-spawn staged", func(t *testing.T, host string, cfg *ReconcileConfig, _ *int) { if err := os.WriteFile(filepath.Join(cfg.DataDir, host+sidecarCoSpawnSuffix), []byte("A=1\n"), 0o600); err != nil { t.Fatal(err) } }}, {"config gate", func(_ *testing.T, _ string, cfg *ReconcileConfig, gates *int) { cfg.ConfigGate = func(context.Context, registry.Route) error { *gates++; return refuse } }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { f, host, cfg := sidecarClaimFixture(t) fx := newClaimEffects(t) cfg.ClaimInstall = fx.hook(t) gates := 0 tc.apply(t, host, &cfg, &gates) f.reconcile(t, cfg) fx.untouched(t, tc.name) if got, err := f.reg.Get(host); err != nil || got != nil { t.Fatalf("refused mint wrote a route: %v", err) } if tc.name == "config gate" && gates != 1 { t.Fatalf("config gate asked %d times, want once", gates) } }) } t.Run("accepted mint reaches the hook once", func(t *testing.T) { f, host, cfg := sidecarClaimFixture(t) fx := newClaimEffects(t) cfg.ClaimInstall = fx.hook(t) cfg.ConfigGate = func(context.Context, registry.Route) error { return nil } f.reconcile(t, cfg) fx.reached(t, 1, "accepted") if got, err := f.reg.Get(host); err != nil || got == nil || got.ClaimOwnership != fx.owner { t.Fatalf("accepted mint lost ownership: %v", err) } }) } // The co-spawn rule is mint-time only: a file staged after the row exists // must not freeze the row's convergence, nor stop its claim hook. func TestClaimKeepStillReachesInstallHookSidecar(t *testing.T) { f, host, cfg := sidecarClaimFixture(t) fx := newClaimEffects(t) cfg.ClaimInstall = fx.hook(t) f.reconcile(t, cfg) fx.reached(t, 1, "mint") minted, err := f.reg.Get(host) if err != nil || minted == nil { t.Fatalf("mint: %v", err) } gates := 0 cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return errors.New("refuse") } if err := os.WriteFile(filepath.Join(cfg.DataDir, host+sidecarCoSpawnSuffix), []byte("A=1\n"), 0o600); err != nil { t.Fatal(err) } f.reconcile(t, cfg) fx.reached(t, 2, "keep") if gates != 0 { t.Fatalf("config gate asked %d times for a kept row", gates) } if got, err := f.reg.Get(host); err != nil || got == nil || *got != *minted { t.Fatalf("keep changed the row: %v", err) } } // With the flag off the claim hook is never consulted, and the same refusals // still decide the mint exactly as before. func TestClaimFlagOffRefusalsUnchanged(t *testing.T) { refuse := func(context.Context, registry.Route) error { return errors.New("refuse") } hookRan := false hook := func(context.Context, ClaimRecord, func(registry.ClaimOwnership) error) error { hookRan = true return ErrClaimAdmission } t.Run("instance", func(t *testing.T) { for _, tc := range []struct { name string apply func(*ReconcileConfig) mint bool }{ {"inbound none", func(c *ReconcileConfig) { c.Inbound = InboundNone }, false}, {"config gate", func(c *ReconcileConfig) { c.ConfigGate = refuse }, false}, {"accepted", func(c *ReconcileConfig) {}, true}, } { s, reg, host, cfg := instanceClaimFixture(t) cfg.ClaimEnabled, cfg.ClaimInstall = false, hook tc.apply(&cfg) if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } got, err := reg.Get(host) if err != nil || (got != nil) != tc.mint { t.Fatalf("%s: minted=%v err=%v, want minted=%v", tc.name, got != nil, err, tc.mint) } if got != nil && got.ClaimOwnership != (registry.ClaimOwnership{}) { t.Fatalf("%s: flag-off mint carries ownership", tc.name) } } }) t.Run("sidecar", func(t *testing.T) { for _, tc := range []struct { name string apply func(*testing.T, string, *ReconcileConfig) mint bool }{ {"co-spawn", func(t *testing.T, host string, c *ReconcileConfig) { if err := os.WriteFile(filepath.Join(c.DataDir, host+sidecarCoSpawnSuffix), nil, 0o600); err != nil { t.Fatal(err) } }, false}, {"config gate", func(_ *testing.T, _ string, c *ReconcileConfig) { c.ConfigGate = refuse }, false}, {"accepted", func(*testing.T, string, *ReconcileConfig) {}, true}, } { f, host, cfg := sidecarClaimFixture(t) cfg.ClaimEnabled, cfg.ClaimInstall = false, hook tc.apply(t, host, &cfg) f.reconcile(t, cfg) got, err := f.reg.Get(host) if err != nil || (got != nil) != tc.mint { t.Fatalf("%s: minted=%v err=%v, want minted=%v", tc.name, got != nil, err, tc.mint) } } }) if hookRan { t.Fatal("flag-off pass consulted the claim hook") } } func TestClaimInstallWrapperRefusesUnownedExistingBeforeHook(t *testing.T) { owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"} calls, writes := 0, 0 hook := func(_ context.Context, _ ClaimRecord, write func(registry.ClaimOwnership) error) error { calls++ return write(owner) } write := func(registry.ClaimOwnership) error { writes++; return nil } cfg := ReconcileConfig{ClaimEnabled: true, ClaimInstall: hook} if err := cfg.claimInstall(ctx, ClaimRecord{}, ®istry.Route{}, write); !errors.Is(err, registry.ErrClaimOwnership) { t.Fatalf("unowned existing row: err = %v, want ErrClaimOwnership", err) } if calls != 0 || writes != 0 { t.Fatalf("unowned existing row reached the hook: calls=%d writes=%d", calls, writes) } // Valid owned rows still keep: the hook runs and its write goes through. if err := cfg.claimInstall(ctx, ClaimRecord{}, ®istry.Route{ClaimOwnership: owner}, write); err != nil { t.Fatal(err) } if calls != 1 || writes != 1 { t.Fatalf("owned existing row: calls=%d writes=%d, want 1 and 1", calls, writes) } // A different valid owner is still refused, by the write, after the hook. other := owner other.ClaimStamp++ if err := cfg.claimInstall(ctx, ClaimRecord{}, ®istry.Route{ClaimOwnership: other}, write); !errors.Is(err, registry.ErrClaimOwnership) { t.Fatalf("foreign owner: err = %v, want ErrClaimOwnership", err) } if calls != 2 || writes != 1 { t.Fatalf("foreign owner: calls=%d writes=%d, want 2 and 1", calls, writes) } // A mint has no existing row and is unaffected. if err := cfg.claimInstall(ctx, ClaimRecord{}, nil, write); err != nil { t.Fatal(err) } if calls != 3 || writes != 2 { t.Fatalf("mint: calls=%d writes=%d, want 3 and 2", calls, writes) } // Flag off: the hook is not consulted and an unowned row is written as before. off := ReconcileConfig{ClaimInstall: hook} if err := off.claimInstall(ctx, ClaimRecord{}, ®istry.Route{}, write); err != nil { t.Fatal(err) } if calls != 3 || writes != 3 { t.Fatalf("flag off: calls=%d writes=%d, want 3 and 3", calls, writes) } } // Existing rows this pass will not keep are refused before the claim hook, so // the guard activation and inventory write it stands for never happen. Each // row's refusal is isolated: the hand-added rows carry a VALID owner, so the // wrapper's ownership check cannot be what stops them. func TestClaimKnownExistingRowRefusalsPrecedeInstallHookInstance(t *testing.T) { cases := []struct { name string setup func(*testing.T, release.Store, *registry.Registry, string, *claimEffects, ReconcileConfig) }{ {"shape mismatch", func(t *testing.T, _ release.Store, reg *registry.Registry, host string, fx *claimEffects, _ ReconcileConfig) { if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", App: "other", ClaimOwnership: fx.owner}); err != nil { t.Fatal(err) } }}, {"legacy owned row without claim owner", func(t *testing.T, s release.Store, reg *registry.Registry, host string, _ *claimEffects, cfg ReconcileConfig) { cfg.ClaimEnabled = false if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } if got, err := reg.Get(host); err != nil || got == nil || !got.Provisioned { t.Fatalf("legacy mint: %v", err) } }}, {"legacy adoptable row without claim owner", func(t *testing.T, s release.Store, reg *registry.Registry, host string, _ *claimEffects, cfg ReconcileConfig) { cfg.ClaimEnabled = false if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } if err := reg.SetProvisioned(host, false); err != nil { t.Fatal(err) } }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s, reg, host, cfg := instanceClaimFixture(t) fx := newClaimEffects(t) tc.setup(t, s, reg, host, fx, cfg) before, err := reg.Get(host) if err != nil || before == nil { t.Fatalf("setup left no row: %v", err) } gates := 0 cfg.ClaimInstall = fx.hook(t) cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return nil } if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } fx.untouched(t, tc.name) if gates != 0 { t.Fatalf("config gate asked %d times for an existing row", gates) } if got, err := reg.Get(host); err != nil || got == nil || *got != *before { t.Fatalf("refused existing row changed: %v", err) } }) } } func TestClaimKnownExistingRowRefusalsPrecedeInstallHookSidecar(t *testing.T) { cases := []struct { name string setup func(*testing.T, *sidecarFixture, string, *claimEffects, ReconcileConfig) }{ {"hand-added row", func(t *testing.T, f *sidecarFixture, host string, fx *claimEffects, _ ReconcileConfig) { f.mustUpsert(t, registry.Route{Host: host, Kind: SidecarKind, Slug: "bot", App: "eleven", Account: f.sqid, ClaimOwnership: fx.owner}) }}, {"legacy owned row without claim owner", func(t *testing.T, f *sidecarFixture, host string, _ *claimEffects, cfg ReconcileConfig) { cfg.ClaimEnabled = false f.reconcile(t, cfg) if got, err := f.reg.Get(host); err != nil || got == nil || !got.Provisioned { t.Fatalf("legacy mint: %v", err) } }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { f, host, cfg := sidecarClaimFixture(t) fx := newClaimEffects(t) tc.setup(t, f, host, fx, cfg) before, err := f.reg.Get(host) if err != nil || before == nil { t.Fatalf("setup left no row: %v", err) } gates := 0 cfg.ClaimInstall = fx.hook(t) cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return nil } f.reconcile(t, cfg) fx.untouched(t, tc.name) if gates != 0 { t.Fatalf("config gate asked %d times for an existing row", gates) } if got, err := f.reg.Get(host); err != nil || got == nil || *got != *before { t.Fatalf("refused existing row changed: %v", err) } }) } } // Flag off, the wrapper never consults the hook: a shape mismatch is still // left alone by the write, and an adoptable unowned row is still adopted. func TestClaimFlagOffExistingRowsUnchanged(t *testing.T) { hookRan := false hook := func(context.Context, ClaimRecord, func(registry.ClaimOwnership) error) error { hookRan = true return ErrClaimAdmission } t.Run("shape mismatch left alone", func(t *testing.T) { s, reg, host, cfg := instanceClaimFixture(t) cfg.ClaimEnabled, cfg.ClaimInstall = false, hook if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", App: "other"}); err != nil { t.Fatal(err) } before := mustGet(t, reg, host) if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } if got := mustGet(t, reg, host); *got != *before { t.Fatal("flag-off pass changed a mismatched row") } }) t.Run("adoptable unowned row adopted", func(t *testing.T) { s, reg, host, cfg := instanceClaimFixture(t) cfg.ClaimEnabled, cfg.ClaimInstall = false, hook if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } if err := reg.SetProvisioned(host, false); err != nil { t.Fatal(err) } if err := Reconcile(ctx, s, reg, cfg); err != nil { t.Fatal(err) } if got := mustGet(t, reg, host); !got.Provisioned || got.ClaimOwnership != (registry.ClaimOwnership{}) { t.Fatalf("flag-off adoption: provisioned=%v owner=%+v", got.Provisioned, got.ClaimOwnership) } }) t.Run("sidecar hand-added row left alone", func(t *testing.T) { f, host, cfg := sidecarClaimFixture(t) cfg.ClaimEnabled, cfg.ClaimInstall = false, hook f.mustUpsert(t, registry.Route{Host: host, Kind: SidecarKind, Slug: "bot", App: "eleven", Account: f.sqid}) before := mustGet(t, f.reg, host) f.reconcile(t, cfg) if got := mustGet(t, f.reg, host); *got != *before { t.Fatal("flag-off pass changed a hand-added sidecar row") } }) if hookRan { t.Fatal("flag-off pass consulted the claim hook") } }