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 hostbox
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/hostclaim"
16 "github.com/carlosframework/platform/internal/hostguard"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/rootstate"
20)
21
22type plan14ColdManifest struct {
23 Base, Data, Locks string
24 Prior hostguard.FenceTarget
25 Next ColdSuccessor
26}
27type plan14ColdDisk struct {
28 rootstate.Store
29 dir string
30 before, after func([]byte)
31}
32
33func (d *plan14ColdDisk) Read(n string) ([]byte, error) { return os.ReadFile(filepath.Join(d.dir, n)) }
34func (d *plan14ColdDisk) Write(n string, b []byte) error {
35 if d.before != nil {
36 d.before(b)
37 }
38 f, err := os.CreateTemp(d.dir, "pending-")
39 if err != nil {
40 return err
41 }
42 defer os.Remove(f.Name())
43 _, err = f.Write(b)
44 if err == nil {
45 err = f.Sync()
46 }
47 err = errors.Join(err, f.Close())
48 if err != nil {
49 return err
50 }
51 if err = os.Rename(f.Name(), filepath.Join(d.dir, n)); err != nil {
52 return err
53 }
54 parent, err := os.Open(d.dir)
55 if err != nil {
56 return err
57 }
58 err = errors.Join(parent.Sync(), parent.Close())
59 if err != nil {
60 return err
61 }
62 if d.after != nil {
63 d.after(b)
64 }
65 return nil
66}
67func (d *plan14ColdDisk) Names() ([]string, error) {
68 es, err := os.ReadDir(d.dir)
69 var names []string
70 for _, e := range es {
71 names = append(names, e.Name())
72 }
73 return names, err
74}
75func (d *plan14ColdDisk) Lock(context.Context, string) (func(), error) { return func() {}, nil }
76func (d *plan14ColdDisk) Close() error { return nil }
77func plan14ColdSetup(t *testing.T) plan14ColdManifest {
78 t.Setenv("CARLOS_HOST_CLAIM", "1")
79 source, store, initial := workFixture(t)
80 ctx := t.Context()
81 base := t.TempDir()
82 rootDir := filepath.Join(base, "root")
83 if err := os.Mkdir(rootDir, 0700); err != nil {
84 t.Fatal(err)
85 }
86 disk := &plan14ColdDisk{dir: rootDir}
87 admit := func(old hostclaim.Observed, generation string) hostclaim.Observed {
88 next, err := hostclaim.Update(ctx, store, old, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: old.Claim.Op.ID, ObservedPhase: old.Claim.Op.Phase, ObservedSession: old.Claim.Holder.Session, ObservedGeneration: old.Claim.Holder.Generation, Box: "box", Region: "ie", Mode: "cold", NewGeneration: generation}))
89 if err != nil {
90 t.Fatal(err)
91 }
92 return next
93 }
94 old := admit(initial, "first")
95 if err := source.Guard.Activate(ctx, old.Host, "box", nil); err != nil {
96 t.Fatal(err)
97 }
98 source.Guard.Root = hostguard.NewRootAuthority(disk)
99 holder := old.Claim.Holder
100 if err := source.Guard.EstablishRoot(ctx, hostguard.RootInstallation{Host: old.Host, Incarnation: holder.Incarnation, Stamp: holder.Stamp, Account: holder.Account, App: holder.App, Record: holder.Key, Box: "box", Enrollment: "enrollment", Migration: 1, InventoryRevision: 1}, func(*hostclaim.Claim) error { return nil }); err != nil {
101 t.Fatal(err)
102 }
103 prior := hostguard.FenceTarget{FenceID: "fence", Region: "ie", Host: old.Host, Incarnation: old.Claim.Holder.Incarnation, Stamp: old.Claim.Holder.Stamp, Session: old.Claim.Holder.Session, Box: "box", Enrollment: "enrollment", BootID: "boot", ControllerID: "controller", ClaimETag: old.ETag, DirectoryRevision: "1", Op: old.Claim.Op.ID, Phase: old.Claim.Op.Phase}
104 if _, err := source.Guard.BeginFence(ctx, prior); err != nil {
105 t.Fatal(err)
106 }
107 if err := source.Guard.CompleteFence(ctx, prior, []byte("verified-root-receipt"), time.Now().UTC(), func(context.Context, hostguard.FenceTarget, []byte, time.Time) error { return nil }); err != nil {
108 t.Fatal(err)
109 }
110 reg, err := registry.Open(filepath.Join(base, "registry.db"))
111 if err != nil {
112 t.Fatal(err)
113 }
114 t.Cleanup(func() { reg.Close() })
115 inventory, err := OpenStopInventoryDB(filepath.Join(base, "inventory.db"))
116 if err != nil {
117 t.Fatal(err)
118 }
119 t.Cleanup(func() { inventory.Close() })
120 launches, err := OpenLaunchJournal(filepath.Join(base, "launches.db"))
121 if err != nil {
122 t.Fatal(err)
123 }
124 t.Cleanup(func() { launches.Close() })
125 owner := registry.ClaimOwnership{ClaimIncarnation: prior.Incarnation, ClaimStamp: prior.Stamp, ClaimOwner: "old-owner", ClaimRowID: "old-route"}
126 dbPath := filepath.Join(source.Guard.DataDir, old.Host+".db")
127 if err = os.WriteFile(dbPath, []byte("untrusted-local-database"), 0600); err != nil {
128 t.Fatal(err)
129 }
130 if err = reg.Upsert(registry.Route{Host: old.Host, Kind: "instance", Socket: "/run/example.sock", DBPath: dbPath, ClaimOwnership: owner, HoldOp: "held-operation"}); err != nil {
131 t.Fatal(err)
132 }
133 if err = reg.UpsertTimer(registry.Timer{Host: old.Host, Name: "daily", Kind: "cron", Spec: "0 0 * * *", NextAt: 123, Attempts: 2}); err != nil {
134 t.Fatal(err)
135 }
136 _, err = reg.GetTimer(old.Host, "daily")
137 if err != nil {
138 t.Fatal(err)
139 }
140 parker := TypedParker{CapturePersistent: true, Roots: map[string]string{"data": source.Guard.DataDir}, ParkedRoot: source.Guard.DataDir, Incarnation: prior.Incarnation, Host: prior.Host, Retirement: "capture"}
141 artifact, err := parker.CaptureArtifact("data", old.Host+".db", owner.ClaimOwner, ArtifactPath)
142 if err != nil {
143 t.Fatal(err)
144 }
145 in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Identity: artifact.Identity, Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}}
146 if err = inventory.WriteStopInventory(ctx, in); err != nil {
147 t.Fatal(err)
148 }
149 successor := admit(old, "second")
150 next := ColdSuccessor{Host: old.Host, Incarnation: prior.Incarnation, Stamp: successor.Claim.Holder.Stamp, Session: successor.Claim.Holder.Session, Generation: successor.Claim.Holder.Generation, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", ETag: successor.ETag, Op: successor.Claim.Op.ID, Phase: successor.Claim.Op.Phase}
151 handover := ColdHandover{Guard: source.Guard, Claims: store, Registry: reg, Inventory: inventory, Launches: launches, Roots: map[string]string{"data": source.Guard.DataDir}, JournalDir: filepath.Join(base, "handover"), VerifyReceipt: func(context.Context, hostguard.FenceReceipt) error { return nil }}
152
153 durable := release.NewDirStore(filepath.Join(base, "deployment"))
154 keys, err := store.List(ctx, "")
155 if err != nil {
156 t.Fatal(err)
157 }
158 for _, key := range keys {
159 body, _, err := store.Get(ctx, key)
160 if err != nil {
161 t.Fatal(err)
162 }
163 if _, err = durable.Put(ctx, key, body, ""); err != nil {
164 t.Fatal(err)
165 }
166 }
167 manifest := plan14ColdManifest{Base: base, Data: source.Guard.DataDir, Locks: source.Guard.LockDir, Prior: prior, Next: next}
168 raw, _ := json.Marshal(manifest)
169 if err = os.WriteFile(filepath.Join(base, "fixture.json"), raw, 0600); err != nil {
170 t.Fatal(err)
171 }
172 _ = handover
173 return manifest
174}
175func plan14ColdOpen(t *testing.T, m plan14ColdManifest) (ColdHandover, *plan14ColdDisk) {
176 t.Helper()
177 reg, err := registry.Open(filepath.Join(m.Base, "registry.db"))
178 if err != nil {
179 t.Fatal(err)
180 }
181 t.Cleanup(func() { reg.Close() })
182 inventory, err := OpenStopInventoryDB(filepath.Join(m.Base, "inventory.db"))
183 if err != nil {
184 t.Fatal(err)
185 }
186 t.Cleanup(func() { inventory.Close() })
187 launches, err := OpenLaunchJournal(filepath.Join(m.Base, "launches.db"))
188 if err != nil {
189 t.Fatal(err)
190 }
191 t.Cleanup(func() { launches.Close() })
192 disk := &plan14ColdDisk{dir: filepath.Join(m.Base, "root")}
193 store := release.NewDirStore(filepath.Join(m.Base, "deployment"))
194 g := hostguard.Guard{DataDir: m.Data, LockDir: m.Locks, Claims: hostguard.StoreReader{Store: store}, Root: hostguard.NewRootAuthorityWithInventory(disk, func(ctx context.Context, host string, c hostguard.Captured) (uint64, error) {
195 _, revision, err := inventory.Snapshot(ctx, host, c)
196 return revision, err
197 })}
198 return ColdHandover{Guard: g, Claims: store, Registry: reg, Inventory: inventory, Launches: launches, Roots: map[string]string{"data": m.Data}, JournalDir: filepath.Join(m.Base, "handover"), VerifyReceipt: func(_ context.Context, r hostguard.FenceReceipt) error {
199 if string(r.Bytes) != "verified-root-receipt" || r.Target != m.Prior {
200 return ErrChanged
201 }
202 return nil
203 }}, disk
204}
205func TestPlan14ColdHandoverProcessBoundaries(t *testing.T) {
206 if phase := os.Getenv("PLAN14_COLD_PHASE"); phase != "" {
207 t.Setenv("CARLOS_HOST_CLAIM", "1")
208 raw, err := os.ReadFile(filepath.Join(os.Getenv("PLAN14_COLD_DIR"), "fixture.json"))
209 if err != nil {
210 t.Fatal(err)
211 }
212 var m plan14ColdManifest
213 if err = json.Unmarshal(raw, &m); err != nil {
214 t.Fatal(err)
215 }
216 h, disk := plan14ColdOpen(t, m)
217 h.checkpoint = func(point string) {
218 if phase == point {
219 os.Exit(73)
220 }
221 }
222 classify := func(raw []byte) string {
223 var r struct {
224 Barrier *hostguard.FenceTarget
225 State struct {
226 Stamp uint64 `json:"active_stamp"`
227 }
228 }
229 if json.Unmarshal(raw, &r) != nil {
230 t.Fatal("bad root")
231 }
232 if r.State.Stamp == 0 {
233 return "retired"
234 }
235 if r.Barrier != nil {
236 return "barrier"
237 }
238 if r.State.Stamp == m.Next.Stamp {
239 return "commit"
240 }
241 return "old"
242 }
243 disk.before = func(raw []byte) {
244 if phase == "before-root-"+classify(raw) {
245 os.Exit(73)
246 }
247 }
248 disk.after = func(raw []byte) {
249 if phase == "after-root-"+classify(raw) {
250 os.Exit(73)
251 }
252 }
253 err = h.Run(t.Context(), m.Prior, m.Next)
254 t.Fatal("boundary not reached", phase, err)
255 }
256 for _, phase := range []string{"intent", "parked", "inventory-intent", "rows", "inventory-materialized", "complete-journal", "before-root-barrier", "after-root-barrier", "before-root-retired", "after-root-retired", "before-root-commit", "after-root-commit"} {
257 t.Run(phase, func(t *testing.T) {
258 m := plan14ColdSetup(t)
259 cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14ColdHandoverProcessBoundaries$")
260 cmd.Dir = m.Base
261 cmd.Env = append(os.Environ(), "PLAN14_COLD_PHASE="+phase, "PLAN14_COLD_DIR="+m.Base)
262 output, err := cmd.CombinedOutput()
263 exit, ok := err.(*exec.ExitError)
264 if !ok || exit.ExitCode() != 73 {
265 t.Fatalf("crash: %v %s", err, output)
266 }
267 h, _ := plan14ColdOpen(t, m)
268 effects := 0
269 allow := func(*hostclaim.Claim) error { return nil }
270 prior := hostguard.Captured{Incarnation: m.Prior.Incarnation, Stamp: m.Prior.Stamp}
271 next := hostguard.Captured{Incarnation: m.Next.Incarnation, Stamp: m.Next.Stamp}
272 if err = h.Guard.With(t.Context(), m.Prior.Host, prior, allow, func() error { effects++; return nil }); err == nil || effects != 0 {
273 t.Fatal("predecessor granted", err, effects)
274 }
275 if phase != "after-root-commit" {
276 if err = h.Guard.With(t.Context(), m.Next.Host, next, allow, func() error { effects++; return nil }); err == nil || effects != 0 {
277 t.Fatal("unfinished successor granted", err, effects)
278 }
279 }
280 if err = h.Recover(t.Context()); err != nil {
281 t.Fatal("resume", err)
282 }
283 if err = h.Run(t.Context(), m.Prior, m.Next); err != nil {
284 t.Fatal("replay", err)
285 }
286 if err = h.Guard.With(t.Context(), m.Next.Host, next, allow, func() error { effects++; return nil }); err != nil || effects != 1 {
287 t.Fatal("successor", err, effects)
288 }
289 receipt, err := h.Guard.FenceReceiptFor(t.Context(), m.Prior)
290 if err != nil || h.VerifyReceipt(t.Context(), receipt) != nil || receipt.Target.BootID != "boot" {
291 t.Fatal("old receipt changed", receipt, err)
292 }
293 if _, err = os.Stat(filepath.Join(m.Data, m.Prior.Host+".db")); !os.IsNotExist(err) {
294 t.Fatal("old data reused", err)
295 }
296 row, err := h.Registry.Get(m.Next.Host)
297 if err != nil || row.ClaimStamp != m.Next.Stamp {
298 t.Fatal("rows", row, err)
299 }
300 view, err := h.Guard.Root.View(t.Context(), m.Next.Host, next)
301 if err != nil || view.Barrier != nil || view.Installation.InventoryRevision == 0 {
302 t.Fatal("root commit", view, err)
303 }
304 })
305 }
306}
307