//go:build linux package host import ( "context" "crypto/ecdh" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io/fs" "os" "path/filepath" "strings" "syscall" "testing" "time" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostfiles" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostrelocate" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/instancekey" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) const installEnv = "# the host's own env\nCARLOS_ADMIN_TOKEN=t0k3n\n CARLOS_INSTANCE_KEY = q+/Zk3vJ0aW1Yx9sR7tUe2oPl4mN8bC6dF5gH0iJ1kA=\nAPP_MODE=prod" // installFixture is a fresh spare at installed, after P's build: the // production box layout (bootstrap), the real EmptyInstallationCensus with // claimGlobalSharedPaths, the real ReconcileAdapter with park.Local, main's // ClaimRoleAssembly.PrepareState, and the claim, record and P's files // manifest as the build left them. The held config render and the held // stage are seams that select their work exactly as production does. type installFixture struct { *buildFixture r ClaimRuntimeResources reg *registry.Registry inv *hostbox.StopInventoryDB actor hostbox.Actor stageErr error // the held stage's answer; nil points live/ at the version renders int configs map[string]string // the rendered config's variables // verifyErr is the held reading's answer once live/ names the // version (release.VerifyHeldLiveUnder's bytes check); lockProbe runs // inside the publication's guard and the reading's lock. verifyErr error lockProbe func(where string) published int } // installWake is the fixture's published wake state: two cron timers. func installWake() string { ws, _ := json.Marshal(hostfiles.WakeState{V: 1, Host: buildHost, NextDue: 1700000000, Timers: []hostfiles.WakeTimer{ {Name: "nightly", Kind: "cron", Spec: "0 3 * * *", Path: "/jobs/nightly", NextAt: 1700000000, CreatedAt: 1690000000, Registration: "r1"}, {Name: "hourly", Kind: "cron", Spec: "0 * * * *", Path: "/jobs/hourly", NextAt: 1700000100, CreatedAt: 1690000000, Registration: "r2"}, }}) return string(ws) } func newInstallFixture(t *testing.T, platform map[string]string) *installFixture { t.Helper() if platform == nil { platform = map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()} } return newInstallFixtureWith(t, buildOptions{Platform: platform, Files: map[string]string{"public/index.html": "

shop

