carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "context"
5 "errors"
6 "github.com/carlosframework/platform/internal/registry"
7 "github.com/carlosframework/platform/internal/release"
8 "os"
9 "path/filepath"
10 "testing"
11)
12
13func TestClaimRemovalUsesTypedHookForBothPasses(t *testing.T) {
14 for _, sidecar := range []bool{false, true} {
15 t.Run(map[bool]string{false: "instance", true: "sidecar"}[sidecar], func(t *testing.T) {
16 r, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
17 if err != nil {
18 t.Fatal(err)
19 }
20 defer r.Close()
21 rt := registry.Route{Host: "shop.example", Kind: "instance", Provisioned: true, ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "route"}}
22 if sidecar {
23 rt.Kind = SidecarKind
24 rt.Host = "worker.sidecar.internal"
25 }
26 if err = r.Upsert(rt); err != nil {
27 t.Fatal(err)
28 }
29 cfg := ReconcileConfig{DataDir: t.TempDir(), ClaimEnabled: true}
30 calls := 0
31 pass := Reconcile
32 if sidecar {
33 pass = ReconcileSidecars
34 }
35 store := release.NewDirStore(t.TempDir())
36 if err = pass(context.Background(), store, r, cfg); err != nil {
37 t.Fatal(err)
38 }
39 if got, e := r.Get(rt.Host); e != nil || got == nil {
40 t.Fatal("nil provider deleted route")
41 }
42 cfg.ClaimRetire = func(_ context.Context, got registry.Route) error {
43 calls++
44 if got.ClaimOwnership != rt.ClaimOwnership {
45 t.Fatal("ownership dropped")
46 }
47 return ErrClaimAdmission
48 }
49 if err = pass(context.Background(), store, r, cfg); err != nil {
50 t.Fatal(err)
51 }
52 if calls != 1 {
53 t.Fatalf("typed calls = %d", calls)
54 }
55 if got, e := r.Get(rt.Host); e != nil || got == nil {
56 t.Fatal("refused retirement deleted route")
57 }
58 })
59 }
60}
61
62func TestClaimInstallGuardsMintAndKeepInBothPasses(t *testing.T) {
63 for _, sidecar := range []bool{false, true} {
64 t.Run(map[bool]string{false: "instance", true: "sidecar"}[sidecar], func(t *testing.T) {
65 var s release.Store
66 var reg *registry.Registry
67 var host string
68 cfg := ReconcileConfig{ClaimEnabled: true, DataDir: t.TempDir()}
69 pass := Reconcile
70 if sidecar {
71 f := sidecarSetup(t)
72 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"run"}, Channel: "stable"})
73 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
74 s, reg, host = f.store, f.reg, f.host("bot", "eleven")
75 pass = ReconcileSidecars
76 } else {
77 store, sqid := setup(t)
78 record := acmeRecord(sqid)
79 if err := CreateRecord(ctx, store, record); err != nil {
80 t.Fatal(err)
81 }
82 s, reg, host = store, regWith(t), record.Host
83 cfg.Region = "ie"
84 cfg.Pools = []string{"tito"}
85 }
86 defer reg.Close()
87 if err := pass(ctx, s, reg, cfg); err != nil {
88 t.Fatal(err)
89 }
90 if got, err := reg.Get(host); err != nil || got != nil {
91 t.Fatal("missing provider minted route")
92 }
93 calls := 0
94 owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
95 cfg.ClaimInstall = func(_ context.Context, rec ClaimRecord, write func(registry.ClaimOwnership) error) error {
96 calls++
97 if rec.Host != host || rec.Key == "" || rec.Family == "" || rec.Value == nil {
98 t.Fatal("selection identity missing")
99 }
100 return write(owner)
101 }
102 for i := 0; i < 2; i++ {
103 if err := pass(ctx, s, reg, cfg); err != nil {
104 t.Fatal(err)
105 }
106 }
107 if calls != 2 {
108 t.Fatalf("mint and keep calls = %d", calls)
109 }
110 got, err := reg.Get(host)
111 if err != nil || got == nil || got.ClaimOwnership != owner {
112 t.Fatalf("mint lost ownership: %v", err)
113 }
114 owner.ClaimStamp++
115 if err := pass(ctx, s, reg, cfg); err != nil {
116 t.Fatal(err)
117 }
118 current, err := reg.Get(host)
119 if err != nil || *current != *got {
120 t.Fatalf("stale keep changed successor: %v", err)
121 }
122 })
123 }
124}
125
126// claimEffects stands in for the production ClaimInstall hook's durable
127// effects: activating the host guard and writing the inventory. Each call
128// leaves a sentinel file for both, so a pass that reached the hook cannot
129// look untouched, whatever the hook went on to decide.
130type claimEffects struct {
131 dir string
132 owner registry.ClaimOwnership
133 calls int
134}
135
136func newClaimEffects(t *testing.T) *claimEffects {
137 t.Helper()
138 return &claimEffects{dir: t.TempDir(), owner: registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}}
139}
140
141func (e *claimEffects) hook(t *testing.T) func(context.Context, ClaimRecord, func(registry.ClaimOwnership) error) error {
142 return func(_ context.Context, rec ClaimRecord, write func(registry.ClaimOwnership) error) error {
143 e.calls++
144 for _, name := range []string{"guard.active", "inventory." + rec.Host} {
145 if err := os.WriteFile(filepath.Join(e.dir, name), []byte(rec.Key), 0o600); err != nil {
146 t.Fatal(err)
147 }
148 }
149 return write(e.owner)
150 }
151}
152
153// untouched fails unless the hook never ran: no call, no guard, no inventory.
154func (e *claimEffects) untouched(t *testing.T, why string) {
155 t.Helper()
156 left, err := os.ReadDir(e.dir)
157 if err != nil {
158 t.Fatal(err)
159 }
160 if e.calls != 0 || len(left) != 0 {
161 t.Fatalf("%s: claim hook ran before refusal: calls=%d sentinels=%d", why, e.calls, len(left))
162 }
163}
164
165// reached fails unless the hook ran exactly n times and left its sentinels,
166// proving the harness can observe the effect untouched() denies.
167func (e *claimEffects) reached(t *testing.T, n int, why string) {
168 t.Helper()
169 left, err := os.ReadDir(e.dir)
170 if err != nil {
171 t.Fatal(err)
172 }
173 if e.calls != n || len(left) == 0 {
174 t.Fatalf("%s: claim hook calls=%d (want %d) sentinels=%d", why, e.calls, n, len(left))
175 }
176}
177
178func instanceClaimFixture(t *testing.T) (release.Store, *registry.Registry, string, ReconcileConfig) {
179 t.Helper()
180 store, sqid := setup(t)
181 record := acmeRecord(sqid)
182 if err := CreateRecord(ctx, store, record); err != nil {
183 t.Fatal(err)
184 }
185 reg := regWith(t)
186 t.Cleanup(func() { reg.Close() })
187 return store, reg, record.Host, ReconcileConfig{ClaimEnabled: true, DataDir: t.TempDir(), Region: "ie", Pools: []string{"tito"}}
188}
189
190func sidecarClaimFixture(t *testing.T) (*sidecarFixture, string, ReconcileConfig) {
191 t.Helper()
192 f := sidecarSetup(t)
193 t.Cleanup(func() { f.reg.Close() })
194 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"run"}, Channel: "stable"})
195 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
196 return f, f.host("bot", "eleven"), ReconcileConfig{ClaimEnabled: true, DataDir: f.dir}
197}
198
199func TestClaimRefusalsPrecedeInstallHookInstanceMint(t *testing.T) {
200 refuse := errors.New("no sealed config")
201 cases := []struct {
202 name string
203 apply func(*ReconcileConfig, *int)
204 gated bool
205 }{
206 {"inbound none", func(cfg *ReconcileConfig, _ *int) { cfg.Inbound = InboundNone }, false},
207 {"config gate", func(cfg *ReconcileConfig, gates *int) {
208 cfg.ConfigGate = func(context.Context, registry.Route) error { *gates++; return refuse }
209 }, true},
210 {"layout unreadable", func(cfg *ReconcileConfig, _ *int) {
211 cfg.Layout = func(context.Context, release.Store) (string, error) { return "", refuse }
212 }, false},
213 }
214 for _, tc := range cases {
215 t.Run(tc.name, func(t *testing.T) {
216 s, reg, host, cfg := instanceClaimFixture(t)
217 fx := newClaimEffects(t)
218 cfg.ClaimInstall = fx.hook(t)
219 gates := 0
220 tc.apply(&cfg, &gates)
221 if err := Reconcile(ctx, s, reg, cfg); err != nil {
222 t.Fatal(err)
223 }
224 fx.untouched(t, tc.name)
225 if got, err := reg.Get(host); err != nil || got != nil {
226 t.Fatalf("refused mint wrote a route: %v", err)
227 }
228 if tc.gated && gates != 1 {
229 t.Fatalf("config gate asked %d times, want once", gates)
230 }
231 })
232 }
233 t.Run("accepted mint reaches the hook once", func(t *testing.T) {
234 s, reg, host, cfg := instanceClaimFixture(t)
235 fx := newClaimEffects(t)
236 cfg.ClaimInstall = fx.hook(t)
237 cfg.ConfigGate = func(context.Context, registry.Route) error { return nil }
238 if err := Reconcile(ctx, s, reg, cfg); err != nil {
239 t.Fatal(err)
240 }
241 fx.reached(t, 1, "accepted")
242 if got, err := reg.Get(host); err != nil || got == nil || got.ClaimOwnership != fx.owner {
243 t.Fatalf("accepted mint lost ownership: %v", err)
244 }
245 })
246}
247
248// Both refusals are mint-time rules. A kept row converges as before, and its
249// claim hook still runs, whatever the box's inbound posture or config gate.
250func TestClaimKeepStillReachesInstallHookInstance(t *testing.T) {
251 s, reg, host, cfg := instanceClaimFixture(t)
252 fx := newClaimEffects(t)
253 cfg.ClaimInstall = fx.hook(t)
254 if err := Reconcile(ctx, s, reg, cfg); err != nil {
255 t.Fatal(err)
256 }
257 fx.reached(t, 1, "mint")
258 minted, err := reg.Get(host)
259 if err != nil || minted == nil {
260 t.Fatalf("mint: %v", err)
261 }
262 gates := 0
263 cfg.Inbound = InboundNone
264 cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return errors.New("refuse") }
265 if err := Reconcile(ctx, s, reg, cfg); err != nil {
266 t.Fatal(err)
267 }
268 fx.reached(t, 2, "keep")
269 if gates != 0 {
270 t.Fatalf("config gate asked %d times for a kept row", gates)
271 }
272 if got, err := reg.Get(host); err != nil || got == nil || *got != *minted {
273 t.Fatalf("keep changed the row: %v", err)
274 }
275}
276
277func TestClaimRefusalsPrecedeInstallHookSidecarMint(t *testing.T) {
278 refuse := errors.New("no sealed config")
279 cases := []struct {
280 name string
281 apply func(*testing.T, string, *ReconcileConfig, *int)
282 }{
283 {"co-spawn staged", func(t *testing.T, host string, cfg *ReconcileConfig, _ *int) {
284 if err := os.WriteFile(filepath.Join(cfg.DataDir, host+sidecarCoSpawnSuffix), []byte("A=1\n"), 0o600); err != nil {
285 t.Fatal(err)
286 }
287 }},
288 {"config gate", func(_ *testing.T, _ string, cfg *ReconcileConfig, gates *int) {
289 cfg.ConfigGate = func(context.Context, registry.Route) error { *gates++; return refuse }
290 }},
291 }
292 for _, tc := range cases {
293 t.Run(tc.name, func(t *testing.T) {
294 f, host, cfg := sidecarClaimFixture(t)
295 fx := newClaimEffects(t)
296 cfg.ClaimInstall = fx.hook(t)
297 gates := 0
298 tc.apply(t, host, &cfg, &gates)
299 f.reconcile(t, cfg)
300 fx.untouched(t, tc.name)
301 if got, err := f.reg.Get(host); err != nil || got != nil {
302 t.Fatalf("refused mint wrote a route: %v", err)
303 }
304 if tc.name == "config gate" && gates != 1 {
305 t.Fatalf("config gate asked %d times, want once", gates)
306 }
307 })
308 }
309 t.Run("accepted mint reaches the hook once", func(t *testing.T) {
310 f, host, cfg := sidecarClaimFixture(t)
311 fx := newClaimEffects(t)
312 cfg.ClaimInstall = fx.hook(t)
313 cfg.ConfigGate = func(context.Context, registry.Route) error { return nil }
314 f.reconcile(t, cfg)
315 fx.reached(t, 1, "accepted")
316 if got, err := f.reg.Get(host); err != nil || got == nil || got.ClaimOwnership != fx.owner {
317 t.Fatalf("accepted mint lost ownership: %v", err)
318 }
319 })
320}
321
322// The co-spawn rule is mint-time only: a file staged after the row exists
323// must not freeze the row's convergence, nor stop its claim hook.
324func TestClaimKeepStillReachesInstallHookSidecar(t *testing.T) {
325 f, host, cfg := sidecarClaimFixture(t)
326 fx := newClaimEffects(t)
327 cfg.ClaimInstall = fx.hook(t)
328 f.reconcile(t, cfg)
329 fx.reached(t, 1, "mint")
330 minted, err := f.reg.Get(host)
331 if err != nil || minted == nil {
332 t.Fatalf("mint: %v", err)
333 }
334 gates := 0
335 cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return errors.New("refuse") }
336 if err := os.WriteFile(filepath.Join(cfg.DataDir, host+sidecarCoSpawnSuffix), []byte("A=1\n"), 0o600); err != nil {
337 t.Fatal(err)
338 }
339 f.reconcile(t, cfg)
340 fx.reached(t, 2, "keep")
341 if gates != 0 {
342 t.Fatalf("config gate asked %d times for a kept row", gates)
343 }
344 if got, err := f.reg.Get(host); err != nil || got == nil || *got != *minted {
345 t.Fatalf("keep changed the row: %v", err)
346 }
347}
348
349// With the flag off the claim hook is never consulted, and the same refusals
350// still decide the mint exactly as before.
351func TestClaimFlagOffRefusalsUnchanged(t *testing.T) {
352 refuse := func(context.Context, registry.Route) error { return errors.New("refuse") }
353 hookRan := false
354 hook := func(context.Context, ClaimRecord, func(registry.ClaimOwnership) error) error {
355 hookRan = true
356 return ErrClaimAdmission
357 }
358 t.Run("instance", func(t *testing.T) {
359 for _, tc := range []struct {
360 name string
361 apply func(*ReconcileConfig)
362 mint bool
363 }{
364 {"inbound none", func(c *ReconcileConfig) { c.Inbound = InboundNone }, false},
365 {"config gate", func(c *ReconcileConfig) { c.ConfigGate = refuse }, false},
366 {"accepted", func(c *ReconcileConfig) {}, true},
367 } {
368 s, reg, host, cfg := instanceClaimFixture(t)
369 cfg.ClaimEnabled, cfg.ClaimInstall = false, hook
370 tc.apply(&cfg)
371 if err := Reconcile(ctx, s, reg, cfg); err != nil {
372 t.Fatal(err)
373 }
374 got, err := reg.Get(host)
375 if err != nil || (got != nil) != tc.mint {
376 t.Fatalf("%s: minted=%v err=%v, want minted=%v", tc.name, got != nil, err, tc.mint)
377 }
378 if got != nil && got.ClaimOwnership != (registry.ClaimOwnership{}) {
379 t.Fatalf("%s: flag-off mint carries ownership", tc.name)
380 }
381 }
382 })
383 t.Run("sidecar", func(t *testing.T) {
384 for _, tc := range []struct {
385 name string
386 apply func(*testing.T, string, *ReconcileConfig)
387 mint bool
388 }{
389 {"co-spawn", func(t *testing.T, host string, c *ReconcileConfig) {
390 if err := os.WriteFile(filepath.Join(c.DataDir, host+sidecarCoSpawnSuffix), nil, 0o600); err != nil {
391 t.Fatal(err)
392 }
393 }, false},
394 {"config gate", func(_ *testing.T, _ string, c *ReconcileConfig) { c.ConfigGate = refuse }, false},
395 {"accepted", func(*testing.T, string, *ReconcileConfig) {}, true},
396 } {
397 f, host, cfg := sidecarClaimFixture(t)
398 cfg.ClaimEnabled, cfg.ClaimInstall = false, hook
399 tc.apply(t, host, &cfg)
400 f.reconcile(t, cfg)
401 got, err := f.reg.Get(host)
402 if err != nil || (got != nil) != tc.mint {
403 t.Fatalf("%s: minted=%v err=%v, want minted=%v", tc.name, got != nil, err, tc.mint)
404 }
405 }
406 })
407 if hookRan {
408 t.Fatal("flag-off pass consulted the claim hook")
409 }
410}
411
412func TestClaimInstallWrapperRefusesUnownedExistingBeforeHook(t *testing.T) {
413 owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}
414 calls, writes := 0, 0
415 hook := func(_ context.Context, _ ClaimRecord, write func(registry.ClaimOwnership) error) error {
416 calls++
417 return write(owner)
418 }
419 write := func(registry.ClaimOwnership) error { writes++; return nil }
420 cfg := ReconcileConfig{ClaimEnabled: true, ClaimInstall: hook}
421
422 if err := cfg.claimInstall(ctx, ClaimRecord{}, &registry.Route{}, write); !errors.Is(err, registry.ErrClaimOwnership) {
423 t.Fatalf("unowned existing row: err = %v, want ErrClaimOwnership", err)
424 }
425 if calls != 0 || writes != 0 {
426 t.Fatalf("unowned existing row reached the hook: calls=%d writes=%d", calls, writes)
427 }
428 // Valid owned rows still keep: the hook runs and its write goes through.
429 if err := cfg.claimInstall(ctx, ClaimRecord{}, &registry.Route{ClaimOwnership: owner}, write); err != nil {
430 t.Fatal(err)
431 }
432 if calls != 1 || writes != 1 {
433 t.Fatalf("owned existing row: calls=%d writes=%d, want 1 and 1", calls, writes)
434 }
435 // A different valid owner is still refused, by the write, after the hook.
436 other := owner
437 other.ClaimStamp++
438 if err := cfg.claimInstall(ctx, ClaimRecord{}, &registry.Route{ClaimOwnership: other}, write); !errors.Is(err, registry.ErrClaimOwnership) {
439 t.Fatalf("foreign owner: err = %v, want ErrClaimOwnership", err)
440 }
441 if calls != 2 || writes != 1 {
442 t.Fatalf("foreign owner: calls=%d writes=%d, want 2 and 1", calls, writes)
443 }
444 // A mint has no existing row and is unaffected.
445 if err := cfg.claimInstall(ctx, ClaimRecord{}, nil, write); err != nil {
446 t.Fatal(err)
447 }
448 if calls != 3 || writes != 2 {
449 t.Fatalf("mint: calls=%d writes=%d, want 3 and 2", calls, writes)
450 }
451 // Flag off: the hook is not consulted and an unowned row is written as before.
452 off := ReconcileConfig{ClaimInstall: hook}
453 if err := off.claimInstall(ctx, ClaimRecord{}, &registry.Route{}, write); err != nil {
454 t.Fatal(err)
455 }
456 if calls != 3 || writes != 3 {
457 t.Fatalf("flag off: calls=%d writes=%d, want 3 and 3", calls, writes)
458 }
459}
460
461// Existing rows this pass will not keep are refused before the claim hook, so
462// the guard activation and inventory write it stands for never happen. Each
463// row's refusal is isolated: the hand-added rows carry a VALID owner, so the
464// wrapper's ownership check cannot be what stops them.
465func TestClaimKnownExistingRowRefusalsPrecedeInstallHookInstance(t *testing.T) {
466 cases := []struct {
467 name string
468 setup func(*testing.T, release.Store, *registry.Registry, string, *claimEffects, ReconcileConfig)
469 }{
470 {"shape mismatch", func(t *testing.T, _ release.Store, reg *registry.Registry, host string, fx *claimEffects, _ ReconcileConfig) {
471 if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", App: "other", ClaimOwnership: fx.owner}); err != nil {
472 t.Fatal(err)
473 }
474 }},
475 {"legacy owned row without claim owner", func(t *testing.T, s release.Store, reg *registry.Registry, host string, _ *claimEffects, cfg ReconcileConfig) {
476 cfg.ClaimEnabled = false
477 if err := Reconcile(ctx, s, reg, cfg); err != nil {
478 t.Fatal(err)
479 }
480 if got, err := reg.Get(host); err != nil || got == nil || !got.Provisioned {
481 t.Fatalf("legacy mint: %v", err)
482 }
483 }},
484 {"legacy adoptable row without claim owner", func(t *testing.T, s release.Store, reg *registry.Registry, host string, _ *claimEffects, cfg ReconcileConfig) {
485 cfg.ClaimEnabled = false
486 if err := Reconcile(ctx, s, reg, cfg); err != nil {
487 t.Fatal(err)
488 }
489 if err := reg.SetProvisioned(host, false); err != nil {
490 t.Fatal(err)
491 }
492 }},
493 }
494 for _, tc := range cases {
495 t.Run(tc.name, func(t *testing.T) {
496 s, reg, host, cfg := instanceClaimFixture(t)
497 fx := newClaimEffects(t)
498 tc.setup(t, s, reg, host, fx, cfg)
499 before, err := reg.Get(host)
500 if err != nil || before == nil {
501 t.Fatalf("setup left no row: %v", err)
502 }
503 gates := 0
504 cfg.ClaimInstall = fx.hook(t)
505 cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return nil }
506 if err := Reconcile(ctx, s, reg, cfg); err != nil {
507 t.Fatal(err)
508 }
509 fx.untouched(t, tc.name)
510 if gates != 0 {
511 t.Fatalf("config gate asked %d times for an existing row", gates)
512 }
513 if got, err := reg.Get(host); err != nil || got == nil || *got != *before {
514 t.Fatalf("refused existing row changed: %v", err)
515 }
516 })
517 }
518}
519
520func TestClaimKnownExistingRowRefusalsPrecedeInstallHookSidecar(t *testing.T) {
521 cases := []struct {
522 name string
523 setup func(*testing.T, *sidecarFixture, string, *claimEffects, ReconcileConfig)
524 }{
525 {"hand-added row", func(t *testing.T, f *sidecarFixture, host string, fx *claimEffects, _ ReconcileConfig) {
526 f.mustUpsert(t, registry.Route{Host: host, Kind: SidecarKind, Slug: "bot", App: "eleven", Account: f.sqid, ClaimOwnership: fx.owner})
527 }},
528 {"legacy owned row without claim owner", func(t *testing.T, f *sidecarFixture, host string, _ *claimEffects, cfg ReconcileConfig) {
529 cfg.ClaimEnabled = false
530 f.reconcile(t, cfg)
531 if got, err := f.reg.Get(host); err != nil || got == nil || !got.Provisioned {
532 t.Fatalf("legacy mint: %v", err)
533 }
534 }},
535 }
536 for _, tc := range cases {
537 t.Run(tc.name, func(t *testing.T) {
538 f, host, cfg := sidecarClaimFixture(t)
539 fx := newClaimEffects(t)
540 tc.setup(t, f, host, fx, cfg)
541 before, err := f.reg.Get(host)
542 if err != nil || before == nil {
543 t.Fatalf("setup left no row: %v", err)
544 }
545 gates := 0
546 cfg.ClaimInstall = fx.hook(t)
547 cfg.ConfigGate = func(context.Context, registry.Route) error { gates++; return nil }
548 f.reconcile(t, cfg)
549 fx.untouched(t, tc.name)
550 if gates != 0 {
551 t.Fatalf("config gate asked %d times for an existing row", gates)
552 }
553 if got, err := f.reg.Get(host); err != nil || got == nil || *got != *before {
554 t.Fatalf("refused existing row changed: %v", err)
555 }
556 })
557 }
558}
559
560// Flag off, the wrapper never consults the hook: a shape mismatch is still
561// left alone by the write, and an adoptable unowned row is still adopted.
562func TestClaimFlagOffExistingRowsUnchanged(t *testing.T) {
563 hookRan := false
564 hook := func(context.Context, ClaimRecord, func(registry.ClaimOwnership) error) error {
565 hookRan = true
566 return ErrClaimAdmission
567 }
568 t.Run("shape mismatch left alone", func(t *testing.T) {
569 s, reg, host, cfg := instanceClaimFixture(t)
570 cfg.ClaimEnabled, cfg.ClaimInstall = false, hook
571 if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", App: "other"}); err != nil {
572 t.Fatal(err)
573 }
574 before := mustGet(t, reg, host)
575 if err := Reconcile(ctx, s, reg, cfg); err != nil {
576 t.Fatal(err)
577 }
578 if got := mustGet(t, reg, host); *got != *before {
579 t.Fatal("flag-off pass changed a mismatched row")
580 }
581 })
582 t.Run("adoptable unowned row adopted", func(t *testing.T) {
583 s, reg, host, cfg := instanceClaimFixture(t)
584 cfg.ClaimEnabled, cfg.ClaimInstall = false, hook
585 if err := Reconcile(ctx, s, reg, cfg); err != nil {
586 t.Fatal(err)
587 }
588 if err := reg.SetProvisioned(host, false); err != nil {
589 t.Fatal(err)
590 }
591 if err := Reconcile(ctx, s, reg, cfg); err != nil {
592 t.Fatal(err)
593 }
594 if got := mustGet(t, reg, host); !got.Provisioned || got.ClaimOwnership != (registry.ClaimOwnership{}) {
595 t.Fatalf("flag-off adoption: provisioned=%v owner=%+v", got.Provisioned, got.ClaimOwnership)
596 }
597 })
598 t.Run("sidecar hand-added row left alone", func(t *testing.T) {
599 f, host, cfg := sidecarClaimFixture(t)
600 cfg.ClaimEnabled, cfg.ClaimInstall = false, hook
601 f.mustUpsert(t, registry.Route{Host: host, Kind: SidecarKind, Slug: "bot", App: "eleven", Account: f.sqid})
602 before := mustGet(t, f.reg, host)
603 f.reconcile(t, cfg)
604 if got := mustGet(t, f.reg, host); *got != *before {
605 t.Fatal("flag-off pass changed a hand-added sidecar row")
606 }
607 })
608 if hookRan {
609 t.Fatal("flag-off pass consulted the claim hook")
610 }
611}
612