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/ecdh"
6 "crypto/ed25519"
7 "crypto/sha256"
8 "encoding/hex"
9 "errors"
10 "fmt"
11 "os"
12 "path/filepath"
13 "testing"
14
15 "github.com/carlosframework/platform/internal/appconfig"
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/moves"
18 "github.com/carlosframework/platform/internal/provision"
19 "github.com/carlosframework/platform/internal/registry"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23// Held writes under the claim (moves after claim Plan 12, Task 7; Paul's T7
24// ruling of 2026-10-12 and platform-1e's answer to Q2.2).
25
26// heldSelections is the claim's selector as the held hooks see it: it
27// records each selection and runs every write it is handed, recording the
28// identity the write names (and, with log, the order of the writes).
29type heldSelections struct {
30 selected []string
31 ids []hostwrite.Identity
32 log *[]string
33 refuse error
34}
35
36func (s *heldSelections) Select(_ context.Context, host, action string) (hostwrite.Work, error) {
37 s.selected = append(s.selected, host+"/"+action)
38 if s.refuse != nil {
39 return nil, s.refuse
40 }
41 return heldSelectionWork{s}, nil
42}
43
44type heldSelectionWork struct{ s *heldSelections }
45
46func (w heldSelectionWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
47 w.s.ids = append(w.s.ids, id)
48 if w.s.log != nil {
49 *w.s.log = append(*w.s.log, "write")
50 }
51 return fn()
52}
53
54func claimWrites(sel *heldSelections) hostwrite.Config {
55 return hostwrite.Config{Enabled: true, Selector: sel}
56}
57
58// TestT7UnderTheClaimStagesWithoutPublishing: under the claim T7 stages the
59// pinned release into the shared version store and verifies it, admitted
60// and authorized, and passes without publishing live/<host> and without any
61// selection (a moving claim grants no held write). A release D3 does not
62// admit is T7's not-proven sentence, a build nothing authorizes fails, and
63// a version store this process cannot write is MV-N455, with nothing relayed
64// to the root stage pass, which has no publication to make under the claim.
65func TestT7UnderTheClaimStagesWithoutPublishing(t *testing.T) {
66 ctx := context.Background()
67 setup := func(t *testing.T) (*phase1Harness, provision.Record) {
68 t.Helper()
69 h := newPhase1Harness(t)
70 rec := h.start(t, "manual")
71 shipPinnedV7(t, h.s)
72 h.pass(t)
73 t.Setenv("CARLOS_HOST_CLAIM", "1")
74 return h, rec
75 }
76
77 t.Run("staged and verified, not published", func(t *testing.T) {
78 h, rec := setup(t)
79 sel := &heldSelections{}
80 data, opt := t.TempDir(), t.TempDir()
81 relayed := false
82 ready, _ := heldReleaseHooks(claimWrites(sel), testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t),
83 func(string, string, error) { relayed = true })
84 for i := 0; i < 2; i++ {
85 if c := ready(ctx, h.s, rec, "v7"); !c.OK {
86 t.Fatalf("T7 under the claim, pass %d = %+v", i+1, c)
87 }
88 }
89 if _, err := os.Stat(release.VersionPath(opt, "bab", "v7", "app")); err != nil {
90 t.Fatalf("v7 was not staged: %v", err)
91 }
92 if _, err := os.Lstat(release.LiveLink(opt, p1Host)); !os.IsNotExist(err) {
93 t.Fatalf("T7 published live/<host> under the claim: %v", err)
94 }
95 if len(sel.selected) != 0 || relayed {
96 t.Fatalf("T7 selected %v (relayed %v); a moving claim grants no held write", sel.selected, relayed)
97 }
98 // Through the target's report: T7 passes.
99 tc := h.targetCfg()
100 tc.DataDir = data
101 tc.ReleaseReady = ready
102 h.targetPass(t, tc)
103 if f := failing(h.targetReport(t)); f[moves.CheckRelease] != "" {
104 t.Fatalf("T7 in the report = %q", f[moves.CheckRelease])
105 }
106 })
107
108 t.Run("not proven", func(t *testing.T) {
109 h, rec := setup(t)
110 box := newHeldProofBox(t)
111 box.require(t, "app")
112 opt := t.TempDir()
113 ready, _ := heldReleaseHooks(claimWrites(&heldSelections{}), testRegistry(t), opt, t.TempDir(), "uk-box", hsPub, hsRatchet(t), box.gate(), nil)
114 if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7NotProven {
115 t.Fatalf("T7 = %+v, want %q", c, t7NotProven)
116 }
117 if _, err := os.Stat(filepath.Join(opt, "versions")); !os.IsNotExist(err) {
118 t.Fatalf("an unproven release was staged: %v", err)
119 }
120 })
121
122 t.Run("not authorized", func(t *testing.T) {
123 h, rec := setup(t)
124 opt := t.TempDir()
125 ready, _ := heldReleaseHooks(claimWrites(&heldSelections{}), testRegistry(t), opt, t.TempDir(), "uk-box", nil, hsRatchet(t), offProofGate(t), nil)
126 if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Failed {
127 t.Fatalf("T7 with no release key = %+v, want %q", c, t7Failed)
128 }
129 if _, err := os.Stat(filepath.Join(opt, "versions")); !os.IsNotExist(err) {
130 t.Fatalf("an unauthorized build was staged: %v", err)
131 }
132 })
133
134 t.Run("blocked", func(t *testing.T) {
135 h, rec := setup(t)
136 prev := heldStageVerified
137 t.Cleanup(func() { heldStageVerified = prev })
138 heldStageVerified = func(context.Context, release.Store, ed25519.PublicKey, *release.DigestRatchet, string, registry.Route, string, *release.ProofGate) (*release.Manifest, error) {
139 return nil, fmt.Errorf("%w: read-only", release.ErrStageBlocked)
140 }
141 data := t.TempDir()
142 relayed := false
143 ready, _ := heldReleaseHooks(claimWrites(&heldSelections{}), testRegistry(t), t.TempDir(), data, "uk-box", hsPub, hsRatchet(t), offProofGate(t),
144 func(string, string, error) { relayed = true })
145 if c := ready(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7NotStaged {
146 t.Fatalf("T7 on a read-only version store = %+v, want %q", c, t7NotStaged)
147 }
148 if _, err := os.Stat(filepath.Join(release.HeldStageDir(data), p1Host+".json")); !os.IsNotExist(err) || relayed {
149 t.Fatalf("a held stage was relayed to the root pass under the claim (%v, %v)", err, relayed)
150 }
151 })
152}
153
154// TestClaimOffT7IsUnchanged: with the claim off, T7 stages and publishes
155// live/<host> exactly as before (heldReleaseReady), relays a blocked stage
156// to the root stage pass, and the configured step's release is that same
157// check.
158func TestClaimOffT7IsUnchanged(t *testing.T) {
159 ctx := context.Background()
160 h := newPhase1Harness(t)
161 rec := h.start(t, "manual")
162 shipPinnedV7(t, h.s)
163 h.pass(t)
164
165 data, opt := t.TempDir(), t.TempDir()
166 ready, live := heldReleaseHooks(hostwrite.Config{}, testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil)
167 if c := ready(ctx, h.s, rec, "v7"); !c.OK {
168 t.Fatalf("T7 = %+v", c)
169 }
170 if v, _ := release.LiveVersion(opt, p1Host); v != "v7" {
171 t.Fatalf("live = %q, want v7: T7 publishes with the claim off", v)
172 }
173 if c := live(ctx, h.s, rec, "v7"); !c.OK {
174 t.Fatalf("the configured step's release = %+v", c)
175 }
176
177 prev := heldMaterialize
178 t.Cleanup(func() { heldMaterialize = prev })
179 heldMaterialize = func(context.Context, release.Store, ed25519.PublicKey, *release.DigestRatchet, string, registry.Route, string, *release.ProofGate, func(func() error) error) (*release.Manifest, error) {
180 return nil, fmt.Errorf("%w: read-only", release.ErrStageBlocked)
181 }
182 data2 := t.TempDir()
183 relayed := false
184 ready2, _ := heldReleaseHooks(hostwrite.Config{}, testRegistry(t), t.TempDir(), data2, "uk-box", hsPub, hsRatchet(t), offProofGate(t),
185 func(string, string, error) { relayed = true })
186 if c := ready2(ctx, h.s, rec, "v7"); c.OK || c.Reason != t7Waiting {
187 t.Fatalf("T7 on a read-only version store = %+v, want %q", c, t7Waiting)
188 }
189 if _, err := os.Stat(filepath.Join(release.HeldStageDir(data2), p1Host+".json")); err != nil || !relayed {
190 t.Fatalf("the blocked stage was not relayed to the root pass (%v, %v)", err, relayed)
191 }
192}
193
194// heldConfigKit is a real held render's inputs: a store with bab/app's
195// default bundle, the secrets key, a data directory and the held row.
196type heldConfigKit struct {
197 s release.Store
198 priv *ecdh.PrivateKey
199 dd string
200 rt registry.Route
201 pin *provision.MoveInputs
202}
203
204func newHeldConfigKit(t *testing.T) heldConfigKit {
205 t.Helper()
206 ctx := context.Background()
207 s := release.NewDirStore(t.TempDir())
208 privB64, _, err := appconfig.GenerateKey()
209 if err != nil {
210 t.Fatal(err)
211 }
212 priv, err := appconfig.ParsePrivateKey(privB64, "test")
213 if err != nil {
214 t.Fatal(err)
215 }
216 if err := appconfig.SetEnv(ctx, s, "bab", "app", map[string]string{"GREETING": "hello"}); err != nil {
217 t.Fatal(err)
218 }
219 _, def, err := s.Get(ctx, appconfig.Key("bab", "app"))
220 if err != nil {
221 t.Fatal(err)
222 }
223 return heldConfigKit{s: s, priv: priv, dd: t.TempDir(),
224 rt: registry.Route{Host: p1Host, Kind: "instance", App: "app", Account: "bab", Status: "landing", HoldOp: p1ID, Socket: provision.DefaultSocket(p1Host)},
225 pin: &provision.MoveInputs{BundleETags: map[string]string{"default": def, "environment": ""}, Release: "v7"}}
226}
227
228// TestConfiguredRendersUnderTheHeldConfigSelection: under the claim the
229// landing's held render is selected as held-config for the host, under the
230// identity of the pinned configuration, and keeps carry (c): the journal
231// learns the sha256 of the bytes before the selection writes them. A render
232// whose merged inputs are not the pins selects and writes nothing and is the
233// landing's "configuration changed" refusal, never the policy's. With the
234// claim off the render is RenderHeldBefore's, unchanged.
235func TestConfiguredRendersUnderTheHeldConfigSelection(t *testing.T) {
236 ctx := context.Background()
237
238 t.Run("the render", func(t *testing.T) {
239 k := newHeldConfigKit(t)
240 t.Setenv("CARLOS_HOST_CLAIM", "1")
241 var log []string
242 sel := &heldSelections{log: &log}
243 render := heldRenderer(claimWrites(sel), k.priv, k.dd, "uk-box")
244 path := appconfig.ConfigEnvPath(k.dd, k.rt)
245 var told string
246 hr, err := render(ctx, k.s, k.rt, p1ID, k.pin, func(sha string) error {
247 log = append(log, "journal")
248 if _, err := os.Stat(path); !os.IsNotExist(err) {
249 t.Errorf("the file was there before the journal: %v", err)
250 }
251 told = sha
252 return nil
253 })
254 if err != nil {
255 t.Fatal(err)
256 }
257 if len(sel.selected) != 1 || sel.selected[0] != p1Host+"/"+movesActionHeldConfig {
258 t.Fatalf("selected %v, want one held-config selection for the host", sel.selected)
259 }
260 want := HeldConfigIdentity(k.pin.BundleETags, k.pin.OverlayETag)
261 if len(sel.ids) == 0 {
262 t.Fatal("nothing was written through the selection")
263 }
264 for _, id := range sel.ids {
265 if id != want {
266 t.Fatalf("a write named %+v, want the pinned configuration %+v", id, want)
267 }
268 }
269 if len(log) < 2 || log[0] != "journal" || log[1] != "write" {
270 t.Fatalf("order %v, want the journal before the writes", log)
271 }
272 body, err := os.ReadFile(path)
273 if err != nil {
274 t.Fatal(err)
275 }
276 sum := sha256.Sum256(body)
277 if got := hex.EncodeToString(sum[:]); told != got || hr.SHA256 != got {
278 t.Fatalf("journal %q, render %q, file %q", told, hr.SHA256, got)
279 }
280 if !sameBundle(hr.BundleETags, k.pin.BundleETags) {
281 t.Fatalf("rendered %v, pinned %v", hr.BundleETags, k.pin.BundleETags)
282 }
283 })
284
285 t.Run("a changed configuration selects nothing", func(t *testing.T) {
286 k := newHeldConfigKit(t)
287 t.Setenv("CARLOS_HOST_CLAIM", "1")
288 if err := appconfig.SetEnv(ctx, k.s, "bab", "app", map[string]string{"GREETING": "changed"}); err != nil {
289 t.Fatal(err)
290 }
291 sel := &heldSelections{}
292 journalled := false
293 _, err := heldRenderer(claimWrites(sel), k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { journalled = true; return nil })
294 if !errors.Is(err, errHeldRenderUnpinned) {
295 t.Fatalf("a changed configuration: %v, want errHeldRenderUnpinned", err)
296 }
297 if len(sel.selected) != 0 || journalled {
298 t.Fatalf("selected %v, journalled %v", sel.selected, journalled)
299 }
300 if _, err := os.Stat(appconfig.ConfigEnvPath(k.dd, k.rt)); !os.IsNotExist(err) {
301 t.Fatalf("a file was written: %v", err)
302 }
303 })
304
305 t.Run("a refused selection writes nothing", func(t *testing.T) {
306 k := newHeldConfigKit(t)
307 t.Setenv("CARLOS_HOST_CLAIM", "1")
308 sel := &heldSelections{refuse: hostwrite.ErrUnavailable}
309 if _, err := heldRenderer(claimWrites(sel), k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) {
310 t.Fatalf("a refused selection: %v", err)
311 }
312 if _, err := os.Stat(appconfig.ConfigEnvPath(k.dd, k.rt)); !os.IsNotExist(err) {
313 t.Fatalf("a file was written: %v", err)
314 }
315 })
316
317 t.Run("claim off", func(t *testing.T) {
318 k := newHeldConfigKit(t)
319 hr, err := heldRenderer(hostwrite.Config{}, k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { return nil })
320 if err != nil || hr.SHA256 == "" {
321 t.Fatalf("the claim-off render: %+v, %v", hr, err)
322 }
323 t.Setenv("CARLOS_HOST_CLAIM", "1")
324 if _, err := heldRenderer(hostwrite.Config{}, k.priv, k.dd, "uk-box")(ctx, k.s, k.rt, p1ID, k.pin, func(string) error { return nil }); !errors.Is(err, appconfig.ErrHeldClaimWired) {
325 t.Fatalf("the legacy render under the flag: %v, want ErrHeldClaimWired as before", err)
326 }
327 })
328
329 t.Run("configured passes the pins and refuses a changed configuration", func(t *testing.T) {
330 h := newLandingHarness(t)
331 h.flip(t)
332 t.Setenv("CARLOS_HOST_CLAIM", "1")
333 withMovesWakeSource(t, true)
334 var pinned []*provision.MoveInputs
335 h.renderPin = func(pin *provision.MoveInputs) { pinned = append(pinned, pin) }
336 h.renderHook = func(func(string) error) (appconfig.HeldRender, error) {
337 return appconfig.HeldRender{}, errHeldRenderUnpinned
338 }
339 h.land(t)
340 m := h.read(t, p1Host).Move
341 if m.Reason != provision.RollbackConfigChanged || m.LandingPhase != provision.LandConfigured {
342 t.Fatalf("after an unpinned render under the claim: %+v", m)
343 }
344 if len(pinned) == 0 || pinned[0] == nil || !sameBundle(pinned[0].BundleETags, m.Inputs.BundleETags) || pinned[0].OverlayETag != m.Inputs.OverlayETag {
345 t.Fatalf("configured rendered with pins %+v, want the record's %+v", pinned, m.Inputs)
346 }
347 })
348}
349
350// TestConfiguredPublishesTheReleaseUnderHeldRelease: under the claim T7
351// leaves live/<host> alone and the configured step publishes it, selected as
352// held-release for the host under the identity naming exactly the pinned
353// version, then reads it back as T7 used to (VerifyHeldLive). A replay finds
354// it live and selects nothing; a refused selection publishes nothing. The
355// landing calls this hook at configured, never T7's.
356func TestConfiguredPublishesTheReleaseUnderHeldRelease(t *testing.T) {
357 ctx := context.Background()
358
359 t.Run("the publication", func(t *testing.T) {
360 h := newPhase1Harness(t)
361 rec := h.start(t, "manual")
362 shipPinnedV7(t, h.s)
363 h.pass(t)
364 t.Setenv("CARLOS_HOST_CLAIM", "1")
365 sel := &heldSelections{}
366 data, opt := t.TempDir(), t.TempDir()
367 ready, live := heldReleaseHooks(claimWrites(sel), testRegistry(t), opt, data, "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil)
368 if c := ready(ctx, h.s, rec, "v7"); !c.OK {
369 t.Fatalf("T7 = %+v", c)
370 }
371 if v, _ := release.LiveVersion(opt, p1Host); v != "" {
372 t.Fatalf("live = %q after T7 under the claim", v)
373 }
374 if c := live(ctx, h.s, rec, "v7"); !c.OK {
375 t.Fatalf("configured's release = %+v", c)
376 }
377 if v, _ := release.LiveVersion(opt, p1Host); v != "v7" {
378 t.Fatalf("live = %q, want v7", v)
379 }
380 if len(sel.selected) != 1 || sel.selected[0] != p1Host+"/"+movesActionHeldRelease {
381 t.Fatalf("selected %v, want one held-release selection for the host", sel.selected)
382 }
383 if len(sel.ids) != 1 || sel.ids[0] != (hostwrite.Identity{Release: "v7"}) {
384 t.Fatalf("published under %v, want the pinned release alone", sel.ids)
385 }
386 if c := live(ctx, h.s, rec, "v7"); !c.OK || len(sel.selected) != 1 {
387 t.Fatalf("a replay: %+v, selections %v", c, sel.selected)
388 }
389 })
390
391 t.Run("a refused selection publishes nothing", func(t *testing.T) {
392 h := newPhase1Harness(t)
393 rec := h.start(t, "manual")
394 shipPinnedV7(t, h.s)
395 h.pass(t)
396 t.Setenv("CARLOS_HOST_CLAIM", "1")
397 sel := &heldSelections{refuse: hostwrite.ErrUnavailable}
398 opt := t.TempDir()
399 _, live := heldReleaseHooks(claimWrites(sel), testRegistry(t), opt, t.TempDir(), "uk-box", hsPub, hsRatchet(t), offProofGate(t), nil)
400 if c := live(ctx, h.s, rec, "v7"); c.OK {
401 t.Fatalf("a refused selection published: %+v", c)
402 }
403 if v, _ := release.LiveVersion(opt, p1Host); v != "" {
404 t.Fatalf("live = %q", v)
405 }
406 })
407
408 t.Run("configured calls the publication", func(t *testing.T) {
409 h := newLandingHarness(t)
410 h.flip(t)
411 t.Setenv("CARLOS_HOST_CLAIM", "1")
412 withMovesWakeSource(t, true)
413 var published []string
414 h.releaseLive = func(_ context.Context, _ release.Store, _ provision.Record, v string) moves.Check {
415 published = append(published, v)
416 return moves.Check{OK: true}
417 }
418 h.release = moves.Check{Reason: "T7's hook was asked at configured"}
419 h.land(t)
420 h.installed(t)
421 if pin := h.read(t, p1Host).Move.Inputs; len(published) == 0 || published[0] != pin.Release {
422 t.Fatalf("configured published %v, want the pinned %q", published, pin.Release)
423 }
424 })
425
426 t.Run("under the claim configured never falls back to T7", func(t *testing.T) {
427 h := newLandingHarness(t)
428 h.flip(t)
429 t.Setenv("CARLOS_HOST_CLAIM", "1")
430 withMovesWakeSource(t, true)
431 h.land(t)
432 if m := h.read(t, p1Host).Move; m.LandingPhase != provision.LandConfigured || m.Reason != "" {
433 t.Fatalf("with no publication hook under the claim: %+v", m)
434 }
435 if _, err := os.Stat(provision.DefaultDBPath(h.tdir, p1Host)); !errors.Is(err, os.ErrNotExist) {
436 t.Fatalf("the database was installed with nothing published: %v", err)
437 }
438 })
439}
440
441// TestCheckingRefusesUntilTheWakeSourceRegisters (Q2.3): under the claim
442// the landing keeps refusing at checking with RollbackClaimWired while the
443// wake-source gate is shut, and lands once it opens. With the claim off the
444// gate does not matter.
445func TestCheckingRefusesUntilTheWakeSourceRegisters(t *testing.T) {
446 t.Run("shut", func(t *testing.T) {
447 h := newLandingHarness(t)
448 h.flip(t)
449 t.Setenv("CARLOS_HOST_CLAIM", "1")
450 withMovesWakeSource(t, false)
451 h.land(t)
452 if m := h.read(t, p1Host).Move; m.Reason != provision.RollbackClaimWired || m.LandingPhase != provision.LandChecking {
453 t.Fatalf("under the claim with the gate shut: %+v", m)
454 }
455 })
456 t.Run("open", func(t *testing.T) {
457 h := newLandingHarness(t)
458 h.flip(t)
459 t.Setenv("CARLOS_HOST_CLAIM", "1")
460 withMovesWakeSource(t, true)
461 h.releaseLive = func(context.Context, release.Store, provision.Record, string) moves.Check {
462 return moves.Check{OK: true}
463 }
464 h.land(t)
465 if m := h.read(t, p1Host).Move; m.Reason != "" {
466 t.Fatalf("under the claim with the gate open: %+v", m)
467 }
468 h.installed(t)
469 })
470 t.Run("claim off", func(t *testing.T) {
471 h := newLandingHarness(t)
472 h.flip(t)
473 withMovesWakeSource(t, false)
474 h.land(t)
475 if m := h.read(t, p1Host).Move; m.Reason != "" {
476 t.Fatalf("with the claim off: %+v", m)
477 }
478 h.installed(t)
479 })
480}
481