", "uploads/a/b.png": "png-bytes"}}) } // newInstallFixtureWith is newInstallFixture over the build options opts // (opts.Data is the bootstrapped box's). func newInstallFixtureWith(t *testing.T, opts buildOptions) *installFixture { t.Helper() ctx := t.Context() for _, k := range []string{"CARLOS_HOST_CLAIM", "CARLOS_MOVES_PHASE1", "CARLOS_REGION_HA"} { t.Setenv(k, "1") } r := claimCensusBox(t) opts.Data = r.DataDir b := newBuildFixtureWith(t, opts) f := &installFixture{buildFixture: b, r: r, actor: hostbox.Actor{Box: "dst", Enrollment: strings.Repeat("a", 32), Region: "ie", Revision: 4, Binding: "4"}} // The record the claim's holder names: a hibernating instance that // derives its paths. if err := provision.CreateRecord(ctx, b.claims, provision.Record{Schema: 1, Host: buildHost, Slug: "shop", App: "app", Account: "acct", Region: "ie", Channel: "stable", CreatedAt: time.Date(2026, 10, 7, 1, 0, 0, 0, time.UTC)}); err != nil && !errors.Is(err, release.ErrCASConflict) { t.Fatal(err) } if err := b.build(); err != nil { t.Fatal(err) } // installed as ToInstalled leaves it: the verified build's prepared // authority, bound to the install stamp the build ran at. auth, done, err := b.verifier.Verify(ctx, b.obs) if err != nil || !done { t.Fatalf("P is not verified: done=%v %v", done, err) } o, err := hostclaim.Update(ctx, b.claims, b.obs, func(c *hostclaim.Claim) error { c.Op.Phase = hostclaim.PhaseInstalled auth.InstallStamp = c.Holder.Stamp c.Op.Journal["prepared_authority"] = string(mustJSON(t, auth)) c.Op.Intent["target_enrollment"] = f.actor.Enrollment c.Op.Journal["exec_ack.src"], c.Op.Journal["exec_ack.dst"] = `{"op":"rel-1"}`, `{"op":"rel-1"}` return nil }) if err != nil { t.Fatal(err) } b.obs = o f.onBox(r, f.actor) b.cfg.render = f.render b.cfg.materialize = f.materialize b.cfg.verifyLive = f.verifyLive return f } // onBox points the fixture's install at box actor over the census box r: // its registry, typed inventory, launch journal and census, its guard over // r's data directory, main's reconciler and role assembly, and the // install's paths and work source. A replacement target is a second call // with a fresh box (newReplacedProbeFilesFixture). func (f *installFixture) onBox(r ClaimRuntimeResources, actor hostbox.Actor) { t, b := f.t, f.buildFixture var err error f.r, f.actor = r, actor if err := os.MkdirAll(filepath.Join(r.DataDir, "hostguard"), 0o770); err != nil { t.Fatal(err) } if f.reg, err = registry.Open(filepath.Join(r.DataDir, "registry.db")); err != nil { t.Fatal(err) } reg := f.reg t.Cleanup(func() { reg.Close() }) if f.inv, err = openLegacyInventory(t, filepath.Join(r.DataDir, ".hostclaims", "inventory.db")); err != nil { t.Fatal(err) } inv := f.inv t.Cleanup(func() { inv.Close() }) launches, err := hostbox.OpenLaunchJournal(filepath.Join(r.DataDir, ".hostclaims", "launches.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { launches.Close() }) census := &hostbox.EmptyInstallationCensus{Inventory: f.inv, Registry: f.reg, Launches: launches, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "4", Roots: map[string]string{"data": r.DataDir, "opt": r.OptDir, "units": r.UnitsDir}, SharedPaths: claimGlobalSharedPaths(r)} if b.a.Guard.DataDir != r.DataDir { b.a.Guard = hostguard.Guard{DataDir: r.DataDir, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: b.claims}} } guard := b.a.Guard b.a.Reconcile = &hostbox.ReconcileAdapter{Registry: f.reg, Inventory: f.inv, Census: census, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "4", Reconciler: hostbox.Reconciler{Claims: b.claims, Guard: guard, Box: actor.Box, Region: "ie", Local: park.Local{Guard: guard, Claims: b.claims, DataDir: r.DataDir, Box: actor.Box}}} b.a.Wake = &ClaimWakeAssembly{Roles: &ClaimRoleAssembly{Inventory: f.inv, Identity: handoverRecoveryIdentity(t, actor), ManagedRoots: map[string]string{"data": r.DataDir}}} b.a.stopLocal = func(context.Context) (claimStopLocal, error) { return claimStopLocal{Actor: actor, BootID: "boot", ControllerID: "ctl", PID: 100, ProcessStart: "5000"}, nil } b.cfg.Registry, b.cfg.OptDir, b.cfg.DataDir, b.cfg.WorkDir = f.reg, r.OptDir, r.DataDir, filepath.Join(r.DataDir, "relocate", "build") b.cfg.Source.Box, b.cfg.Source.Guard, b.cfg.Source.Now = actor.Box, guard, time.Now } // render stands for appconfig.RenderHeldUnder: it selects its work with // the merged versions, as production does, and writes the held config in // the host's instance directory through that work. func (f *installFixture) render(ctx context.Context, _ release.Store, _ *ecdh.PrivateKey, dataDir string, rt registry.Route, op, box string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) { f.renders++ pre := appconfig.HeldRender{BundleETags: map[string]string{"default": "d1", "environment": "e1"}, OverlayETag: "o1"} w, err := work(pre) if err != nil { return appconfig.HeldRender{}, err } if rt.Status != "relocating" || rt.HoldOp != op || box != f.actor.Box { return appconfig.HeldRender{}, appconfig.ErrNotHeld } var body strings.Builder for k, v := range f.configs { body.WriteString(k + "=" + v + "\n") } path := filepath.Join(provision.InstanceDir(dataDir, rt.Host), "config.env") if err := w.With(ctx, hostwrite.Identity{}, func() error { return os.WriteFile(path, []byte(body.String()), 0o600) }); err != nil { return appconfig.HeldRender{}, err } sum := sha256.Sum256([]byte(body.String())) pre.Path, pre.SHA256 = path, hex.EncodeToString(sum[:]) return pre, nil } // materialize stands for release.MaterializeHeldUnder: blocked (the edge // unit cannot write the version store) or, through its work and inside // guard (the caller's held-stage lock), live/ pointed at the pinned // version. func (f *installFixture) materialize(ctx context.Context, _ release.Store, optDir string, rt registry.Route, version string, _ *release.ProofGate, work hostwrite.Work, guard func(func() error) error) (*release.Manifest, error) { if f.stageErr != nil { return nil, f.stageErr } if guard == nil { guard = func(publish func() error) error { return publish() } } return &release.Manifest{}, work.With(ctx, hostwrite.Identity{Release: version}, func() error { return guard(func() error { if f.lockProbe != nil { f.lockProbe("publish") } f.published++ return f.publishLive(rt.Account, rt.App, version) }) }) } // verifyLive stands for release.VerifyHeldLiveUnder: a peek at the link, // then, under lock, the link names the version and the bytes check answers // verifyErr. func (f *installFixture) verifyLive(_ context.Context, _ release.Store, optDir string, rt registry.Route, version string, proof *release.ProofGate, lock func(func() error) error) error { if lock == nil || proof == nil { return errors.New("a held reading without its lock or gate") } if live, err := release.LiveVersion(optDir, rt.Host); err != nil || live != version { return errors.Join(release.ErrHeldNotLive, err) } return lock(func() error { if f.lockProbe != nil { f.lockProbe("verify") } if live, err := release.LiveVersion(optDir, rt.Host); err != nil || live != version { return errors.Join(release.ErrHeldNotLive, err) } return f.verifyErr }) } func (f *installFixture) publishLive(acct, app, version string) error { // The box's version store already holds the release (bootstrap staged // it); a fresh store gets a directory for it. target := release.VersionPath(f.r.OptDir, acct, version, app) if _, err := os.Lstat(target); errors.Is(err, os.ErrNotExist) { if err := os.MkdirAll(target, 0o755); err != nil { return err } } link := release.LiveLink(f.r.OptDir, buildHost) if err := os.MkdirAll(filepath.Dir(link), 0o755); err != nil { return err } _ = os.Remove(link) return os.Symlink(target, link) } func (f *installFixture) install() (hostclaim.LocalInstall, error) { return f.a.relocationInstall(f.cfg)(f.t.Context(), f.read()) } func (f *installFixture) read() hostclaim.Observed { f.t.Helper() o, err := hostclaim.Read(f.t.Context(), f.claims, buildHost) if err != nil { f.t.Fatal(err) } return o } func (f *installFixture) paths() relocationPaths { return relocationPathsFor(f.r.DataDir, registry.Route{Host: buildHost, DBPath: provision.NestedDBPath(f.r.DataDir, buildHost), Socket: provision.NestedSocket(buildHost)}) } func (f *installFixture) effects() *RelocationEffects { return &RelocationEffects{Claims: f.claims, Install: f.a.relocationInstall(f.cfg), VerifyLocal: f.a.relocationVerifyLocal(f.cfg), Receipt: f.a.relocationReceipt(f.cfg)} } // Findings 3 and 5, Review Focus 2: from an empty target, the install mints // the held row first (the census initialises), makes the instance // directory through main's PrepareState, restores files and env into the // nested paths, imports the wake state (timers owned by the row), renders // the held config and stages the pinned release, with the flags on. func TestRelocationInstallFromAnEmptyTargetThroughTheProductionCensus(t *testing.T) { f := newInstallFixture(t, nil) ctx := t.Context() l, err := f.install() if err != nil { t.Fatal(err) } stamp := f.obs.Claim.Holder.Stamp row, err := f.reg.Get(buildHost) if err != nil || row == nil { t.Fatalf("row: %v", err) } if row.Status != "relocating" || row.HoldOp != "rel-1" || row.DBPath != provision.NestedDBPath(f.r.DataDir, buildHost) || row.ClaimOwnership.ClaimStamp != stamp { t.Fatalf("row %+v", row) } timers, err := f.reg.TimersFor(buildHost) if err != nil || len(timers) != 2 { t.Fatalf("timers %+v %v", timers, err) } for _, tm := range timers { if tm.ClaimOwner != row.ClaimOwnership.ClaimOwner { t.Fatalf("timer %s is not owned by the row: %+v", tm.Name, tm) } } p := f.paths() for rel, want := range map[string]string{"public/index.html": "

