package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // install-channel --policy (claim trial prev-rehearsal, ruling R1). The box // these tests start from is made by the platform's own update path, never // by hand: v1.3.0 on disk, the channel promoted to v1.4.0, the agent's // request and cache written as systemLeg writes them, then the real apply // (systemApplier.Run) moving v1.3.0 to carlos.prev. That is the trial's // leo-climbing: applied.json names the channel release, carlos is its // artifact, carlos.prev the release before, and no channel identity. // updatedChannelBox returns that box. old and body are the two releases' // bytes. func updatedChannelBox(t *testing.T) (f *channelFixture, old []byte) { t.Helper() f = &channelFixture{applyFixture: newApplyFixture(t, "stable", "v1.3.0")} old, err := os.ReadFile(systemBinaryPath(f.a.optDir)) if err != nil { t.Fatal(err) } f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") applyChannelUpdate(t, f, "v1.4.0", f.body, true) if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) { t.Fatalf("the update left %q and %q", cur, prev) } if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("an update wrote a channel identity: %v", err) } f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { f.fetches++ return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) }, own: func(path string) error { f.owned = append(f.owned, path) return nil }} return f, old } // applyChannelUpdate moves f's box to version through the platform's update // path: the promote (and, with identity, the release identity), the agent's // fetch and request (systemLeg), then the real apply. func applyChannelUpdate(t *testing.T, f *channelFixture, version string, body []byte, identity bool) { t.Helper() applyChannelUpdateWith(t, f, version, body, func() { if identity { f.publish(t, version, body) return } rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{ "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hostfloor.Digest(body)}}} if _, err := release.PromoteSystem(context.Background(), f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } }) } // applyChannelUpdateWith is applyChannelUpdate with the promote given. func applyChannelUpdateWith(t *testing.T, f *channelFixture, version string, body []byte, promote func()) { t.Helper() ctx := context.Background() promote() p, _, err := release.GetSystemPointer(ctx, f.store, f.pub, release.SystemComponentRouter, "stable") if err != nil { t.Fatal(err) } leg := &systemLeg{dataDir: f.a.dataDir, fetchTo: func(ctx context.Context, key, dst, sha string) error { return release.FetchToFile(ctx, f.store, key, dst, systemArtifactLimit, sha) }} pending := &systemPending{version: version, promotedAt: p.PromotedAt, artifact: p.Current.Artifacts["linux-arm64"]} if err := leg.fetch(ctx, pending); err != nil { t.Fatal(err) } if err := leg.writeRequest(pending, f.now); err != nil { t.Fatal(err) } // The candidate ships the instance layout's mount, so the layout guard // does not open the registry (a post-layout release, as every one the // trial installs is). 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" for _, path := range []string{filepath.Join(f.a.stagingDir(), "carlos"), systemPrevPath(f.a.optDir), systemBinaryPath(f.a.optDir)} { f.answer["plan:"+path], f.answer["units:"+path] = plan, units // Every binary answers `version` from its own bytes. delete(f.answer, "version:"+path) } if err := f.a.Run(ctx, false); err != nil { t.Fatal(err) } if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied || r.Version != version { t.Fatalf("the update to %s did not apply: %+v", version, r) } if cur, _ := os.ReadFile(systemBinaryPath(f.a.optDir)); !bytes.Equal(cur, body) { t.Fatalf("the update left %q", cur) } } // signedPolicy is what claims run sign-policy writes for floor. func signedPolicy(t *testing.T, key ed25519.PrivateKey, bucket string, floor hostfloor.Floor) []byte { t.Helper() p, err := hostfloor.ArmedPolicy(release.RootSigner(key), bucket, floor) if err != nil { t.Fatal(err) } raw, err := json.Marshal(p) if err != nil { t.Fatal(err) } return raw } // boxFiles is every file install-channel or the update keeps in the root // directory and the two slots, by name, so a refusal can be shown to have // changed nothing. func boxFiles(t *testing.T, f *channelFixture) map[string]string { t.Helper() got := map[string]string{} for _, dir := range []string{f.a.optDir, systemRootDir(f.a.optDir)} { ents, err := os.ReadDir(dir) if err != nil { t.Fatal(err) } for _, e := range ents { path := filepath.Join(dir, e.Name()) if fi, err := os.Lstat(path); err == nil && fi.Mode().IsRegular() { b, _ := os.ReadFile(path) got[path] = string(b) } else if err == nil { got[path] = fi.Mode().String() } } } return got } func TestInstallChannelPolicyArmsAnUpdatedBoxAtTheChannelHead(t *testing.T) { f, old := updatedChannelBox(t) ctx := context.Background() // Without --policy the box is refused exactly as before, and not // changed: with no floor published, for that (the trial's loop), and // with one, as an updated box (the sibling // TestInstallChannelRefusesAnUpdatedBoxAtTheChannelHead). before := boxFiles(t, f) 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" { t.Fatalf("without --policy: %v", err) } if after := boxFiles(t, f); !equalFiles(before, after) { t.Fatalf("a refused run changed the box: %v", diffFiles(before, after)) } applied, _ := os.ReadFile(f.a.file("applied.json")) seen, _ := os.ReadFile(f.a.file("seen.json")) f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") got, err := f.in.run(ctx) if err != nil { t.Fatal(err) } if !got.Replaced || got.Already || got.Version != "v1.4.0" || got.Tag != "v1.4.0" { t.Fatalf("installed %+v", got) } 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" { t.Fatalf("line %q", line) } cur, prev := f.slots(t) if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || bytes.Equal(prev, old) { t.Fatalf("slots hold %q and %q", cur, prev) } // Nothing was fetched: carlos already is the artifact. if f.fetches != 0 { t.Fatalf("%d fetches", f.fetches) } // The update's records stand; the version sidecar names the release // carlos.prev now holds. if a, _ := os.ReadFile(f.a.file("applied.json")); !bytes.Equal(a, applied) { t.Fatalf("applied.json changed: %s", a) } if s, _ := os.ReadFile(f.a.file("seen.json")); !bytes.Equal(s, seen) { t.Fatalf("seen.json changed: %s", s) } if v, ok := readLine(prevVersionPath(f.a.optDir)); !ok || v != "v1.4.0" { t.Fatalf("carlos.prev's version sidecar is %q %v", v, ok) } // The identity kept beside the slots is the published one, and // guard-arm's two checks pass: its inputs, and both slots. key, _ := release.IdentityKey("v1.4.0") published, _, err := f.store.Get(ctx, key) if err != nil { t.Fatal(err) } held, err := os.ReadFile(channelIdentityPath(f.a.optDir)) if err != nil || !bytes.Equal(held, published) { t.Fatalf("identity %v", err) } _, floor, err := guardArmInputs(f.in.policy, held, f.pub, "bucket", "v1.4.0", "linux-arm64", hostfloor.Digest(f.body)) if err != nil { t.Fatalf("guard-arm's inputs: %v", err) } for _, slot := range []string{systemBinaryPath(f.a.optDir), systemPrevPath(f.a.optDir)} { b, _ := os.ReadFile(slot) if err := hostfloor.CheckImage(f.pub, floor, held, "v1.4.0", "linux-arm64", hostfloor.Digest(b)); err != nil { t.Fatalf("guard-arm would refuse %s: %v", slot, err) } } // Run again: nothing to do. again, err := f.in.run(ctx) if err != nil || !again.Already || again.Replaced || f.fetches != 0 { t.Fatalf("second run %+v %v", again, err) } // The floor, once published from the same plan, is the policy's own // bytes, so the run still passes; the same with half of it published, // as a publication cut off after its first write leaves it. var policy hostfloor.Policy if err := json.Unmarshal(f.in.policy, &policy); err != nil { t.Fatal(err) } if err := release.CreateExact(ctx, f.store, hostfloor.AuthKey, policy.Authorization); err != nil { t.Fatal(err) } if got, err := f.in.run(ctx); err != nil || !got.Already { t.Fatalf("half published: %+v %v", got, err) } if err := hostfloor.Publish(ctx, f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}); err != nil { t.Fatal(err) } if got, err := f.in.run(ctx); err != nil || !got.Already { t.Fatalf("published: %+v %v", got, err) } } // Each refusal of --policy on the updated box, and on a fresh one, leaves // the box as it was and fetches nothing. func TestInstallChannelPolicyRefusals(t *testing.T) { t.Parallel() _, otherKey, _ := ed25519.GenerateKey(nil) for _, c := range []struct { name string setup func(t *testing.T, f *channelFixture) want string }{ {"signed by another key", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, otherKey, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"}, {"for another bucket", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "another-bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"}, {"re-indented", func(t *testing.T, f *channelFixture) { var b bytes.Buffer if err := json.Indent(&b, signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}), "", " "); err != nil { t.Fatal(err) } f.in.policy = b.Bytes() f.in.identity = f.identityOf(t, "v1.4.0") }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"}, {"unpublished mode", func(t *testing.T, f *channelFixture) { f.in.policy = []byte(`{"schema":1,"deployment_bucket":"bucket","mode":"unpublished"}`) f.in.identity = f.identityOf(t, "v1.4.0") }, "the --policy file is not an armed policy signed for this deployment, so nothing was installed"}, {"an armed guard", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), f.in.policy, 0o644); err != nil { t.Fatal(err) } }, "this box's host lifecycle guard is armed, so carlos system update installs its releases"}, {"another release", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") f.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n")) }, "this box already runs v1.4.0, so carlos system update installs its releases"}, {"below the floor", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v2.0.0"}) f.in.identity = f.identityOf(t, "v1.4.0") }, "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"}, {"another floor published", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.3.0"}); err != nil { t.Fatal(err) } }, "the published host floor is not the floor in the --policy file, so nothing was installed"}, {"another floor half published", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") _, auth, err := hostfloor.Authorize(release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.3.0"}) if err != nil { t.Fatal(err) } if err := release.CreateExact(context.Background(), f.store, hostfloor.AuthKey, auth); err != nil { t.Fatal(err) } }, "the published host floor is not the floor in the --policy file, so nothing was installed"}, {"an unreadable floor", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") open := f.a.openStore f.a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) { s, k, err := open(ctx) return floorUnreadable{s}, k, err } }, "the host floor could not be read, so nothing was installed: door restarting"}, {"a foreign carlos", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.WriteFile(systemBinaryPath(f.a.optDir), []byte("#!/bin/sh\necho carlos v1.4.0 rebuilt\n"), 0o755); err != nil { t.Fatal(err) } }, "%opt%/carlos holds a build this command did not install, so nothing was installed"}, {"a symlinked carlos.prev", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") elsewhere := filepath.Join(t.TempDir(), "carlos.prev") if err := os.Rename(systemPrevPath(f.a.optDir), elsewhere); err != nil { t.Fatal(err) } if err := os.Symlink(elsewhere, systemPrevPath(f.a.optDir)); err != nil { t.Fatal(err) } }, "%opt%/carlos.prev holds a build this command did not install, so nothing was installed"}, {"an unreadable install record", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.WriteFile(f.a.file("applied.json"), []byte("{"), 0o644); err != nil { t.Fatal(err) } }, "this box's install record could not be read, so nothing was installed: unexpected end of JSON input"}, {"a carlos.prev that is a directory", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.Remove(systemPrevPath(f.a.optDir)); err != nil { t.Fatal(err) } if err := os.Mkdir(systemPrevPath(f.a.optDir), 0o755); err != nil { t.Fatal(err) } }, "%opt%/carlos.prev holds a build this command did not install, so nothing was installed"}, {"an unreadable guard", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), "policy.json"), []byte("{"), 0o644); err != nil { t.Fatal(err) } }, "this box's host lifecycle guard could not be read, so nothing was installed: hostfloor: authenticated floor required"}, {"an open update journal", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.WriteFile(f.a.file("journal.json"), []byte("{}"), 0o600); err != nil { t.Fatal(err) } }, "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"}, {"an update-begun marker", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if err := os.MkdirAll(systemGuardDir(f.a.optDir), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(systemGuardDir(f.a.optDir), updateBegunFile), []byte("{}"), 0o644); err != nil { t.Fatal(err) } }, "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"}, {"a census that cannot be read", func(t *testing.T, f *channelFixture) { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") f.a.rootMoves = func() (rootFloorIdentity, bool, error) { return rootFloorIdentity{}, true, errors.New("census unreadable") } }, "the stable channel could not be read, so nothing was installed: census unreadable"}, } { t.Run(c.name, func(t *testing.T) { f, _ := updatedChannelBox(t) c.setup(t, f) before := boxFiles(t, f) _, err := f.in.run(context.Background()) want := strings.ReplaceAll(c.want, "%opt%", f.a.optDir) if err == nil || err.Error() != want { t.Fatalf("err %v, want %q", err, want) } if after := boxFiles(t, f); !equalFiles(before, after) { t.Fatalf("a refused run changed the box: %v", diffFiles(before, after)) } if f.fetches != 0 { t.Fatalf("%d fetches", f.fetches) } if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("a refused run kept an identity: %v", err) } }) } } // On a fresh box --policy stands in for the floor that is not published // yet, and the install is the one without it: both slots, .prev first. func TestInstallChannelPolicyInstallsAFreshBoxBeforeTheFloor(t *testing.T) { f := newChannelFixture(t, "") ctx := context.Background() 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" { t.Fatalf("without --policy: %v", err) } f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") got, err := f.in.run(ctx) if err != nil || got.Already || got.Replaced { t.Fatalf("%+v %v", got, err) } if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" { t.Fatalf("line %q", line) } if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || f.fetches != 1 { t.Fatalf("slots %q %q after %d fetches", cur, prev, f.fetches) } // With --policy carlos.prev's version record is part of the install. if !prevRecorded(f.a.optDir, "v1.4.0") { t.Fatal("the fresh install kept no version record for carlos.prev") } if got, err := f.in.run(ctx); err != nil || !got.Already { t.Fatalf("again %+v %v", got, err) } // A hand-rolled slot is still refused with --policy. g := newChannelFixture(t, "") g.in.policy = signedPolicy(t, g.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) g.in.identity = g.identityOf(t, "v1.4.0") if err := os.WriteFile(systemPrevPath(g.a.optDir), []byte("#!/bin/sh\necho hand rolled\n"), 0o755); err != nil { t.Fatal(err) } 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 { t.Fatalf("hand-rolled: %v", err) } } // failPrevRecord makes completePrev's write of carlos.prev's version record // fail until the returned function restores it. func failPrevRecord(t *testing.T) func() { t.Helper() real := channelPrevVersionWrite channelPrevVersionWrite = func(string, string) error { return errors.New("disk full") } restore := func() { channelPrevVersionWrite = real } t.Cleanup(restore) return restore } const prevRecordRefusal = "the record of the release in carlos.prev could not be written, so the install is not complete. run it again: disk full" // Fix round 1: the version record of carlos.prev is mandatory in the // --policy refresh, armed floor or not. A run whose record cannot be // written fails, keeps no identity and reports nothing done; the retry // completes it. func TestInstallChannelPolicyFailsWhenThePrevRecordCannotBeWritten(t *testing.T) { f, _ := updatedChannelBox(t) ctx := context.Background() f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") restore := failPrevRecord(t) got, err := f.in.run(ctx) if err == nil || err.Error() != prevRecordRefusal || got.Replaced || got.Already { t.Fatalf("a failed record: %+v %v", got, err) } if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("a failed record kept the identity: %v", err) } if _, ok := readLine(prevVersionPath(f.a.optDir)); ok { t.Fatal("a failed record left a version record") } // Still failing: never "already installed". if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already { t.Fatalf("the second failed record: %+v %v", got, err) } restore() // The failed run had already copied carlos.prev, so the retry replaces // nothing and its line says only what is installed (M4). got, err = f.in.run(ctx) 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" { t.Fatalf("the retry: %+v %v", got, err) } if !prevRecorded(f.a.optDir, "v1.4.0") { t.Fatal("the retry did not record carlos.prev") } if _, err := os.Stat(channelIdentityPath(f.a.optDir)); err != nil { t.Fatal(err) } if again, err := f.in.run(ctx); err != nil || !again.Already { t.Fatalf("after the retry: %+v %v", again, err) } } // Fix round 1, the interruption with a matching identity already present: // install A, update to B, update back to A, and the --policy refresh cut // off after carlos.prev is copied and before its record is written. Both // slots and the identity then match A, which the shortcut alone would call // "already installed"; the retry completes the record instead. func TestInstallChannelPolicyCompletesACutOffRefreshOverAMatchingIdentity(t *testing.T) { f := newChannelFixture(t, "") ctx := context.Background() f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if got, err := f.in.run(ctx); err != nil || got.Already || got.Replaced { t.Fatalf("install A: %+v %v", got, err) } identityA, err := os.ReadFile(channelIdentityPath(f.a.optDir)) if err != nil { t.Fatal(err) } b := []byte("#!/bin/sh\necho carlos v1.5.0\n") applyChannelUpdate(t, f, "v1.5.0", b, true) applyChannelUpdate(t, f, "v1.4.0", f.body, false) if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, b) || !prevRecorded(f.a.optDir, "v1.5.0") { t.Fatalf("after the updates: %q %q", cur, prev) } // The cut-off refresh: carlos.prev copied, its record not written. failPrevRecord(t) if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already || got.Replaced { t.Fatalf("the cut-off refresh: %+v %v", got, err) } if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { t.Fatal("the cut-off refresh did not copy carlos.prev") } if held, _ := os.ReadFile(channelIdentityPath(f.a.optDir)); !bytes.Equal(held, identityA) { t.Fatal("the identity is not A's") } if _, ok := readLine(prevVersionPath(f.a.optDir)); ok { t.Fatal("the cut-off refresh left a version record") } // Still failing: the shortcut does not answer over it. if got, err := f.in.run(ctx); err == nil || err.Error() != prevRecordRefusal || got.Already { t.Fatalf("over incomplete metadata: %+v %v", got, err) } channelPrevVersionWrite = writePrevVersion got, err := f.in.run(ctx) if err != nil || !got.Already { t.Fatalf("the retry: %+v %v", got, err) } if !prevRecorded(f.a.optDir, "v1.4.0") { t.Fatal("the retry did not complete carlos.prev's record") } if f.fetches != 1 { t.Fatalf("%d fetches", f.fetches) } } // --policy's file is refused before anything else is read when it cannot // be read or is not an armed policy at all. func TestInstallChannelPolicyFileIsReadUpFront(t *testing.T) { dir := t.TempDir() missing := filepath.Join(dir, "policy.json") if _, err := readChannelPolicy(missing); err == nil || !strings.HasPrefix(err.Error(), "the policy "+missing+" could not be read, so nothing was installed: ") { t.Fatalf("missing: %v", err) } for _, body := range []string{"", "{", `{"mode":"unpublished"}`, `{"mode":"enforced"}`} { if err := os.WriteFile(missing, []byte(body), 0o600); err != nil { t.Fatal(err) } if _, err := readChannelPolicy(missing); err == nil || err.Error() != "the --policy file is not an armed policy signed for this deployment, so nothing was installed" { t.Fatalf("%q: %v", body, err) } } _, key, _ := ed25519.GenerateKey(nil) raw := signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) if err := os.WriteFile(missing, raw, 0o600); err != nil { t.Fatal(err) } if got, err := readChannelPolicy(missing); err != nil || !bytes.Equal(got, raw) { t.Fatalf("a signed policy: %v", err) } } // signedPolicyBytes is a well-formed armed policy under a throwaway key. func signedPolicyBytes(t *testing.T) []byte { t.Helper() _, key, _ := ed25519.GenerateKey(nil) return signedPolicy(t, key, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) } // An updated box with no carlos.prev at all gets the release in it and its // record; nothing was replaced, so the line says what is installed. func TestInstallChannelPolicyFillsAnAbsentPrev(t *testing.T) { f, _ := updatedChannelBox(t) if err := os.Remove(systemPrevPath(f.a.optDir)); err != nil { t.Fatal(err) } f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") got, err := f.in.run(context.Background()) if err != nil || got.Replaced || got.Already { t.Fatalf("%+v %v", got, err) } if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" { t.Fatalf("line %q", line) } if _, prev := f.slots(t); !bytes.Equal(prev, f.body) || !prevRecorded(f.a.optDir, "v1.4.0") || f.fetches != 0 { t.Fatalf("carlos.prev %q", prev) } } // I2: on a box with a root participation census, the --policy path takes // the update's root moves floor branch. Here, not root, the branch cannot // keep the release's identity in root's guarded state, so the run refuses // with the box unchanged, where the census-less branch would go on. The // production constructor sets the census source only with --policy. func TestInstallChannelPolicyTakesTheCensusBranch(t *testing.T) { if os.Geteuid() == 0 { t.Skip("the root branch succeeds as root; TestRootInstallChannelPolicyCensusBox covers it") } f, _ := updatedChannelBox(t) f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") f.a.rootMoves = func() (rootFloorIdentity, bool, error) { return rootFloorIdentity{Deployment: "bucket", Box: "box", Enrollment: "en"}, true, nil } before := boxFiles(t, f) if _, err := f.in.run(context.Background()); err == nil || !strings.HasPrefix(err.Error(), "the stable channel could not be read, so nothing was installed: ") { t.Fatalf("a census box off root: %v", err) } if after := boxFiles(t, f); !equalFiles(before, after) { t.Fatalf("changed: %v", diffFiles(before, after)) } if newChannelInstall("stable", nil, nil).a.rootMoves != nil { t.Fatal("the no-policy applier reads the census") } if newChannelInstall("stable", []byte("{}"), []byte("{}")).a.rootMoves == nil { t.Fatal("the --policy applier does not read the census") } } // abaReturn is the fix round 2 box: installed at A (v1.4.0) with --policy, // updated to B and back to A, so A's channel identity is still held, with // a moves floor armed at A on the box so that judging carlos.prev writes a // verdict. func abaReturn(t *testing.T) *channelFixture { t.Helper() f := newChannelFixture(t, "") f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = f.identityOf(t, "v1.4.0") if got, err := f.in.run(context.Background()); err != nil || got.Already || got.Replaced { t.Fatalf("install A: %+v %v", got, err) } applyChannelUpdate(t, f, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"), true) applyChannelUpdate(t, f, "v1.4.0", f.body, false) armLocalFloor(t, f.a.dataDir, "v1.4.0") return f } // failJudge makes the floor's judgement of carlos.prev fail (as a cut-off // or failing judgement) until restored; calls counts every judgement. func failJudge(t *testing.T, calls *int) func() { t.Helper() real := channelPrevJudge channelPrevJudge = func(a *systemApplier, ctx context.Context) error { *calls++ return errors.New("judgement cut off") } restore := func() { channelPrevJudge = func(a *systemApplier, ctx context.Context) error { *calls++ return real(a, ctx) } } t.Cleanup(func() { channelPrevJudge = real }) return restore } // Fix round 2: the refresh cut off after carlos.prev's record is written // and before its judgement finishes, over a matching identity. Binaries, // identity and record then all match A; the retry still judges, and only // then answers "already installed". func TestInstallChannelPolicyFinishesAJudgementCutOffAfterTheRecord(t *testing.T) { f := abaReturn(t) ctx := context.Background() calls := 0 restore := failJudge(t, &calls) if got, err := f.in.run(ctx); err == nil || err.Error() != "judgement cut off" || got.Already || got.Replaced { t.Fatalf("the cut-off refresh: %+v %v", got, err) } if !prevRecorded(f.a.optDir, "v1.4.0") { t.Fatal("the cut-off refresh did not write the record first") } if _, err := os.Stat(prevAllowedPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("a verdict before the judgement: %v", err) } if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { t.Fatal("slots") } restore() got, err := f.in.run(ctx) if err != nil || !got.Already || calls != 2 { t.Fatalf("the retry: %+v %v after %d judgements", got, err, calls) } if v, ok := readLine(prevAllowedPath(f.a.optDir)); !ok || v != "v1.4.0" { t.Fatalf("the retry did not finish the judgement: %q %v", v, ok) } // Every --policy "already installed" judges again. if got, err := f.in.run(ctx); err != nil || !got.Already || calls != 3 { t.Fatalf("a rerun: %+v %v after %d judgements", got, err, calls) } } // Fix round 2: a judgement that keeps failing is reported every time, over // a record and an identity that match; never "already installed". func TestInstallChannelPolicyReportsAFailingJudgement(t *testing.T) { f := abaReturn(t) ctx := context.Background() calls := 0 failJudge(t, &calls) for i := 0; i < 3; i++ { if got, err := f.in.run(ctx); err == nil || err.Error() != "judgement cut off" || got.Already { t.Fatalf("run %d: %+v %v", i, got, err) } } if calls != 3 || !prevRecorded(f.a.optDir, "v1.4.0") { t.Fatalf("%d judgements", calls) } if _, err := os.Stat(prevAllowedPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("a verdict from a failed judgement: %v", err) } } // floorUnreadable fails every read of the host floor's two objects. type floorUnreadable struct{ release.Store } func (s floorUnreadable) Get(ctx context.Context, key string) ([]byte, string, error) { if key == hostfloor.Key || key == hostfloor.AuthKey { return nil, "", errors.New("door restarting") } return s.Store.Get(ctx, key) } func equalFiles(a, b map[string]string) bool { return len(diffFiles(a, b)) == 0 } func diffFiles(a, b map[string]string) []string { var d []string for k, v := range a { if w, ok := b[k]; !ok || w != v { d = append(d, k) } } for k := range b { if _, ok := a[k]; !ok { d = append(d, k) } } return d }