carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "context"
7 "crypto/ed25519"
8 "crypto/rand"
9 "errors"
10 "os"
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 execlease "github.com/carlosframework/platform/internal/hostexec"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/registry"
22 "github.com/carlosframework/platform/internal/release"
23)
24
25const participationHost = "shop.example"
26
27// participationBox is box "dst" of a relocation of shop.example from
28// "src", with main's participation owner as production builds it: a real
29// productionClaimLocal in agent mode over a DirStore control bucket with a
30// signed roster, real hostexec controllers (their locks under the N12 test
31// seam), the real census over empty roots, the launch journal, and a
32// recording transport where root would be asked for a stop.
33type participationBox struct {
34 t *testing.T
35 store *release.DirStore
36 data string
37 actor hostbox.Actor
38 guard hostguard.Guard
39 p *productionClaimLocal
40 stops int // stops asked of root
41 census *hostbox.EmptyInstallationCensus
42 launches *hostbox.LaunchJournal
43}
44
45func newParticipationBox(t *testing.T) *participationBox {
46 t.Helper()
47 ctx := t.Context()
48 b := &participationBox{t: t, store: release.NewDirStore(t.TempDir()), data: t.TempDir()}
49 for _, name := range []string{"hostguard", ".hostexec"} {
50 if err := os.Mkdir(filepath.Join(b.data, name), 0o770); err != nil {
51 t.Fatal(err)
52 }
53 }
54 pub, key, err := ed25519.GenerateKey(rand.Reader)
55 if err != nil {
56 t.Fatal(err)
57 }
58 enrollment := func(box string) string { return strings.Repeat(map[string]string{"src": "b", "dst": "c"}[box], 32) }
59 roster := hostactors.Roster{V: 1, DeploymentBucket: "bucket", Revision: 4, Regions: map[string]hostactors.Region{"ie": {Account: "123", AWSRegion: "eu-west-1"}},
60 Boxes: map[string]hostactors.Box{}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}}
61 for _, box := range []string{"src", "dst"} {
62 roster.Boxes[box] = hostactors.Box{Enrollment: enrollment(box), Namespace: "platform", BoxID: box, Label: box, Region: "ie", Pool: "default",
63 Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-" + box}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"}
64 }
65 raw, err := release.SignRootEnvelope(release.RootSigner(key), roster)
66 if err != nil {
67 t.Fatal(err)
68 }
69 if _, err := b.store.Put(ctx, hostactors.Key, raw, ""); err != nil {
70 t.Fatal(err)
71 }
72 b.actor = hostbox.Actor{Box: "dst", Enrollment: enrollment("dst"), Region: "ie", Revision: 4, Binding: "4"}
73
74 created := time.Date(2026, 10, 7, 1, 0, 0, 0, time.UTC)
75 nh := hostclaim.NewHolder{Family: "instance", Key: "apps/acct/app/instances/" + participationHost + ".json", Account: "acct", App: "app", Host: participationHost, Region: "ie", RecordRegion: "ie", CreatedAt: created}
76 body := []byte(`{"host":"` + participationHost + `","account":"acct","app":"app","region":"ie","created_at":"2026-10-07T01:00:00Z"}`)
77 obs, err := hostclaim.Read(ctx, b.store, participationHost)
78 if err != nil {
79 t.Fatal(err)
80 }
81 for _, tr := range []func(*hostclaim.Claim) error{
82 hostclaim.BeginCreateOn(nh, body, "c1", "console", created, hostclaim.ExecutorSelection{Box: "src", Enrollment: enrollment("src")}),
83 hostclaim.CompleteCreate("c1", "src"),
84 hostclaim.AdmitWriter(hostclaim.Admission{Box: "src", Mode: "cold", NewGeneration: "g1", Region: "ie", KeepStamp: true}),
85 func(c *hostclaim.Claim) error {
86 c.Op = hostclaim.Op{Kind: hostclaim.KindRelocating, ID: "rel-1", Phase: hostclaim.PhaseInstalling, Exec: "dst", Participants: []string{"src", "dst"},
87 Intent: map[string]string{"region": "ie", "source_box": "src", "target_box": "dst", "target_enrollment": enrollment("dst")}}
88 return nil
89 },
90 } {
91 if obs, err = hostclaim.Update(ctx, b.store, obs, tr); err != nil {
92 t.Fatal(err)
93 }
94 }
95 locks := t.TempDir()
96 for _, name := range []string{"hostguard", "hostexec"} {
97 if err := os.Mkdir(filepath.Join(locks, name), 0o700); err != nil {
98 t.Fatal(err)
99 }
100 }
101 b.guard = hostguard.Guard{DataDir: b.data, LockDir: filepath.Join(locks, "hostguard"), Claims: hostguard.StoreReader{Store: b.store}}
102 inventory, err := openLegacyInventory(t, filepath.Join(t.TempDir(), "inventory.db"))
103 if err != nil {
104 t.Fatal(err)
105 }
106 t.Cleanup(func() { inventory.Close() })
107 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
108 if err != nil {
109 t.Fatal(err)
110 }
111 t.Cleanup(func() { reg.Close() })
112 launches, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db"))
113 if err != nil {
114 t.Fatal(err)
115 }
116 t.Cleanup(func() { launches.Close() })
117 b.launches = launches
118 b.census = &hostbox.EmptyInstallationCensus{Inventory: inventory, Registry: reg, Launches: launches, Box: "dst", Enrollment: b.actor.Enrollment, DirectoryRevision: "4",
119 Roots: map[string]string{"data": b.data, "opt": t.TempDir(), "units": t.TempDir()}, SharedPaths: map[string][]string{"data": {".hostexec", "hostguard"}}}
120 a := &ClaimAssembly{Claims: b.store, Guard: b.guard, Wake: &ClaimWakeAssembly{}, Launches: launches, Reconcile: &hostbox.ReconcileAdapter{Census: b.census},
121 collectStop: func(context.Context, StopRequest, StopDrainer) (StopResult, error) {
122 b.stops++
123 return StopResult{}, errors.New("root: no stop in this fixture")
124 }}
125 b.p = &productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: b.data, OptDir: t.TempDir()}, Identity: handoverRecoveryIdentity(t, b.actor),
126 Directory: hostactors.Directory{Store: b.store, PublicKey: pub, Bucket: "bucket"}, Store: b.store, slots: map[string]*claimParticipation{},
127 assembly: a, lifetime: ctx, lockRoot: locks}
128 t.Cleanup(b.closeAll)
129 return b
130}
131
132func (b *participationBox) read() hostclaim.Observed {
133 b.t.Helper()
134 o, err := hostclaim.Read(context.Background(), b.store, participationHost)
135 if err != nil {
136 b.t.Fatal(err)
137 }
138 return o
139}
140
141func (b *participationBox) update(tr func(*hostclaim.Claim) error) hostclaim.Observed {
142 b.t.Helper()
143 o, err := hostclaim.Update(context.Background(), b.store, b.read(), tr)
144 if err != nil {
145 b.t.Fatal(err)
146 }
147 return o
148}
149
150// records is the box's .hostexec records.
151func (b *participationBox) records() []string {
152 b.t.Helper()
153 entries, err := os.ReadDir(filepath.Join(b.data, ".hostexec"))
154 if err != nil {
155 b.t.Fatal(err)
156 }
157 var out []string
158 for _, e := range entries {
159 out = append(out, e.Name())
160 }
161 return out
162}
163
164// closeAll stands for the agent's exit: every open controller is closed.
165func (b *participationBox) closeAll() {
166 b.p.mu.Lock()
167 slots := b.p.slots
168 b.p.mu.Unlock()
169 for _, s := range slots {
170 if s.Controller != nil {
171 _ = s.Controller.Close(context.Background())
172 }
173 }
174}
175
176// Finding 1, ruling 42: one controller per {host, op}, whichever asks
177// first. The relocation pass's acknowledgement (Acknowledged, through
178// RelocationEffects) and main's participation (Prepare, as the loop and
179// WorkSource.Prepare call it) open or reuse the box's one slot: one
180// .hostexec record, one lease, one controller id. When the op ends without
181// the box, main's next Prepare closes the controller, runs its cleanup,
182// clears the record and drops the slot, and the relocation's Ready refuses.
183func TestRelocationAcknowledgesThroughMainsParticipation(t *testing.T) {
184 t.Parallel()
185 for _, first := range []string{"relocation pass", "participation loop"} {
186 t.Run(first+" first", func(t *testing.T) {
187 ctx := t.Context()
188 b := newParticipationBox(t)
189 obs := b.read()
190 effects := &RelocationEffects{Participation: b.p.Acknowledged}
191 var ack hostclaim.ExecutionAcknowledgement
192 acknowledge := func() {
193 t.Helper()
194 var err error
195 if ack, err = effects.Acknowledge(ctx, obs); err != nil {
196 t.Fatalf("acknowledge: %v", err)
197 }
198 }
199 participate := func() {
200 t.Helper()
201 if err := b.p.Prepare(ctx, obs); err != nil {
202 t.Fatalf("main's Prepare: %v", err)
203 }
204 }
205 if first == "relocation pass" {
206 acknowledge()
207 participate()
208 } else {
209 participate()
210 acknowledge()
211 }
212 b.p.mu.Lock()
213 slot := b.p.slots[participationHost]
214 n := len(b.p.slots)
215 b.p.mu.Unlock()
216 if slot == nil || n != 1 || slot.Controller == nil {
217 t.Fatalf("want one slot with a controller, have %d", n)
218 }
219 opened, err := slot.Controller.Ready(ctx)
220 if err != nil {
221 t.Fatal(err)
222 }
223 if ack.Op != "rel-1" || ack.ControllerID != opened.ControllerID || ack.LeaseETag == "" || ack.LeaseETag != opened.LeaseETag || ack.Enrollment != b.actor.Enrollment {
224 t.Fatalf("the acknowledgement is not main's controller: %+v, opened %+v", ack, opened.Binding)
225 }
226 again, err := effects.Acknowledge(ctx, obs)
227 if err != nil || again.ControllerID != ack.ControllerID || again.LeaseETag != ack.LeaseETag {
228 t.Fatalf("a second acknowledgement saw another controller: %+v %v", again, err)
229 }
230 if err := effects.Ready(ctx, obs); err != nil {
231 t.Fatalf("ready: %v", err)
232 }
233 if got := b.records(); len(got) != 1 || got[0] != participationHost+".json" {
234 t.Fatalf("records %v", got)
235 }
236
237 // The op ends without this box: the source serves again.
238 dropped := b.update(func(c *hostclaim.Claim) error {
239 c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "src", Participants: []string{"src"}}
240 return nil
241 })
242 if err := b.p.Prepare(ctx, dropped); err != nil {
243 t.Fatalf("main's Prepare did not discharge the participation: %v", err)
244 }
245 if !slot.CleanupSucceeded || len(b.p.slots) != 0 || len(b.records()) != 0 {
246 t.Fatalf("the participation outlived its op: cleanup=%v slots=%d records=%v", slot.CleanupSucceeded, len(b.p.slots), b.records())
247 }
248 if err := effects.Ready(ctx, dropped); !errors.Is(err, hostbox.ErrChanged) {
249 t.Fatalf("ready after the op ended: %v", err)
250 }
251 if _, err := execlease.InspectLocal(b.data, participationHost); !errors.Is(err, os.ErrNotExist) {
252 t.Fatalf("record: %v", err)
253 }
254 })
255 }
256}
257