shop

", "uploads/a/b.png": "png-bytes"} { got, err := os.ReadFile(filepath.Join(p.FilesDir, filepath.FromSlash(rel))) if err != nil || string(got) != want { t.Fatalf("%s: %q %v", rel, got, err) } } if env, err := os.ReadFile(p.EnvPath); err != nil || string(env) != installEnv { t.Fatalf("env %q %v", env, err) } if live, err := release.LiveVersion(f.r.OptDir, buildHost); err != nil || live != "v1" { t.Fatalf("live %q %v", live, err) } path, _ := relocationInstallPath(f.r.DataDir, "rel-1") raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } sum := sha256.Sum256(raw) if l.Box != "dst" || l.Enrollment != f.actor.Enrollment || l.InstallStamp != stamp || l.JournalSHA256 != hex.EncodeToString(sum[:]) || l.FilesManifestVersion != 0 { t.Fatalf("receipt %+v", l) } again, err := f.install() if err != nil || again != l { t.Fatalf("a second install: %+v %v", again, err) } if ok, err := f.a.relocationVerifyLocal(f.cfg)(ctx, f.read(), l); err != nil || !ok { t.Fatalf("verify: %v %v", ok, err) } } // Rev 3 finding 3, ruling 57: the instance directory is main's typed path // entry, intended then materialized with its device and inode, so the // probe's own PrepareState accepts it as its own. func TestRelocationInstallMaterializesTheInstanceDirectoryTyped(t *testing.T) { f := newInstallFixture(t, nil) if _, err := f.install(); err != nil { t.Fatal(err) } h := f.read().Claim.Holder in, err := f.inv.ReadInstallation(t.Context(), buildHost, h.Incarnation, h.Stamp) if err != nil { t.Fatal(err) } dir := provision.InstanceDir(f.r.DataDir, buildHost) rel, _ := filepath.Rel(f.r.DataDir, dir) info, err := os.Stat(dir) if err != nil { t.Fatal(err) } found := false for _, e := range in.Entries { if e.Kind == "path" && e.ManagedRoot == "data" && e.RelativePath == rel { found = e.Materialization == "materialized" && e.Inode == inodeOf(t, info) } } if !found { t.Fatalf("no materialized path entry for %s: %+v", rel, in.Entries) } // The probe's own PrepareState finds it as its own. work, err := relocationInstallSource(f.cfg.Source, f.reg).SelectAt(t.Context(), buildHost, relocationActionInstall, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}) if err != nil { t.Fatal(err) } if err := f.a.Wake.Roles.PrepareState(t.Context(), f.read(), work, ClaimLaunchInputs{DBPath: provision.NestedDBPath(f.r.DataDir, buildHost)}); err != nil { t.Fatalf("PrepareState refused the install's directory: %v", err) } } // O3: the env file is P's EnvItem byte for byte, its comments, spacing and // unterminated last line included; nothing is minted. func TestRelocationInstallKeepsThePublishedEnvByteForByte(t *testing.T) { f := newInstallFixture(t, nil) if _, err := f.install(); err != nil { t.Fatal(err) } p := f.paths() if env, err := os.ReadFile(p.EnvPath); err != nil || string(env) != installEnv { t.Fatalf("env %q %v", env, err) } if _, err := os.Stat(filepath.Join(p.FilesDir, hostrelocate.InstanceKeyPath)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("an instance key was minted: %v", err) } } // Astra round 3 finding 5 and ruling 68, amended in Task 8c (absence is // never evidence): no code writes a platform tombstone, so a source whose // env file was deleted publishes a manifest without the item, exactly as // one whose env file was never published does. Such a manifest, under a // pin of published files, refuses the install before anything is written: // no row, no files, no env file, no journal, and a stray env file at the // path is left as it was. func TestRelocationInstallRefusesAManifestWithoutTheEnvItem(t *testing.T) { f := newInstallFixture(t, map[string]string{hostfiles.WakeItem: installWake()}) before := f.read().Claim.Gen p := f.paths() if err := os.MkdirAll(filepath.Dir(p.EnvPath), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(p.EnvPath, []byte("STALE=1\n"), 0o600); err != nil { t.Fatal(err) } if _, err := f.install(); !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("a manifest without the env item: %v", err) } if row, err := f.reg.Get(buildHost); err != nil || row != nil { t.Fatalf("a row was minted: %+v %v", row, err) } if _, err := os.Stat(filepath.Join(p.FilesDir, "public", "index.html")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("files were restored: %v", err) } if env, err := os.ReadFile(p.EnvPath); err != nil || string(env) != "STALE=1\n" { t.Fatalf("the env path was touched: %q %v", env, err) } if after := f.read().Claim.Gen; after != before { t.Fatalf("the claim was written: gen %d -> %d", before, after) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a refused install left a receipt") } } // The wake half of the same rule: a manifest without wake state (a // failover's, which no release flush wrote) is not a host without timers, // and refuses the install. func TestRelocationInstallRefusesAManifestWithoutWakeState(t *testing.T) { f := newInstallFixture(t, map[string]string{hostfiles.EnvItem: installEnv}) if _, err := f.install(); !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("a manifest without wake state: %v", err) } if row, err := f.reg.Get(buildHost); err != nil || row != nil { t.Fatalf("a row was minted: %+v %v", row, err) } } // Ruling 68's positive evidence: a pin of a host that never published // files (no digest, no retained copy) is no env file and no timers. The // install writes no env file and imports no wake state, and a repeated // install (a crash before its journal) removes a stray env file at the // path. VerifyLocal reads the same evidence: whole with no env file, not // whole once one appears. func TestRelocationInstallRemovesAStaleEnvWhenThePinEvidencesNoFiles(t *testing.T) { f := newInstallFixtureWith(t, buildOptions{NoFiles: true}) ctx := t.Context() l, err := f.install() if err != nil { t.Fatal(err) } p := f.paths() if _, err := os.Stat(p.EnvPath); !errors.Is(err, os.ErrNotExist) { t.Fatalf("an env file was written for a host with none: %v", err) } if timers, err := f.reg.TimersFor(buildHost); err != nil || len(timers) != 0 { t.Fatalf("timers %+v %v", timers, err) } verify := f.a.relocationVerifyLocal(f.cfg) if ok, err := verify(ctx, f.read(), l); err != nil || !ok { t.Fatalf("verify with no env file: %v %v", ok, err) } if err := os.WriteFile(p.EnvPath, []byte("STALE=1\n"), 0o600); err != nil { t.Fatal(err) } if ok, err := verify(ctx, f.read(), l); err != nil || ok { t.Fatalf("verify with a stray env file: %v %v", ok, err) } // A crash before the journal, and the env file at the path meanwhile. journal, _ := relocationInstallPath(f.r.DataDir, "rel-1") if err := os.Remove(journal); err != nil { t.Fatal(err) } if _, err := f.install(); err != nil { t.Fatal(err) } if _, err := os.Stat(p.EnvPath); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a stale env survived: %v", err) } } // The O3' check reads the env file the install wrote: one gone by the time // the key is checked is not an env with no key of its own. func TestRelocationInstallKeyCheckNeedsTheEnvItWrote(t *testing.T) { const configured = "configured-key" f := newInstallFixture(t, map[string]string{hostfiles.EnvItem: "CARLOS_ADMIN_TOKEN=t\n", hostfiles.WakeItem: installWake()}) f.configs = map[string]string{"CARLOS_INSTANCE_KEY": configured} repinWithKey(t, f.buildFixture, &hostrelocate.KeyPin{Source: instancekey.SourceConfig, SHA256: instancekey.Digest(configured)}) f.cfg.render = func(ctx context.Context, s release.Store, k *ecdh.PrivateKey, d string, rt registry.Route, op, box string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) { if err := os.Remove(f.paths().EnvPath); err != nil { return appconfig.HeldRender{}, err } return f.render(ctx, s, k, d, rt, op, box, work) } if _, err := f.install(); !errors.Is(err, hostrelocate.ErrRefused) || !errors.Is(err, os.ErrNotExist) { t.Fatalf("the key check without the env it wrote: %v", err) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a receipt without the env file") } } // Ruling 44 and K25: nothing region HA leaves under the data root names the // host outside the host's own installation, except main's own shared paths // and moves' held-stage relay (census-opaque, N11). The build's scratch // left by a crash is removed by the install. func TestNoRelocationFileNamesTheHostOutsideItsInstallation(t *testing.T) { f := newInstallFixture(t, nil) scratch := filepath.Join(f.cfg.WorkDir, "rel-1", buildHost+".db") if err := os.MkdirAll(filepath.Dir(scratch), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(scratch, []byte("left by a crash"), 0o600); err != nil { t.Fatal(err) } f.stageErr = release.ErrStageBlocked if _, err := f.install(); !errors.Is(err, hostrelocate.ErrNotYet) { t.Fatalf("blocked stage: %v", err) } f.stageErr = nil if _, err := f.install(); err != nil { t.Fatal(err) } inst := provision.InstanceDir(f.r.DataDir, buildHost) allowed := []string{inst, filepath.Join(f.r.DataDir, "hostguard"), filepath.Join(f.r.DataDir, ".hostexec"), filepath.Join(f.r.DataDir, ".hostclaims"), release.HeldStageDir(f.r.DataDir), release.HeldStagedDir(f.r.DataDir), filepath.Join(f.r.DataDir, ".moves", heldStageLockFile(buildHost))} err := filepath.WalkDir(f.r.DataDir, func(path string, d fs.DirEntry, err error) error { if err != nil || !strings.Contains(path, buildHost) { return err } for _, a := range allowed { if path == a || strings.HasPrefix(path, a+string(filepath.Separator)) { return nil } } t.Errorf("%s names the host outside its installation", path) return nil }) if err != nil { t.Fatal(err) } ops, err := os.ReadDir(filepath.Join(f.r.DataDir, "relocate", "ops", "rel-1")) if err != nil || len(ops) != 1 || ops[0].Name() != "install.json" { t.Fatalf("relocate/ops/rel-1: %v %v", ops, err) } if _, err := os.Stat(scratch); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the build's scratch survived the install: %v", err) } } // Finding 5, M3: MaterializeHeld is never called; a blocked stage leaves // root's relay request at install_release (never the route's version) and // the receipt waits for live/. func TestRelocationInstallStagesThroughTheClaimPathWithClaimsOn(t *testing.T) { f := newInstallFixture(t, nil) f.stageErr = release.ErrStageBlocked if _, err := f.install(); !errors.Is(err, hostrelocate.ErrNotYet) { t.Fatalf("got %v", err) } raw, err := os.ReadFile(filepath.Join(release.HeldStageDir(f.r.DataDir), buildHost+".json")) if err != nil { t.Fatal(err) } var req release.HeldStageRequest if err := json.Unmarshal(raw, &req); err != nil || req.Version != "v1" || req.Op != "rel-1" || req.Box != "dst" || req.Host != buildHost || req.App != "app" { t.Fatalf("request %+v %v", req, err) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a receipt before the release is live") } // Root's pass serves the request. if err := f.publishLive("acct", "app", "v1"); err != nil { t.Fatal(err) } if l, err := f.install(); err != nil || l.InstallStamp == 0 { t.Fatalf("after the stage: %+v %v", l, err) } } // Finding 10: every install write runs main's Local first. With the // execution lease lost after the install work and before the render, the // held-config selection refuses: no config, no request, no journal, and // the claim is not written. func TestRelocationInstallStopsWhenTheLeaseIsLost(t *testing.T) { f := newInstallFixture(t, nil) f.cfg.render = func(ctx context.Context, s release.Store, k *ecdh.PrivateKey, d string, rt registry.Route, op, box string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) { f.localErr = execlease.ErrLost return f.render(ctx, s, k, d, rt, op, box, work) } before := f.read() if _, err := f.install(); !errors.Is(err, execlease.ErrLost) { t.Fatalf("got %v", err) } if _, err := os.Stat(filepath.Join(provision.InstanceDir(f.r.DataDir, buildHost), "config.env")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a config was written: %v", err) } if _, err := os.Stat(filepath.Join(release.HeldStageDir(f.r.DataDir), buildHost+".json")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a stage request was written: %v", err) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a journal was written") } // park's RecordInventory wrote the claim's inventory once, before; the // lost lease wrote nothing more. after := f.read() if after.Claim.Gen != before.Claim.Gen+1 || after.Claim.Inventory["dst"].Layout != "nested" { t.Fatalf("the claim moved: gen %d -> %d", before.Claim.Gen, after.Claim.Gen) } } // O3' (instance-key plan rev 3): when the pin carries the key by an // adoption record, a configuration that no longer delivers that key // refuses the install before anything launches. func TestRelocationInstallRefusesAKeyChangedSinceThePin(t *testing.T) { const configured = "configured-key" for _, tc := range []struct { name string config string want error }{ {"the configured key the pin names", configured, nil}, {"a key changed since the pin", "another-key", hostrelocate.ErrRefused}, } { t.Run(tc.name, func(t *testing.T) { f := newInstallFixture(t, map[string]string{hostfiles.EnvItem: "CARLOS_ADMIN_TOKEN=t\n", hostfiles.WakeItem: installWake()}) f.configs = map[string]string{"CARLOS_INSTANCE_KEY": tc.config} repinWithKey(t, f.buildFixture, &hostrelocate.KeyPin{Source: instancekey.SourceConfig, SHA256: instancekey.Digest(configured)}) _, err := f.install() if !errors.Is(err, tc.want) { t.Fatalf("got %v, want %v", err, tc.want) } if tc.want != nil { if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a refused key left a receipt") } } }) } } // repinWithKey rewrites the fixture's pin object with an instance key pin // and journals its new digest, as the pinner would have. func repinWithKey(t *testing.T, f *buildFixture, key *hostrelocate.KeyPin) { t.Helper() ctx := t.Context() o, err := hostclaim.Read(ctx, f.claims, buildHost) if err != nil { t.Fatal(err) } var ref hostclaim.RecoveryPin if err := json.Unmarshal([]byte(o.Claim.Op.Journal["pin"]), &ref); err != nil { t.Fatal(err) } pin, err := hostrelocate.ReadPin(ctx, f.claims, ref) if err != nil { t.Fatal(err) } pin.InstanceKey = key raw, _ := json.Marshal(pin) ref.Key += ".keyed" if _, err := f.claims.Put(ctx, ref.Key, raw, ""); err != nil { t.Fatal(err) } sum := sha256.Sum256(raw) ref.SHA256 = hex.EncodeToString(sum[:]) refRaw, _ := json.Marshal(ref) if f.obs, err = hostclaim.Update(ctx, f.claims, o, func(c *hostclaim.Claim) error { c.Op.Journal["pin"] = string(refRaw); return nil }); err != nil { t.Fatal(err) } } // Ruling 52 and finding 13: a damaged install is repaired under its active // stamp, in installed and in probing before P; a retired stamp refuses and // is never reactivated. func TestRelocationEnsureLocalRefusesARetiredStamp(t *testing.T) { f := newInstallFixture(t, nil) ctx := t.Context() l, err := f.install() if err != nil { t.Fatal(err) } receipt, _ := json.Marshal(l) if f.obs, err = hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Op.Journal["local_install"] = string(receipt); return nil }); err != nil { t.Fatal(err) } e := f.effects() file := filepath.Join(f.paths().FilesDir, "public", "index.html") for _, phase := range []string{hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing} { o, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Op.Phase = phase; return nil }) if err != nil { t.Fatal(err) } if err := os.Remove(file); err != nil { t.Fatal(err) } if err := e.EnsureLocal(ctx, o); err != nil { t.Fatalf("%s: %v", phase, err) } if got, err := os.ReadFile(file); err != nil || string(got) != "

