carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13func newHolder() NewHolder {
14 return NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + testHost + ".json",
15 Account: "bab", App: "jam", Host: testHost, Region: "ie", RecordRegion: "ie",
16 CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)}
17}
18
19var record = []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
20
21func TestCreateLifecycle(t *testing.T) {
22 ctx := context.Background()
23 s := release.NewDirStore(t.TempDir())
24 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
25 reg, _ := Read(ctx, s, testHost)
26 o, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now))
27 if err != nil || o.Claim.Op.Kind != KindCreating || o.Claim.Op.Intent["record"] != string(record) {
28 t.Fatalf("begin = %+v, %v", o, err)
29 }
30 if o.Claim.Holder.Location != "instances/"+testHost+"/"+o.Claim.Holder.Incarnation {
31 t.Fatalf("location = %q", o.Claim.Holder.Location)
32 }
33 o, err = Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting"))
34 if err != nil || o.Claim.Op.Kind != KindServing || o.Claim.Op.Exec != "adi-knitting" || o.Claim.Holder.Stamp != o.Claim.Gen {
35 t.Fatalf("complete = %+v, %v", o.Claim, err)
36 }
37 if len(o.Claim.Touched.Boxes) != 1 || o.Claim.Touched.Boxes[0] != "adi-knitting" {
38 t.Fatalf("executor not recorded as touched: %+v", o.Claim.Touched)
39 }
40 if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); !errors.Is(err, ErrStaleOp) {
41 t.Fatalf("replay = %v, want ErrStaleOp", err)
42 }
43 if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c2", "console", now)); !errors.Is(err, ErrConflict) {
44 t.Fatalf("stale create = %v, want ErrConflict", err)
45 }
46}
47
48func TestBeginCreateRefusesAnotherHostOrKey(t *testing.T) {
49 ctx := context.Background()
50 s := release.NewDirStore(t.TempDir())
51 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
52 reg, _ := Read(ctx, s, testHost)
53 other := newHolder()
54 other.Host = "other.bab.oncarlos.com"
55 if _, err := Update(ctx, s, reg, BeginCreate(other, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
56 t.Fatalf("another host's create on this claim = %v", err)
57 }
58 badKey := newHolder()
59 badKey.Key = "apps/zzz/jam/instances/" + testHost + ".json"
60 if _, err := Update(ctx, s, reg, BeginCreate(badKey, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
61 t.Fatalf("mismatched record key = %v", err)
62 }
63 other2 := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T09:00:00Z"}`)
64 if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), other2, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
65 t.Fatalf("record body with another CreatedAt = %v", err)
66 }
67 if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), []byte("not json"), "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
68 t.Fatalf("record body not JSON = %v", err)
69 }
70 wrongRegion := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"uk","created_at":"2026-10-04T01:00:00Z"}`)
71 if _, err := Update(ctx, s, reg, BeginCreate(newHolder(), wrongRegion, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
72 t.Fatalf("record body in another region = %v", err)
73 }
74 // A sidecar key cannot claim an instance-shaped host (P1, plan review round 2).
75 sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/shop.json", Account: "bab", App: "jam", Name: "shop",
76 Host: testHost, Region: "ie", RecordRegion: "ie", CreatedAt: newHolder().CreatedAt}
77 if _, err := Update(ctx, s, reg, BeginCreate(sc, record, "op_c1", "console", now)); !errors.Is(err, ErrWrongState) {
78 t.Fatalf("sidecar key on a non-sidecar host = %v", err)
79 }
80}
81
82func TestBeginCreateSidecar(t *testing.T) {
83 ctx := context.Background()
84 s := release.NewDirStore(t.TempDir())
85 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
86 host := "bot.jam.bab.sidecar.internal"
87 sc := NewHolder{Family: "sidecar", Key: "apps/bab/jam/sidecars/bot.json", Account: "bab", App: "jam", Name: "bot",
88 Host: host, Region: "ie", RecordRegion: "ie", CreatedAt: time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)}
89 body := []byte(`{"name":"bot","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
90 reg, _ := Read(ctx, s, host)
91 if _, err := Update(ctx, s, reg, BeginCreate(sc, body, "op_s1", "console", now)); err != nil {
92 t.Fatalf("sidecar create = %v", err)
93 }
94 s2 := release.NewDirStore(t.TempDir())
95 reg2, _ := Read(ctx, s2, host)
96 wrongName := []byte(`{"name":"other","host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
97 if _, err := Update(ctx, s2, reg2, BeginCreate(sc, wrongName, "op_s1", "console", now)); !errors.Is(err, ErrWrongState) {
98 t.Fatalf("sidecar body with another name = %v", err)
99 }
100}
101
102func TestCancelRetiresAndRefusesReuse(t *testing.T) {
103 ctx := context.Background()
104 s := release.NewDirStore(t.TempDir())
105 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
106 reg, _ := Read(ctx, s, testHost)
107 o, _ := Update(ctx, s, reg, BeginCreate(newHolder(), record, "op_c1", "console", now))
108 inc := o.Claim.Holder.Incarnation
109 o, err := Update(ctx, s, o, CancelCreate("op_c1", now))
110 if err != nil || !o.Claim.Free() || len(o.Claim.CancelledCreates) != 1 || o.Claim.RetiredIncarnations[0] != inc {
111 t.Fatalf("cancel = %+v, %v", o.Claim, err)
112 }
113 if _, err := Update(ctx, s, o, BeginCreate(newHolder(), record, "op_c3", "console", now)); !errors.Is(err, ErrRetiredIncarnation) {
114 t.Fatalf("reuse = %v, want ErrRetiredIncarnation", err)
115 }
116}
117
118func TestCancelCreateRefusesExecutionAuthority(t *testing.T) {
119 for _, tc := range []struct {
120 name string
121 exec string
122 participants []string
123 stamp uint64
124 }{
125 {name: "executor", exec: "adi-knitting"},
126 {name: "participants", participants: []string{"adi-knitting"}},
127 {name: "stamp", stamp: 1},
128 {name: "all", exec: "adi-knitting", participants: []string{"adi-knitting"}, stamp: 1},
129 } {
130 t.Run(tc.name, func(t *testing.T) {
131 ctx := context.Background()
132 s := release.NewDirStore(t.TempDir())
133 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
134 reg, err := Read(ctx, s, testHost)
135 if err != nil {
136 t.Fatal(err)
137 }
138 o, err := Update(ctx, s, reg, func(c *Claim) error {
139 if err := BeginCreate(newHolder(), record, "op_c1", "console", now)(c); err != nil {
140 return err
141 }
142 c.Op.Exec = tc.exec
143 c.Op.Participants = tc.participants
144 c.Holder.Stamp = tc.stamp
145 return nil
146 })
147 if err != nil {
148 t.Fatalf("store creating claim with authority: %v", err)
149 }
150 key, err := Key(testHost)
151 if err != nil {
152 t.Fatal(err)
153 }
154 before, beforeETag, err := s.Get(ctx, key)
155 if err != nil {
156 t.Fatal(err)
157 }
158 if _, err := Update(ctx, s, o, CancelCreate("op_c1", now)); !errors.Is(err, ErrWrongState) {
159 t.Errorf("cancel with authority = %v, want ErrWrongState", err)
160 }
161 after, afterETag, err := s.Get(ctx, key)
162 if err != nil {
163 t.Fatal(err)
164 }
165 if !bytes.Equal(before, after) || beforeETag != afterETag {
166 t.Errorf("cancel changed stored claim: before ETag %q, after ETag %q", beforeETag, afterETag)
167 }
168 })
169 }
170}
171
172func TestCreateHomeRegionParksEffectiveRegion(t *testing.T) {
173 ctx := context.Background()
174 s := release.NewDirStore(t.TempDir())
175 nh := newHolder()
176 nh.RecordRegion = ""
177 body := bytes.Replace(record, []byte(`"region":"ie"`), []byte(`"region":""`), 1)
178 obs, err := Read(ctx, s, testHost)
179 if err != nil {
180 t.Fatal(err)
181 }
182 o, err := Update(ctx, s, obs, BeginCreate(nh, body, "op_c", "console", time.Now()))
183 if err != nil {
184 t.Fatal(err)
185 }
186 if len(o.Claim.Touched.Regions) != 1 || o.Claim.Touched.Regions[0] != "ie" || o.Claim.Op.Intent["record"] != string(body) {
187 t.Fatalf("home-region create = %+v", o.Claim)
188 }
189 o, err = Update(ctx, s, o, CompleteCreate("op_c", "B0"))
190 if err != nil {
191 t.Fatal(err)
192 }
193 o, err = Update(ctx, s, o, BeginDelete("op_d", "console", `"e"`, time.Now(), nil, nil, ""))
194 if err != nil {
195 t.Fatal(err)
196 }
197 o, err = Update(ctx, s, o, ToParking("op_d", nil, nil, counter()))
198 if err != nil {
199 t.Fatal(err)
200 }
201 assertParkingTargets(t, o.Claim.Obligations, []Obligation{
202 {ParkTarget: "instances/" + testHost + "/" + o.Claim.Holder.Incarnation, Op: "op_d", Kind: "park-replica", Region: "ie", Owner: "region:ie"},
203 {Op: "op_d", Kind: "park-local", Box: "B0", Owner: "box:B0"},
204 })
205}
206
207func TestBeginCreateRefusesEmptyEffectiveRegion(t *testing.T) {
208 ctx := context.Background()
209 s := release.NewDirStore(t.TempDir())
210 nh := newHolder()
211 nh.Region, nh.RecordRegion = "", ""
212 body := bytes.Replace(record, []byte(`"region":"ie"`), []byte(`"region":""`), 1)
213 obs, err := Read(ctx, s, testHost)
214 if err != nil {
215 t.Fatal(err)
216 }
217 if _, err := Update(ctx, s, obs, BeginCreate(nh, body, "op_c", "console", time.Now())); !errors.Is(err, ErrWrongState) {
218 t.Errorf("empty effective region = %v, want ErrWrongState", err)
219 }
220 after, err := Read(ctx, s, testHost)
221 if err != nil || !after.Absent {
222 t.Fatalf("read after refused create = %+v, %v; want absent", after, err)
223 }
224}
225
226func TestCompleteCreateRefusesExecutionAuthority(t *testing.T) {
227 for _, tc := range []struct {
228 name string
229 exec string
230 participants []string
231 stamp uint64
232 }{
233 {name: "executor", exec: "adi-knitting"},
234 {name: "participants", participants: []string{"adi-knitting"}},
235 {name: "stamp", stamp: 1},
236 {name: "all", exec: "adi-knitting", participants: []string{"adi-knitting"}, stamp: 1},
237 } {
238 t.Run(tc.name, func(t *testing.T) {
239 ctx := context.Background()
240 s := release.NewDirStore(t.TempDir())
241 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
242 reg, err := Read(ctx, s, testHost)
243 if err != nil {
244 t.Fatal(err)
245 }
246 o, err := Update(ctx, s, reg, func(c *Claim) error {
247 if err := BeginCreate(newHolder(), record, "op_c1", "console", now)(c); err != nil {
248 return err
249 }
250 c.Op.Exec = tc.exec
251 c.Op.Participants = tc.participants
252 c.Holder.Stamp = tc.stamp
253 return nil
254 })
255 if err != nil {
256 t.Fatalf("store creating claim with authority: %v", err)
257 }
258 key, err := Key(testHost)
259 if err != nil {
260 t.Fatal(err)
261 }
262 before, beforeETag, err := s.Get(ctx, key)
263 if err != nil {
264 t.Fatal(err)
265 }
266 if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "new-executor")); !errors.Is(err, ErrWrongState) {
267 t.Errorf("complete with authority = %v, want ErrWrongState", err)
268 }
269 after, afterETag, err := s.Get(ctx, key)
270 if err != nil {
271 t.Fatal(err)
272 }
273 if !bytes.Equal(before, after) || beforeETag != afterETag {
274 t.Errorf("complete changed stored claim: before ETag %q, after ETag %q", beforeETag, afterETag)
275 }
276 })
277 }
278}
279
280func TestCreateCompletesOnlyOnTheSelectedExecutor(t *testing.T) {
281 ctx := context.Background()
282 s := release.NewDirStore(t.TempDir())
283 now := time.Date(2026, 10, 4, 1, 0, 1, 0, time.UTC)
284 reg, _ := Read(ctx, s, testHost)
285 sel := ExecutorSelection{Box: "bri-knitting", Enrollment: "enrollment-b"}
286 if _, err := Update(ctx, s, reg, BeginCreateOn(newHolder(), record, "op_c1", "console", now, ExecutorSelection{})); !errors.Is(err, ErrMalformed) {
287 t.Fatalf("create with no selection: %v", err)
288 }
289 o, err := Update(ctx, s, reg, BeginCreateOn(newHolder(), record, "op_c1", "console", now, sel))
290 if err != nil {
291 t.Fatal(err)
292 }
293 if o.Claim.Op.Intent["selected_executor"] != sel.Box || o.Claim.Op.Intent["executor_enrollment"] != sel.Enrollment || o.Claim.Op.Exec != "" {
294 t.Fatalf("intent: %+v", o.Claim.Op)
295 }
296 if _, err := Update(ctx, s, o, CompleteCreate("op_c1", "adi-knitting")); !errors.Is(err, ErrWrongState) {
297 t.Fatalf("completion substituted a box: %v", err)
298 }
299 if o, err = Update(ctx, s, o, CompleteCreate("op_c1", sel.Box)); err != nil || o.Claim.Op.Exec != sel.Box {
300 t.Fatalf("complete: %v %+v", err, o.Claim.Op)
301 }
302}
303