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/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostactors"
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 "github.com/carlosframework/platform/internal/hostevidence"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/release"
22)
23
24// releaseWorld is a relocating claim in releasing on the source "box",
25// a root-signed roster naming the box's census key, and the box's key to
26// sign root's receipt with (main's plan11_release_test fixture).
27type releaseWorld struct {
28 s release.Store
29 dir hostactors.Directory
30 boxKey ed25519.PrivateKey
31 en string
32 obs hostclaim.Observed
33}
34
35func newReleaseWorld(t *testing.T) releaseWorld {
36 t.Helper()
37 return newReleaseWorldWith(t, nil)
38}
39
40// newReleaseWorldWith is newReleaseWorld with the roster's boxes changed
41// by roster before it is signed (nil leaves them).
42func newReleaseWorldWith(t *testing.T, roster func(map[string]hostactors.Box)) releaseWorld {
43 t.Helper()
44 ctx := context.Background()
45 s := release.NewDirStore(t.TempDir())
46 root := ed25519.NewKeyFromSeed(make([]byte, 32))
47 boxKey := ed25519.NewKeyFromSeed([]byte(strings.Repeat("b", 32)))
48 en := strings.Repeat("a", 32)
49 box := func(name, enrollment string, key ed25519.PrivateKey) hostactors.Box {
50 return hostactors.Box{Enrollment: enrollment, BoxID: name, Label: name, Namespace: "platform", Region: "ie", Pool: "default", State: "active",
51 Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-" + name}, EvidenceSHA256: strings.Repeat("d", 64), CensusPublicKey: key.Public().(ed25519.PublicKey)}
52 }
53 boxes := map[string]hostactors.Box{"box": box("box", en, boxKey), "other": box("other", strings.Repeat("c", 32), otherBoxKey)}
54 if roster != nil {
55 roster(boxes)
56 }
57 signed := hostactors.Roster{V: 1, Revision: 1, DeploymentBucket: "bucket", Boxes: boxes,
58 Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
59 wire, err := release.SignRootEnvelope(release.RootSigner(root), signed)
60 if err != nil {
61 t.Fatal(err)
62 }
63 if _, err := s.Put(ctx, hostactors.Key, wire, ""); err != nil {
64 t.Fatal(err)
65 }
66 host := "shop.example"
67 created := time.Date(2026, 10, 9, 0, 0, 0, 0, time.UTC)
68 inc, _ := hostclaim.Incarnation("bab", "jam", host, created)
69 claim := hostclaim.Claim{V: 2, Gen: 1, Host: host, Holder: &hostclaim.Holder{Family: "instance", Key: "apps/bab/jam/instances/" + host + ".json", Account: "bab", App: "jam", Incarnation: inc, CreatedAt: created,
70 Location: "instances/" + host + "/" + inc, Stamp: 7, Session: 7, WriterBox: "box"},
71 Op: hostclaim.Op{ID: "reloc", Kind: hostclaim.KindRelocating, Phase: hostclaim.PhaseReleasing, Exec: "box", Intent: map[string]string{"source_box": "box", "source_enrollment": en, "region": "ie"}}}
72 raw, _ := json.Marshal(claim)
73 key, _ := hostclaim.Key(host)
74 tag, err := s.Put(ctx, key, raw, "")
75 if err != nil {
76 t.Fatal(err)
77 }
78 return releaseWorld{s: s, dir: hostactors.Directory{Store: s, PublicKey: root.Public().(ed25519.PublicKey), Bucket: "bucket"}, boxKey: boxKey, en: en,
79 obs: hostclaim.Observed{Host: host, Claim: &claim, ETag: tag}}
80}
81
82// otherBoxKey is another roster box's census key.
83var otherBoxKey = ed25519.NewKeyFromSeed([]byte(strings.Repeat("o", 32)))
84
85// receipt publishes root's signed full stop of the claim's session.
86func (w releaseWorld) receipt(t *testing.T) (string, string) {
87 t.Helper()
88 return w.receiptWith(t, nil, w.boxKey)
89}
90
91// receiptWith publishes the receipt changed by mutate, signed by signer
92// (nil: the body alone, unsigned).
93func (w releaseWorld) receiptWith(t *testing.T, mutate func(*StopReceipt), signer ed25519.PrivateKey) (string, string) {
94 t.Helper()
95 c := w.obs.Claim
96 r := StopReceipt{V: 1, ID: "stop-1", Kind: "stopped", Host: c.Host, Incarnation: c.Holder.Incarnation, InstallationStamp: c.Holder.Stamp, WriterSession: c.Holder.Session, OperationID: c.Op.ID,
97 Phase: hostclaim.PhaseReleasing, ObligationID: RelocationReleaseObligation, Box: "box", Enrollment: w.en, Region: "ie", EvidenceAt: time.Now(), RequestLimitNS: int64(hostbox.RequestLimit), ClaimGen: 1,
98 FenceID: "fence", InventorySHA256: strings.Repeat("e", 64), LocalJournalSHA256: strings.Repeat("f", 64), ProducerDrainDigest: strings.Repeat("a", 64), CgroupIdentities: []string{"root-role"}}
99 if mutate != nil {
100 mutate(&r)
101 }
102 body, _ := json.Marshal(r)
103 key := stopEvidenceKey(c.Host, c.Op.ID, "box", r.ID)
104 if signer == nil {
105 if _, err := w.s.Put(context.Background(), key, body, ""); err != nil {
106 t.Fatal(err)
107 }
108 } else if err := hostevidence.Publish(context.Background(), w.s, signer, key, body); err != nil {
109 t.Fatal(err)
110 }
111 sum := sha256.Sum256(body)
112 return key, hex.EncodeToString(sum[:])
113}
114
115// A release runs main's relocation-release lifecycle: the obligation is
116// queued once before root's stop, root's exact receipt discharges it, and
117// only then does the journaled release count for the pin.
118func TestReleaseObligationQueuesOnceAndDischargesWithRootsReceipt(t *testing.T) {
119 ctx := context.Background()
120 w := newReleaseWorld(t)
121 o := ReleaseObligation{Receipts: RelocationReleaseReceipts{Store: w.s, Directory: w.dir}}
122 queued, err := o.Queue(ctx, w.obs)
123 if err != nil {
124 t.Fatal(err)
125 }
126 if len(queued.Claim.Obligations) != 1 || queued.Claim.Obligations[0].Kind != RelocationReleaseObligation || queued.Claim.Obligations[0].Retirement != "reloc/7/7" {
127 t.Fatalf("queued: %+v", queued.Claim.Obligations)
128 }
129 again, err := o.Queue(ctx, queued)
130 if err != nil || again.ETag != queued.ETag {
131 t.Fatalf("a second queue wrote the claim: %v", err)
132 }
133 key, digest := w.receipt(t)
134 rel := hostclaim.Release{ReleasedBy: "box", Enrollment: w.en, Generation: "", Session: 7, Receipt: key, ReceiptSHA256: digest}
135 if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, *queued.Claim, rel); !errors.Is(err, ErrClaimStopReceipt) {
136 t.Fatalf("a release counted before its receipt discharged the obligation: %v", err)
137 }
138 done, err := o.Complete(ctx, queued, key, digest)
139 if err != nil {
140 t.Fatal(err)
141 }
142 if len(done.Claim.Obligations) != 0 || done.Claim.Op.Journal["release_stop_receipt"] != key {
143 t.Fatalf("not discharged: %+v %v", done.Claim.Obligations, done.Claim.Op.Journal)
144 }
145 if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, *done.Claim, rel); err != nil {
146 t.Fatalf("the discharged release: %v", err)
147 }
148 // A replay: the discharged release queues nothing and completes again.
149 if replayed, err := o.Queue(ctx, done); err != nil || replayed.ETag != done.ETag {
150 t.Fatalf("a replay queued again: %v", err)
151 }
152 if _, err := o.Complete(ctx, done, key, digest); err != nil {
153 t.Fatalf("a replayed completion: %v", err)
154 }
155 for name, change := range map[string]func(*hostclaim.Release){
156 "another receipt": func(r *hostclaim.Release) { r.Receipt = key + "x" },
157 "another digest": func(r *hostclaim.Release) { r.ReceiptSHA256 = strings.Repeat("0", 64) },
158 "no receipt": func(r *hostclaim.Release) { r.Receipt = "" },
159 } {
160 other := rel
161 change(&other)
162 if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, *done.Claim, other); !errors.Is(err, ErrClaimStopReceipt) {
163 t.Fatalf("%s counted: %v", name, err)
164 }
165 }
166 // Main discharges a relocation release only for the source in
167 // releasing: a failed target's release in probe-failed never counts.
168 failed := *done.Claim
169 failed.Op.Phase = hostclaim.PhaseProbeFailed
170 if err := (StopReceiptReleases{Receipts: o.Receipts}).VerifyRelease(ctx, failed, rel); !errors.Is(err, ErrClaimStopReceipt) {
171 t.Fatalf("a probe-failed release counted: %v", err)
172 }
173}
174
175func TestReleaseObligationRefusesAnotherRelease(t *testing.T) {
176 ctx := context.Background()
177 w := newReleaseWorld(t)
178 o := ReleaseObligation{Receipts: RelocationReleaseReceipts{Store: w.s, Directory: w.dir}}
179 queued, err := o.Queue(ctx, w.obs)
180 if err != nil {
181 t.Fatal(err)
182 }
183 // The same operation's release of a later session is another
184 // retirement identity: the owed obligation is not it.
185 later := *queued.Claim
186 h := *later.Holder
187 h.Session++
188 later.Holder = &h
189 if _, err := o.Queue(ctx, hostclaim.Observed{Host: queued.Host, Claim: &later, ETag: queued.ETag}); !errors.Is(err, hostclaim.ErrWrongState) {
190 t.Fatalf("another session's release reused the obligation: %v", err)
191 }
192 // probe-failed has no relocation-release in main's claim.
193 w2 := newReleaseWorld(t)
194 w2.obs.Claim.Op.Phase = hostclaim.PhaseProbeFailed
195 if _, err := (ReleaseObligation{Receipts: RelocationReleaseReceipts{Store: w2.s, Directory: w2.dir}}).Queue(ctx, w2.obs); !errors.Is(err, hostclaim.ErrWrongState) {
196 t.Fatalf("a probe-failed target queued a relocation release: %v", err)
197 }
198 if _, err := (ReleaseObligation{}).Queue(ctx, w.obs); !errors.Is(err, ErrClaimStopUnavailable) {
199 t.Fatalf("no store: %v", err)
200 }
201}
202
203// discharged is the world's claim as Complete leaves it: the receipt's key
204// and digest journaled and no obligation owed.
205func (w releaseWorld) discharged(key, digest string) hostclaim.Claim {
206 c := *w.obs.Claim
207 c.Op.Journal = map[string]string{"release_stop_receipt": key, "release_stop_sha256": digest}
208 c.Obligations = nil
209 return c
210}
211
212// The pin's verifier consumes root's signed receipt itself (Plan 11 Task
213// E), never the journaled digest alone: every receipt main's Complete
214// would refuse is refused at the pin, though the claim journals it as
215// discharged. (The deleted branch verifier's signature tests, against
216// main's receipt.)
217func TestStopReceiptReleasesVerifiesRootsSignedReceiptAtThePin(t *testing.T) {
218 ctx := context.Background()
219 verify := func(t *testing.T, w releaseWorld, key, digest string, dir hostactors.Directory) error {
220 t.Helper()
221 rel := hostclaim.Release{ReleasedBy: "box", Enrollment: w.en, Session: 7, Receipt: key, ReceiptSHA256: digest}
222 return (StopReceiptReleases{Receipts: RelocationReleaseReceipts{Store: w.s, Directory: dir}}).VerifyRelease(ctx, w.discharged(key, digest), rel)
223 }
224 w := newReleaseWorld(t)
225 key, digest := w.receipt(t)
226 if err := verify(t, w, key, digest, w.dir); err != nil {
227 t.Fatalf("root's signed receipt: %v", err)
228 }
229 if now, err := hostclaim.Read(ctx, w.s, w.obs.Host); err != nil || now.ETag != w.obs.ETag {
230 t.Fatalf("verifying wrote the claim: %v", err)
231 }
232 t.Run("an unsigned receipt", func(t *testing.T) {
233 w := newReleaseWorld(t)
234 key, digest := w.receiptWith(t, nil, nil)
235 if err := verify(t, w, key, digest, w.dir); err == nil {
236 t.Fatal("an unsigned receipt counted")
237 }
238 })
239 t.Run("signed by another box's key", func(t *testing.T) {
240 w := newReleaseWorld(t)
241 key, digest := w.receiptWith(t, nil, otherBoxKey)
242 if err := verify(t, w, key, digest, w.dir); err == nil {
243 t.Fatal("another box's signature counted")
244 }
245 })
246 t.Run("a roster entry of another enrollment", func(t *testing.T) {
247 w := newReleaseWorldWith(t, func(b map[string]hostactors.Box) {
248 box := b["box"]
249 box.Enrollment = strings.Repeat("d", 32)
250 b["box"] = box
251 })
252 key, digest := w.receipt(t)
253 if err := verify(t, w, key, digest, w.dir); !errors.Is(err, ErrClaimStopIdentity) {
254 t.Fatalf("another enrollment's key counted: %v", err)
255 }
256 })
257 t.Run("a box the roster does not name", func(t *testing.T) {
258 w := newReleaseWorldWith(t, func(b map[string]hostactors.Box) { delete(b, "box") })
259 key, digest := w.receipt(t)
260 if err := verify(t, w, key, digest, w.dir); !errors.Is(err, ErrClaimStopIdentity) {
261 t.Fatalf("a box missing from the roster counted: %v", err)
262 }
263 })
264 t.Run("no roster", func(t *testing.T) {
265 w := newReleaseWorld(t)
266 key, digest := w.receipt(t)
267 if err := verify(t, w, key, digest, hostactors.Directory{Store: w.s, Bucket: "bucket"}); err == nil {
268 t.Fatal("a release counted without a roster to name its key")
269 }
270 })
271 t.Run("a digest that is not the bytes", func(t *testing.T) {
272 w := newReleaseWorld(t)
273 key, _ := w.receipt(t)
274 if err := verify(t, w, key, strings.Repeat("0", 64), w.dir); !errors.Is(err, ErrClaimStopReceipt) {
275 t.Fatalf("another digest counted: %v", err)
276 }
277 })
278 for name, mutate := range map[string]func(*StopReceipt){
279 "another session": func(r *StopReceipt) { r.WriterSession++ },
280 "another installation stamp": func(r *StopReceipt) { r.InstallationStamp++ },
281 "another operation's phase": func(r *StopReceipt) { r.Phase = hostclaim.PhaseProbeFailed },
282 "a retirement's stop": func(r *StopReceipt) { r.ObligationID = "park-local" },
283 "another enrollment": func(r *StopReceipt) { r.Enrollment = strings.Repeat("f", 32) },
284 "no stopped cgroups": func(r *StopReceipt) { r.CgroupIdentities = nil },
285 "a malformed producer digest": func(r *StopReceipt) { r.ProducerDrainDigest = "drain" },
286 } {
287 t.Run(name, func(t *testing.T) {
288 w := newReleaseWorld(t)
289 key, digest := w.receiptWith(t, mutate, w.boxKey)
290 if err := verify(t, w, key, digest, w.dir); !errors.Is(err, ErrClaimStopReceipt) {
291 t.Fatalf("accepted: %v", err)
292 }
293 })
294 }
295 t.Run("no receipts store", func(t *testing.T) {
296 w := newReleaseWorld(t)
297 key, digest := w.receipt(t)
298 rel := hostclaim.Release{ReleasedBy: "box", Enrollment: w.en, Session: 7, Receipt: key, ReceiptSHA256: digest}
299 if err := (StopReceiptReleases{}).VerifyRelease(ctx, w.discharged(key, digest), rel); !errors.Is(err, ErrClaimStopUnavailable) {
300 t.Fatalf("verified with no store: %v", err)
301 }
302 })
303}
304
305// rootCollect models root's FenceAndStop for a relocation release: it
306// refuses unless the claim in the store owes the obligation at exactly the
307// request's ETag (hostguard.StopAuthorityMatches, the expected ETag), runs
308// the agent's drain, then publishes its signed receipt. calls counts the
309// requests that reached it.
310func (w releaseWorld) rootCollect(t *testing.T, calls *int) func(context.Context, StopRequest, StopDrainer) (StopResult, error) {
311 return func(ctx context.Context, r StopRequest, d StopDrainer) (StopResult, error) {
312 *calls++
313 o, err := hostclaim.Read(ctx, w.s, r.Host)
314 if err != nil {
315 return StopResult{}, err
316 }
317 owed := false
318 for _, ob := range o.Claim.Obligations {
319 owed = owed || (ob.Kind == RelocationReleaseObligation && ob.Op == r.OperationID && ob.Box == r.Box)
320 }
321 if !owed || o.ETag != r.ExpectedClaimETag {
322 return StopResult{}, ErrClaimStopIdentity
323 }
324 if _, err := d.DrainClaim(ctx, r, "fence"); err != nil {
325 return StopResult{}, err
326 }
327 key, digest := w.receipt(t)
328 return StopResult{Key: key, Digest: digest}, nil
329 }
330}
331
332// releaseEffects is one agent process's RelocationEffects over the world:
333// the real obligation (main's Queue and Complete), durable positions in
334// dir, root modelled by rootCollect, and this process's identity.
335func (w releaseWorld) releaseEffects(t *testing.T, dir string, pid int, calls *int) *RelocationEffects {
336 steps := &[]string{}
337 return &RelocationEffects{
338 Identity: func(context.Context) (RelocationIdentity, error) {
339 return RelocationIdentity{Actor: hostbox.Actor{Box: "box", Enrollment: w.en, Region: "ie", Revision: 1, Binding: "1"}, BootID: "boot", ControllerID: fmt.Sprint("ctl-", pid), PID: pid, ProcessStart: fmt.Sprint(pid * 10)}, nil
340 },
341 Producers: func(context.Context, hostclaim.Observed) (StopDrainer, error) { return recordingDrain{steps}, nil },
342 Collect: w.rootCollect(t, calls),
343 WritersStopped: func(_ context.Context, o hostclaim.Observed) (ClaimWritersDrained, WritersStoppedEvidence, error) {
344 h := o.Claim.Holder
345 at := time.Date(2026, 10, 9, 1, 0, 0, 0, time.UTC)
346 target := hostguard.FenceTarget{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Box: "box", Enrollment: w.en, BootID: "boot", ControllerID: fmt.Sprint("ctl-", pid)}
347 return ClaimWritersDrained{Target: target, Generation: h.Generation, WriterBox: h.WriterBox, FenceClosed: true, LaunchJoined: true, WorkDrained: true, SpawnsSettled: true,
348 CgroupEmpty: true, Cgroup: "carlos/shop", DBPaths: []string{"/data/shop.db"}, CheckedAt: at},
349 WritersStoppedEvidence{Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, WriterSession: h.Session, Box: "box", Enrollment: w.en, CheckedAt: at}, nil
350 },
351 Releases: ReleaseObligation{Receipts: RelocationReleaseReceipts{Store: w.s, Directory: w.dir}},
352 FlushDB: func(context.Context, hostclaim.Observed) (uint64, error) { return 41, nil },
353 FlushFiles: func(context.Context, hostclaim.Observed) (int64, error) { return 8, nil },
354 Positions: ReleasePositionsFile{Dir: dir},
355 }
356}
357
358// Review finding 4: once root's receipt has discharged the obligation the
359// claim has moved on and owes root nothing, so a second stop is refused.
360// A release whose journal CAS was lost, or whose agent restarted after
361// Complete, must still be returned: from the journaled receipt, verified
362// again, and the recorded positions, never by asking root.
363func TestADischargedReleaseReplaysAfterALostJournalOrARestart(t *testing.T) {
364 ctx := context.Background()
365 w := newReleaseWorld(t)
366 dir := filepath.Join(t.TempDir(), "relocate")
367 calls := 0
368 first, err := w.releaseEffects(t, dir, 100, &calls).Release(ctx, w.obs)
369 if err != nil {
370 t.Fatal(err)
371 }
372 if calls != 1 || first.FinalTXID != 41 || first.ManifestSeq != 8 {
373 t.Fatalf("first release: %+v, root asked %d times", first, calls)
374 }
375 fresh := func() hostclaim.Observed {
376 o, err := hostclaim.Read(ctx, w.s, w.obs.Host)
377 if err != nil {
378 t.Fatal(err)
379 }
380 return o
381 }
382 discharged := fresh()
383 if len(discharged.Claim.Obligations) != 0 || discharged.Claim.Op.Journal["release_stop_receipt"] != first.Receipt {
384 t.Fatalf("not discharged: %+v", discharged.Claim)
385 }
386 // The journal CAS lost: the same process retries at the claim as it is.
387 again, err := w.releaseEffects(t, dir, 100, &calls).Release(ctx, discharged)
388 if err != nil || again != first {
389 t.Fatalf("same process retry: %+v %v", again, err)
390 }
391 // The agent restarted after Complete: a new process, new controller.
392 restarted, err := w.releaseEffects(t, dir, 200, &calls).Release(ctx, fresh())
393 if err != nil || restarted != first {
394 t.Fatalf("after a restart: %+v %v", restarted, err)
395 }
396 if calls != 1 {
397 t.Fatalf("root was asked again for a discharged release: %d calls", calls)
398 }
399 // The release journals, and nothing was written by the replays.
400 if now := fresh(); now.ETag != discharged.ETag {
401 t.Fatal("a replay wrote the claim")
402 }
403 if _, err := hostclaim.Update(ctx, w.s, fresh(), hostclaim.JournalRelease("reloc", restarted)); err != nil {
404 t.Fatalf("the replayed release does not journal: %v", err)
405 }
406 // Without a recorded flush the replay refuses rather than guess.
407 w2 := newReleaseWorld(t)
408 calls2 := 0
409 if _, err := w2.releaseEffects(t, filepath.Join(t.TempDir(), "relocate"), 100, &calls2).Release(ctx, w2.obs); err != nil {
410 t.Fatal(err)
411 }
412 o2, err := hostclaim.Read(ctx, w2.s, w2.obs.Host)
413 if err != nil {
414 t.Fatal(err)
415 }
416 if _, err := w2.releaseEffects(t, filepath.Join(t.TempDir(), "elsewhere"), 300, &calls2).Release(ctx, o2); !errors.Is(err, ErrClaimStopReceipt) || calls2 != 1 {
417 t.Fatalf("a discharged release with no recorded positions: %v, root asked %d times", err, calls2)
418 }
419}
420