package host import ( "bytes" "context" "crypto/ed25519" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "path/filepath" "reflect" "strings" "testing" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // channelFixture is a fresh box (applyFixture with no binary on it) whose // stable channel names v1.4.0, whose identity is published, and whose // deployment has a host floor. type channelFixture struct { *applyFixture in channelInstall body []byte fetches int owned []string } func newChannelFixture(t *testing.T, floor string) *channelFixture { t.Helper() f := &channelFixture{applyFixture: newApplyFixture(t, "stable", "bootstrap")} if err := os.Remove(systemBinaryPath(f.a.optDir)); err != nil { t.Fatal(err) } f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") f.publish(t, "v1.4.0", f.body) if floor != "" { if err := hostfloor.Publish(context.Background(), f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: floor}); err != nil { t.Fatal(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 } // publish promotes version on stable with body as its linux-arm64 // artifact and publishes its release identity. func (f *channelFixture) publish(t *testing.T, version string, body []byte) { t.Helper() ctx := context.Background() sum := sha256.Sum256(body) rel := release.SystemRelease{Version: version, ReleaseTag: version, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{ "linux-arm64": {Key: release.SystemArtifactKey(version, "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}}} if _, err := f.store.Put(ctx, rel.Artifacts["linux-arm64"].Key, body, ""); err != nil { t.Fatal(err) } if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } wire, _, err := f.store.Get(ctx, release.SystemPointerKey("router", "stable")) if err != nil { t.Fatal(err) } if err := release.PublishSystemIdentity(ctx, f.store, f.pub, wire); err != nil { t.Fatal(err) } } func (f *channelFixture) slots(t *testing.T) (cur, prev []byte) { t.Helper() cur, _ = os.ReadFile(systemBinaryPath(f.a.optDir)) prev, _ = os.ReadFile(systemPrevPath(f.a.optDir)) return cur, prev } func TestInstallChannelPutsTheChannelReleaseInBothSlots(t *testing.T) { f := newChannelFixture(t, "v1.2.0") got, err := f.in.run(context.Background()) if err != nil { t.Fatal(err) } if got.Version != "v1.4.0" || got.Tag != "v1.4.0" || got.Already { t.Fatalf("installed %+v", got) } cur, prev := f.slots(t) if !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { t.Fatalf("slots hold %q and %q, not the channel's artifact", cur, prev) } // The identity guard-arm will be given is the channel release's. held, err := os.ReadFile(channelIdentityPath(f.a.optDir)) if err != nil { t.Fatal(err) } p, err := release.VerifySystemIdentity(f.pub, held) if err != nil || p.Current.Version != "v1.4.0" { t.Fatalf("identity %v %v", p, err) } // An update later finds the binary on disk known good, and has seen // this promote. applied, err := readSystemApplied(f.a.optDir) if err != nil || applied == nil || applied.Version != "v1.4.0" || applied.Channel != "stable" { t.Fatalf("applied %+v %v", applied, err) } seen, err := readSystemSeen(f.a.optDir) if err != nil || seen["stable"].IsZero() { t.Fatalf("seen %v %v", seen, err) } // Nothing is left in staging or the agent's cache. if _, err := os.Stat(f.a.stagingDir()); !os.IsNotExist(err) { t.Fatalf("staging left: %v", err) } if _, err := os.Stat(filepath.Dir(systemCachePath(f.a.dataDir, "v1.4.0"))); !os.IsNotExist(err) { t.Fatalf("cache left: %v", err) } // The candidate was asked its version from staging before it was put // in place. if !f.called("run carlos version") { t.Fatalf("calls %v", f.calls) } // R2, S2: every directory on the cache's path is handed to the // service user, and nothing else, so the agent's own update can // download the next release beside them and ask for it. if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) { t.Fatalf("handed to the service user: %v, want %v", f.owned, want) } // Run again: nothing to do, nothing fetched. again, err := f.in.run(context.Background()) if err != nil || !again.Already || f.fetches != 1 { t.Fatalf("second run %+v %v, %d fetches", again, err, f.fetches) } if line := installedLine(again); line != "v1.4.0 (v1.4.0) from the stable channel is already installed" { t.Fatalf("line %q", line) } // The agent, its next release promoted, downloads it and asks for it // in the directories install-channel left. 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) }} next := []byte("#!/bin/sh\necho carlos v1.5.0\n") f.publish(t, "v1.5.0", next) sum := sha256.Sum256(next) pending := &systemPending{version: "v1.5.0", artifact: release.SystemArtifact{Key: release.SystemArtifactKey("v1.5.0", "linux-arm64"), SHA256: hex.EncodeToString(sum[:])}} if err := leg.fetch(context.Background(), pending); err != nil { t.Fatalf("the agent's next download: %v", err) } if err := leg.writeRequest(pending, f.now); err != nil { t.Fatalf("the agent's next request: %v", err) } } // cachePathDirs are the directories install-channel hands the service // user for version: system, system/router and the version's own. func cachePathDirs(dataDir, version string) []string { return []string{filepath.Join(dataDir, "system"), filepath.Join(dataDir, "system", "router"), filepath.Join(dataDir, "system", "router", version)} } // S2: a run whose ownership handoff fails installs nothing; the next run // reconciles every directory on the path, including the ones the failed // run created, not only the ones it creates itself. func TestInstallChannelReconcilesOwnershipOnARetry(t *testing.T) { f := newChannelFixture(t, "v1.2.0") own := f.in.own f.in.own = func(string) error { return errors.New("chown: operation not permitted") } if _, err := f.in.run(context.Background()); err == nil { t.Fatal("a failed handoff installed") } if cur, prev := f.slots(t); cur != nil || prev != nil || f.fetches != 0 { t.Fatalf("a failed handoff wrote %q %q after %d fetches", cur, prev, f.fetches) } // The failed run created system, as root, and never handed it over. if _, err := os.Stat(filepath.Join(f.a.dataDir, "system")); err != nil { t.Fatalf("the failed run's parent: %v", err) } f.in.own = own if _, err := f.in.run(context.Background()); err != nil { t.Fatal(err) } if want := cachePathDirs(f.a.dataDir, "v1.4.0"); !reflect.DeepEqual(f.owned, want) { t.Fatalf("the retry handed %v, want %v", f.owned, want) } } // S2: a symlinked parent is refused before anything is fetched, and never // followed; the real handoff opens each directory without following one. func TestInstallChannelRefusesASymlinkedParent(t *testing.T) { f := newChannelFixture(t, "v1.2.0") elsewhere := t.TempDir() if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil { t.Fatal(err) } if _, err := f.in.run(context.Background()); err == nil || f.fetches != 0 || len(f.owned) != 0 { t.Fatalf("symlinked parent: %v, %d fetches, owned %v", err, f.fetches, f.owned) } if ents, _ := os.ReadDir(elsewhere); len(ents) != 0 { t.Fatalf("followed the symlink: %v", ents) } if err := ownDir(filepath.Join(f.a.dataDir, "system"), os.Getuid(), os.Getgid()); err == nil { t.Fatal("the handoff followed a symlink") } if err := ownDir(elsewhere, os.Getuid(), os.Getgid()); err != nil { t.Fatalf("the handoff of a real directory: %v", err) } } func TestInstallChannelRefusesBelowTheFloorAndWithoutOne(t *testing.T) { for _, c := range []struct{ name, floor, want string }{ {"below the floor", "v2.0.0", "below the host floor v2.0.0"}, {"no floor", "", "no host floor is published"}, } { t.Run(c.name, func(t *testing.T) { f := newChannelFixture(t, c.floor) _, err := f.in.run(context.Background()) if err == nil || !strings.Contains(err.Error(), c.want) { t.Fatalf("err %v", err) } cur, prev := f.slots(t) if cur != nil || prev != nil || f.fetches != 0 { t.Fatalf("refused install wrote %q %q after %d fetches", cur, prev, f.fetches) } }) } } func TestInstallChannelRefusesAnUnreadableStoreAndABadArtifact(t *testing.T) { f := newChannelFixture(t, "v1.2.0") f.a.openStore = func(context.Context) (release.Store, ed25519.PublicKey, error) { return nil, nil, errors.New("no route to bucket") } if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be read") { t.Fatalf("unreachable store: %v", err) } g := newChannelFixture(t, "v1.2.0") // The object no longer matches the signed sha256. _, etag, _ := g.store.Get(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64")) if _, err := g.store.Put(context.Background(), release.SystemArtifactKey("v1.4.0", "linux-arm64"), []byte("#!/bin/sh\necho carlos v0.0.1\n"), etag); err != nil { t.Fatal(err) } if _, err := g.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "could not be fetched") { t.Fatalf("swapped artifact: %v", err) } if cur, prev := g.slots(t); cur != nil || prev != nil { t.Fatalf("swapped artifact installed %q %q", cur, prev) } } func TestInstallChannelLeavesAnInstalledBoxToUpdates(t *testing.T) { // An armed guard. f := newChannelFixture(t, "v1.2.0") ctx := context.Background() raw, _, _ := f.store.Get(ctx, hostfloor.Key) auth, _, _ := f.store.Get(ctx, hostfloor.AuthKey) policy, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "armed", Floor: raw, Authorization: auth}) 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"), policy, 0o644); err != nil { t.Fatal(err) } if _, err := f.in.run(ctx); err == nil || !strings.Contains(err.Error(), "guard is armed") { t.Fatalf("armed guard: %v", err) } // A box an earlier install or update put another release on. g := newChannelFixture(t, "v1.2.0") if _, err := g.in.run(ctx); err != nil { t.Fatal(err) } g.publish(t, "v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n")) if _, err := g.in.run(ctx); err == nil || !strings.Contains(err.Error(), "already runs v1.4.0") { t.Fatalf("installed box: %v", err) } if cur, _ := g.slots(t); !bytes.Equal(cur, g.body) { t.Fatalf("installed box changed to %q", cur) } } // A refused run removes nothing through a symlinked parent: the cleanup of // the version directory is made only after the directories are made, and // goes by descriptor, so root cannot be walked out of /data. func TestInstallChannelRemovesNothingThroughASymlink(t *testing.T) { f := newChannelFixture(t, "v1.2.0") elsewhere := t.TempDir() keep := filepath.Join(elsewhere, "router", "v1.4.0", "keep") if err := os.MkdirAll(filepath.Dir(keep), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(keep, []byte("mine"), 0o644); err != nil { t.Fatal(err) } if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, "system")); err != nil { t.Fatal(err) } if _, err := f.in.run(context.Background()); err == nil { t.Fatal("a symlinked parent installed") } if b, err := os.ReadFile(keep); err != nil || string(b) != "mine" { t.Fatalf("a refused run removed what the symlink led to: %q %v", b, err) } // The removal itself, called directly: a symlinked parent, and a // version directory that is itself a symlink, are neither entered nor // followed. if err := removeCacheEntry(f.a.dataDir, "v1.4.0"); err == nil { t.Fatal("the removal went through a symlinked parent") } if b, err := os.ReadFile(keep); err != nil || string(b) != "mine" { t.Fatalf("removal followed a symlinked parent: %q %v", b, err) } g := newChannelFixture(t, "v1.2.0") router := filepath.Join(g.a.dataDir, "system", "router") if err := os.MkdirAll(router, 0o755); err != nil { t.Fatal(err) } other := t.TempDir() if err := os.WriteFile(filepath.Join(other, "keep"), []byte("mine"), 0o644); err != nil { t.Fatal(err) } if err := os.Symlink(other, filepath.Join(router, "v1.4.0")); err != nil { t.Fatal(err) } if err := removeCacheEntry(g.a.dataDir, "v1.4.0"); err == nil { t.Fatal("the removal entered a symlinked version directory") } if b, err := os.ReadFile(filepath.Join(other, "keep")); err != nil || string(b) != "mine" { t.Fatalf("removal followed the version directory's symlink: %q %v", b, err) } // And a real entry, with a nested directory and a symlink inside it, is // removed whole, its parents left. h := newChannelFixture(t, "v1.2.0") entry := filepath.Join(h.a.dataDir, "system", "router", "v1.4.0") if err := os.MkdirAll(filepath.Join(entry, "sub"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(entry, "sub", "carlos"), []byte("x"), 0o755); err != nil { t.Fatal(err) } if err := os.Symlink(other, filepath.Join(entry, "link")); err != nil { t.Fatal(err) } if err := removeCacheEntry(h.a.dataDir, "v1.4.0"); err != nil { t.Fatal(err) } if _, err := os.Stat(entry); !os.IsNotExist(err) { t.Fatalf("entry left: %v", err) } if _, err := os.Stat(filepath.Join(h.a.dataDir, "system", "router")); err != nil { t.Fatalf("a parent was removed: %v", err) } if b, err := os.ReadFile(filepath.Join(other, "keep")); err != nil || string(b) != "mine" { t.Fatalf("removal followed a symlink inside the entry: %q %v", b, err) } if err := removeCacheEntry(h.a.dataDir, "v1.4.0"); err != nil { t.Fatalf("an entry already gone: %v", err) } } // A box an update has managed, at the channel's head, has applied.json and no // channel identity: its releases come through carlos system update, and its // carlos.prev is not overwritten. func TestInstallChannelRefusesAnUpdatedBoxAtTheChannelHead(t *testing.T) { f := newChannelFixture(t, "v1.2.0") if err := os.WriteFile(systemBinaryPath(f.a.optDir), f.body, 0o755); err != nil { t.Fatal(err) } prev := []byte("#!/bin/sh\necho carlos v1.3.0\n") if err := os.WriteFile(systemPrevPath(f.a.optDir), prev, 0o755); err != nil { t.Fatal(err) } if err := writeJSONDurable(f.a.file("applied.json"), systemApplied{Version: "v1.4.0", Channel: "stable", At: f.now}, 0o644); err != nil { t.Fatal(err) } _, err := f.in.run(context.Background()) if err == nil || err.Error() != "this box already runs v1.4.0, so carlos system update installs its releases" { t.Fatalf("updated box: %v", err) } if cur, p := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(p, prev) || f.fetches != 0 { t.Fatalf("an updated box was changed: %q %q after %d fetches", cur, p, f.fetches) } } // A binary this command did not put in a slot, with no applied.json, is // refused and left where it is; a run cut off part way, which leaves only the // artifact behind, is completed. func TestInstallChannelRefusesAHandRolledBoxAndCompletesACutOffRun(t *testing.T) { for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} { f := newChannelFixture(t, "v1.2.0") path := slot(f.a.optDir) if err := os.WriteFile(path, []byte("#!/bin/sh\necho hand rolled\n"), 0o755); err != nil { t.Fatal(err) } _, err := f.in.run(context.Background()) if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" { t.Fatalf("hand-rolled %s: %v", path, err) } if b, _ := os.ReadFile(path); string(b) != "#!/bin/sh\necho hand rolled\n" || f.fetches != 0 { t.Fatalf("a hand-rolled binary was replaced: %q after %d fetches", b, f.fetches) } if _, err := os.Stat(systemCachePath(f.a.dataDir, "v1.4.0")); !os.IsNotExist(err) { t.Fatalf("a refused run left a cache entry: %v", err) } } // Cut off after carlos.prev, or after both slots: no applied.json, and // only the artifact on disk. for name, slots := range map[string][]func(string) string{ "prev only": {systemPrevPath}, "both slots": {systemPrevPath, systemBinaryPath}, } { t.Run(name, func(t *testing.T) { f := newChannelFixture(t, "v1.2.0") for _, s := range slots { if err := os.WriteFile(s(f.a.optDir), f.body, 0o755); err != nil { t.Fatal(err) } } got, err := f.in.run(context.Background()) if err != nil || got.Already { t.Fatalf("retry %+v %v", got, err) } if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) { t.Fatalf("slots hold %q %q", cur, prev) } if applied, err := readSystemApplied(f.a.optDir); err != nil || applied == nil || applied.Version != "v1.4.0" { t.Fatalf("applied %+v %v", applied, err) } if _, err := os.Stat(channelIdentityPath(f.a.optDir)); err != nil { t.Fatal(err) } }) } } // A slot that is a symlink is refused, even one to a file that holds the // artifact, and neither the link nor its target is touched. func TestInstallChannelRefusesASymlinkedSlot(t *testing.T) { for _, slot := range []func(string) string{systemBinaryPath, systemPrevPath} { f := newChannelFixture(t, "v1.2.0") target := filepath.Join(t.TempDir(), "elsewhere") if err := os.WriteFile(target, f.body, 0o755); err != nil { t.Fatal(err) } path := slot(f.a.optDir) if err := os.Symlink(target, path); err != nil { t.Fatal(err) } _, err := f.in.run(context.Background()) if err == nil || err.Error() != path+" holds a build this command did not install, so nothing was installed" || f.fetches != 0 { t.Fatalf("symlinked %s: %v after %d fetches", path, err, f.fetches) } if l, err := os.Readlink(path); err != nil || l != target { t.Fatalf("the link was changed: %q %v", l, err) } if b, _ := os.ReadFile(target); !bytes.Equal(b, f.body) { t.Fatalf("the link's target was changed: %q", b) } // A slot that cannot be read at all is an error, not a pass. if err := os.Remove(path); err != nil { t.Fatal(err) } if err := os.Mkdir(path, 0o755); err != nil { t.Fatal(err) } if _, err := f.in.run(context.Background()); err == nil || !strings.Contains(err.Error(), "holds a build this command did not install") { t.Fatalf("a directory in a slot: %v", err) } } }