shop

" { t.Fatalf("%s: not restored: %q %v", phase, got, err) } } if err := f.a.Guard.RetireInstallation(ctx, buildHost, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } if err := os.Remove(file); err != nil { t.Fatal(err) } if err := e.EnsureLocal(ctx, f.read()); !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("a retired stamp was repaired: %v", err) } if _, err := os.Stat(file); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a retired stamp's install was written: %v", err) } if c, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); err == nil && c.Stamp == l.InstallStamp { t.Fatalf("the retired stamp was reactivated: %+v", c) } } // Stage B final review A3 (Astra 3): a repair after the held configuration // changed re-renders it, so the repaired journal's digest is not the // journaled receipt's. EnsureLocal replaces the receipt in one CAS on a // fresh read before any admission, so the pre-admission verification, the // receipt check after P opens (VerifyReceipt) and the post-admission check // (relocationAfterAdmission) all compare the digest the box holds with the // one the claim journals. A CAS that fails once leaves the old receipt, and // the next pass repairs to the same journal and records it. func TestRelocationEnsureLocalRepairsAcrossAConfigurationChange(t *testing.T) { f := newInstallFixture(t, nil) ctx := t.Context() l, err := f.install() if err != nil { t.Fatal(err) } receipt, _ := json.Marshal(l) if f.obs, err = hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Op.Phase = hostclaim.PhaseRelocatingProbing c.Op.Journal["local_install"] = string(receipt) return nil }); err != nil { t.Fatal(err) } e := f.effects() file := filepath.Join(f.paths().FilesDir, "public", "index.html") journaled := func() hostclaim.LocalInstall { t.Helper() var got hostclaim.LocalInstall if err := json.Unmarshal([]byte(f.read().Claim.Op.Journal["local_install"]), &got); err != nil { t.Fatal(err) } return got } onDisk := func() hostclaim.LocalInstall { t.Helper() got, ok, err := f.a.relocationReceipt(f.cfg)(ctx, f.read()) if err != nil || !ok { t.Fatalf("the durable receipt: %v %v", ok, err) } return got } agree := func(want string) { t.Helper() if j, d := journaled(), onDisk(); j != d { t.Fatalf("%s: the claim journals %+v, the box holds %+v", want, j, d) } if ok, err := e.VerifyReceipt(ctx, f.read()); err != nil || !ok { t.Fatalf("%s: VerifyReceipt %v %v", want, ok, err) } prior := f.read() admitted := f.read() admitted.Claim.Holder.WriterBox, admitted.Claim.Holder.Generation = "dst", admitted.Claim.Op.Journal["prepared"] if err := relocationAfterAdmission(f.r.DataDir, "dst")(ctx, prior, admitted); err != nil { t.Fatalf("%s: after admission: %v", want, err) } } agree("after the install") // The configuration changes, then a file is lost: the repair renders // the new configuration and records the new receipt. f.configs = map[string]string{"CARLOS_FEATURE": "2"} if err := os.Remove(file); err != nil { t.Fatal(err) } before := f.read().Claim.Gen if err := e.EnsureLocal(ctx, f.read()); err != nil { t.Fatalf("the repair across a configuration change: %v", err) } if got, err := os.ReadFile(file); err != nil || string(got) != "

