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 "net"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/cgroup"
18 "github.com/carlosframework/platform/internal/hostbox"
19 "github.com/carlosframework/platform/internal/hostclaim"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/instancectl"
22 "github.com/carlosframework/platform/internal/provision"
23 "github.com/carlosframework/platform/internal/release"
24 "github.com/carlosframework/platform/internal/rootstate"
25 "github.com/carlosframework/platform/internal/safefile"
26)
27
28type generationDisk struct {
29 *rollbackDisk
30 fail int
31}
32
33func (s *generationDisk) Write(k string, b []byte) error {
34 if err := s.rollbackDisk.Write(k, b); err != nil {
35 return err
36 }
37 if s.fail > 0 {
38 s.fail--
39 return os.ErrPermission
40 }
41 return nil
42}
43
44func generationLaunchFixture(t *testing.T) (*launchFixture, *generationDisk, provision.Record) {
45 return generationLaunchFixtureHost(t, "shop.example")
46}
47
48func generationLaunchFixtureHost(t *testing.T, host string) (*launchFixture, *generationDisk, provision.Record) {
49 t.Helper()
50 f := newLaunchFixtureHost(t, servingOp(), host)
51 store := release.NewDirStore(t.TempDir())
52 rec := provision.Record{Host: f.target.Host, Account: "bab", App: "shop", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)}
53 plan14PutRecord(t, store, rec)
54 inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
55 if err != nil {
56 t.Fatal(err)
57 }
58 previous, err := f.journal.Read(t.Context(), f.target)
59 if err != nil {
60 t.Fatal(err)
61 }
62 f.target.Incarnation = inc
63 f.req.Reservation.Target = f.target
64 previous.Target = f.target
65 previous.Groups[0].RelativePath = filepath.Join("system.slice/carlos-edge.service", cgroup.ExecName(claimContainmentName(f.target)))
66 if err := f.journal.Prelaunch(t.Context(), previous); err != nil {
67 t.Fatal(err)
68 }
69 f.claims.set(func(c *hostclaim.Claim) {
70 c.Holder.Incarnation = inc
71 c.Holder.Account = rec.Account
72 c.Holder.App = rec.App
73 c.Holder.Key = provision.RecordKey(rec.Account, rec.App, rec.Host)
74 c.Holder.Admissions = []hostclaim.AdmissionRecord{{Box: f.target.Box, Region: f.target.Region, Session: f.target.Session, Generation: c.Holder.Generation}}
75 })
76 f.root.Guard.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()})
77 in := hostguard.RootInstallation{Artifacts: hostguard.InstallationArtifacts{Schema: 1, StateDirectory: true}, Host: rec.Host, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Incarnation: inc, Stamp: f.target.Stamp, Box: f.target.Box, Enrollment: f.target.Enrollment, Migration: 1, InventoryRevision: 1}
78 if err := f.root.Guard.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil {
79 t.Fatal(err)
80 }
81 history := &release.RootHistory{Store: &rollbackDisk{dir: t.TempDir()}}
82 if err := history.RecordCreation(release.HistoryBinding{Host: rec.Host, Account: rec.Account, App: rec.App}, rootHistoryInstallation(in)); err != nil {
83 t.Fatal(err)
84 }
85 f.root.history = func(ctx context.Context, fn func(*release.RootHistory) error) error { return fn(history) }
86 f.root.installedProgram = func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) {
87 return "observed/program", nil
88 }
89 opt := t.TempDir()
90 body := []byte("the signed build")
91 if err := release.StageArtifact(opt, rec.Account, "v1", rec.App, body); err != nil {
92 t.Fatal(err)
93 }
94 path := release.VersionPath(opt, rec.Account, "v1", rec.App)
95 file, err := os.Open(path)
96 if err != nil {
97 t.Fatal(err)
98 }
99 id, err := rootstate.Identity(file)
100 file.Close()
101 if err != nil {
102 t.Fatal(err)
103 }
104 sum := sha256.Sum256(body)
105 f.kernel.path = path
106 f.kernel.digest = hex.EncodeToString(sum[:])
107 f.kernel.want = hostbox.LaunchFile{Device: id.Device, Inode: id.Inode}
108 f.kernel.observed.Executable = f.kernel.want
109 f.kernel.observed.Running = f.kernel.want
110 f.req.Executable = path
111 f.req.Reservation.Executable = f.kernel.want
112 f.req.Spec.Executable = path
113 f.req.Spec.Digest = f.kernel.digest
114 digest, err := f.req.Spec.Hash()
115 if err != nil {
116 t.Fatal(err)
117 }
118 f.req.Reservation.Inputs = digest
119 f.claims.set(func(c *hostclaim.Claim) {
120 a := *c.Holder.WakeRetry.Attempt
121 a.LaunchInputs = digest
122 r := *c.Holder.WakeRetry
123 r.Attempt = &a
124 c.Holder.WakeRetry = &r
125 })
126 pub, priv, err := ed25519.GenerateKey(nil)
127 if err != nil {
128 t.Fatal(err)
129 }
130 m := release.Manifest{App: rec.App, Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}}
131 if err := release.Ship(t.Context(), store, rec.Account, m, map[string][]byte{release.AdoptPlatform(): body}); err != nil {
132 t.Fatal(err)
133 }
134 if err := release.Promote(t.Context(), store, priv, rec.Account, rec.App, "canary/console", "v1"); err != nil {
135 t.Fatal(err)
136 }
137 disk := &generationDisk{rollbackDisk: &rollbackDisk{dir: t.TempDir()}}
138 f.root.generations = &rootGenerationSource{store: store, optDir: opt, pub: func() (ed25519.PublicKey, error) { return pub, nil }, open: func() (rootstate.Store, error) { return disk, nil }, openFile: func(p string) (*os.File, error) { return safefile.OpenFile(p, os.O_RDONLY, 0) }}
139 return f, disk, rec
140}
141
142func readGenerationLaunch(t *testing.T, d *generationDisk) release.GenerationReservation {
143 t.Helper()
144 names, err := d.Names()
145 if err != nil || len(names) != 1 {
146 t.Fatalf("reservations: %v %v", names, err)
147 }
148 raw, err := d.Read(names[0])
149 if err != nil {
150 t.Fatal(err)
151 }
152 var r release.GenerationReservation
153 if err := json.Unmarshal(raw, &r); err != nil {
154 t.Fatal(err)
155 }
156 return r
157}
158
159func TestPlan14GenerationReservedBeforeClaimSpawn(t *testing.T) {
160 f, d, _ := generationLaunchFixture(t)
161 checked := false
162 f.units.beforeCheck = func() {
163 r := readGenerationLaunch(t, d)
164 if r.ID == "" || r.Binding.Path != f.req.Executable || r.Binding.Digest != f.req.Spec.Digest || r.Binding.Binding.Account != "bab" || r.Binding.Binding.App != "shop" || r.Process.PID != 0 {
165 t.Fatalf("pre-spawn record: %+v", r)
166 }
167 checked = true
168 }
169 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil {
170 t.Fatal(err)
171 }
172 if !checked {
173 t.Fatal("spawn boundary was not checked")
174 }
175 r := readGenerationLaunch(t, d)
176 if r.Process.PID != f.units.started.MainPID || r.Process.TenantPID != f.kernel.observed.TenantPID || r.Process.GroupIdentity.Inode != f.kernel.observed.Group.Inode {
177 t.Fatalf("missing process evidence: %+v", r)
178 }
179 if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil {
180 t.Fatal(err)
181 }
182 if after := readGenerationLaunch(t, d); after != r {
183 t.Fatal("unit settlement retired the generation pin")
184 }
185}
186
187func TestPlan14GenerationLaunchRefusals(t *testing.T) {
188 for _, attack := range []string{"open", "sync", "no-lock", "unsigned", "wrong-account", "wrong-digest", "tree-replaced", "record-changed", "reservation-erased"} {
189 t.Run(attack, func(t *testing.T) {
190 f, d, rec := generationLaunchFixture(t)
191 switch attack {
192 case "open":
193 f.root.generations.open = func() (rootstate.Store, error) { return nil, os.ErrPermission }
194 case "sync":
195 d.fail = 1
196 case "no-lock":
197 f.root.history = nil
198 case "unsigned":
199 f.root.generations.pub = func() (ed25519.PublicKey, error) { p, _, e := ed25519.GenerateKey(nil); return p, e }
200 case "wrong-account":
201 rec.Account = "bap"
202 plan14PutRecord(t, f.root.generations.store, rec)
203 if err := f.root.generations.store.Delete(t.Context(), provision.RecordKey("bab", rec.App, rec.Host)); err != nil {
204 t.Fatal(err)
205 }
206 case "wrong-digest":
207 if err := os.WriteFile(f.req.Executable, []byte("replaced"), 0755); err != nil {
208 t.Fatal(err)
209 }
210 case "tree-replaced":
211 f.units.beforeCheck = func() {
212 dir := filepath.Dir(f.req.Executable)
213 if err := os.Rename(dir, dir+".old"); err != nil {
214 t.Fatal(err)
215 }
216 if err := os.Mkdir(dir, 0755); err != nil {
217 t.Fatal(err)
218 }
219 if err := os.Rename(filepath.Join(dir+".old", filepath.Base(f.req.Executable)), f.req.Executable); err != nil {
220 t.Fatal(err)
221 }
222 }
223 case "record-changed":
224 f.units.beforeCheck = func() { rec.Environment = "changed"; plan14PutRecord(t, f.root.generations.store, rec) }
225 case "reservation-erased":
226 f.units.beforeCheck = func() {
227 names, err := d.Names()
228 if err != nil {
229 t.Fatal(err)
230 }
231 for _, n := range names {
232 if err := d.Remove(n); err != nil {
233 t.Fatal(err)
234 }
235 }
236 }
237 }
238 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil {
239 t.Fatal("unreserved launch succeeded")
240 }
241 if n, _ := f.units.counts(); n != 0 {
242 t.Fatalf("refused launch started %d units", n)
243 }
244 })
245 }
246}
247
248func TestPlan14GenerationStartedSyncFailureRetainsPin(t *testing.T) {
249 f, d, _ := generationLaunchFixture(t)
250 f.kernel.observe = func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) {
251 d.fail = 1
252 return f.kernel.observed, nil
253 }
254 if _, err := f.root.Serve(t.Context(), f.peer, f.req); err == nil {
255 t.Fatal("unsynced launch reported success")
256 }
257 if n, stops := f.units.counts(); n != 1 || stops != 1 {
258 t.Fatalf("starts=%d stops=%d", n, stops)
259 }
260 r := readGenerationLaunch(t, d)
261 if r.ID == "" || r.Binding.Path != f.req.Executable {
262 t.Fatal("lost failed launch pin")
263 }
264 d.fail = 0
265 if _, err := (&release.RootGenerations{Store: d}).Read(r.Binding); err != nil {
266 t.Fatal(err)
267 }
268}
269
270func TestPlan14GenerationReservationProcessCrash(t *testing.T) {
271 if boundary := os.Getenv("PLAN14_GENERATION_CRASH"); boundary != "" {
272 f, d, _ := generationLaunchFixture(t)
273 d.dir = os.Getenv("PLAN14_GENERATION_DIR")
274 d.afterWrite = func(_ string, raw []byte) {
275 var r release.GenerationReservation
276 if err := json.Unmarshal(raw, &r); err != nil {
277 t.Fatal(err)
278 }
279 if boundary == "reserved" && r.Revision == 1 || boundary == "observed" && r.Revision == 2 {
280 os.Exit(73)
281 }
282 }
283 if boundary == "before-spawn" {
284 f.units.beforeCheck = func() { os.Exit(73) }
285 }
286 _, err := f.root.Serve(t.Context(), f.peer, f.req)
287 t.Fatal("crash boundary not reached", err)
288 }
289 for _, boundary := range []string{"reserved", "before-spawn", "observed"} {
290 t.Run(boundary, func(t *testing.T) {
291 dir := t.TempDir()
292 cmd := exec.Command(os.Args[0], "-test.run=^TestPlan14GenerationReservationProcessCrash$", "-test.count=1")
293 cmd.Env = append(os.Environ(), "PLAN14_GENERATION_CRASH="+boundary, "PLAN14_GENERATION_DIR="+dir)
294 out, err := cmd.CombinedOutput()
295 var exit *exec.ExitError
296 if !errors.As(err, &exit) || exit.ExitCode() != 73 {
297 t.Fatalf("child: %v %s", err, out)
298 }
299 d := &generationDisk{rollbackDisk: &rollbackDisk{dir: dir}}
300 r := readGenerationLaunch(t, d)
301 got, err := (&release.RootGenerations{Store: d}).Read(r.Binding)
302 if err != nil || got != r || r.ID == "" || r.Binding.Executable.Inode == 0 || r.Binding.TreeIdentity.Inode == 0 {
303 t.Fatalf("crash lost pin: %+v %v", got, err)
304 }
305 if (r.Process.PID > 0) != (boundary == "observed") {
306 t.Fatalf("wrong process evidence: %+v", r)
307 }
308 })
309 }
310}
311
312func TestPlan14GenerationProductionWiring(t *testing.T) {
313 t.Setenv("PATH", t.TempDir())
314 oldOpen, oldShapes := openClaimUnitBindings, productionClaimUnitShapes
315 oldMaterial := openClaimUnitMaterializations
316 openClaimUnitBindings = func() (rootstate.Store, error) { return &rollbackMemory{files: map[string][]byte{}}, nil }
317 openClaimUnitMaterializations = func() (rootstate.Store, error) { return &rollbackMemory{files: map[string][]byte{}}, nil }
318 productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil }
319 t.Cleanup(func() {
320 openClaimUnitBindings, productionClaimUnitShapes = oldOpen, oldShapes
321 openClaimUnitMaterializations = oldMaterial
322 })
323 f, _, _ := generationLaunchFixture(t)
324 root := ClaimRootHTTP{Launches: f.journal, Stop: ClaimStopHTTP{Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }, Coordinator: ClaimStopCoordinator{Kernel: stopKernelFunc(nil), Guard: f.root.Guard}}}
325 assembly := &ClaimAssembly{Claims: f.root.generations.store, ManagedRoots: map[string]string{"opt": f.root.generations.optDir, "data": "/data"}, Local: f.root.Local}
326 ctx := context.WithValue(t.Context(), claimEntryKey{}, claimEntry{Enabled: true, FlagRead: true, Assembly: assembly})
327 handler, err := WithProductionClaimLaunch(ctx, root)
328 if err != nil {
329 t.Fatal(err)
330 }
331 launch := handler.(ClaimRootWithLaunch).Launch
332 if _, ok := launch.Units.(claimUnitDispatch); !ok || launch.unitBindings == nil {
333 t.Fatal("production unit dispatch missing")
334 }
335 if err := assembly.Close(t.Context()); err != nil {
336 t.Fatal(err)
337 }
338 if launch.generations == nil || launch.generations.open == nil || launch.generations.openFile == nil || launch.generations.pub == nil || launch.generations.store != assembly.Claims || launch.history == nil {
339 t.Fatal("production launch lacks a mandatory root reservation")
340 }
341}
342