carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/hostclaim"
11)
12
13// wakeResetFixture builds a serving claim whose holder has an attempting
14// wake-retry episode, as a reset finds it after StopAttempt.
15func wakeResetFixture(t *testing.T) (*ControlWriter, hostclaim.Observed, func() time.Time) {
16 t.Helper()
17 w, raw, clock := testWriter(t)
18 ctx := t.Context()
19 seedAdmission(t, raw, "bab", "demo", openAdmission("app"))
20 host := "x.example"
21 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()}
22 record := []byte(`{"schema":1,"host":"x.example","account":"bab","app":"demo","region":"ie","created_at":"2026-10-07T12:00:00Z"}`)
23 o, err := hostclaim.Read(ctx, raw, host)
24 if err != nil {
25 t.Fatal(err)
26 }
27 for _, tr := range []func(*hostclaim.Claim) error{
28 hostclaim.BeginCreate(nh, record, "create", "console", clock()),
29 hostclaim.CompleteCreate("create", "box"),
30 } {
31 if o, err = hostclaim.Update(ctx, raw, o, tr); err != nil {
32 t.Fatal(err)
33 }
34 }
35 attempt := hostclaim.WakeRetryAttempt{ID: "a1", Box: "box", Enrollment: "enr", BootID: "boot", ControllerID: "ctl", InvocationID: "inv", LaunchInputs: "in"}
36 if o, err = hostclaim.Update(ctx, raw, o, hostclaim.ReserveWakeRetryAttempt(hostclaim.WakeRetryReservation{HolderIncarnation: o.Claim.Holder.Incarnation, EpisodeID: "ep1", Attempt: attempt, Now: clock()})); err != nil {
37 t.Fatal(err)
38 }
39 return w, o, clock
40}
41
42// claimStep returns the exact before and after bytes of one transition on
43// o, as hostclaim.Update would write them.
44func claimStep(t *testing.T, o hostclaim.Observed, tr func(*hostclaim.Claim) error) MutationTarget {
45 t.Helper()
46 before, err := json.Marshal(o.Claim)
47 if err != nil {
48 t.Fatal(err)
49 }
50 var next hostclaim.Claim
51 if err := json.Unmarshal(before, &next); err != nil {
52 t.Fatal(err)
53 }
54 if err := tr(&next); err != nil {
55 t.Fatal(err)
56 }
57 next.Gen++
58 after, err := json.Marshal(&next)
59 if err != nil {
60 t.Fatal(err)
61 }
62 return MutationTarget{Method: "Put", Key: "control/hosts/" + o.Host + ".json", Before: before, After: after}
63}
64
65func registerFor(t *testing.T, w *ControlWriter, o hostclaim.Observed, id, kind, incarnation string, clock func() time.Time) context.Context {
66 t.Helper()
67 h, err := w.RegisterStart(t.Context(), "bab", "demo", "app", id, Registration{Kind: kind, Host: o.Host, Owner: testOwner, AppID: "app", Epoch: 1, Claim: SavedObservation(o), Incarnation: incarnation, CreatedAt: clock()}, clock())
68 if err != nil {
69 t.Fatal(err)
70 }
71 return h.Lifecycle(t.Context())
72}
73
74func resetStep(incarnation, observed, episode, request string) func(*hostclaim.Claim) error {
75 return hostclaim.ResetWakeRetry(hostclaim.WakeRetryReset{HolderIncarnation: incarnation, ObservedEpisode: observed, NewEpisodeID: episode, OperatorRequest: request, AttemptSettled: true})
76}
77
78// R2: a reset-wake-retry registration's lifecycle grant admits only the
79// exact ResetWakeRetry transition for its registered holder incarnation.
80// Any other same-app claim write under it is refused.
81func TestPlan9C2ResetGrantAdmitsOnlyResetWakeRetry(t *testing.T) {
82 w, o, clock := wakeResetFixture(t)
83 inc := o.Claim.Holder.Incarnation
84 ctx := registerFor(t, w, o, "reset", RegistrationWakeRetryReset, inc, clock)
85 cases := []struct {
86 name string
87 tr func(*hostclaim.Claim) error
88 admit bool
89 }{
90 {"exact reset", resetStep(inc, "ep1", "ep2", "req"), true},
91 {"generation only", func(*hostclaim.Claim) error { return nil }, false},
92 {"stamp bump", func(c *hostclaim.Claim) error { c.Holder.Stamp++; return nil }, false},
93 {"reset plus stamp", func(c *hostclaim.Claim) error {
94 if err := resetStep(inc, "ep1", "ep2", "req")(c); err != nil {
95 return err
96 }
97 c.Holder.Stamp++
98 return nil
99 }, false},
100 {"reset plus retired incarnation", func(c *hostclaim.Claim) error {
101 if err := resetStep(inc, "ep1", "ep2", "req")(c); err != nil {
102 return err
103 }
104 c.RetiredIncarnations = append(c.RetiredIncarnations, "old")
105 return nil
106 }, false},
107 {"drop wake retry", func(c *hostclaim.Claim) error { c.Holder.WakeRetry = nil; return nil }, false},
108 }
109 for _, tc := range cases {
110 t.Run(tc.name, func(t *testing.T) {
111 err := authorizeClaimWrite(ctx, w.raw, o.Host, claimStep(t, o, tc.tr))
112 if tc.admit && err != nil {
113 t.Fatalf("exact reset refused: %v", err)
114 }
115 if !tc.admit && !errors.Is(err, ErrPending) {
116 t.Fatalf("reset grant admitted %s: %v", tc.name, err)
117 }
118 })
119 }
120}
121
122// R2: the reset grant is bound to the incarnation the registration saved,
123// not to whichever holder the claim names at write time.
124func TestPlan9C2ResetGrantRefusesOtherIncarnation(t *testing.T) {
125 w, o, clock := wakeResetFixture(t)
126 ctx := registerFor(t, w, o, "reset", RegistrationWakeRetryReset, "some-other-incarnation", clock)
127 err := authorizeClaimWrite(ctx, w.raw, o.Host, claimStep(t, o, resetStep(o.Claim.Holder.Incarnation, "ep1", "ep2", "req")))
128 if !errors.Is(err, ErrPending) {
129 t.Fatalf("reset grant admitted a reset for an unregistered incarnation: %v", err)
130 }
131}
132
133// R2 keeps every other registration kind's grant as main has it.
134func TestPlan9C2OtherKindsKeepSameAppGrant(t *testing.T) {
135 w, o, clock := wakeResetFixture(t)
136 ctx := registerFor(t, w, o, "create2", "create", o.Claim.Holder.Incarnation, clock)
137 if err := authorizeClaimWrite(ctx, w.raw, o.Host, claimStep(t, o, func(*hostclaim.Claim) error { return nil })); err != nil {
138 t.Fatalf("create grant lost its same-app write: %v", err)
139 }
140}
141
142// 5-M10: a host-census report needs no lifecycle grant, but only as a
143// create-only Put. Authenticity is the reader's: ProductionAuthority.census
144// verifies each report against the roster's per-box census key (see the
145// forged-report case in TestReconcileFrozenBackfillRepairsLegacyHolder).
146func TestPlan9C2CensusReportsAreCreateOnlyWithoutGrant(t *testing.T) {
147 w, raw, _ := testWriter(t)
148 key := "control/host-census/res/box.json"
149 for _, tc := range []struct {
150 name string
151 t MutationTarget
152 ok bool
153 }{
154 {"create", MutationTarget{Method: "Put", Key: key, After: []byte(`{}`)}, true},
155 {"overwrite", MutationTarget{Method: "Put", Key: key, ETag: "tag", After: []byte(`{}`)}, false},
156 {"delete", MutationTarget{Method: "Delete", Key: key, ETag: "tag"}, false},
157 } {
158 family, matched, err := lifecycleFamily(t.Context(), raw, w, tc.t)
159 if !matched || (tc.ok && (err != nil || family != "lifecycle-census")) || (!tc.ok && !errors.Is(err, ErrIdentity)) {
160 t.Fatalf("%s: %q %v %v", tc.name, family, matched, err)
161 }
162 }
163}
164