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 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "testing"
13
14 "github.com/carlosframework/platform/internal/hostfloor"
15 "github.com/carlosframework/platform/internal/release"
16)
17
18// install-channel --policy (claim trial prev-rehearsal, ruling R1). The box
19// these tests start from is made by the platform's own update path, never
20// by hand: v1.3.0 on disk, the channel promoted to v1.4.0, the agent's
21// request and cache written as systemLeg writes them, then the real apply
22// (systemApplier.Run) moving v1.3.0 to carlos.prev. That is the trial's
23// leo-climbing: applied.json names the channel release, carlos is its
24// artifact, carlos.prev the release before, and no channel identity.
25
26// updatedChannelBox returns that box. old and body are the two releases'
27// bytes.
28func updatedChannelBox(t *testing.T) (f *channelFixture, old []byte) {
29 t.Helper()
30 f = &channelFixture{applyFixture: newApplyFixture(t, "stable", "v1.3.0")}
31 old, err := os.ReadFile(systemBinaryPath(f.a.optDir))
32 if err != nil {
33 t.Fatal(err)
34 }
35 f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n")
36 applyChannelUpdate(t, f, "v1.4.0", f.body, true)
37 if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) {
38 t.Fatalf("the update left %q and %q", cur, prev)
39 }
40 if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) {
41 t.Fatalf("an update wrote a channel identity: %v", err)
42 }
43 f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error {
44 f.fetches++
45 return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha)
46 }, own: func(path string) error {
47 f.owned = append(f.owned, path)
48 return nil
49 }}
50 return f, old
51}
52
53// applyChannelUpdate moves f's box to version through the platform's update
54// path: the promote (and, with identity, the release identity), the agent's
55// fetch and request (systemLeg), then the real apply.
56func applyChannelUpdate(t *testing.T, f *channelFixture, version string, body []byte, identity bool) {
57 t.Helper()
58 applyChannelUpdateWith(t, f, version, body, func() {
59 if identity {
60 f.publish(t, version, body)
61 return
62 }
63 rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{
64 "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hostfloor.Digest(body)}}}
65 if _, err := release.PromoteSystem(context.Background(), f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil {
66 t.Fatal(err)
67 }
68 })
69}
70
71// applyChannelUpdateWith is applyChannelUpdate with the promote given.
72func applyChannelUpdateWith(t *testing.T, f *channelFixture, version string, body []byte, promote func()) {
73 t.Helper()
74 ctx := context.Background()
75 promote()
76 p, _, err := release.GetSystemPointer(ctx, f.store, f.pub, release.SystemComponentRouter, "stable")
77 if err != nil {
78 t.Fatal(err)
79 }
80 leg := &systemLeg{dataDir: f.a.dataDir, fetchTo: func(ctx context.Context, key, dst, sha string) error {
81 return release.FetchToFile(ctx, f.store, key, dst, systemArtifactLimit, sha)
82 }}
83 pending := &systemPending{version: version, promotedAt: p.PromotedAt, artifact: p.Current.Artifacts["linux-arm64"]}
84 if err := leg.fetch(ctx, pending); err != nil {
85 t.Fatal(err)
86 }
87 if err := leg.writeRequest(pending, f.now); err != nil {
88 t.Fatal(err)
89 }
90 // The candidate ships the instance layout's mount, so the layout guard
91 // does not open the registry (a post-layout release, as every one the
92 // trial installs is).
93 plan, units := `{"enabled":["carlos-edge.service","carlos-adopt.timer","carlos-adopt.path","carlos-stage.path","carlos-restart.path","carlos-system-update.path","carlos-system-recover.service"],"inert":["carlos-restore-verify.timer"]}`, "carlos-edge.service carlos-adopt.timer carlos-adopt.path carlos-stage.path carlos-restart.path carlos-system-update.path carlos-system-update.service carlos-system-recover.service carlos-instance-control.service data-instances.mount"
94 for _, path := range []string{filepath.Join(f.a.stagingDir(), "carlos"), systemPrevPath(f.a.optDir), systemBinaryPath(f.a.optDir)} {
95 f.answer["plan:"+path], f.answer["units:"+path] = plan, units
96 // Every binary answers `version` from its own bytes.
97 delete(f.answer, "version:"+path)
98 }
99 if err := f.a.Run(ctx, false); err != nil {
100 t.Fatal(err)
101 }
102 if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied || r.Version != version {
103 t.Fatalf("the update to %s did not apply: %+v", version, r)
104 }
105 if cur, _ := os.ReadFile(systemBinaryPath(f.a.optDir)); !bytes.Equal(cur, body) {
106 t.Fatalf("the update left %q", cur)
107 }
108}
109
110// signedPolicy is what claims run sign-policy writes for floor.
111func signedPolicy(t *testing.T, key ed25519.PrivateKey, bucket string, floor hostfloor.Floor) []byte {
112 t.Helper()
113 p, err := hostfloor.ArmedPolicy(release.RootSigner(key), bucket, floor)
114 if err != nil {
115 t.Fatal(err)
116 }
117 raw, err := json.Marshal(p)
118 if err != nil {
119 t.Fatal(err)
120 }
121 return raw
122}
123
124// boxFiles is every file install-channel or the update keeps in the root
125// directory and the two slots, by name, so a refusal can be shown to have
126// changed nothing.
127func boxFiles(t *testing.T, f *channelFixture) map[string]string {
128 t.Helper()
129 got := map[string]string{}
130 for _, dir := range []string{f.a.optDir, systemRootDir(f.a.optDir)} {
131 ents, err := os.ReadDir(dir)
132 if err != nil {
133 t.Fatal(err)
134 }
135 for _, e := range ents {
136 path := filepath.Join(dir, e.Name())
137 if fi, err := os.Lstat(path); err == nil && fi.Mode().IsRegular() {
138 b, _ := os.ReadFile(path)
139 got[path] = string(b)
140 } else if err == nil {
141 got[path] = fi.Mode().String()
142 }
143 }
144 }
145 return got
146}
147
148func TestInstallChannelPolicyArmsAnUpdatedBoxAtTheChannelHead(t *testing.T) {
149 f, old := updatedChannelBox(t)
150 ctx := context.Background()
151 // Without --policy the box is refused exactly as before, and not
152 // changed: with no floor published, for that (the trial's loop), and
153 // with one, as an updated box (the sibling
154 // TestInstallChannelRefusesAnUpdatedBoxAtTheChannelHead).
155 before := boxFiles(t, f)
156 if _, err := f.in.run(ctx); err == nil || err.Error() != "no host floor is published, so this box could not arm its guard and nothing was installed" {
157 t.Fatalf("without --policy: %v", err)
158 }
159 if after := boxFiles(t, f); !equalFiles(before, after) {
160 t.Fatalf("a refused run changed the box: %v", diffFiles(before, after))
161 }
162 applied, _ := os.ReadFile(f.a.file("applied.json"))
163 seen, _ := os.ReadFile(f.a.file("seen.json"))
164 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
165 f.in.identity = f.identityOf(t, "v1.4.0")
166 got, err := f.in.run(ctx)
167 if err != nil {
168 t.Fatal(err)
169 }
170 if !got.Replaced || got.Already || got.Version != "v1.4.0" || got.Tag != "v1.4.0" {
171 t.Fatalf("installed %+v", got)
172 }
173 if line := installedLine(got); line != "replaced the rollback copy carlos.prev with v1.4.0 (v1.4.0) from the stable channel, the release carlos already runs. the release carlos.prev held before is no longer on this box" {
174 t.Fatalf("line %q", line)
175 }
176 cur, prev := f.slots(t)
177 if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || bytes.Equal(prev, old) {
178 t.Fatalf("slots hold %q and %q", cur, prev)
179 }
180 // Nothing was fetched: carlos already is the artifact.
181 if f.fetches != 0 {
182 t.Fatalf("%d fetches", f.fetches)
183 }
184 // The update's records stand; the version sidecar names the release
185 // carlos.prev now holds.
186 if a, _ := os.ReadFile(f.a.file("applied.json")); !bytes.Equal(a, applied) {
187 t.Fatalf("applied.json changed: %s", a)
188 }
189 if s, _ := os.ReadFile(f.a.file("seen.json")); !bytes.Equal(s, seen) {
190 t.Fatalf("seen.json changed: %s", s)
191 }
192 if v, ok := readLine(prevVersionPath(f.a.optDir)); !ok || v != "v1.4.0" {
193 t.Fatalf("carlos.prev's version sidecar is %q %v", v, ok)
194 }
195 // The identity kept beside the slots is the published one, and
196 // guard-arm's two checks pass: its inputs, and both slots.
197 key, _ := release.IdentityKey("v1.4.0")
198 published, _, err := f.store.Get(ctx, key)
199 if err != nil {
200 t.Fatal(err)
201 }
202 held, err := os.ReadFile(channelIdentityPath(f.a.optDir))
203 if err != nil || !bytes.Equal(held, published) {
204 t.Fatalf("identity %v", err)
205 }
206 _, floor, err := guardArmInputs(f.in.policy, held, f.pub, "bucket", "v1.4.0", "linux-arm64", hostfloor.Digest(f.body))
207 if err != nil {
208 t.Fatalf("guard-arm's inputs: %v", err)
209 }
210 for _, slot := range []string{systemBinaryPath(f.a.optDir), systemPrevPath(f.a.optDir)} {
211 b, _ := os.ReadFile(slot)
212 if err := hostfloor.CheckImage(f.pub, floor, held, "v1.4.0", "linux-arm64", hostfloor.Digest(b)); err != nil {
213 t.Fatalf("guard-arm would refuse %s: %v", slot, err)
214 }
215 }
216 // Run again: nothing to do.
217 again, err := f.in.run(ctx)
218 if err != nil || !again.Already || again.Replaced || f.fetches != 0 {
219 t.Fatalf("second run %+v %v", again, err)
220 }
221 // The floor, once published from the same plan, is the policy's own
222 // bytes, so the run still passes; the same with half of it published,
223 // as a publication cut off after its first write leaves it.
224 var policy hostfloor.Policy
225 if err := json.Unmarshal(f.in.policy, &policy); err != nil {
226 t.Fatal(err)
227 }
228 if err := release.CreateExact(ctx, f.store, hostfloor.AuthKey, policy.Authorization); err != nil {
229 t.Fatal(err)
230 }
231 if got, err := f.in.run(ctx); err != nil || !got.Already {
232 t.Fatalf("half published: %+v %v", got, err)
233 }
234 if err := hostfloor.Publish(ctx, f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}); err != nil {
235 t.Fatal(err)
236 }
237 if got, err := f.in.run(ctx); err != nil || !got.Already {
238 t.Fatalf("published: %+v %v", got, err)
239 }
240}
241
242// Each refusal of --policy on the updated box, and on a fresh one, leaves
243// the box as it was and fetches nothing.
244func TestInstallChannelPolicyRefusals(t *testing.T) {
245 t.Parallel()
246 _, otherKey, _ := ed25519.GenerateKey(nil)
247 for _, c := range []struct {
248 name string
249 setup func(t *testing.T, f *channelFixture)
250 want string
251 }{
252 {"signed by another key", func(t *testing.T, f *channelFixture) {
253 f.in.policy = signedPolicy(t, otherKey, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
254 f.in.identity = f.identityOf(t, "v1.4.0")
255 }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
256 {"for another bucket", func(t *testing.T, f *channelFixture) {
257 f.in.policy = signedPolicy(t, f.priv, "another-bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
258 f.in.identity = f.identityOf(t, "v1.4.0")
259 }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
260 {"re-indented", func(t *testing.T, f *channelFixture) {
261 var b bytes.Buffer
262 if err := json.Indent(&b, signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}), "", " "); err != nil {
263 t.Fatal(err)
264 }
265 f.in.policy = b.Bytes()
266 f.in.identity = f.identityOf(t, "v1.4.0")
267 }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
268 {"unpublished mode", func(t *testing.T, f *channelFixture) {
269 f.in.policy = []byte(`{"schema":1,"deployment_bucket":"bucket","mode":"unpublished"}`)
270 f.in.identity = f.identityOf(t, "v1.4.0")
271 }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"},
272 {"an armed guard", func(t *testing.T, f *channelFixture) {
273 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
274 f.in.identity = f.identityOf(t, "v1.4.0")
275 if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil {
276 t.Fatal(err)
277 }
278 if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), f.in.policy, 0o644); err != nil {
279 t.Fatal(err)
280 }
281 }, "this box's host lifecycle guard is armed, so carlos system update installs its releases"},
282 {"another release", func(t *testing.T, f *channelFixture) {
283 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
284 f.in.identity = f.identityOf(t, "v1.4.0")
285 f.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"))
286 }, "this box already runs v1.4.0, so carlos system update installs its releases"},
287 {"below the floor", func(t *testing.T, f *channelFixture) {
288 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v2.0.0"})
289 f.in.identity = f.identityOf(t, "v1.4.0")
290 }, "v1.4.0 (v1.4.0) is below the host floor v2.0.0 or is not the release its identity names, so nothing was installed"},
291 {"another floor published", func(t *testing.T, f *channelFixture) {
292 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
293 f.in.identity = f.identityOf(t, "v1.4.0")
294 if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.3.0"}); err != nil {
295 t.Fatal(err)
296 }
297 }, "the published host floor is not the floor in the --policy file, so nothing was installed"},
298 {"another floor half published", func(t *testing.T, f *channelFixture) {
299 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
300 f.in.identity = f.identityOf(t, "v1.4.0")
301 _, auth, err := hostfloor.Authorize(release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.3.0"})
302 if err != nil {
303 t.Fatal(err)
304 }
305 if err := release.CreateExact(context.Background(), f.store, hostfloor.AuthKey, auth); err != nil {
306 t.Fatal(err)
307 }
308 }, "the published host floor is not the floor in the --policy file, so nothing was installed"},
309 {"an unreadable floor", func(t *testing.T, f *channelFixture) {
310 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
311 f.in.identity = f.identityOf(t, "v1.4.0")
312 open := f.a.openStore
313 f.a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) {
314 s, k, err := open(ctx)
315 return floorUnreadable{s}, k, err
316 }
317 }, "the host floor could not be read, so nothing was installed: door restarting"},
318 {"a foreign carlos", func(t *testing.T, f *channelFixture) {
319 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
320 f.in.identity = f.identityOf(t, "v1.4.0")
321 if err := os.WriteFile(systemBinaryPath(f.a.optDir), []byte("#!/bin/sh\necho carlos v1.4.0 rebuilt\n"), 0o755); err != nil {
322 t.Fatal(err)
323 }
324 }, "%opt%/carlos holds a build this command did not install, so nothing was installed"},
325 {"a symlinked carlos.prev", func(t *testing.T, f *channelFixture) {
326 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
327 f.in.identity = f.identityOf(t, "v1.4.0")
328 elsewhere := filepath.Join(t.TempDir(), "carlos.prev")
329 if err := os.Rename(systemPrevPath(f.a.optDir), elsewhere); err != nil {
330 t.Fatal(err)
331 }
332 if err := os.Symlink(elsewhere, systemPrevPath(f.a.optDir)); err != nil {
333 t.Fatal(err)
334 }
335 }, "%opt%/carlos.prev holds a build this command did not install, so nothing was installed"},
336 {"an unreadable install record", func(t *testing.T, f *channelFixture) {
337 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
338 f.in.identity = f.identityOf(t, "v1.4.0")
339 if err := os.WriteFile(f.a.file("applied.json"), []byte("{"), 0o644); err != nil {
340 t.Fatal(err)
341 }
342 }, "this box's install record could not be read, so nothing was installed: unexpected end of JSON input"},
343 {"a carlos.prev that is a directory", func(t *testing.T, f *channelFixture) {
344 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
345 f.in.identity = f.identityOf(t, "v1.4.0")
346 if err := os.Remove(systemPrevPath(f.a.optDir)); err != nil {
347 t.Fatal(err)
348 }
349 if err := os.Mkdir(systemPrevPath(f.a.optDir), 0o755); err != nil {
350 t.Fatal(err)
351 }
352 }, "%opt%/carlos.prev holds a build this command did not install, so nothing was installed"},
353 {"an unreadable guard", func(t *testing.T, f *channelFixture) {
354 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
355 f.in.identity = f.identityOf(t, "v1.4.0")
356 if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil {
357 t.Fatal(err)
358 }
359 if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), []byte("{"), 0o644); err != nil {
360 t.Fatal(err)
361 }
362 }, "this box's host lifecycle guard could not be read, so nothing was installed: hostfloor: authenticated floor required"},
363 {"an open update journal", func(t *testing.T, f *channelFixture) {
364 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
365 f.in.identity = f.identityOf(t, "v1.4.0")
366 if err := os.WriteFile(f.a.file("journal.json"), []byte("{}"), 0o600); err != nil {
367 t.Fatal(err)
368 }
369 }, "an update on this box has not finished, so nothing was installed: %opt%/system/router/journal.json is open; let the update finish or recover first"},
370 {"an update-begun marker", func(t *testing.T, f *channelFixture) {
371 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
372 f.in.identity = f.identityOf(t, "v1.4.0")
373 if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil {
374 t.Fatal(err)
375 }
376 if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), updateBegunFile), []byte("{}"), 0o644); err != nil {
377 t.Fatal(err)
378 }
379 }, "an update on this box has not finished, so nothing was installed: %opt%/system/guard/" + updateBegunFile + " is present; let the update finish or recover first"},
380 {"a census that cannot be read", func(t *testing.T, f *channelFixture) {
381 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
382 f.in.identity = f.identityOf(t, "v1.4.0")
383 f.a.rootMoves = func() (rootFloorIdentity, bool, error) {
384 return rootFloorIdentity{}, true, errors.New("census unreadable")
385 }
386 }, "the stable channel could not be read, so nothing was installed: census unreadable"},
387 } {
388 t.Run(c.name, func(t *testing.T) {
389 f, _ := updatedChannelBox(t)
390 c.setup(t, f)
391 before := boxFiles(t, f)
392 _, err := f.in.run(context.Background())
393 want := strings.ReplaceAll(c.want, "%opt%", f.a.optDir)
394 if err == nil || err.Error() != want {
395 t.Fatalf("err %v, want %q", err, want)
396 }
397 if after := boxFiles(t, f); !equalFiles(before, after) {
398 t.Fatalf("a refused run changed the box: %v", diffFiles(before, after))
399 }
400 if f.fetches != 0 {
401 t.Fatalf("%d fetches", f.fetches)
402 }
403 if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) {
404 t.Fatalf("a refused run kept an identity: %v", err)
405 }
406 })
407 }
408}
409
410// On a fresh box --policy stands in for the floor that is not published
411// yet, and the install is the one without it: both slots, .prev first.
412func TestInstallChannelPolicyInstallsAFreshBoxBeforeTheFloor(t *testing.T) {
413 f := newChannelFixture(t, "")
414 ctx := context.Background()
415 if _, err := f.in.run(ctx); err == nil || err.Error() != "no host floor is published, so this box could not arm its guard and nothing was installed" {
416 t.Fatalf("without --policy: %v", err)
417 }
418 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
419 f.in.identity = f.identityOf(t, "v1.4.0")
420 got, err := f.in.run(ctx)
421 if err != nil || got.Already || got.Replaced {
422 t.Fatalf("%+v %v", got, err)
423 }
424 if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" {
425 t.Fatalf("line %q", line)
426 }
427 if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || f.fetches != 1 {
428 t.Fatalf("slots %q %q after %d fetches", cur, prev, f.fetches)
429 }
430 // With --policy carlos.prev's version record is part of the install.
431 if !prevRecorded(f.a.optDir, "v1.4.0") {
432 t.Fatal("the fresh install kept no version record for carlos.prev")
433 }
434 if got, err := f.in.run(ctx); err != nil || !got.Already {
435 t.Fatalf("again %+v %v", got, err)
436 }
437 // A hand-rolled slot is still refused with --policy.
438 g := newChannelFixture(t, "")
439 g.in.policy = signedPolicy(t, g.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
440 g.in.identity = g.identityOf(t, "v1.4.0")
441 if err := os.WriteFile(systemPrevPath(g.a.optDir), []byte("#!/bin/sh\necho hand rolled\n"), 0o755); err != nil {
442 t.Fatal(err)
443 }
444 if _, err := g.in.run(ctx); err == nil || err.Error() != systemPrevPath(g.a.optDir)+" holds a build this command did not install, so nothing was installed" || g.fetches != 0 {
445 t.Fatalf("hand-rolled: %v", err)
446 }
447}
448
449// failPrevRecord makes completePrev's write of carlos.prev's version record
450// fail until the returned function restores it.
451func failPrevRecord(t *testing.T) func() {
452 t.Helper()
453 real := channelPrevVersionWrite
454 channelPrevVersionWrite = func(string, string) error { return errors.New("disk full") }
455 restore := func() { channelPrevVersionWrite = real }
456 t.Cleanup(restore)
457 return restore
458}
459
460const prevRecordRefusal = "the record of the release in carlos.prev could not be written, so the install is not complete. run it again: disk full"
461
462// Fix round 1: the version record of carlos.prev is mandatory in the
463// --policy refresh, armed floor or not. A run whose record cannot be
464// written fails, keeps no identity and reports nothing done; the retry
465// completes it.
466func TestInstallChannelPolicyFailsWhenThePrevRecordCannotBeWritten(t *testing.T) {
467 f, _ := updatedChannelBox(t)
468 ctx := context.Background()
469 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
470 f.in.identity = f.identityOf(t, "v1.4.0")
471 restore := failPrevRecord(t)
472 got, err := f.in.run(ctx)
473 if err == nil || err.Error() != prevRecordRefusal || got.Replaced || got.Already {
474 t.Fatalf("a failed record: %+v %v", got, err)
475 }
476 if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) {
477 t.Fatalf("a failed record kept the identity: %v", err)
478 }
479 if _, ok := readLine(prevVersionPath(f.a.optDir)); ok {
480 t.Fatal("a failed record left a version record")
481 }
482 // Still failing: never "already installed".
483 if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already {
484 t.Fatalf("the second failed record: %+v %v", got, err)
485 }
486 restore()
487 // The failed run had already copied carlos.prev, so the retry replaces
488 // nothing and its line says only what is installed (M4).
489 got, err = f.in.run(ctx)
490 if err != nil || got.Replaced || got.Already || installedLine(got) != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" {
491 t.Fatalf("the retry: %+v %v", got, err)
492 }
493 if !prevRecorded(f.a.optDir, "v1.4.0") {
494 t.Fatal("the retry did not record carlos.prev")
495 }
496 if _, err := os.Stat(channelIdentityPath(f.a.optDir)); err != nil {
497 t.Fatal(err)
498 }
499 if again, err := f.in.run(ctx); err != nil || !again.Already {
500 t.Fatalf("after the retry: %+v %v", again, err)
501 }
502}
503
504// Fix round 1, the interruption with a matching identity already present:
505// install A, update to B, update back to A, and the --policy refresh cut
506// off after carlos.prev is copied and before its record is written. Both
507// slots and the identity then match A, which the shortcut alone would call
508// "already installed"; the retry completes the record instead.
509func TestInstallChannelPolicyCompletesACutOffRefreshOverAMatchingIdentity(t *testing.T) {
510 f := newChannelFixture(t, "")
511 ctx := context.Background()
512 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
513 f.in.identity = f.identityOf(t, "v1.4.0")
514 if got, err := f.in.run(ctx); err != nil || got.Already || got.Replaced {
515 t.Fatalf("install A: %+v %v", got, err)
516 }
517 identityA, err := os.ReadFile(channelIdentityPath(f.a.optDir))
518 if err != nil {
519 t.Fatal(err)
520 }
521 b := []byte("#!/bin/sh\necho carlos v1.5.0\n")
522 applyChannelUpdate(t, f, "v1.5.0", b, true)
523 applyChannelUpdate(t, f, "v1.4.0", f.body, false)
524 if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, b) || !prevRecorded(f.a.optDir, "v1.5.0") {
525 t.Fatalf("after the updates: %q %q", cur, prev)
526 }
527 // The cut-off refresh: carlos.prev copied, its record not written.
528 failPrevRecord(t)
529 if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already || got.Replaced {
530 t.Fatalf("the cut-off refresh: %+v %v", got, err)
531 }
532 if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) {
533 t.Fatal("the cut-off refresh did not copy carlos.prev")
534 }
535 if held, _ := os.ReadFile(channelIdentityPath(f.a.optDir)); !bytes.Equal(held, identityA) {
536 t.Fatal("the identity is not A's")
537 }
538 if _, ok := readLine(prevVersionPath(f.a.optDir)); ok {
539 t.Fatal("the cut-off refresh left a version record")
540 }
541 // Still failing: the shortcut does not answer over it.
542 if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already {
543 t.Fatalf("over incomplete metadata: %+v %v", got, err)
544 }
545 channelPrevVersionWrite = writePrevVersion
546 got, err := f.in.run(ctx)
547 if err != nil || !got.Already {
548 t.Fatalf("the retry: %+v %v", got, err)
549 }
550 if !prevRecorded(f.a.optDir, "v1.4.0") {
551 t.Fatal("the retry did not complete carlos.prev's record")
552 }
553 if f.fetches != 1 {
554 t.Fatalf("%d fetches", f.fetches)
555 }
556}
557
558// --policy's file is refused before anything else is read when it cannot
559// be read or is not an armed policy at all.
560func TestInstallChannelPolicyFileIsReadUpFront(t *testing.T) {
561 dir := t.TempDir()
562 missing := filepath.Join(dir, "policy.json")
563 if _, err := readChannelPolicy(missing); err == nil || !strings.HasPrefix(err.Error(), "the policy "+missing+" could not be read, so nothing was installed: ") {
564 t.Fatalf("missing: %v", err)
565 }
566 for _, body := range []string{"", "{", `{"mode":"unpublished"}`, `{"mode":"enforced"}`} {
567 if err := os.WriteFile(missing, []byte(body), 0o600); err != nil {
568 t.Fatal(err)
569 }
570 if _, err := readChannelPolicy(missing); err == nil || err.Error() != "the --policy file is not an armed policy signed for this deployment, so nothing was installed" {
571 t.Fatalf("%q: %v", body, err)
572 }
573 }
574 _, key, _ := ed25519.GenerateKey(nil)
575 raw := signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
576 if err := os.WriteFile(missing, raw, 0o600); err != nil {
577 t.Fatal(err)
578 }
579 if got, err := readChannelPolicy(missing); err != nil || !bytes.Equal(got, raw) {
580 t.Fatalf("a signed policy: %v", err)
581 }
582}
583
584// signedPolicyBytes is a well-formed armed policy under a throwaway key.
585func signedPolicyBytes(t *testing.T) []byte {
586 t.Helper()
587 _, key, _ := ed25519.GenerateKey(nil)
588 return signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
589}
590
591// An updated box with no carlos.prev at all gets the release in it and its
592// record; nothing was replaced, so the line says what is installed.
593func TestInstallChannelPolicyFillsAnAbsentPrev(t *testing.T) {
594 f, _ := updatedChannelBox(t)
595 if err := os.Remove(systemPrevPath(f.a.optDir)); err != nil {
596 t.Fatal(err)
597 }
598 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
599 f.in.identity = f.identityOf(t, "v1.4.0")
600 got, err := f.in.run(context.Background())
601 if err != nil || got.Replaced || got.Already {
602 t.Fatalf("%+v %v", got, err)
603 }
604 if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" {
605 t.Fatalf("line %q", line)
606 }
607 if _, prev := f.slots(t); !bytes.Equal(prev, f.body) || !prevRecorded(f.a.optDir, "v1.4.0") || f.fetches != 0 {
608 t.Fatalf("carlos.prev %q", prev)
609 }
610}
611
612// I2: on a box with a root participation census, the --policy path takes
613// the update's root moves floor branch. Here, not root, the branch cannot
614// keep the release's identity in root's guarded state, so the run refuses
615// with the box unchanged, where the census-less branch would go on. The
616// production constructor sets the census source only with --policy.
617func TestInstallChannelPolicyTakesTheCensusBranch(t *testing.T) {
618 if os.Geteuid() == 0 {
619 t.Skip("the root branch succeeds as root; TestRootInstallChannelPolicyCensusBox covers it")
620 }
621 f, _ := updatedChannelBox(t)
622 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
623 f.in.identity = f.identityOf(t, "v1.4.0")
624 f.a.rootMoves = func() (rootFloorIdentity, bool, error) {
625 return rootFloorIdentity{Deployment: "bucket", Box: "box", Enrollment: "en"}, true, nil
626 }
627 before := boxFiles(t, f)
628 if _, err := f.in.run(context.Background()); err == nil || !strings.HasPrefix(err.Error(), "the stable channel could not be read, so nothing was installed: ") {
629 t.Fatalf("a census box off root: %v", err)
630 }
631 if after := boxFiles(t, f); !equalFiles(before, after) {
632 t.Fatalf("changed: %v", diffFiles(before, after))
633 }
634 if newChannelInstall("stable", nil, nil).a.rootMoves != nil {
635 t.Fatal("the no-policy applier reads the census")
636 }
637 if newChannelInstall("stable", []byte("{}"), []byte("{}")).a.rootMoves == nil {
638 t.Fatal("the --policy applier does not read the census")
639 }
640}
641
642// abaReturn is the fix round 2 box: installed at A (v1.4.0) with --policy,
643// updated to B and back to A, so A's channel identity is still held, with
644// a moves floor armed at A on the box so that judging carlos.prev writes a
645// verdict.
646func abaReturn(t *testing.T) *channelFixture {
647 t.Helper()
648 f := newChannelFixture(t, "")
649 f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"})
650 f.in.identity = f.identityOf(t, "v1.4.0")
651 if got, err := f.in.run(context.Background()); err != nil || got.Already || got.Replaced {
652 t.Fatalf("install A: %+v %v", got, err)
653 }
654 applyChannelUpdate(t, f, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"), true)
655 applyChannelUpdate(t, f, "v1.4.0", f.body, false)
656 armLocalFloor(t, f.a.dataDir, "v1.4.0")
657 return f
658}
659
660// failJudge makes the floor's judgement of carlos.prev fail (as a cut-off
661// or failing judgement) until restored; calls counts every judgement.
662func failJudge(t *testing.T, calls *int) func() {
663 t.Helper()
664 real := channelPrevJudge
665 channelPrevJudge = func(a *systemApplier, ctx context.Context) error {
666 *calls++
667 return errors.New("judgement cut off")
668 }
669 restore := func() {
670 channelPrevJudge = func(a *systemApplier, ctx context.Context) error {
671 *calls++
672 return real(a, ctx)
673 }
674 }
675 t.Cleanup(func() { channelPrevJudge = real })
676 return restore
677}
678
679// Fix round 2: the refresh cut off after carlos.prev's record is written
680// and before its judgement finishes, over a matching identity. Binaries,
681// identity and record then all match A; the retry still judges, and only
682// then answers "already installed".
683func TestInstallChannelPolicyFinishesAJudgementCutOffAfterTheRecord(t *testing.T) {
684 f := abaReturn(t)
685 ctx := context.Background()
686 calls := 0
687 restore := failJudge(t, &calls)
688 if got, err := f.in.run(ctx); err == nil || err.Error() != "judgement cut off" || got.Already || got.Replaced {
689 t.Fatalf("the cut-off refresh: %+v %v", got, err)
690 }
691 if !prevRecorded(f.a.optDir, "v1.4.0") {
692 t.Fatal("the cut-off refresh did not write the record first")
693 }
694 if _, err := os.Stat(prevAllowedPath(f.a.optDir)); !os.IsNotExist(err) {
695 t.Fatalf("a verdict before the judgement: %v", err)
696 }
697 if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) {
698 t.Fatal("slots")
699 }
700 restore()
701 got, err := f.in.run(ctx)
702 if err != nil || !got.Already || calls != 2 {
703 t.Fatalf("the retry: %+v %v after %d judgements", got, err, calls)
704 }
705 if v, ok := readLine(prevAllowedPath(f.a.optDir)); !ok || v != "v1.4.0" {
706 t.Fatalf("the retry did not finish the judgement: %q %v", v, ok)
707 }
708 // Every --policy "already installed" judges again.
709 if got, err := f.in.run(ctx); err != nil || !got.Already || calls != 3 {
710 t.Fatalf("a rerun: %+v %v after %d judgements", got, err, calls)
711 }
712}
713
714// Fix round 2: a judgement that keeps failing is reported every time, over
715// a record and an identity that match; never "already installed".
716func TestInstallChannelPolicyReportsAFailingJudgement(t *testing.T) {
717 f := abaReturn(t)
718 ctx := context.Background()
719 calls := 0
720 failJudge(t, &calls)
721 for i := 0; i < 3; i++ {
722 if got, err := f.in.run(ctx); err == nil || err.Error() != "judgement cut off" || got.Already {
723 t.Fatalf("run %d: %+v %v", i, got, err)
724 }
725 }
726 if calls != 3 || !prevRecorded(f.a.optDir, "v1.4.0") {
727 t.Fatalf("%d judgements", calls)
728 }
729 if _, err := os.Stat(prevAllowedPath(f.a.optDir)); !os.IsNotExist(err) {
730 t.Fatalf("a verdict from a failed judgement: %v", err)
731 }
732}
733
734// floorUnreadable fails every read of the host floor's two objects.
735type floorUnreadable struct{ release.Store }
736
737func (s floorUnreadable) Get(ctx context.Context, key string) ([]byte, string, error) {
738 if key == hostfloor.Key || key == hostfloor.AuthKey {
739 return nil, "", errors.New("door restarting")
740 }
741 return s.Store.Get(ctx, key)
742}
743
744func equalFiles(a, b map[string]string) bool { return len(diffFiles(a, b)) == 0 }
745
746func diffFiles(a, b map[string]string) []string {
747 var d []string
748 for k, v := range a {
749 if w, ok := b[k]; !ok || w != v {
750 d = append(d, k)
751 }
752 }
753 for k := range b {
754 if _, ok := a[k]; !ok {
755 d = append(d, k)
756 }
757 }
758 return d
759}
760