| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "errors" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/hostclaim" |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | func TestWakeRetryResetFinishRevokesGrantAndCannotFinishOtherKinds(t *testing.T) { |
| 13 | for _, kind := range []string{RegistrationWakeRetryReset, "create"} { |
| 14 | t.Run(kind, func(t *testing.T) { |
| 15 | w, raw, clock := testWriter(t) |
| 16 | seedAdmission(t, raw, "bab", "demo", openAdmission("app")) |
| 17 | h, err := w.RegisterStart(t.Context(), "bab", "demo", "app", "reset", Registration{Kind: kind, Host: "x.example", Owner: testOwner, AppID: "app", Epoch: 1, Claim: SavedClaim{Absent: true}}, clock()) |
| 18 | if err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | err = h.FinishWakeRetryReset(t.Context()) |
| 22 | if kind != RegistrationWakeRetryReset { |
| 23 | if !errors.Is(err, ErrPending) { |
| 24 | t.Fatalf("finished %s with reset handle: %v", kind, err) |
| 25 | } |
| 26 | return |
| 27 | } |
| 28 | if err != nil { |
| 29 | t.Fatal(err) |
| 30 | } |
| 31 | grants := h.Lifecycle(t.Context()).Value(lifecycleGrantKey{}).([]lifecycleGrant) |
| 32 | if live, err := grantLive(t.Context(), raw, grants[0]); err != nil || live { |
| 33 | t.Fatalf("finished grant still live: %v %v", live, err) |
| 34 | } |
| 35 | if err := h.Renew(t.Context(), clock()); !errors.Is(err, ErrPending) { |
| 36 | t.Fatalf("finished registration renewed: %v", err) |
| 37 | } |
| 38 | if err := w.SettleRegistration(t.Context(), "bab", "demo", "app", h.ID(), clock()); err != nil { |
| 39 | t.Fatal(err) |
| 40 | } |
| 41 | }) |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | func TestPurgeDrainsFailedWakeResetWithoutChangingServingClaim(t *testing.T) { |
| 46 | w, raw, clock := testWriter(t) |
| 47 | ctx := t.Context() |
| 48 | seedAdmission(t, raw, "bab", "demo", openAdmission("app")) |
| 49 | host := "x.example" |
| 50 | nh := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/demo/instances/x.example.json", Account: "bab", App: "demo", Host: host, Region: "ie", RecordRegion: "ie", CreatedAt: clock()} |
| 51 | record := []byte(`{"schema":1,"host":"x.example","account":"bab","app":"demo","region":"ie","created_at":"2026-10-07T12:00:00Z"}`) |
| 52 | o, err := hostclaim.Read(ctx, raw, host) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | o, err = hostclaim.Update(ctx, raw, o, hostclaim.BeginCreate(nh, record, "create", "console", clock())) |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | o, err = hostclaim.Update(ctx, raw, o, hostclaim.CompleteCreate("create", "box")) |
| 61 | if err != nil { |
| 62 | t.Fatal(err) |
| 63 | } |
| 64 | h, err := w.RegisterStart(ctx, "bab", "demo", "app", "reset", Registration{Kind: RegistrationWakeRetryReset, Host: host, Owner: testOwner, AppID: "app", Epoch: 1, Claim: SavedObservation(o), Incarnation: o.Claim.Holder.Incarnation, CreatedAt: clock()}, clock()) |
| 65 | if err != nil { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | tombstone := release.AppTombstoneKey("bab", "demo") |
| 69 | tag, err := raw.Put(ctx, tombstone, []byte(`{}`), "") |
| 70 | if err != nil { |
| 71 | t.Fatal(err) |
| 72 | } |
| 73 | j := PurgeJournal{V: 1, Purge: Purge{ID: "purge", Phase: "reserved"}, AppID: "app", Account: "bab", App: "demo", Hosts: map[string]PurgeHost{}, Tombstone: PurgeObject{Key: tombstone, ETag: tag, Body: json.RawMessage(`{}`)}} |
| 74 | if _, err := w.ReservePurge(ctx, j, testOwner, clock()); err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | if _, err := w.BeginPurge(ctx, "bab", "demo", "app", "purge", testOwner); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | if err := w.DrainPurgeRegistrations(ctx, "bab", "demo", "app", "purge", clock(), nil); !errors.Is(err, ErrPending) { |
| 81 | t.Fatalf("live reset drained by purge: %v", err) |
| 82 | } |
| 83 | if err := h.FinishWakeRetryReset(ctx); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | if err := w.DrainPurgeRegistrations(ctx, "bab", "demo", "app", "purge", clock(), nil); err != nil { |
| 87 | t.Fatalf("failed reset blocked purge: %v", err) |
| 88 | } |
| 89 | fresh, err := hostclaim.Read(ctx, raw, host) |
| 90 | if err != nil || fresh.ETag != o.ETag { |
| 91 | t.Fatalf("draining reset mutated serving claim: %v", err) |
| 92 | } |
| 93 | j, _, err = ReadPurgeJournal(ctx, raw, "bab", "demo", "purge", "app") |
| 94 | if err != nil || len(j.Hosts[host].Registrations["reset"]) == 0 || j.Hosts[host].FenceState != "" { |
| 95 | t.Fatalf("registration was not journaled without a claim fence: %+v %v", j.Hosts[host], err) |
| 96 | } |
| 97 | adm, err := ReadAdmission(ctx, raw, "bab", "demo", "app") |
| 98 | if err != nil || len(adm.Value.Registrations) != 0 { |
| 99 | t.Fatalf("drained registration remains: %+v %v", adm.Value.Registrations, err) |
| 100 | } |
| 101 | } |
| 102 | |