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 "fmt"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19type proofRootEnv struct {
20 root proofRoot
21 store release.Store
22 bake string
23 keyFile string
24 pub ed25519.PublicKey
25 pubB64 string
26 sha string
27 now time.Time
28 logs []string
29}
30
31func provePolicy(keyFile string) string {
32 return fmt.Sprintf(`{"schema":1,"prove":{"apps":["console"],"prover":"dev","after":"15m","key_file":%q}}`, keyFile)
33}
34
35// newProofRootEnv: one named-unit console route, bound, running its live
36// build for 20 minutes, ready. policy builds the policy file from the key
37// file's path; nil writes no policy file.
38func newProofRootEnv(t *testing.T, policy func(keyFile string) string) *proofRootEnv {
39 t.Helper()
40 opt, bake, etc := t.TempDir(), t.TempDir(), t.TempDir()
41 liveTo(t, opt, observedHost, "v1")
42 reg := restartTestRegistry(t)
43 rt := registry.Route{Host: observedHost, Kind: "instance", App: "console",
44 Channel: "canary/console", Unit: "console.service", Status: "ready"}
45 if err := reg.Upsert(rt); err != nil {
46 t.Fatal(err)
47 }
48 seed, pubB64, err := release.GenerateKey()
49 if err != nil {
50 t.Fatal(err)
51 }
52 pub, err := release.ParsePublicKey(pubB64, "test")
53 if err != nil {
54 t.Fatal(err)
55 }
56 keyFile := filepath.Join(etc, "proving.key")
57 if err := os.WriteFile(keyFile, []byte(seed+"\n"), 0o600); err != nil {
58 t.Fatal(err)
59 }
60 policyPath := filepath.Join(etc, "proof-policy.json")
61 if policy != nil {
62 if err := os.WriteFile(policyPath, []byte(policy(keyFile)), 0o644); err != nil {
63 t.Fatal(err)
64 }
65 }
66 staged := filepath.Join(opt, "versions", "v1", "console")
67 sha, err := release.FileSHA256(staged)
68 if err != nil {
69 t.Fatal(err)
70 }
71 e := &proofRootEnv{store: release.NewDirStore(t.TempDir()), bake: bake, keyFile: keyFile, pub: pub, pubB64: pubB64,
72 sha: sha, now: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)}
73 units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "i1", MainPID: 7,
74 ExecPath: release.LiveLink(opt, observedHost), ActiveEnterTimestamp: e.now.Add(-20 * time.Minute)}}}
75 e.root = proofRoot{
76 mintCensus: func() (bool, error) { return true, nil },
77 obs: proofObserver{sys: units, procExe: func(int) string { return staged }, optDir: opt,
78 now: func() time.Time { return e.now }, resolve: proofFixtureResolver(rt), ready: func(registry.Route) func() error { return func() error { return nil } }},
79 reg: reg, store: e.store, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey,
80 logf: func(f string, a ...any) { e.logs = append(e.logs, fmt.Sprintf(f, a...)) },
81 }
82 return e
83}
84
85func (e *proofRootEnv) proof(t *testing.T) *release.Proof {
86 t.Helper()
87 p, err := release.GetProof(context.Background(), e.store, "bab", "console", e.sha, "dev")
88 if err != nil {
89 return nil
90 }
91 return p
92}
93
94// signed is a proof for the env's live build, signed with its proving key.
95func (e *proofRootEnv) signed(t *testing.T, provenAt time.Time) *release.Proof {
96 t.Helper()
97 seed, err := os.ReadFile(e.keyFile)
98 if err != nil {
99 t.Fatal(err)
100 }
101 key, err := release.ParseSigningKey(string(seed), "test")
102 if err != nil {
103 t.Fatal(err)
104 }
105 p, err := release.SignProof(key, release.Proof{App: "console", Platform: release.AdoptPlatform(), SHA256: e.sha,
106 Version: "v1", Prover: "dev", ActiveSince: e.now.Add(-time.Hour), ProvenAt: provenAt})
107 if err != nil {
108 t.Fatal(err)
109 }
110 return p
111}
112
113func TestProofRootMintsWhenEveryConditionHolds(t *testing.T) {
114 t.Parallel()
115 e := newProofRootEnv(t, provePolicy)
116 e.root.run(context.Background())
117 p := e.proof(t)
118 if p == nil {
119 t.Fatalf("no proof minted; logs: %v", e.logs)
120 }
121 if err := p.Check(e.pub, "console", release.AdoptPlatform(), e.sha, "dev"); err != nil {
122 t.Fatalf("minted proof: %v", err)
123 }
124 if p.Version != "v1" || !p.ActiveSince.Equal(e.now.Add(-20*time.Minute)) || !p.ProvenAt.Equal(e.now) {
125 t.Fatalf("proof = %+v", p)
126 }
127}
128
129func TestProofRootDoesNotMintARefusedBuild(t *testing.T) {
130 e := newProofRootEnv(t, provePolicy)
131 if err := release.WriteRefusal(e.bake, release.Refusal{Host: observedHost, App: "console", Version: "v1", SHA256: e.sha}); err != nil {
132 t.Fatal(err)
133 }
134 e.root.run(context.Background())
135 if e.proof(t) != nil {
136 t.Fatal("minted a proof for a build a refusal record names")
137 }
138}
139
140// D3-held fix round 1: a move's held refusal (MoveOp set) is scoped to
141// that move's held stage and never stops this box minting a proof for a
142// build it runs. Since final review finding 3 it lives in the held
143// namespace, which minting never reads.
144func TestProofRootMintsPastAMovesHeldRefusal(t *testing.T) {
145 e := newProofRootEnv(t, provePolicy)
146 if err := release.WriteRefusal(release.HeldRefusalBase(e.bake), release.Refusal{Host: "held.example.com", App: "console", Version: "v1", SHA256: e.sha,
147 Kind: release.RefusalNotProven, MoveOp: "mv_abcdefghijklmnopqrstuvwxyz"}); err != nil {
148 t.Fatal(err)
149 }
150 e.root.run(context.Background())
151 if e.proof(t) == nil {
152 t.Fatalf("a move's held refusal stopped the mint; logs: %v", e.logs)
153 }
154}
155
156func TestProofRootNeverOverwritesAProof(t *testing.T) {
157 t.Parallel()
158 e := newProofRootEnv(t, provePolicy)
159 first := e.signed(t, e.now.Add(-time.Minute))
160 if err := release.PutProof(context.Background(), e.store, "bab", first); err != nil {
161 t.Fatal(err)
162 }
163 e.root.run(context.Background())
164 if p := e.proof(t); p == nil || !p.ProvenAt.Equal(first.ProvenAt) {
165 t.Fatalf("proof = %+v, want the first one untouched", p)
166 }
167}
168
169// racedStore hides one key from the first Get, as if another pass minted it
170// between this pass's existence check and its write.
171type racedStore struct {
172 release.Store
173 hidden string
174 gets int
175}
176
177func (s *racedStore) Get(ctx context.Context, key string) ([]byte, string, error) {
178 if key == s.hidden {
179 s.gets++
180 if s.gets == 1 {
181 return nil, "", fmt.Errorf("raced: %s: %w", key, os.ErrNotExist)
182 }
183 }
184 return s.Store.Get(ctx, key)
185}
186
187// PutProof is create-only: a second mint of one build carries a different
188// proven_at and conflicts. That conflict means "already minted", not an error.
189func TestProofRootTreatsAMintRaceAsAlreadyMinted(t *testing.T) {
190 e := newProofRootEnv(t, provePolicy)
191 first := e.signed(t, e.now.Add(-time.Minute))
192 if err := release.PutProof(context.Background(), e.store, "bab", first); err != nil {
193 t.Fatal(err)
194 }
195 e.root.store = &racedStore{Store: e.store, hidden: release.ProofKey("bab", "console", e.sha, "dev")}
196 e.root.run(context.Background())
197 logs := strings.Join(e.logs, "\n")
198 if strings.Contains(logs, "writing the proof") {
199 t.Fatalf("a mint race was logged as a write failure: %v", e.logs)
200 }
201 if strings.Contains(logs, "proof: minted") {
202 t.Fatalf("a mint race was logged as this pass's mint: %v", e.logs)
203 }
204 if p := e.proof(t); p == nil || !p.ProvenAt.Equal(first.ProvenAt) {
205 t.Fatalf("proof = %+v, want the first one untouched", p)
206 }
207}
208
209func TestProofRootRefusesAKeyOthersCanRead(t *testing.T) {
210 e := newProofRootEnv(t, provePolicy)
211 if err := os.Chmod(e.keyFile, 0o644); err != nil {
212 t.Fatal(err)
213 }
214 e.root.run(context.Background())
215 if e.proof(t) != nil {
216 t.Fatal("minted with a world-readable key")
217 }
218 if !strings.Contains(strings.Join(e.logs, "\n"), "readable by root only") {
219 t.Fatalf("logs = %v, want the key-mode refusal", e.logs)
220 }
221}
222
223// Task 2's carry: a policy file that does not parse is closed. A proving box
224// with a typo mints nothing, and says so on every pass.
225func TestProofRootBrokenPolicyMintsNothingAndSaysSoEveryPass(t *testing.T) {
226 e := newProofRootEnv(t, func(keyFile string) string {
227 return strings.Replace(provePolicy(keyFile), `"schema":1`, `"schema":1,"requier":[]`, 1)
228 })
229 for pass := 0; pass < 2; pass++ {
230 e.logs = nil
231 e.root.hourly = pass == 1
232 e.root.run(context.Background())
233 if e.proof(t) != nil {
234 t.Fatalf("pass %d minted under a policy that does not parse", pass)
235 }
236 if !strings.Contains(strings.Join(e.logs, "\n"), "nothing is minted until it parses") {
237 t.Fatalf("pass %d logs = %v, want the policy error", pass, e.logs)
238 }
239 }
240}
241
242type noReadStore struct {
243 release.Store
244 t *testing.T
245}
246
247func (s noReadStore) Get(ctx context.Context, key string) ([]byte, string, error) {
248 s.t.Errorf("bucket GET %s with no policy file", key)
249 return s.Store.Get(ctx, key)
250}
251
252// Review focus 3's source: with no policy file at all, the build already
253// live enters the healthy history, and no proof is read or minted.
254func TestProofRootRecordsTheLiveBuildAsHealthy(t *testing.T) {
255 e := newProofRootEnv(t, nil)
256 e.root.store = noReadStore{Store: e.store, t: t}
257 e.root.run(context.Background())
258 h := release.ReadHealthy(e.bake, "bab", "console")
259 if len(h) != 1 || h[0].SHA256 != e.sha || h[0].How != "live" || h[0].Version != "v1" {
260 t.Fatalf("history = %+v, want the live build", h)
261 }
262 if e.proof(t) != nil {
263 t.Fatal("a proof was minted with no policy file")
264 }
265}
266
267// A box that never had the requirement (Tito, a self-hoster) has nothing
268// switched off and must stay quiet; one that had it and lost it logs hourly.
269func TestProofRootHourlyLineOnlyWhereTheRequirementWasOnce(t *testing.T) {
270 e := newProofRootEnv(t, func(string) string { return `{"schema":1}` })
271 e.root.hourly = true
272 e.root.run(context.Background())
273 if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") {
274 t.Fatalf("a box that never required a proof logged that the requirement is off: %v", e.logs)
275 }
276}
277
278func TestProofRootHourlyLineWhenRequirementIsOff(t *testing.T) {
279 e := newProofRootEnv(t, func(string) string { return `{"schema":1}` })
280 marker := filepath.Join(e.root.bakeDir, "proof-required", "console")
281 if err := os.MkdirAll(filepath.Dir(marker), 0o755); err != nil {
282 t.Fatal(err)
283 }
284 if err := os.WriteFile(marker, nil, 0o644); err != nil {
285 t.Fatal(err)
286 }
287 e.root.hourly = true
288 e.root.run(context.Background())
289 if !strings.Contains(strings.Join(e.logs, "\n"), "requirement is off for console") {
290 t.Fatalf("logs = %v, want the hourly requirement-off line", e.logs)
291 }
292 e.logs = nil
293 e.root.hourly = false
294 e.root.run(context.Background())
295 if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") {
296 t.Fatalf("a stage-only pass printed the hourly line: %v", e.logs)
297 }
298}
299
300// The marker is written by any root pass that reads a require entry, not
301// only the hourly one, so a requirement switched on and off between two
302// hourly passes still leaves its trail. Deleting the whole file (the other
303// break glass) is "off" too.
304func TestProofRootRequireEntryLeavesTheMarker(t *testing.T) {
305 t.Parallel()
306 e := newProofRootEnv(t, nil)
307 if err := os.WriteFile(e.root.policyPath, []byte(fmt.Sprintf(
308 `{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, e.pubB64)), 0o644); err != nil {
309 t.Fatal(err)
310 }
311 marker := filepath.Join(e.bake, "proof-required", "console")
312 e.root.run(context.Background())
313 if _, err := os.Stat(marker); err != nil {
314 t.Fatalf("a stage-only pass that read a require entry left no marker: %v", err)
315 }
316 if strings.Contains(strings.Join(e.logs, "\n"), "requirement is off") {
317 t.Fatalf("logged the requirement off while it is on: %v", e.logs)
318 }
319 if err := os.Remove(e.root.policyPath); err != nil {
320 t.Fatal(err)
321 }
322 e.logs = nil
323 e.root.hourly = true
324 e.root.run(context.Background())
325 if !strings.Contains(strings.Join(e.logs, "\n"), "requirement is off for console") {
326 t.Fatalf("logs = %v, want the hourly requirement-off line once the file is gone", e.logs)
327 }
328}
329
330// Final review H4: the first adoption after a requirement is switched on.
331// A has run well for two hours with an empty healthy history; B is promoted
332// with a valid dev proof. One RunAdopt-shaped run (proofRoot.around a real
333// release.Pass) adopts B, and A, the build B replaced, is in the history
334// too, so a later rollback to A does not wait for a dev proof. B enters it
335// through the settle hook.
336func TestProofRootRecordsTheReplacedBuildBeforeAdoption(t *testing.T) {
337 ctx := context.Background()
338 opt, bake, etc := t.TempDir(), t.TempDir(), t.TempDir()
339 s := release.NewDirStore(t.TempDir())
340 plat := release.AdoptPlatform()
341 pub, priv, err := ed25519.GenerateKey(nil)
342 if err != nil {
343 t.Fatal(err)
344 }
345 for ver, body := range map[string]string{"a": "build a", "b": "build b"} {
346 m := release.Manifest{App: "console", Version: ver, Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}}
347 if err := release.Ship(ctx, s, "bab", m, map[string][]byte{plat: []byte(body)}); err != nil {
348 t.Fatalf("Ship %s: %v", ver, err)
349 }
350 }
351 if err := release.StageArtifact(opt, "bab", "a", "console", []byte("build a")); err != nil {
352 t.Fatal(err)
353 }
354 if err := release.PointLive(opt, observedHost, "bab", "a", "console"); err != nil {
355 t.Fatal(err)
356 }
357 if err := release.Promote(ctx, s, priv, "bab", "console", "canary/console", "b"); err != nil {
358 t.Fatal(err)
359 }
360 reg := restartTestRegistry(t)
361 rt := registry.Route{Host: observedHost, Kind: "instance", App: "console",
362 Channel: "canary/console", Unit: "console.service", Status: "ready"}
363 if err := reg.Upsert(rt); err != nil {
364 t.Fatal(err)
365 }
366 shaA := sha256Hex("build a")
367 shaB := sha256Hex("build b")
368
369 seed, proverPub, err := release.GenerateKey()
370 if err != nil {
371 t.Fatal(err)
372 }
373 proverKey, err := release.ParseSigningKey(seed, "test")
374 if err != nil {
375 t.Fatal(err)
376 }
377 now := time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)
378 p, err := release.SignProof(proverKey, release.Proof{App: "console", Platform: plat, SHA256: shaB,
379 Version: "b", Prover: "dev", ActiveSince: now.Add(-time.Hour), ProvenAt: now})
380 if err != nil {
381 t.Fatal(err)
382 }
383 if err := release.PutProof(ctx, s, "bab", p); err != nil {
384 t.Fatal(err)
385 }
386 policyPath := filepath.Join(etc, "proof-policy.json")
387 if err := os.WriteFile(policyPath, []byte(fmt.Sprintf(
388 `{"schema":1,"require":[{"app":"console","prover":"dev","key":%q}]}`, proverPub)), 0o644); err != nil {
389 t.Fatal(err)
390 }
391
392 link := release.LiveLink(opt, observedHost)
393 units := &fakeUnits{shows: []unitProps{{ActiveState: "active", InvocationID: "a1", MainPID: 7,
394 ExecPath: link, ActiveEnterTimestamp: now.Add(-2 * time.Hour)}}}
395 procExe := func(int) string { return link } // the process runs whatever the link names
396 clock := func() time.Time { return now }
397 root := proofRoot{
398 obs: proofObserver{sys: units, procExe: procExe, optDir: opt, now: clock,
399 resolve: proofFixtureResolver(rt), ready: func(registry.Route) func() error { return func() error { return nil } }},
400 reg: reg, store: s, bakeDir: bake, policyPath: policyPath, readKey: readProvingKey,
401 logf: func(f string, a ...any) { t.Logf(f, a...) },
402 }
403 // The restart starts B's process and settles, as checkedUnitRestart
404 // would, and its settled hook records what it runs.
405 restart := func(host string) error {
406 units.shows, units.n = []unitProps{{ActiveState: "active", InvocationID: "b1", MainPID: 8,
407 ExecPath: link, ActiveEnterTimestamp: now}}, 0
408 if err := root.obs.recordSettled(bake)(rt); err != nil {
409 return err
410 }
411 return nil
412 }
413 if err := root.around(ctx, func() error {
414 return release.Pass(ctx, reg, s, pub, opt, restart, release.BakeDir(bake),
415 release.WithProofGate(release.NewProofGate(policyPath, bake)))
416 }); err != nil {
417 t.Fatal(err)
418 }
419 if v, _ := release.LiveVersion(opt, observedHost); v != "b" {
420 t.Fatalf("live = %q, want b adopted on its proof", v)
421 }
422 got := map[string]string{}
423 for _, h := range release.ReadHealthy(bake, "bab", "console") {
424 got[h.SHA256] = h.How
425 }
426 if got[shaA] != "live" {
427 t.Fatalf("history = %+v, want a (sha256 %s) recorded live before the pass replaced it", got, shaA)
428 }
429 if got[shaB] != "settle" {
430 t.Fatalf("history = %+v, want b (sha256 %s) recorded by the settle hook", got, shaB)
431 }
432}
433
434// A route the pass did not touch is observed once: the second half reuses
435// the first half's look when the link and the process are unchanged, and
436// looks again when the process was restarted under the same link.
437func TestProofRootReusesAnUnchangedObservation(t *testing.T) {
438 e := newProofRootEnv(t, provePolicy)
439 units := e.root.obs.sys.(*fakeUnits)
440 obs := e.root.recordLive(context.Background())
441 if o := obs.byHost[observedHost]; o.err != nil {
442 t.Fatalf("first half: %v", o.err)
443 }
444 rt := registry.Route{Host: observedHost, App: "console", Unit: "console.service"}
445 if !e.root.stillLive(rt, obs.byHost[observedHost]) {
446 t.Fatal("an untouched route is not still live")
447 }
448 units.shows = []unitProps{{ActiveState: "active", InvocationID: "i2", MainPID: 9,
449 ExecPath: units.shows[0].ExecPath, ActiveEnterTimestamp: e.now}}
450 units.n = 0
451 if e.root.stillLive(rt, obs.byHost[observedHost]) {
452 t.Fatal("a restarted process under the same link counts as still live")
453 }
454 e.root.mintAndLog(context.Background(), obs)
455 if e.proof(t) != nil {
456 t.Fatal("minted from the first half's look at a process that has since restarted")
457 }
458}
459
460func sha256Hex(s string) string {
461 h := sha256.Sum256([]byte(s))
462 return hex.EncodeToString(h[:])
463}
464
465func TestPlan14NoCensusProofKeepsMain(t *testing.T) {
466 for _, flag := range []string{"", "1"} {
467 t.Run("flag="+flag, func(t *testing.T) {
468 t.Setenv("CARLOS_HOST_CLAIM", flag)
469 e := newProofRootEnv(t, provePolicy)
470 e.root.mintCensus = nil
471 before, _ := os.ReadDir(e.bake)
472 history, err := configureRootProof(&e.root, rootRestartChecks{legacy: func() (bool, error) { return true, nil }}, func() (*release.RootHistoryPass, error) {
473 t.Fatal("no-census history creation")
474 return nil, os.ErrPermission
475 })
476 if err != nil || history != nil || e.root.recordProgram != nil || e.root.refusals != nil || e.root.mintCensus != nil {
477 t.Fatal("new root policy on legacy box", err)
478 }
479 after, _ := os.ReadDir(e.bake)
480 if len(before) != len(after) {
481 t.Fatal("configuration wrote state")
482 }
483 e.root.run(t.Context())
484 if e.proof(t) == nil {
485 t.Fatal("legacy proving box stopped minting", e.logs)
486 }
487 })
488 }
489}
490