carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "encoding/json"
8 "errors"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/hostactors"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/hostfence"
16 "github.com/carlosframework/platform/internal/hostrelocate"
17 "github.com/carlosframework/platform/internal/moves"
18 "github.com/carlosframework/platform/internal/park"
19 "github.com/carlosframework/platform/internal/provision"
20 "github.com/carlosframework/platform/internal/release"
21 "github.com/carlosframework/platform/internal/termfence"
22 "github.com/superfly/ltx"
23)
24
25// landingWorld is a landing claim at probed whose target dst is
26// terminated (its fence in the roster's bucket), the authoritative record
27// of its move (committed or still landing) and a signed roster in which
28// wrk, the converting worker, is as change leaves it.
29type landingWorld struct {
30 store release.Store
31 directory hostactors.Directory
32 obs hostclaim.Observed
33 now time.Time
34 enrollment map[string]string
35}
36
37func newLandingWorld(t *testing.T, committed bool, change func(*hostactors.Roster)) landingWorld {
38 t.Helper()
39 return newLandingWorldHome(t, committed, change, "")
40}
41
42// newLandingWorldHome is newLandingWorld with the landing into the home
43// when home is the fleet's home region (ie): the signed roster designates
44// it, the move is admitted under it (MoveAdmission.HomeRegion, its "to"
45// park.HomeRegion, as BeginClaimMove writes it) and the record's To is the
46// home's placement code "".
47func newLandingWorldHome(t *testing.T, committed bool, change func(*hostactors.Roster), home string) landingWorld {
48 t.Helper()
49 ctx := t.Context()
50 const host, id = "shop.example", "mv_abcdefghijklmnopqrstuvwxyz"
51 w := landingWorld{store: release.NewDirStore(t.TempDir()), now: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC),
52 enrollment: map[string]string{"src": strings.Repeat("b", 32), "dst": strings.Repeat("c", 32), "spa": strings.Repeat("d", 32), "wrk": strings.Repeat("e", 32)}}
53 pub, key, err := ed25519.GenerateKey(rand.Reader)
54 if err != nil {
55 t.Fatal(err)
56 }
57 roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 1,
58 Regions: map[string]hostactors.Region{"ie": {Account: "123", AWSRegion: "eu-west-1"}, "uk": {Account: "123", AWSRegion: "eu-west-2"}},
59 Boxes: map[string]hostactors.Box{}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
60 for box, enr := range w.enrollment {
61 roster.Boxes[box] = hostactors.Box{Enrollment: enr, Namespace: "platform", BoxID: box, Label: box, Region: "ie", Pool: "default",
62 Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-" + box}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"}
63 }
64 roster.HomeRegion = home
65 if change != nil {
66 change(&roster)
67 }
68 raw, err := release.SignRootEnvelope(release.RootSigner(key), roster)
69 if err != nil {
70 t.Fatal(err)
71 }
72 if _, err := w.store.Put(ctx, hostactors.Key, raw, ""); err != nil {
73 t.Fatal(err)
74 }
75 w.directory = hostactors.Directory{Store: w.store, PublicKey: pub, Bucket: "bucket"}
76 w.obs = landingAtProbedHome(t, w.store, host, id, w.now, w.enrollment, home)
77 dst := roster.Boxes["dst"]
78 fence, _ := json.Marshal(termfence.Record{V: 1, Box: "dst", Enrollment: dst.Enrollment, Cloud: dst.Cloud, State: "terminated", ObservedAt: w.now.Add(-time.Minute).Format(time.RFC3339Nano),
79 Via: termfence.ViaDescribe, RequestID: "req", WrittenBy: termfence.WrittenBy{Box: "spa", Enrollment: w.enrollment["spa"]}})
80 if _, err := w.store.Put(ctx, hostfence.FenceKey(dst.Enrollment), fence, ""); err != nil {
81 t.Fatal(err)
82 }
83 created := time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)
84 state, committedAt, flipped := provision.MoveLanding, (*time.Time)(nil), w.now.Add(-90*time.Minute)
85 if committed {
86 at := w.now.Add(-30 * time.Minute)
87 state, committedAt = provision.MoveCommitted, &at
88 }
89 to := "ie"
90 if home != "" {
91 to = "" // the home's placement code
92 }
93 record := provision.Record{Schema: 1, Host: host, Slug: "shop", App: "jam", Account: "bab", Region: to, Channel: "stable", CreatedAt: created,
94 Move: &provision.MoveState{ID: id, Incarnation: w.obs.Claim.Holder.Incarnation, From: "uk", To: to, State: state, CommittedAt: committedAt, FlippedAt: &flipped}}
95 if err := provision.CreateRecord(ctx, w.store, record); err != nil {
96 t.Fatal(err)
97 }
98 return w
99}
100
101// homeWorker is worker with the box's verified home binding.
102func (w landingWorld) homeWorker(enrollment, region, home string) hostrelocate.Worker {
103 x := w.worker(enrollment, region)
104 x.HomeRegion = home
105 return x
106}
107
108// worker is the converting worker: wrk, running as enrollment, in region.
109func (w landingWorld) worker(enrollment, region string) hostrelocate.Worker {
110 now := func() time.Time { return w.now }
111 return hostrelocate.Worker{Claims: w.store, Directory: w.directory, Fences: hostfence.ClaimFenceAuthority{Directory: w.directory, Now: now},
112 Discharge: func(context.Context, hostactors.Box, termfence.Record) *hostclaim.LocalDischarge { return nil },
113 Self: termfence.WrittenBy{Box: "wrk", Enrollment: enrollment}, Region: region, NewID: newClaimID, Now: now}
114}
115
116// J3: the pass converts a landing whose target box is terminated into a
117// failover relocation when the move committed, through
118// CommittedLandingRecords over the authoritative record; a move that has
119// not committed is moves' case, and nothing is written.
120func TestRelocatePassConvertsAFencedCommittedLanding(t *testing.T) {
121 for _, committed := range []bool{true, false} {
122 name := "uncommitted"
123 if committed {
124 name = "committed"
125 }
126 t.Run(name, func(t *testing.T) {
127 ctx := t.Context()
128 w := newLandingWorld(t, committed, nil)
129 if err := convertFencedLanding(w.worker(w.enrollment["wrk"], "ie"), w.store)(ctx, w.store, w.obs); err != nil {
130 t.Fatalf("convert: %v", err)
131 }
132 after, err := hostclaim.Read(ctx, w.store, w.obs.Host)
133 if err != nil {
134 t.Fatal(err)
135 }
136 if !committed {
137 if after.ETag != w.obs.ETag || after.Claim.Op.Kind != hostclaim.KindLanding {
138 t.Fatalf("an uncommitted landing was written: %+v", after.Claim.Op)
139 }
140 return
141 }
142 c := after.Claim
143 if c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != hostclaim.PhaseStarting || c.Op.Intent["source_box"] != "dst" || c.Op.Journal["conversion"] != "committed-landing" {
144 t.Fatalf("not converted: %+v", c.Op)
145 }
146 })
147 }
148}
149
150// Astra round 6, finding 4: the conversion is the worker's, under Step's
151// own checks of the caller. A worker whose roster entry is another
152// enrollment (revoked and re-enrolled), one that is not active, one that
153// is fenced, or one enrolled outside the region it claims, is refused
154// with ErrNotWorker; a worker of another region leaves the landing alone.
155// None of them writes the claim, though the move committed and its target
156// is fenced.
157func TestRelocatePassConversionRefusesAnUnauthorizedWorker(t *testing.T) {
158 t.Parallel()
159 type tc struct {
160 name string
161 change func(*hostactors.Roster)
162 enrollment, region string
163 want error
164 }
165 moved := func(state, region string) func(*hostactors.Roster) {
166 return func(r *hostactors.Roster) {
167 b := r.Boxes["wrk"]
168 if state != "" {
169 b.State = state
170 }
171 if region != "" {
172 b.Region = region
173 }
174 r.Boxes["wrk"] = b
175 }
176 }
177 for _, c := range []tc{
178 {name: "a revoked enrollment", enrollment: strings.Repeat("f", 32), region: "ie", want: hostrelocate.ErrNotWorker},
179 {name: "a retired worker", change: moved("retired", ""), region: "ie", want: hostrelocate.ErrNotWorker},
180 {name: "enrolled in another region", change: moved("", "uk"), region: "ie", want: hostrelocate.ErrNotWorker},
181 {name: "a worker of another region", change: moved("", "uk"), region: "uk"},
182 } {
183 t.Run(c.name, func(t *testing.T) {
184 ctx := t.Context()
185 w := newLandingWorld(t, true, c.change)
186 if c.enrollment == "" {
187 c.enrollment = w.enrollment["wrk"]
188 }
189 err := convertFencedLanding(w.worker(c.enrollment, c.region), w.store)(ctx, w.store, w.obs)
190 if c.want == nil && err != nil || c.want != nil && !errors.Is(err, c.want) {
191 t.Fatalf("got %v, want %v", err, c.want)
192 }
193 after, err := hostclaim.Read(ctx, w.store, w.obs.Host)
194 if err != nil {
195 t.Fatal(err)
196 }
197 if after.ETag != w.obs.ETag || after.Claim.Op.Kind != hostclaim.KindLanding {
198 t.Fatalf("an unauthorized worker wrote the claim: %+v", after.Claim.Op)
199 }
200 })
201 }
202 t.Run("a fenced worker", func(t *testing.T) {
203 ctx := t.Context()
204 w := newLandingWorld(t, true, nil)
205 o, err := w.directory.Read(ctx)
206 if err != nil {
207 t.Fatal(err)
208 }
209 wrk := o.Roster.Boxes["wrk"]
210 fence, _ := json.Marshal(termfence.Record{V: 1, Box: "wrk", Enrollment: wrk.Enrollment, Cloud: wrk.Cloud, State: "terminated", ObservedAt: w.now.Add(-time.Minute).Format(time.RFC3339Nano),
211 Via: termfence.ViaDescribe, RequestID: "req", WrittenBy: termfence.WrittenBy{Box: "spa", Enrollment: w.enrollment["spa"]}})
212 if _, err := w.store.Put(ctx, hostfence.FenceKey(wrk.Enrollment), fence, ""); err != nil {
213 t.Fatal(err)
214 }
215 if err := convertFencedLanding(w.worker(w.enrollment["wrk"], "ie"), w.store)(ctx, w.store, w.obs); !errors.Is(err, hostrelocate.ErrNotWorker) {
216 t.Fatalf("got %v", err)
217 }
218 if after, err := hostclaim.Read(ctx, w.store, w.obs.Host); err != nil || after.ETag != w.obs.ETag {
219 t.Fatalf("a fenced worker wrote the claim: %v", err)
220 }
221 })
222}
223
224// landingAtProbed is a landing claim as moves leaves it at probed: the
225// move from src to dst flipped, the target's probe admitted. Its seal is a
226// real one, so moves.CommitScopes reads it.
227func landingAtProbed(t *testing.T, store release.Store, host, id string, now time.Time, enrollment map[string]string) hostclaim.Observed {
228 t.Helper()
229 return landingAtProbedHome(t, store, host, id, now, enrollment, "")
230}
231
232// landingAtProbedHome is landingAtProbed into the home when home is set:
233// the move is admitted under the home binding with "to" park.HomeRegion.
234func landingAtProbedHome(t *testing.T, store release.Store, host, id string, now time.Time, enrollment map[string]string, home string) hostclaim.Observed {
235 t.Helper()
236 ctx := t.Context()
237 created := time.Date(2026, 10, 4, 1, 0, 0, 0, time.UTC)
238 nh := hostclaim.NewHolder{Family: "instance", Key: provision.RecordKey("bab", "jam", host), Account: "bab", App: "jam", Host: host, Region: "ie", RecordRegion: "ie", CreatedAt: created}
239 body := []byte(`{"host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-04T01:00:00Z"}`)
240 o, err := hostclaim.Read(ctx, store, host)
241 if err != nil {
242 t.Fatal(err)
243 }
244 step := func(tr func(*hostclaim.Claim) error) {
245 t.Helper()
246 if o, err = hostclaim.Update(ctx, store, o, tr); err != nil {
247 t.Fatal(err)
248 }
249 }
250 seal := moves.Seal{V: moves.SealVersion, N: 7, FromDB: true, SrcCovered: true, SealedAt: now.Add(-2 * time.Hour), LeaseUntil: now.Add(-time.Hour),
251 Replica: []moves.SealedObject{{Key: "moves/" + host + "/" + id + "/replica/0009/" + ltx.FormatFilename(1, 7), VersionID: "v1", Size: 100}}}
252 step(hostclaim.BeginCreateOn(nh, body, "c1", "console", created, hostclaim.ExecutorSelection{Box: "src", Enrollment: enrollment["src"]}))
253 step(hostclaim.CompleteCreate("c1", "src"))
254 step(hostclaim.AdmitWriter(hostclaim.Admission{Box: "src", Mode: "cold", NewGeneration: "g1", Region: "ie", KeepStamp: true}))
255 to := "ie"
256 if home != "" {
257 to = park.HomeRegion
258 }
259 step(hostclaim.BeginAdmittedMove(id, "ops", "src", "dst", to, now.Add(-3*time.Hour), hostclaim.MoveAdmission{HomeRegion: home, AppID: "app-canonical", AdmissionID: id, SourceEnrollment: enrollment["src"], TargetEnrollment: enrollment["dst"]}))
260 step(hostclaim.ExecToNone(id, "src", "moves/"+host+"/"+id+"/stopped-source.json", hostclaim.JournalEvidenceSource))
261 step(hostclaim.SetJournal(id, map[string]string{hostclaim.JournalSeal: seal.Encode(), hostclaim.JournalCutSession: "1"}))
262 step(hostclaim.SetMovePhase(id, hostclaim.KindMoving, hostclaim.PhaseMoving, hostclaim.PhaseSealed))
263 step(hostclaim.Flip(id, "dst", o.Claim.Holder.Stamp, o.Claim.Holder.Generation))
264 for _, p := range [][2]string{{"checking", "parking"}, {"parking", "building"}, {"building", "published"}, {"published", "local"}, {"local", "configured"}, {"configured", "restored"}, {"restored", "probing"}, {"probing", "probed"}} {
265 step(hostclaim.SetMovePhase(id, hostclaim.KindLanding, p[0], p[1]))
266 }
267 step(func(c *hostclaim.Claim) error {
268 h := c.Holder
269 h.Session++
270 h.Admissions = append(h.Admissions, hostclaim.AdmissionRecord{Region: "ie", PredecessorRegion: "ie", Session: h.Session, Box: "dst", Generation: "gen-landed", Predecessor: h.Generation, Mode: "cold", Coverage: "sealed"})
271 h.Generation, h.WriterBox = "gen-landed", "dst"
272 return nil
273 })
274 return o
275}
276
277// Main's home-region parks name a move into the home by park.HomeRegion
278// ("to") while the roster, the fence binding and the region worker name it
279// by its roster name (ie). Under the binding the box and the move agree on
280// (the signed roster's home), the home's worker converts the committed
281// landing: the relocation's region is the roster identity (as
282// ConvertMovingToRelocating's is), the move's home binding is kept, and the
283// commit's parks are queued in engine regions exactly as moves' commit
284// queues them (the source's location in uk, the staging lane in
285// park.HomeRegion). A box with no binding, or a binding the roster does not
286// designate, writes nothing.
287func TestRelocatePassConvertsACommittedLandingIntoTheHome(t *testing.T) {
288 ctx := t.Context()
289 w := newLandingWorldHome(t, true, nil, "ie")
290 if err := convertFencedLanding(w.homeWorker(w.enrollment["wrk"], "ie", "ie"), w.store)(ctx, w.store, w.obs); err != nil {
291 t.Fatalf("convert: %v", err)
292 }
293 after, err := hostclaim.Read(ctx, w.store, w.obs.Host)
294 if err != nil {
295 t.Fatal(err)
296 }
297 c := after.Claim
298 if c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != hostclaim.PhaseStarting || c.Op.Intent["source_box"] != "dst" || c.Op.Journal["conversion"] != "committed-landing" {
299 t.Fatalf("not converted: %+v", c.Op)
300 }
301 if c.Op.Intent["region"] != "ie" || c.Op.Intent["home_region"] != "ie" || c.Op.Intent["move_to_region"] != park.HomeRegion || c.Op.Journal["move_from_region"] != "uk" {
302 t.Fatalf("intent %v journal from %q", c.Op.Intent, c.Op.Journal["move_from_region"])
303 }
304 parks := map[string]int{}
305 for _, o := range c.Obligations {
306 if o.Kind == "park-replica" {
307 parks[o.Region+" "+o.Owner]++
308 }
309 }
310 if parks["uk region:uk"] != 1 || parks[park.HomeRegion+" region:"+park.HomeRegion] != 1 || len(parks) != 2 {
311 t.Fatalf("the commit's parks: %v", parks)
312 }
313
314 for _, tc := range []struct {
315 name string
316 change func(*hostactors.Roster)
317 home string
318 want error
319 }{
320 {name: "the box holds no home binding", want: park.ErrUnsafe},
321 {name: "the roster designates no home", change: func(r *hostactors.Roster) { r.HomeRegion = "" }, home: "ie", want: hostrelocate.ErrNotWorker},
322 } {
323 t.Run(tc.name, func(t *testing.T) {
324 w := newLandingWorldHome(t, true, tc.change, "ie")
325 err := convertFencedLanding(w.homeWorker(w.enrollment["wrk"], "ie", tc.home), w.store)(ctx, w.store, w.obs)
326 if !errors.Is(err, tc.want) {
327 t.Fatalf("got %v, want %v", err, tc.want)
328 }
329 if after, err := hostclaim.Read(ctx, w.store, w.obs.Host); err != nil || after.ETag != w.obs.ETag {
330 t.Fatalf("the claim was written: %v", err)
331 }
332 })
333 }
334}
335
336// CommittedLandingRecords answers the record's placement codes as engine
337// regions (park.PlacementRegion), the regions moves' commit queues its
338// parks in: the home's "" is park.HomeRegion, and a code that is no
339// placement refuses.
340func TestCommittedLandingRecordsAnswersEngineRegions(t *testing.T) {
341 ctx := t.Context()
342 for _, tc := range []struct {
343 name, from, to string
344 wantFrom, wantTo string
345 refused bool
346 }{
347 {name: "named", from: "uk", to: "ie", wantFrom: "uk", wantTo: "ie"},
348 {name: "into the home", from: "uk", to: "", wantFrom: "uk", wantTo: park.HomeRegion},
349 {name: "out of the home", from: "", to: "uk", wantFrom: park.HomeRegion, wantTo: "uk"},
350 {name: "not a placement code", from: "UK", to: "ie", refused: true},
351 } {
352 t.Run(tc.name, func(t *testing.T) {
353 w := newLandingWorld(t, true, nil)
354 c := w.obs.Claim
355 read := func(ctx context.Context, account, app, host string) (*provision.Record, error) {
356 rec, err := provision.GetRecord(ctx, w.store, account, app, host)
357 if err != nil {
358 return nil, err
359 }
360 rec.Move.From, rec.Move.To = tc.from, tc.to
361 return rec, nil
362 }
363 got, err := CommittedLandingRecords{Records: read}.CheckLandingCommitted(ctx, w.obs, c.Op.ID)
364 if tc.refused {
365 if !errors.Is(err, hostclaim.ErrWrongState) {
366 t.Fatalf("got %+v, %v", got, err)
367 }
368 return
369 }
370 if err != nil || got.FromRegion != tc.wantFrom || got.ToRegion != tc.wantTo {
371 t.Fatalf("got %+v, %v", got, err)
372 }
373 })
374 }
375}
376