| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "github.com/carlosframework/platform/internal/lease" |
| 11 | "github.com/carlosframework/platform/internal/lease/leasetest" |
| 12 | "github.com/carlosframework/platform/internal/release" |
| 13 | ) |
| 14 | |
| 15 | func TestPlan9B8VersionedRecordWritesCannotReopenSequence(t *testing.T) { |
| 16 | r := newResolveFixture(t, false) |
| 17 | ctx := context.Background() |
| 18 | if got, err := ResolveMoveStart(ctx, r.h, r.moving, moveStartID); err != nil || got != OutcomeNotApplied { |
| 19 | t.Fatalf("resolve: %q %v", got, err) |
| 20 | } |
| 21 | versions := leasetest.NewVersionedBucket() |
| 22 | versions.AddBucket("control") |
| 23 | old := versions.Put("control", r.raw.recordKey, r.before) |
| 24 | guarded, err := NewVersionedBoundary(true, versions, r.raw, r.w, "control", nil, nil) |
| 25 | if err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | issued, settle, err := r.h.Runner(r.w.now).Issue(ctx, Effect{ID: "versions", Resource: r.raw.recordKey, Action: "test-versions"}) |
| 29 | if err != nil { |
| 30 | t.Fatal(err) |
| 31 | } |
| 32 | defer func() { _ = settle("", ErrNotApplied) }() |
| 33 | if _, err = guarded.PutIfAbsent(issued, "control", r.raw.recordKey, r.before); !errors.Is(err, ErrContract) { |
| 34 | t.Fatalf("PutIfAbsent: %v", err) |
| 35 | } |
| 36 | if _, _, err = guarded.CopyVersion(issued, "control", r.raw.recordKey, old, "control", r.raw.recordKey, false); !errors.Is(err, ErrContract) { |
| 37 | t.Fatalf("CopyVersion: %v", err) |
| 38 | } |
| 39 | if err = guarded.DeleteVersion(issued, "control", r.raw.recordKey, old); !errors.Is(err, ErrContract) { |
| 40 | t.Fatalf("DeleteVersion: %v", err) |
| 41 | } |
| 42 | retained, err := versions.GetVersion(ctx, "control", r.raw.recordKey, old) |
| 43 | if err != nil || string(retained) != string(r.before) { |
| 44 | t.Fatalf("old version changed: %v", err) |
| 45 | } |
| 46 | // Even the private sweep scope cannot delete a live record's version. |
| 47 | sweep := context.WithValue(issued, retiredRecordSweepKey{}, r.raw.recordKey) |
| 48 | if err = guarded.DeleteVersion(sweep, "control", r.raw.recordKey, old); !errors.Is(err, ErrPending) { |
| 49 | t.Fatalf("live sweep: %v", err) |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestPlan9B8PurgeSweepRechecksRetiredRecord(t *testing.T) { |
| 54 | ctx := context.Background() |
| 55 | control := release.NewDirStore(t.TempDir()) |
| 56 | versions := leasetest.NewVersionedBucket() |
| 57 | versions.AddBucket("control") |
| 58 | key := "apps/bab/demo/instances/shop.example.com.json" |
| 59 | old := versions.Put("control", key, []byte(`{"record_seq":1}`)) |
| 60 | journal := PurgeJournal{V: 1, Purge: Purge{ID: "p", Phase: "retired"}, AppID: "old", Account: "bab", App: "demo", Hosts: map[string]PurgeHost{}} |
| 61 | w := seedPurge(t, ctx, control, time.Now(), journal) |
| 62 | if err := PreparePurgeSweep(ctx, control, w, versions, "control", "bab", "demo", "p", "old"); err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | tag, err := control.Put(ctx, key, []byte(`{"record_seq":2}`), "") |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | if err := DeletePurgeSweep(ctx, control, w, versions, "control", "bab", "demo", "p", "old"); !errors.Is(err, ErrPending) { |
| 70 | t.Fatalf("late live record: %v", err) |
| 71 | } |
| 72 | if _, err := versions.GetVersion(ctx, "control", key, old); err != nil { |
| 73 | t.Fatalf("live version removed: %v", err) |
| 74 | } |
| 75 | if err := release.DeleteIfMatch(ctx, control, key, tag); err != nil { |
| 76 | t.Fatal(err) |
| 77 | } |
| 78 | if err := DeletePurgeSweep(ctx, control, w, versions, "control", "bab", "demo", "p", "old"); err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | if _, err := versions.GetVersion(ctx, "control", key, old); !errors.Is(err, lease.ErrMirrorNotFound) { |
| 82 | t.Fatalf("retired version not swept: %v", err) |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | func TestPlan9B8FenceRefusesChangedClaim(t *testing.T) { |
| 87 | r := newResolveFixture(t, false) |
| 88 | ctx := context.Background() |
| 89 | if _, err := hostclaim.Update(r.h.Lifecycle(ctx), r.data, r.moving, hostclaim.ReleaseMove(moveStartID, "ie-box")); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | if got, err := ResolveMoveStart(ctx, r.h, r.moving, moveStartID); got != OutcomeUncertain || !errors.Is(err, hostclaim.ErrConflict) { |
| 93 | t.Fatalf("stale fence: %q %v", got, err) |
| 94 | } |
| 95 | if r.control.fences != 0 || r.effect(t).State != "uncertain" { |
| 96 | t.Fatal("stale observation fenced or settled") |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | type sweepWithoutResurrection struct { |
| 101 | *leasetest.VersionedBucket |
| 102 | t *testing.T |
| 103 | key string |
| 104 | } |
| 105 | |
| 106 | func (v sweepWithoutResurrection) DeleteVersion(ctx context.Context, bucket, key, id string) error { |
| 107 | if err := v.VersionedBucket.DeleteVersion(ctx, bucket, key, id); err != nil { |
| 108 | return err |
| 109 | } |
| 110 | if _, live := v.Current(bucket, v.key); live { |
| 111 | v.t.Fatal("sweep resurrected a record body") |
| 112 | } |
| 113 | return nil |
| 114 | } |
| 115 | func TestPlan9B8PurgeRemovesRecordMarkersLast(t *testing.T) { |
| 116 | ctx := context.Background() |
| 117 | control := release.NewDirStore(t.TempDir()) |
| 118 | versions := leasetest.NewVersionedBucket() |
| 119 | versions.AddBucket("control") |
| 120 | key := "apps/bab/demo/instances/shop.example.com.json" |
| 121 | versions.Put("control", key, []byte(`{"record_seq":1}`)) |
| 122 | versions.BareDelete("control", key) |
| 123 | versions.Put("control", key, []byte(`{"record_seq":2}`)) |
| 124 | marker := versions.BareDelete("control", key) |
| 125 | if err := retiredRecordVersion(ctx, control, versions, "control", key, marker); !errors.Is(err, ErrPending) { |
| 126 | t.Fatalf("marker could expose old body: %v", err) |
| 127 | } |
| 128 | journal := PurgeJournal{V: 1, Purge: Purge{ID: "p", Phase: "retired"}, AppID: "old", Account: "bab", App: "demo", Hosts: map[string]PurgeHost{}} |
| 129 | w := seedPurge(t, ctx, control, time.Now(), journal) |
| 130 | guarded := sweepWithoutResurrection{versions, t, key} |
| 131 | if err := PreparePurgeSweep(ctx, control, w, guarded, "control", "bab", "demo", "p", "old"); err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | if err := DeletePurgeSweep(ctx, control, w, guarded, "control", "bab", "demo", "p", "old"); err != nil { |
| 135 | t.Fatal(err) |
| 136 | } |
| 137 | all, err := versions.ListVersions(ctx, "control", key) |
| 138 | if err != nil || len(all) != 0 { |
| 139 | t.Fatalf("retained versions: %+v %v", all, err) |
| 140 | } |
| 141 | } |
| 142 | |