shop

" { t.Fatalf("not restored: %q %v", got, err) } repaired := journaled() if repaired == l || repaired.Box != l.Box || repaired.Enrollment != l.Enrollment || repaired.InstallStamp != l.InstallStamp || f.read().Claim.Gen == before { t.Fatalf("the receipt was not replaced for the same installation: %+v (was %+v)", repaired, l) } agree("after the repair") // Whole now: nothing is reinstalled or rewritten. gen := f.read().Claim.Gen if err := e.EnsureLocal(ctx, f.read()); err != nil || f.read().Claim.Gen != gen { t.Fatalf("a whole install was repaired again: %v gen %d -> %d", err, gen, f.read().Claim.Gen) } // The configuration changes again and the CAS fails once: the old // receipt stays in the claim, the box holds the new journal, and the // next pass repairs to the same journal and records it. f.configs = map[string]string{"CARLOS_FEATURE": "3"} if err := os.Remove(file); err != nil { t.Fatal(err) } e.Claims = errPutStore{Store: f.claims} if err := e.EnsureLocal(ctx, f.read()); err == nil { t.Fatal("a failed receipt CAS was reported as a repair") } if j, d := journaled(), onDisk(); j != repaired || d == repaired { t.Fatalf("after the failed CAS: the claim journals %+v, the box holds %+v", j, d) } if ok, err := e.VerifyReceipt(ctx, f.read()); err != nil || ok { t.Fatalf("the old receipt verified against the new journal: %v %v", ok, err) } e.Claims = f.claims if err := e.EnsureLocal(ctx, f.read()); err != nil { t.Fatalf("the pass after the failed CAS: %v", err) } if journaled() == repaired { t.Fatal("the receipt was not replaced on the next pass") } agree("after the retried repair") } func TestRelocationVerifyLocalReadsTheInstallBack(t *testing.T) { f := newInstallFixture(t, nil) ctx := t.Context() l, err := f.install() if err != nil { t.Fatal(err) } verify := f.a.relocationVerifyLocal(f.cfg) if ok, err := verify(ctx, f.read(), l); err != nil || !ok { t.Fatalf("after the install: %v %v", ok, err) } other := l other.InstallStamp++ if ok, err := verify(ctx, f.read(), other); err != nil || ok { t.Fatalf("another receipt verified: %v %v", ok, err) } if err := os.Remove(filepath.Join(f.paths().FilesDir, "uploads", "a", "b.png")); err != nil { t.Fatal(err) } if ok, err := verify(ctx, f.read(), l); err != nil || ok { t.Fatalf("a missing file verified: %v %v", ok, err) } // Final review minor 10: P's manifest is read as the install reads it // (hostfiles.Load); one that is gone is a refusal, not an install to // repair (the install refuses without it too). o := f.read() if err := f.cfg.Data.Delete(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(o.Claim.Holder.Location, o.Claim.Op.Journal["prepared"]))); err != nil { t.Fatal(err) } if ok, err := verify(ctx, o, l); ok || !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("a missing manifest: %v %v", ok, err) } } func TestRelocationReceiptIsTheDurableJournal(t *testing.T) { f := newInstallFixture(t, nil) ctx := t.Context() receipt := f.a.relocationReceipt(f.cfg) if _, ok, err := receipt(ctx, f.read()); ok || err != nil { t.Fatalf("a receipt before the install: %v %v", ok, err) } l, err := f.install() if err != nil { t.Fatal(err) } if got, ok, err := receipt(ctx, f.read()); err != nil || !ok || got != l { t.Fatalf("receipt %+v %v %v", got, ok, err) } // Final review minor 1: a journal that is present but cannot be read, // or is not this operation's completed install, is an error, never // "no receipt" that EnsureLocal would reinstall over. journaled, _ := json.Marshal(l) if _, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Op.Journal["local_install"] = string(journaled); return nil }); err != nil { t.Fatal(err) } path, _ := relocationInstallPath(f.r.DataDir, "rel-1") for name, body := range map[string]string{ "a damaged journal": `{"v":1,"host":"`, "another operation's": `{"v":1,"host":"` + buildHost + `","op":"rel-2","steps":{"materialize":"v1"}}`, "another host's": `{"v":1,"host":"other.example","op":"rel-1","steps":{"materialize":"v1"}}`, "no materialized release": `{"v":1,"host":"` + buildHost + `","op":"rel-1","steps":{}}`, } { if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } if _, ok, err := receipt(ctx, f.read()); ok || !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("%s: %v %v", name, ok, err) } if err := f.effects().EnsureLocal(ctx, f.read()); !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("%s: EnsureLocal reinstalled over it: %v", name, err) } } if err := os.Remove(path); err != nil { t.Fatal(err) } if _, ok, err := receipt(ctx, f.read()); ok || err != nil { t.Fatalf("no journal: %v %v", ok, err) } } // Rev 3 finding 2, ruling 62: Participant.Step over the real effects // records local_install on the first pass (the install wrote the claim's // inventory, so the receipt is recorded on a fresh read), then enters // probing on the next, with no conflict loop. func TestRelocationInstallRecordsItsReceiptThroughParticipantStep(t *testing.T) { f := newInstallFixture(t, nil) e := f.effects() e.Participation = func(context.Context, hostclaim.Observed) (execlease.State, error) { return execlease.State{Binding: execlease.Binding{Host: buildHost, Incarnation: f.obs.Claim.Holder.Incarnation, Op: "rel-1"}}, nil } p := hostrelocate.Participant{Claims: f.claims, Box: "dst", Enrollment: f.actor.Enrollment, Effects: e, Now: time.Now} o, err := p.Step(t.Context(), buildHost) if err != nil || o.Claim.Op.Journal["local_install"] == "" || o.Claim.Op.Phase != hostclaim.PhaseInstalled { t.Fatalf("first pass: %v %+v", err, o.Claim.Op.Journal) } o, err = p.Step(t.Context(), buildHost) if err != nil { t.Fatalf("second pass: %v", err) } if o.Claim.Op.Phase != hostclaim.PhaseRelocatingProbing { t.Fatalf("second pass: %s", o.Claim.Op.Phase) } } func inodeOf(t *testing.T, info os.FileInfo) uint64 { t.Helper() st, ok := info.Sys().(*syscall.Stat_t) if !ok { t.Fatal("no stat") } return st.Ino } // Fix round 1, M1: every precondition is checked before anything is // written. An assembly without main's role assembly (the instance // directory's typed protocol) refuses with no row and no claim write. func TestRelocationInstallRefusesAMisconfiguredAssemblyBeforeAnyWrite(t *testing.T) { f := newInstallFixture(t, nil) before := f.read().Claim.Gen f.a.Wake = nil if _, err := f.install(); !errors.Is(err, ErrClaimStopUnavailable) { t.Fatalf("no role assembly: %v", err) } if row, err := f.reg.Get(buildHost); err != nil || row != nil { t.Fatalf("a row was minted: %+v %v", row, err) } if after := f.read().Claim.Gen; after != before { t.Fatalf("the claim was written: gen %d -> %d", before, after) } } // Fix round 1, I1: the held release is published under main's held-stage // lock, and read back under the same lock before the receipt, as moves' // T7 does (heldReleaseReady). Another holder of the lock is refused while // each runs. func TestRelocationInstallPublishesAndReadsUnderTheHeldStageLock(t *testing.T) { prev := heldLockWait t.Cleanup(func() { heldLockWait = prev }) heldLockWait = 50 * time.Millisecond f := newInstallFixture(t, nil) held := map[string]bool{} f.lockProbe = func(where string) { err := withHeldStageLock(f.r.DataDir, buildHost, func() error { return nil }) held[where] = errors.Is(err, errHeldLockBusy) } if _, err := f.install(); err != nil { t.Fatal(err) } if f.published != 1 || !held["publish"] || !held["verify"] { t.Fatalf("published %d, under the lock %v", f.published, held) } } // Fix round 1, I1: a link alone is never evidence. A link the reading does // not prove (the bytes it names, or the policy, refuse) leaves no receipt, // whether this install published it or root's relay did; once proven, the // install completes without publishing again. func TestRelocationInstallWritesNoReceiptOnAnUnprovenLink(t *testing.T) { for _, byRoot := range []bool{false, true} { f := newInstallFixture(t, nil) if byRoot { // The edge cannot write the version store: the install leaves // root's request, and root's relay publishes the link. f.stageErr = release.ErrStageBlocked if _, err := f.install(); !errors.Is(err, hostrelocate.ErrNotYet) { t.Fatalf("blocked: %v", err) } f.stageErr = nil if err := f.publishLive("acct", "app", "v1"); err != nil { t.Fatal(err) } } f.verifyErr = release.ErrHeldNotProven if _, err := f.install(); !errors.Is(err, release.ErrHeldNotProven) { t.Fatalf("root %v: an unproven link: %v", byRoot, err) } if live, err := release.LiveVersion(f.r.OptDir, buildHost); err != nil || live != "v1" { t.Fatalf("root %v: live %q %v", byRoot, live, err) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatalf("root %v: a receipt on an unproven link", byRoot) } f.verifyErr = nil if _, err := f.install(); err != nil { t.Fatalf("root %v: proven: %v", byRoot, err) } if want := map[bool]int{false: 1, true: 0}[byRoot]; f.published != want { t.Fatalf("root %v: published %d times, want %d", byRoot, f.published, want) } } } // Fix round 1, I3: each of the install's write groups runs inside a With // of its selection, so main's Local (the execution lease's deadline and // loss latch) stops it. The lease is lost at one group at a time; that // group and everything after it write nothing. (The held config's group // is TestRelocationInstallStopsWhenTheLeaseIsLost.) func TestRelocationInstallStopsAtEachWriteGroupWhenTheLeaseIsLost(t *testing.T) { materializedDir := func(f *installFixture) bool { h := f.read().Claim.Holder in, err := f.inv.ReadInstallation(context.Background(), buildHost, h.Incarnation, h.Stamp) if err != nil { return false } rel, _ := filepath.Rel(f.r.DataDir, provision.InstanceDir(f.r.DataDir, buildHost)) for _, e := range in.Entries { if e.Kind == "path" && e.ManagedRoot == "data" && e.RelativePath == rel && e.Materialization == "materialized" { return true } } return false } live := func(f *installFixture) bool { v, err := release.LiveVersion(f.r.OptDir, buildHost) return err == nil && v == "v1" } for _, tc := range []struct { name string lose func(*installFixture, hostbox.Selection) bool row bool // the row was minted before the loss file bool // the files were restored before the loss link bool // the release was published before the loss }{ {"the row", func(*installFixture, hostbox.Selection) bool { return true }, false, false, false}, {"the files, env and wake state", func(f *installFixture, s hostbox.Selection) bool { return s.Action == relocationActionInstall && materializedDir(f) }, true, false, false}, {"the held release", func(_ *installFixture, s hostbox.Selection) bool { return s.Action == relocationActionHeldRelease }, true, true, false}, {"the journal", func(f *installFixture, s hostbox.Selection) bool { return s.Action == relocationActionInstall && live(f) }, true, true, true}, } { t.Run(tc.name, func(t *testing.T) { f := newInstallFixture(t, nil) f.localHook = func(s hostbox.Selection) error { if tc.lose(f, s) { return execlease.ErrLost } return nil } if _, err := f.install(); !errors.Is(err, execlease.ErrLost) { t.Fatalf("got %v", err) } row, err := f.reg.Get(buildHost) if err != nil || (row != nil) != tc.row { t.Fatalf("row %+v %v, want minted %v", row, err, tc.row) } p := f.paths() _, ferr := os.Stat(filepath.Join(p.FilesDir, "public", "index.html")) _, eerr := os.Stat(p.EnvPath) timers, _ := f.reg.TimersFor(buildHost) if (ferr == nil) != tc.file || (eerr == nil) != tc.file || (len(timers) == 2) != tc.file { t.Fatalf("files %v, env %v, timers %d; want restored %v", ferr, eerr, len(timers), tc.file) } if live(f) != tc.link { t.Fatalf("live/ published %v, want %v", live(f), tc.link) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a journal was written") } }) } } // Fix round 1, M2: VerifyLocal reads the guard record and the row back. A // stamp activated since the install, and a row whose hold was released or // taken by another op, are not the receipt's install. func TestRelocationVerifyLocalNeedsTheStampAndTheHold(t *testing.T) { ctx := t.Context() t.Run("the hold", func(t *testing.T) { f := newInstallFixture(t, nil) l, err := f.install() if err != nil { t.Fatal(err) } verify := f.a.relocationVerifyLocal(f.cfg) if changed, err := f.reg.ReleaseHold(buildHost, "rel-1"); err != nil || !changed { t.Fatalf("release: %v %v", changed, err) } if ok, err := verify(ctx, f.read(), l); err != nil || ok { t.Fatalf("a released hold verified: %v %v", ok, err) } if err := f.reg.SetHold(buildHost, "relocating", "other-op"); err != nil { t.Fatal(err) } if ok, err := verify(ctx, f.read(), l); err != nil || ok { t.Fatalf("another op's hold verified: %v %v", ok, err) } }) t.Run("the stamp", func(t *testing.T) { f := newInstallFixture(t, nil) l, err := f.install() if err != nil { t.Fatal(err) } o, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Holder.Stamp++; return nil }) if err != nil { t.Fatal(err) } if err := f.a.Guard.Activate(ctx, buildHost, "dst", nil); err != nil { t.Fatal(err) } if c, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); err != nil || c.Stamp == l.InstallStamp { t.Fatalf("not re-activated: %+v %v", c, err) } if ok, err := f.a.relocationVerifyLocal(f.cfg)(ctx, o, l); err != nil || ok { t.Fatalf("another stamp verified: %v %v", ok, err) } }) } // O3' reads the held config and the env file it wrote from the instance // directory, the tenant's to write, through main's safe read: a symlink // (at the name or the directory), a FIFO or an oversized file at either // refuses the install, leaves no receipt, and never blocks it. Each plant // keeps the bytes a following or unbounded read would accept. func TestRelocationInstallKeyCheckRefusesWhatTheTenantPlants(t *testing.T) { const configured = "configured-key" for _, target := range []string{"config", "env"} { for _, p := range tenantPlants { if target == "env" && p.name == "a symlinked parent directory" { continue // the config's directory too: the config case covers it } t.Run(target+"/"+p.name, func(t *testing.T) { f := newInstallFixture(t, map[string]string{hostfiles.EnvItem: "CARLOS_ADMIN_TOKEN=t\n", hostfiles.WakeItem: installWake()}) f.configs = map[string]string{"CARLOS_INSTANCE_KEY": configured} repinWithKey(t, f.buildFixture, &hostrelocate.KeyPin{Source: instancekey.SourceConfig, SHA256: instancekey.Digest(configured)}) f.cfg.render = func(ctx context.Context, s release.Store, k *ecdh.PrivateKey, d string, rt registry.Route, op, box string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) { r, err := f.render(ctx, s, k, d, rt, op, box, work) if err != nil { return r, err } path := r.Path if target == "env" { path = f.paths().EnvPath } body, err := os.ReadFile(path) if err != nil { return r, err } plantAt(t, p, path, body, instancekey.EnvReadLimit, '\n') return r, nil } var err error if e := bounded(t, func() error { _, err = f.install(); return nil }); e != nil { t.Fatal(e) } if !errors.Is(err, p.want) { t.Fatalf("install: %v; want %v", err, p.want) } if target == "env" && !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("an unreadable env file is not a refusal: %v", err) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a refused install left a receipt") } }) } } } // The held render always writes its file, so a config missing at the key // check is an error, never a configuration without a key. func TestRelocationInstallKeyCheckNeedsTheConfigItRendered(t *testing.T) { const configured = "configured-key" f := newInstallFixture(t, map[string]string{hostfiles.EnvItem: "CARLOS_ADMIN_TOKEN=t\nCARLOS_INSTANCE_KEY=" + configured + "\n", hostfiles.WakeItem: installWake()}) repinWithKey(t, f.buildFixture, &hostrelocate.KeyPin{Source: instancekey.SourcePlatform, SHA256: instancekey.Digest(configured)}) f.cfg.render = func(ctx context.Context, s release.Store, k *ecdh.PrivateKey, d string, rt registry.Route, op, box string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) { r, err := f.render(ctx, s, k, d, rt, op, box, work) if err != nil { return r, err } return r, os.Remove(r.Path) } if _, err := f.install(); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the key check without the config it rendered: %v", err) } if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { t.Fatal("a receipt without the config") } }