carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "os"
10 "path/filepath"
11 "reflect"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/carlosframework/platform/internal/registry"
17 "github.com/carlosframework/platform/internal/updatewords"
18)
19
20func settingsRoute(t *testing.T, reg *registry.Registry, host, app, channel string, u UpdateSettings, bs BuildStateCopy) {
21 t.Helper()
22 addRoute(t, reg, host, app, channel, true)
23 if err := reg.SetUpdateSettings(host, u.Column()); err != nil {
24 t.Fatal(err)
25 }
26 if err := reg.SetBuildState(host, bs.Column()); err != nil {
27 t.Fatal(err)
28 }
29}
30
31func passWithSettings(t *testing.T, reg *registry.Registry, s Store, pub []byte, opt string, more ...PassOption) string {
32 t.Helper()
33 var out bytes.Buffer
34 withCapturedStdout(t, &out, func() {
35 opts := append([]PassOption{WithUpdateSettings(nil, nil, true)}, more...)
36 if err := Pass(context.Background(), reg, s, pub, opt, nil, opts...); err != nil {
37 t.Fatal(err)
38 }
39 })
40 return out.String()
41}
42
43// Spec §8 "Root pass": a manual instance on a fresh box installs the build
44// state's build, not the head.
45func TestAManualInstanceOnAFreshBoxInstallsItsLastBuild(t *testing.T) {
46 s := NewDirStore(t.TempDir())
47 ctx := context.Background()
48 priv, pub := testKeys(t)
49 opt, reg := t.TempDir(), testRegistry(t)
50 for _, v := range []string{"v1", "v2"} {
51 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
52 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
53 t.Fatal(err)
54 }
55 }
56 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"},
57 BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1"}})
58 passWithSettings(t, reg, s, pub, opt)
59 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
60 t.Fatalf("live %q, want v1 (the data's build), not the head", v)
61 }
62}
63
64// Spec §8: an existing instance with a replica but no build state is held as
65// unknown, never moved to the head.
66func TestAnUnknownBuildInstallsNothing(t *testing.T) {
67 s := NewDirStore(t.TempDir())
68 ctx := context.Background()
69 priv, pub := testKeys(t)
70 opt, reg := t.TempDir(), testRegistry(t)
71 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
72 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
73 t.Fatal(err)
74 }
75 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"}, BuildStateCopy{Known: true, Replica: true})
76 passWithSettings(t, reg, s, pub, opt)
77 if v, _ := LiveVersion(opt, "a.example.com"); v != "" {
78 t.Fatalf("live %q for an unknown build", v)
79 }
80 if c, _ := ReadConsumed(opt, "a.example.com"); c == nil || c.Reason != string(ReasonUnknown) {
81 t.Fatalf("consumed = %+v; want reason unknown recorded", c)
82 }
83}
84
85// Spec §8 "Evidence": a pin of a pruned and reused version name, with a
86// replayed pointer, is refused for lack of a matching record.
87func TestAPinNeedsAMatchingRecord(t *testing.T) {
88 s := NewDirStore(t.TempDir())
89 ctx := context.Background()
90 priv, pub := testKeys(t)
91 opt, reg := t.TempDir(), testRegistry(t)
92 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("promoted"))
93 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
94 t.Fatal(err)
95 }
96 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("v2"))
97 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil {
98 t.Fatal(err)
99 }
100 if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil {
101 t.Fatal(err)
102 }
103 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("nobody promoted these"))
104 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
105 out := passWithSettings(t, reg, s, pub, opt)
106 if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" {
107 t.Fatal("the pin installed a reused name's new bytes")
108 }
109 if !strings.Contains(out, updatewords.Text("settings.evidence.no-record")) {
110 t.Fatalf("no refusal:\n%s", out)
111 }
112}
113
114// §4.1, invariant 7: a record whose digest the reshipped manifest does not
115// match is a refusal, never a fallback to unbound staging.
116func TestAPinWithAMismatchedRecordIsRefused(t *testing.T) {
117 s := NewDirStore(t.TempDir())
118 ctx := context.Background()
119 priv, pub := testKeys(t)
120 opt, reg := t.TempDir(), testRegistry(t)
121 for _, v := range []string{"v1", "v2"} {
122 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
123 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
124 t.Fatal(err)
125 }
126 }
127 reship(t, s, "bab", "km", "v1", []byte("not what was promoted")) // the record stays
128 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
129 out := passWithSettings(t, reg, s, pub, opt)
130 if v, _ := LiveVersion(opt, "a.example.com"); v != "" {
131 t.Fatalf("live %q: the pin installed bytes its record does not bind", v)
132 }
133 if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil {
134 t.Fatal("the reshipped bytes were staged")
135 }
136 if !strings.Contains(out, "a.example.com") {
137 t.Fatalf("no refusal line:\n%s", out)
138 }
139}
140
141// Spec §8: a build promoted before the feature is recovered from a staged
142// copy, refused elsewhere.
143func TestAPreFeatureBuildIsRecoveredFromAStagedCopy(t *testing.T) {
144 s := NewDirStore(t.TempDir())
145 ctx := context.Background()
146 priv, pub := testKeys(t)
147 opt, reg := t.TempDir(), testRegistry(t)
148 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
149 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
150 t.Fatal(err)
151 }
152 if err := s.Delete(ctx, PromotionRecordKey("bab", "km", "v1")); err != nil { // promoted "before the feature"
153 t.Fatal(err)
154 }
155 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("v2"))
156 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil {
157 t.Fatal(err)
158 }
159 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"},
160 BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1"}})
161 passWithSettings(t, reg, s, pub, opt) // no staged copy: refused
162 if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" {
163 t.Fatal("recovered with neither a record nor a staged copy")
164 }
165 if _, err := ensureStaged(ctx, s, opt, "bab", "km", "v1", "seed"); err != nil { // the box already had it
166 t.Fatal(err)
167 }
168 // A staged copy whose bytes changed since it was verified recovers nothing.
169 path := VersionPath(opt, "bab", "v1", "km")
170 good, err := os.ReadFile(path)
171 if err != nil {
172 t.Fatal(err)
173 }
174 if err := os.WriteFile(path, []byte("tampered"), 0o755); err != nil {
175 t.Fatal(err)
176 }
177 passWithSettings(t, reg, s, pub, opt)
178 if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" {
179 t.Fatal("recovered from a staged copy that no longer matches its manifest")
180 }
181 if err := os.WriteFile(path, good, 0o755); err != nil {
182 t.Fatal(err)
183 }
184 passWithSettings(t, reg, s, pub, opt)
185 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
186 t.Fatalf("live %q, want v1 recovered from the staged copy", v)
187 }
188}
189
190// Spec §8 "Root pass": an install request is consumed exactly once.
191func TestAnInstallRequestResolvesOnceAndWaitsOnBake(t *testing.T) {
192 s := NewDirStore(t.TempDir())
193 ctx := context.Background()
194 priv, pub := testKeys(t)
195 opt, reg := t.TempDir(), testRegistry(t)
196 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
197 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
198 t.Fatal(err)
199 }
200 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesManual, Channel: "canary/x", InstallRequest: 1},
201 BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v0"}})
202 passWithSettings(t, reg, s, pub, opt)
203 c, _ := ReadConsumed(opt, "a.example.com")
204 if c == nil || c.Resolved == nil || c.Resolved.ID != 1 || c.Resolved.Target != "v1" {
205 t.Fatalf("consumed = %+v; want {1, v1} written", c)
206 }
207 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
208 t.Fatalf("live %q, want T", v)
209 }
210 shipRelease(t, s, "bab", "km", "v2", adoptPlatform, []byte("v2"))
211 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v2"); err != nil {
212 t.Fatal(err)
213 }
214 passWithSettings(t, reg, s, pub, opt)
215 if c2, _ := ReadConsumed(opt, "a.example.com"); c2 == nil || c2.Resolved == nil || c2.Resolved.Target != "v1" {
216 t.Fatalf("the request was resolved again: %+v", c2)
217 }
218 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
219 t.Fatalf("live %q, want T (v1) kept, not the new head", v)
220 }
221}
222
223// Astra I1 #9: a record-authorized pin installs on a box that cannot read
224// the channel pointer.
225func TestAPinInstallsWithoutAChannelPointer(t *testing.T) {
226 s := NewDirStore(t.TempDir())
227 ctx := context.Background()
228 priv, pub := testKeys(t)
229 opt, reg := t.TempDir(), testRegistry(t)
230 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
231 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
232 t.Fatal(err)
233 }
234 if err := s.Delete(ctx, ChannelKey("bab", "km", "canary/x")); err != nil { // the pointer is gone
235 t.Fatal(err)
236 }
237 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
238 passWithSettings(t, reg, s, pub, opt)
239 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
240 t.Fatalf("live %q, want the pin v1 with no pointer", v)
241 }
242}
243
244// Astra I1 #8: a request waiting on bake is reported beside a major hold,
245// and never replaces it; once the window passes, root resolves it to the
246// head and installs it.
247func TestABakingRequestKeepsTheHeldReason(t *testing.T) {
248 s := NewDirStore(t.TempDir())
249 ctx := context.Background()
250 priv, pub := testKeys(t)
251 opt, reg := t.TempDir(), testRegistry(t)
252 ratchetedProductionApp(t, s, ctx, priv, pub, opt, reg) // adopt_test.go: myapp's production head v2 inside its 24h window on this box
253 settingsRoute(t, reg, "held.example.com", "myapp", "production", UpdateSettings{Updates: UpdatesManual, Channel: "production", InstallRequest: 1},
254 BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1"}})
255 // A new held instance's first head bakes too (§4.3 rule 3), as rule 4
256 // does for p.example.com, the fixture's default route.
257 settingsRoute(t, reg, "new.example.com", "myapp", "production", UpdateSettings{Updates: UpdatesManual, Channel: "production"},
258 BuildStateCopy{Known: true})
259 passWithSettings(t, reg, s, pub, opt)
260 c, _ := ReadConsumed(opt, "held.example.com")
261 if c == nil || c.Reason != string(ReasonManual) || c.Baking != "v2" || c.Resolved != nil {
262 t.Fatalf("consumed = %+v; want reason manual, the baking head v2 beside it, nothing resolved", c)
263 }
264 if v, _ := LiveVersion(opt, "held.example.com"); v != "v1" {
265 t.Fatalf("live %q while baking, want the held build v1", v)
266 }
267 for _, h := range []string{"new.example.com", "p.example.com"} {
268 if v, _ := LiveVersion(opt, h); v == "v2" {
269 t.Fatalf("%s live on the baking head v2", h)
270 }
271 }
272
273 // The window passes on this box's own clock.
274 key := channelClockKey("bab", "myapp", "production")
275 st := LoadFirstSeen(FirstSeenPath(opt))
276 seen, ok := st.Channels[key]["v2"]
277 if !ok {
278 t.Fatal("no first sighting recorded for v2")
279 }
280 st.Channels[key]["v2"] = seen.Add(-31 * time.Hour)
281 if err := st.Save(); err != nil {
282 t.Fatal(err)
283 }
284 passWithSettings(t, reg, s, pub, opt)
285 c, _ = ReadConsumed(opt, "held.example.com")
286 if c == nil || c.Resolved == nil || c.Resolved.ID != 1 || c.Resolved.Target != "v2" || c.Baking != "" || c.Reason != string(ReasonRequest) {
287 t.Fatalf("consumed = %+v; want {1, v2} resolved, nothing baking, reason request", c)
288 }
289 if v, _ := LiveVersion(opt, "held.example.com"); v != "v2" {
290 t.Fatalf("live %q, want the resolved T v2", v)
291 }
292 if v, _ := LiveVersion(opt, "new.example.com"); v != "v2" {
293 t.Fatalf("new.example.com live %q, want the head v2 once eligible", v)
294 }
295}
296
297func TestDefaultSettingsAreTodaysAdoption(t *testing.T) {
298 s := NewDirStore(t.TempDir())
299 ctx := context.Background()
300 priv, pub := testKeys(t)
301 opt, reg := t.TempDir(), testRegistry(t)
302 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
303 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
304 t.Fatal(err)
305 }
306 addRoute(t, reg, "a.example.com", "km", "canary/x", true)
307 // The same box twice: once without the option (today), once with it. The
308 // journal and the outcome must match line for line.
309 optToday := t.TempDir()
310 var today bytes.Buffer
311 withCapturedStdout(t, &today, func() {
312 if err := Pass(ctx, reg, s, pub, optToday, nil); err != nil {
313 t.Fatal(err)
314 }
315 })
316 out := passWithSettings(t, reg, s, pub, opt)
317 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
318 t.Fatalf("live %q", v)
319 }
320 if c, _ := ReadConsumed(opt, "a.example.com"); c != nil {
321 t.Fatalf("a default route wrote a consumed file: %+v", c)
322 }
323 if strings.ReplaceAll(out, opt, "OPT") != strings.ReplaceAll(today.String(), optToday, "OPT") {
324 t.Fatalf("journal differs from today's:\nwith settings:\n%s\ntoday:\n%s", out, today.String())
325 }
326 // A failure book never reaches a default unmanaged route, readable or not.
327 reg2, opt2 := testRegistry(t), t.TempDir()
328 addRoute(t, reg2, "a.example.com", "km", "canary/x", true)
329 broken := func() (FailedBuild, error) { return nil, errors.New("swap-failures.json: unreadable") }
330 var buf bytes.Buffer
331 withCapturedStdout(t, &buf, func() {
332 if err := Pass(ctx, reg2, s, pub, opt2, nil, WithUpdateSettings(broken, func(registry.Route) bool { return false }, true)); err != nil {
333 t.Fatal(err)
334 }
335 })
336 if v, _ := LiveVersion(opt2, "a.example.com"); v != "v1" {
337 t.Fatalf("live %q: an unreadable book held a default unmanaged route", v)
338 }
339}
340
341// Astra I2 #8: the proof gate checks the build the decision selected, not
342// the channel head. Pinned to v1 with the head at v2: a proof for v2 alone
343// installs nothing, and a proof for v1 installs v1.
344func TestProofGateSeesTheSelectedBuild(t *testing.T) {
345 s := NewDirStore(t.TempDir())
346 ctx := context.Background()
347 priv, pub := testKeys(t)
348 opt, reg, bake := t.TempDir(), testRegistry(t), t.TempDir()
349 for _, v := range []string{"v1", "v2"} {
350 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
351 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
352 t.Fatal(err)
353 }
354 }
355 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
356 prover, _, proverPub := proverKey(t) // proof_test.go
357 policy := filepath.Join(t.TempDir(), "proof-policy.json")
358 if err := os.WriteFile(policy, []byte(fmt.Sprintf(`{"schema":1,"require":[{"app":"km","prover":"dev","key":%q}]}`, proverPub)), 0o644); err != nil {
359 t.Fatal(err)
360 }
361 gate := NewProofGate(policy, bake)
362 prove := func(v string) {
363 sha := etagOf([]byte(v))
364 p, err := SignProof(prover, Proof{App: "km", Platform: adoptPlatform, SHA256: sha, Version: v, Prover: "dev",
365 ActiveSince: time.Now().Add(-time.Hour), ProvenAt: time.Now()})
366 if err != nil {
367 t.Fatal(err)
368 }
369 b, _ := json.Marshal(p)
370 if _, err := s.Put(ctx, ProofKey("bab", "km", sha, "dev"), b, ""); err != nil {
371 t.Fatal(err)
372 }
373 }
374 pass := func() {
375 var out bytes.Buffer
376 withCapturedStdout(t, &out, func() {
377 if err := Pass(ctx, reg, s, pub, opt, nil, BakeDir(bake), WithProofGate(gate), WithUpdateSettings(nil, nil, true)); err != nil {
378 t.Fatal(err)
379 }
380 })
381 }
382 prove("v2") // the head is proven, the pinned build is not
383 pass()
384 if v, _ := LiveVersion(opt, "a.example.com"); v == "v1" {
385 t.Fatal("the pinned v1 went live on the head's proof")
386 }
387 prove("v1")
388 gate.clear("a.example.com") // drop the 30-second miss cache
389 pass()
390 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
391 t.Fatalf("live %q, want the proven pin v1", v)
392 }
393}
394
395// §4.3 rule 2: only root resolves. An agent pass applies the decision, never
396// writes the consumed file, and installs nothing a resolution would decide.
397func TestAnAgentPassNeverResolves(t *testing.T) {
398 s := NewDirStore(t.TempDir())
399 ctx := context.Background()
400 priv, pub := testKeys(t)
401 opt, reg := t.TempDir(), testRegistry(t)
402 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
403 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
404 t.Fatal(err)
405 }
406 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesManual, Channel: "canary/x", InstallRequest: 1},
407 BuildStateCopy{Known: true, Replica: true})
408 var out bytes.Buffer
409 withCapturedStdout(t, &out, func() {
410 if err := Pass(ctx, reg, s, pub, opt, nil, WithUpdateSettings(nil, nil, false)); err != nil {
411 t.Fatal(err)
412 }
413 })
414 if c, err := ReadConsumed(opt, "a.example.com"); c != nil || err != nil {
415 t.Fatalf("an agent pass wrote the consumed file: %+v %v", c, err)
416 }
417 if v, _ := LiveVersion(opt, "a.example.com"); v != "" {
418 t.Fatalf("live %q: an agent pass installed an unresolved request's head", v)
419 }
420 passWithSettings(t, reg, s, pub, opt) // root
421 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
422 t.Fatalf("live %q after root's pass, want T v1", v)
423 }
424 // The agent now follows root's resolution from the consumed file.
425 if err := os.Remove(LiveLink(opt, "a.example.com")); err != nil {
426 t.Fatal(err)
427 }
428 withCapturedStdout(t, &out, func() {
429 if err := Pass(ctx, reg, s, pub, opt, nil, WithUpdateSettings(nil, nil, false)); err != nil {
430 t.Fatal(err)
431 }
432 })
433 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
434 t.Fatalf("live %q: the agent did not apply root's resolution", v)
435 }
436}
437
438// §4.3: the failure records outrank every rule, for settings routes. A
439// default route keeps today's adoption, which never read the book; an
440// unreadable book holds every settings route and every managed route.
441func TestTheFailureBookBindsSettingsRoutes(t *testing.T) {
442 s := NewDirStore(t.TempDir())
443 ctx := context.Background()
444 priv, pub := testKeys(t)
445 opt, reg := t.TempDir(), testRegistry(t)
446 for _, v := range []string{"v1", "v2"} {
447 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
448 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
449 t.Fatal(err)
450 }
451 }
452 settingsRoute(t, reg, "pin.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
453 addRoute(t, reg, "plain.example.com", "km", "canary/x", true)
454 // cleared.example.com's settings were cleared after root recorded a held
455 // decision: rule 4, byte for byte, and the record follows it.
456 addRoute(t, reg, "cleared.example.com", "km", "canary/x", true)
457 if err := WriteConsumed(opt, Consumed{Host: "cleared.example.com", Desired: "v1", Reason: string(ReasonManual), Detail: "v1"}); err != nil {
458 t.Fatal(err)
459 }
460 type call struct {
461 build, host string
462 served bool
463 }
464 var calls []call
465 book := func() (FailedBuild, error) {
466 return func(acct, app, build, host string, served bool) bool {
467 calls = append(calls, call{build, host, served})
468 return acct == "bab" && app == "km" && !served
469 }, nil
470 }
471 none := func(registry.Route) bool { return false }
472 passWithSettings(t, reg, s, pub, opt, WithUpdateSettings(book, none, true))
473 if v, _ := LiveVersion(opt, "pin.example.com"); v != "" {
474 t.Fatalf("live %q: a pin onto a failed build installed", v)
475 }
476 if c, _ := ReadConsumed(opt, "pin.example.com"); c == nil || c.Reason != string(ReasonFailed) || c.Detail != "v1" {
477 t.Fatalf("consumed = %+v; want reason failed naming v1", c)
478 }
479 for _, h := range []string{"plain.example.com", "cleared.example.com"} {
480 if v, _ := LiveVersion(opt, h); v != "v2" {
481 t.Fatalf("%s live %q: the book reached a default route", h, v)
482 }
483 }
484 if c, _ := ReadConsumed(opt, "cleared.example.com"); c == nil || c.Reason != string(ReasonAdopt) || c.Desired != "v2" {
485 t.Fatalf("cleared consumed = %+v; want root's record to follow rule 4", c)
486 }
487 for _, c := range calls {
488 if c.host != "pin.example.com" {
489 t.Fatalf("the book was asked about %s", c.host)
490 }
491 }
492
493 // served is v == GoodVersion.
494 calls = nil
495 if err := reg.SetGoodBuild("pin.example.com", registry.GoodBuild{}, registry.GoodBuild{Version: "v1", Digest: etagOf([]byte("v1"))}); err != nil {
496 t.Fatal(err)
497 }
498 passWithSettings(t, reg, s, pub, opt, WithUpdateSettings(book, none, true))
499 if v, _ := LiveVersion(opt, "pin.example.com"); v != "v1" {
500 t.Fatalf("live %q: a build the instance served is not blocked", v)
501 }
502 if len(calls) == 0 || !calls[0].served {
503 t.Fatalf("calls = %+v; want served for the GoodVersion build", calls)
504 }
505
506 // An unreadable book: nothing for a settings route or a managed default
507 // route, today's adoption for an unmanaged default one; no consumed write.
508 opt2, reg2 := t.TempDir(), testRegistry(t)
509 settingsRoute(t, reg2, "pin.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
510 addRoute(t, reg2, "managed.example.com", "km", "canary/x", true)
511 addRoute(t, reg2, "plain.example.com", "km", "canary/x", true)
512 broken := func() (FailedBuild, error) { return nil, errors.New("swap-failures.json: unreadable") }
513 managed := func(rt registry.Route) bool { return rt.Host == "managed.example.com" }
514 out := passWithSettings(t, reg2, s, pub, opt2, WithUpdateSettings(broken, managed, true))
515 for _, h := range []string{"pin.example.com", "managed.example.com"} {
516 if v, _ := LiveVersion(opt2, h); v != "" {
517 t.Fatalf("%s live %q with an unreadable failure book", h, v)
518 }
519 if c, _ := ReadConsumed(opt2, h); c != nil {
520 t.Fatalf("%s: an unreadable book wrote a decision: %+v", h, c)
521 }
522 }
523 if v, _ := LiveVersion(opt2, "plain.example.com"); v != "v2" {
524 t.Fatalf("plain live %q, want today's adoption", v)
525 }
526 if !strings.Contains(out, "failure book unreadable") {
527 t.Fatalf("no line for the unreadable book:\n%s", out)
528 }
529}
530
531// R-MERGE: the consumed-file update is one pure function, the one root runs
532// and the simulation runs.
533func TestNextConsumed(t *testing.T) {
534 head := &ChannelPointer{Channel: "stable", Version: "v9"}
535 known := func(st *BuildState) BuildStateCopy { return BuildStateCopy{Known: true, Replica: true, State: st} }
536 manual := UpdateSettings{Updates: UpdatesManual}
537 req := func(id uint64) UpdateSettings { return UpdateSettings{Updates: UpdatesManual, InstallRequest: id} }
538 at := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
539 cases := []struct {
540 name string
541 in TargetInput
542 dec TargetDecision
543 want Consumed
544 }{
545 {
546 name: "fresh: the decision only",
547 in: TargetInput{Pointer: head, Settings: manual, Build: known(&BuildState{LastExecuted: "v1"})},
548 dec: TargetDecision{Version: "v1", Reason: ReasonManual, Detail: "v1"},
549 want: Consumed{Host: "a.example.com", Desired: "v1", Reason: "manual", Detail: "v1"},
550 },
551 {
552 name: "a resolution is recorded",
553 in: TargetInput{Pointer: head, Settings: req(3), Build: known(nil)},
554 dec: TargetDecision{Version: "v9", Reason: ReasonRequest, Resolve: &ResolvedRequest{ID: 3, Target: "v9"}, Detail: "v9"},
555 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v9"}, Desired: "v9", Reason: "request", Detail: "v9"},
556 },
557 {
558 name: "a failed T completes above the old watermark",
559 in: TargetInput{Pointer: head, Settings: req(3), Build: known(nil),
560 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v8"}, Completed: 2}},
561 dec: TargetDecision{Reason: ReasonManual, Fail: 3},
562 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v8"}, Completed: 3, CompletedFailed: true, Reason: "manual"},
563 },
564 {
565 name: "a completed T that failed later is marked once, not reopened",
566 in: TargetInput{Pointer: head, Settings: req(3), Build: known(nil),
567 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v8"}, Completed: 3}},
568 dec: TargetDecision{Reason: ReasonFailed, Fail: 3, Detail: "v8"},
569 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v8"}, Completed: 3, CompletedFailed: true, Reason: "failed", Detail: "v8"},
570 },
571 {
572 name: "an older failure never lowers the watermark",
573 in: TargetInput{Pointer: head, Settings: req(5), Build: known(nil),
574 Consumed: &Consumed{Host: "a.example.com", Completed: 5}},
575 dec: TargetDecision{Reason: ReasonManual, Fail: 4},
576 want: Consumed{Host: "a.example.com", Completed: 5, Reason: "manual"},
577 },
578 {
579 name: "R-TARGET-FAILEDHEAD: resolved and completed failed in one write",
580 in: TargetInput{Pointer: head, Settings: req(4), Build: known(&BuildState{LastExecuted: "v1"})},
581 dec: TargetDecision{Version: "v1", Reason: ReasonManual, Resolve: &ResolvedRequest{ID: 4, Target: "v9"}, Fail: 4, Detail: "v1"},
582 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 4, Target: "v9"}, Completed: 4, CompletedFailed: true, Desired: "v1", Reason: "manual", Detail: "v1"},
583 },
584 {
585 name: "baking is reported beside the held reason",
586 in: TargetInput{Pointer: head, HeadHeld: "in its supply-chain bake window", Settings: req(2), Build: known(&BuildState{LastExecuted: "v1"})},
587 dec: TargetDecision{Version: "v1", Reason: ReasonManual, Detail: "v1"},
588 want: Consumed{Host: "a.example.com", Desired: "v1", Reason: "manual", Detail: "v1", Baking: "v9"},
589 },
590 {
591 name: "baking for a new request beside an older, completed resolution",
592 in: TargetInput{Pointer: head, HeadHeld: "held", Settings: req(2), Build: known(&BuildState{LastExecuted: "v1", Completed: 1}),
593 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 1, Target: "v1"}, Completed: 1}},
594 dec: TargetDecision{Version: "v1", Reason: ReasonManual, Detail: "v1"},
595 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 1, Target: "v1"}, Completed: 1, Desired: "v1", Reason: "manual", Detail: "v1", Baking: "v9"},
596 },
597 {
598 name: "no baking while the build state is unread",
599 in: TargetInput{Pointer: head, HeadHeld: "held", Settings: req(2), Build: BuildStateCopy{}},
600 dec: TargetDecision{Reason: ReasonUnknown},
601 want: Consumed{Host: "a.example.com", Reason: "unknown"},
602 },
603 {
604 name: "no baking for a fast-tracked request",
605 in: TargetInput{Pointer: head, HeadHeld: "held", Settings: UpdateSettings{Updates: UpdatesManual, InstallRequest: 2, InstallFastTrack: true}, Build: known(nil)},
606 dec: TargetDecision{Version: "v9", Reason: ReasonRequest, Resolve: &ResolvedRequest{ID: 2, Target: "v9"}, Detail: "v9"},
607 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 2, Target: "v9"}, Desired: "v9", Reason: "request", Detail: "v9"},
608 },
609 {
610 name: "no baking once R has a T",
611 in: TargetInput{Pointer: head, HeadHeld: "held", Settings: req(2), Build: known(&BuildState{Resolved: &ResolvedRequest{ID: 2, Target: "v8"}}),
612 Consumed: &Consumed{Host: "a.example.com", Baking: "v8"}},
613 dec: TargetDecision{Version: "v8", Reason: ReasonRequest, Evidence: EvidenceRecord, Detail: "v8"},
614 want: Consumed{Host: "a.example.com", Desired: "v8", Reason: "request", Detail: "v8"},
615 },
616 {
617 name: "no baking for a completed request; a stale At is dropped",
618 in: TargetInput{Pointer: head, HeadHeld: "held", Settings: req(2), Build: known(&BuildState{Completed: 2}),
619 Consumed: &Consumed{Host: "a.example.com", Baking: "v9", Desired: "v1", Reason: "manual", At: at}},
620 dec: TargetDecision{Version: "v9", Reason: ReasonAdopt, Bake: true, Detail: "v9"},
621 want: Consumed{Host: "a.example.com", Desired: "v9", Reason: "adopt", Detail: "v9"},
622 },
623 {
624 name: "R-RESOLUTION-STATE-WINS: a file disagreeing with a read build state takes its T",
625 in: TargetInput{Pointer: head, Settings: req(3), Build: known(&BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 3, Target: "v4"}}),
626 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v1"}}},
627 dec: TargetDecision{Version: "v4", Reason: ReasonRequest, Evidence: EvidenceRecord, Detail: "v4"},
628 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v4"}, Desired: "v4", Reason: "request", Detail: "v4"},
629 },
630 {
631 name: "R-RESOLUTION-STATE-WINS: an unread build state leaves the file alone",
632 in: TargetInput{Pointer: head, Settings: req(3), Build: BuildStateCopy{},
633 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v1"}}},
634 dec: TargetDecision{Reason: ReasonUnknown},
635 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v1"}, Reason: "unknown"},
636 },
637 {
638 name: "R-RESOLUTION-STATE-WINS: another request's resolution in the build state changes nothing",
639 in: TargetInput{Pointer: head, Settings: req(3), Build: known(&BuildState{LastExecuted: "v1", Resolved: &ResolvedRequest{ID: 2, Target: "v4"}, Completed: 2}),
640 Consumed: &Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v1"}}},
641 dec: TargetDecision{Version: "v1", Reason: ReasonRequest, Evidence: EvidenceRecord, Detail: "v1"},
642 want: Consumed{Host: "a.example.com", Resolved: &ResolvedRequest{ID: 3, Target: "v1"}, Desired: "v1", Reason: "request", Detail: "v1"},
643 },
644 {
645 name: "no baking with no readable head",
646 in: TargetInput{HeadHeld: "", Settings: req(2), Build: known(&BuildState{LastExecuted: "v1"})},
647 dec: TargetDecision{Version: "v1", Reason: ReasonManual, Detail: "v1"},
648 want: Consumed{Host: "a.example.com", Desired: "v1", Reason: "manual", Detail: "v1"},
649 },
650 }
651 for _, tc := range cases {
652 t.Run(tc.name, func(t *testing.T) {
653 tc.in.Route = registry.Route{Host: "a.example.com", UpdateSettings: tc.in.Settings.Column()}
654 // Every decision names the column it was made for (R-HELD-PURE).
655 tc.want.Settings = SettingsDigest(tc.in.Settings.Column())
656 // And the route's tenant (R22): a route with no account of its
657 // own is the platform's.
658 tc.want.Account = OpsAccount
659 var before *Consumed
660 if tc.in.Consumed != nil {
661 cp := *tc.in.Consumed
662 before = &cp
663 }
664 got := NextConsumed(tc.in, tc.dec)
665 if !reflect.DeepEqual(got, tc.want) {
666 t.Fatalf("NextConsumed =\n %+v\nwant\n %+v", got, tc.want)
667 }
668 if before != nil && !reflect.DeepEqual(*tc.in.Consumed, *before) {
669 t.Fatalf("NextConsumed changed its input: %+v", *tc.in.Consumed)
670 }
671 if got.Resolved != nil && tc.in.Consumed != nil && got.Resolved == tc.in.Consumed.Resolved {
672 t.Fatal("NextConsumed shares the input's Resolved")
673 }
674 })
675 }
676}
677
678// installAt stages ver of km on this box and points host at it, as an
679// earlier pass would have.
680func installAt(t *testing.T, s Store, opt, host, ver string) {
681 t.Helper()
682 if _, err := ensureStaged(context.Background(), s, opt, "bab", "km", ver, host); err != nil {
683 t.Fatal(err)
684 }
685 if err := PointLive(opt, host, "bab", ver, "km"); err != nil {
686 t.Fatal(err)
687 }
688}
689
690// R-SETTINGS-UNREADABLE (a): a control name this binary does not know (a
691// newer console's) is ignored by root; the rest of the column still holds.
692func TestAnUnknownControlNameKeepsTheHold(t *testing.T) {
693 s := NewDirStore(t.TempDir())
694 ctx := context.Background()
695 priv, pub := testKeys(t)
696 opt, reg := t.TempDir(), testRegistry(t)
697 for _, v := range []string{"v1", "v2"} {
698 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
699 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
700 t.Fatal(err)
701 }
702 }
703 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{}, BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1"}})
704 if err := reg.SetUpdateSettings("a.example.com", `{"updates":"manual","channel":"canary/x","control":["schedule","pin","schedule"]}`); err != nil {
705 t.Fatal(err)
706 }
707 installAt(t, s, opt, "a.example.com", "v1")
708 passWithSettings(t, reg, s, pub, opt)
709 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
710 t.Fatalf("live %q: an unknown control name lifted the manual hold", v)
711 }
712 if c, _ := ReadConsumed(opt, "a.example.com"); c == nil || c.Reason != string(ReasonManual) {
713 t.Fatalf("consumed = %+v; want reason manual", c)
714 }
715 u, err := ParseUpdateSettingsLenient(`{"updates":"manual","control":["schedule","pin","pin"]}`)
716 if err != nil || u.Updates != UpdatesManual || !reflect.DeepEqual(u.Control, []string{ControlPin}) {
717 t.Fatalf("lenient parse = %+v, %v; want manual with control [pin]", u, err)
718 }
719 if _, err := ParseUpdateSettings(`{"updates":"manual","control":["schedule"]}`); err == nil {
720 t.Fatal("the strict parse (the console's) accepted an unknown control name")
721 }
722}
723
724// R-SETTINGS-UNREADABLE (b): a column that still does not parse holds the
725// route where it is, and root records reason unknown for the wake check.
726func TestAnUnreadableSettingsColumnHolds(t *testing.T) {
727 s := NewDirStore(t.TempDir())
728 ctx := context.Background()
729 priv, pub := testKeys(t)
730 opt, reg := t.TempDir(), testRegistry(t)
731 for _, v := range []string{"v1", "v2"} {
732 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
733 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
734 t.Fatal(err)
735 }
736 }
737 for host, col := range map[string]string{
738 "later.example.com": `{"updates":"scheduled","channel":"canary/x"}`,
739 "garbage.example.com": `{not json`,
740 } {
741 settingsRoute(t, reg, host, "km", "canary/x", UpdateSettings{}, BuildStateCopy{Known: true, Replica: true, State: &BuildState{LastExecuted: "v1"}})
742 if err := reg.SetUpdateSettings(host, col); err != nil {
743 t.Fatal(err)
744 }
745 }
746 installAt(t, s, opt, "later.example.com", "v1")
747 var agent bytes.Buffer
748 withCapturedStdout(t, &agent, func() {
749 if err := Pass(ctx, reg, s, pub, opt, nil, WithUpdateSettings(nil, nil, false)); err != nil {
750 t.Fatal(err)
751 }
752 })
753 if strings.Contains(agent.String(), "update settings") {
754 t.Fatalf("an agent pass printed the settings line:\n%s", agent.String())
755 }
756 if c, _ := ReadConsumed(opt, "later.example.com"); c != nil {
757 t.Fatalf("an agent pass wrote %+v", c)
758 }
759 out := passWithSettings(t, reg, s, pub, opt)
760 for host, want := range map[string]string{"later.example.com": "v1", "garbage.example.com": ""} {
761 if v, _ := LiveVersion(opt, host); v != want {
762 t.Fatalf("%s live %q, want %q kept", host, v, want)
763 }
764 c, err := ReadConsumed(opt, host)
765 if err != nil || c == nil || c.Reason != string(ReasonUnknown) || !strings.Contains(c.Detail, "update settings") || c.Desired != "" {
766 t.Fatalf("%s consumed = %+v, %v; want reason unknown naming the parse problem", host, c, err)
767 }
768 }
769 if !strings.Contains(out, "update settings unreadable") {
770 t.Fatalf("root printed no line:\n%s", out)
771 }
772}
773
774// R-AGENT-RELAY: an agent pass that stops for root's resolution asks root
775// for a pass, once, through the stage relay, with the request's sentinel.
776func TestAnAgentPassRelaysAPendingRequestToRoot(t *testing.T) {
777 s := NewDirStore(t.TempDir())
778 ctx := context.Background()
779 priv, pub := testKeys(t)
780 opt, reg := t.TempDir(), testRegistry(t)
781 shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("v1"))
782 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
783 t.Fatal(err)
784 }
785 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesManual, Channel: "canary/x", InstallRequest: 1},
786 BuildStateCopy{Known: true, Replica: true})
787 type relay struct {
788 host, ver string
789 err error
790 }
791 var got []relay
792 var out bytes.Buffer
793 withCapturedStdout(t, &out, func() {
794 err := MaterializeHost(ctx, reg, s, pub, opt, t.TempDir(), "a.example.com",
795 OnStageBlocked(func(h, v string, err error) { got = append(got, relay{h, v, err}) }),
796 WithUpdateSettings(nil, nil, false))
797 if err == nil {
798 t.Fatal("materialized a request root has not resolved")
799 }
800 })
801 if len(got) != 1 || got[0].host != "a.example.com" || got[0].ver != "v1" || !errors.Is(got[0].err, ErrInstallRequestNeedsRoot) {
802 t.Fatalf("relays = %+v; want exactly one for a.example.com v1 with ErrInstallRequestNeedsRoot", got)
803 }
804 // Root's pass resolves; the relay's head is then installed.
805 passWithSettings(t, reg, s, pub, opt)
806 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
807 t.Fatalf("live %q after root's pass", v)
808 }
809}
810
811// R-T6-MISC fix round 1, amended by Task 12a: the up-to-date refusal clear
812// judges both the selected build and the head. Pinned and live on v1: a
813// refusal of the head v2 that is still current is kept (unpinning would
814// otherwise retry a refused promotion once); a refusal of v1 itself (its
815// rollback failed) stays too.
816func TestTheLiveBuildsRefusalIsJudgedOnTheSelectedBuild(t *testing.T) {
817 s := NewDirStore(t.TempDir())
818 ctx := context.Background()
819 priv, pub := testKeys(t)
820 opt, reg, bake := t.TempDir(), testRegistry(t), t.TempDir()
821 for _, v := range []string{"v1", "v2"} {
822 shipRelease(t, s, "bab", "km", v, adoptPlatform, []byte(v))
823 if err := Promote(ctx, s, priv, "bab", "km", "canary/x", v); err != nil {
824 t.Fatal(err)
825 }
826 }
827 head, err := Channel(ctx, s, pub, "bab", "km", "canary/x")
828 if err != nil {
829 t.Fatal(err)
830 }
831 settingsRoute(t, reg, "a.example.com", "km", "canary/x", UpdateSettings{Updates: UpdatesAuto, Channel: "canary/x", Pin: "v1"}, BuildStateCopy{Known: true})
832 installAt(t, s, opt, "a.example.com", "v1")
833 pass := func() {
834 var out bytes.Buffer
835 withCapturedStdout(t, &out, func() {
836 if err := Pass(ctx, reg, s, pub, opt, nil, BakeDir(bake), WithUpdateSettings(nil, nil, true)); err != nil {
837 t.Fatal(err)
838 }
839 })
840 }
841 if err := WriteRefusal(bake, Refusal{Host: "a.example.com", App: "km", Version: "v2", PointerPromotedAt: head.PromotedAt, Reason: "did not come up"}); err != nil {
842 t.Fatal(err)
843 }
844 pass()
845 if r, err := ReadRefusal(bake, "a.example.com"); err != nil || r == nil || r.Version != "v2" {
846 t.Fatalf("refusal = %+v, %v; the pin cleared a refusal of the head that is still current", r, err)
847 }
848 // A refusal of an earlier promotion of v2 blocks neither build: cleared.
849 if err := WriteRefusal(bake, Refusal{Host: "a.example.com", App: "km", Version: "v2", PointerPromotedAt: head.PromotedAt.Add(-time.Hour), Reason: "did not come up"}); err != nil {
850 t.Fatal(err)
851 }
852 pass()
853 if r, err := ReadRefusal(bake, "a.example.com"); err != nil || r != nil {
854 t.Fatalf("refusal = %+v, %v; a refusal that blocks nothing was kept", r, err)
855 }
856 if err := WriteRefusal(bake, Refusal{Host: "a.example.com", App: "km", Version: "v1", Reason: "did not come up"}); err != nil {
857 t.Fatal(err)
858 }
859 pass()
860 if r, err := ReadRefusal(bake, "a.example.com"); err != nil || r == nil || r.Version != "v1" {
861 t.Fatalf("refusal = %+v, %v; the live build's own refusal was cleared", r, err)
862 }
863 if v, _ := LiveVersion(opt, "a.example.com"); v != "v1" {
864 t.Fatalf("live %q", v)
865 }
866}
867
868// failingGets is a Store whose Get of any key in fail returns err, as a
869// bucket that cannot be read at that moment.
870type failingGets struct {
871 Store
872 fail map[string]bool
873 err error
874}
875
876func (f failingGets) Get(ctx context.Context, key string) ([]byte, string, error) {
877 if f.fail[key] {
878 return nil, "", f.err
879 }
880 return f.Store.Get(ctx, key)
881}
882
883// Astra round 1 finding 7 (§4.1): this box's staged copy whose bytes still
884// hash to its sibling manifest is recovered without remote evidence, so a
885// promotion record GET or a manifest GET that fails does not block it. With
886// no verified copy the same failures still refuse, and nothing is downloaded:
887// bytes that are fetched or replaced need the record's digest.
888func TestAVerifiedLocalCopyNeedsNoRemoteEvidence(t *testing.T) {
889 ctx := context.Background()
890 priv, pub := testKeys(t)
891 base := NewDirStore(t.TempDir())
892 shipRelease(t, base, "bab", "km", "v1", adoptPlatform, []byte("v1"))
893 if err := Promote(ctx, base, priv, "bab", "km", "canary/x", "v1"); err != nil {
894 t.Fatal(err)
895 }
896 blip := errors.New("s3: 503 slow down")
897 for name, fail := range map[string]map[string]bool{
898 "record GET fails": {PromotionRecordKey("bab", "km", "v1"): true},
899 "manifest GET fails": {ManifestKey("bab", "km", "v1"): true},
900 } {
901 t.Run(name, func(t *testing.T) {
902 opt := t.TempDir()
903 s := failingGets{Store: base, fail: fail, err: blip}
904 // No copy on this box: refused, nothing staged.
905 if _, err := stageAuthorized(ctx, s, pub, opt, "bab", "km", "v1", "a.example.com", stageCheck{}); err == nil {
906 t.Fatal("staged with no verified copy and no readable remote evidence")
907 }
908 if _, err := os.Stat(VersionPath(opt, "bab", "v1", "km")); err == nil {
909 t.Fatal("bytes were downloaded without remote evidence")
910 }
911 // The box's verified copy (staged earlier, sibling beside it).
912 if _, err := ensureStaged(ctx, base, opt, "bab", "km", "v1", "seed"); err != nil {
913 t.Fatal(err)
914 }
915 m, err := stageAuthorized(ctx, s, pub, opt, "bab", "km", "v1", "a.example.com", stageCheck{})
916 if err != nil || m == nil || m.Version != "v1" {
917 t.Fatalf("stageAuthorized = %+v, %v; want the verified local copy recovered", m, err)
918 }
919 // A copy whose bytes no longer match its sibling is not recovered.
920 if err := os.WriteFile(VersionPath(opt, "bab", "v1", "km"), []byte("tampered"), 0o755); err != nil {
921 t.Fatal(err)
922 }
923 if _, err := stageAuthorized(ctx, s, pub, opt, "bab", "km", "v1", "a.example.com", stageCheck{}); err == nil {
924 t.Fatal("recovered a staged copy that no longer matches its sibling")
925 }
926 })
927 }
928}
929
930// Ruling R-HELD-PURE: root's consumed decision records the settings column
931// it was made for, so the held wake check can tell a decision for the
932// current settings from a stale one. A different column has another digest.
933func TestNextConsumedRecordsTheSettingsItDecidedFor(t *testing.T) {
934 one := 1
935 u := UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: &one}
936 in := TargetInput{Route: registry.Route{Host: "a.example.com", UpdateSettings: u.Column()}, Settings: u, Build: BuildStateCopy{Known: true}}
937 got := NextConsumed(in, TargetDecision{Reason: ReasonMajor})
938 if got.Settings == "" || got.Settings != SettingsDigest(u.Column()) {
939 t.Fatalf("Settings = %q, want the digest of %s", got.Settings, u.Column())
940 }
941 two := 2
942 if other := (UpdateSettings{Updates: UpdatesAuto, Channel: "stable", Major: &two}).Column(); SettingsDigest(other) == got.Settings {
943 t.Fatal("two columns share a digest")
944 }
945}
946
947// Astra r2 N1 (ruling R22): root's consumed file names the tenant it decided
948// for, at both of the pass's write sites (the decision, and the unreadable
949// column's reason unknown), and a hostname that moves to another tenant is
950// rewritten naming the new one, never keeping the old file's tenant.
951func TestRootsConsumedFileNamesTheRoutesTenant(t *testing.T) {
952 s := NewDirStore(t.TempDir())
953 ctx := context.Background()
954 priv, pub := testKeys(t)
955 opt, reg := t.TempDir(), testRegistry(t)
956 const host = "a.example.com"
957 for _, ta := range [][2]string{{"cdf", "km"}, {"dee", "blog"}} {
958 shipRelease(t, s, ta[0], ta[1], "v1", adoptPlatform, []byte("v1"))
959 if err := Promote(ctx, s, priv, ta[0], ta[1], "canary/x", "v1"); err != nil {
960 t.Fatal(err)
961 }
962 }
963 route := func(account, app string) {
964 t.Helper()
965 if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: account, App: app, Channel: "canary/x", Hibernate: true}); err != nil {
966 t.Fatal(err)
967 }
968 if err := reg.SetUpdateSettings(host, UpdateSettings{Updates: UpdatesManual, Channel: "canary/x"}.Column()); err != nil {
969 t.Fatal(err)
970 }
971 if err := reg.SetBuildState(host, BuildStateCopy{Known: true, Replica: true}.Column()); err != nil {
972 t.Fatal(err)
973 }
974 }
975 check := func(when, account, app string) {
976 t.Helper()
977 c, err := ReadConsumed(opt, host)
978 if err != nil || c == nil {
979 t.Fatalf("%s: consumed = %+v, %v", when, c, err)
980 }
981 if c.Account != account || c.App != app || c.Reason != string(ReasonUnknown) {
982 t.Fatalf("%s: consumed names %q/%q (reason %q), want %s/%s", when, c.Account, c.App, c.Reason, account, app)
983 }
984 }
985 route("cdf", "km")
986 passWithSettings(t, reg, s, pub, opt)
987 check("tenant A's decision", "cdf", "km")
988 // Claims off: the row is retired by deletion, and B's arrives on the same
989 // hostname. Root's next pass reads A's file and writes over it.
990 if err := reg.Delete(host); err != nil {
991 t.Fatal(err)
992 }
993 route("dee", "blog")
994 passWithSettings(t, reg, s, pub, opt)
995 check("tenant B's decision over A's file", "dee", "blog")
996 // The other write site: a column that does not parse is recorded as
997 // reason unknown, for the route's tenant too (A's again, over B's file).
998 if err := reg.Delete(host); err != nil {
999 t.Fatal(err)
1000 }
1001 if err := reg.Upsert(registry.Route{Host: host, Kind: "instance", Account: "cdf", App: "km", Channel: "canary/x", Hibernate: true}); err != nil {
1002 t.Fatal(err)
1003 }
1004 if err := reg.SetUpdateSettings(host, "{not json"); err != nil {
1005 t.Fatal(err)
1006 }
1007 out := passWithSettings(t, reg, s, pub, opt)
1008 if !strings.Contains(out, "update settings unreadable") {
1009 t.Fatalf("the pass did not take the unreadable-column path:\n%s", out)
1010 }
1011 check("an unreadable column", "cdf", "km")
1012}
1013