region ha: the relocation's prepared-files binding holds P's build stamp and the replacement's install stamp apart, each exactly, so a target replaced at installed, or in probing before P's first admission, which keeps P and its prepared authority under a new install stamp, establishes P's files through main's C8 provider (Astra round 7, P2).
The install now reads the claim's prepared_authority. Its install journal's files_manifest step binds P's key, op, digest and exact version at P's build stamp, prepared_authority.install_stamp, and the install refuses a manifest whose digest is not the authority's. The journal itself, like the local_install receipt, carries this box's install stamp, the claim's current install_stamp. The reader checks each side against its own source: - the step: decoded against the authority's stamp, its digest equal to the authority's; - the receipt, the journal and the holder: against install_stamp; - the build stamp must be set and never past the install stamp. Neither stamp stands in for the other. Tests: - The install fixture journals the prepared authority as ToInstalled does, and can move to a fresh replacement box (onBox). - A replacement at installed and one in probing establish P's files through the real provider. Both failed on the previous code. - The replaced target's install journal, with or without its receipt, refuses, and so does an authority stamped at, or past, the install stamp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+275
−51
internal/host/movesfiles.go+20 −13internal/host/relocationinstall.go+24 −7internal/host/relocationinstall_linux_test.go+42 −14internal/host/relocationprobefiles_linux_test.go+189 −17
diff --git a/internal/host/movesfiles.go b/internal/host/movesfiles.go| index 245b8053e..683d50f21 100644 |
| --- a/internal/host/movesfiles.go |
| +++ b/internal/host/movesfiles.go |
| @@ -711,16 +711,23 @@ func movesPreparedManifest(dataDir string, o hostclaim.Observed, gen, key string |
| return decodePreparedManifest(raw, key, o.Claim.Op.ID, stamp) |
| } |
| -// relocationPreparedManifest is a relocation's binding (region HA §5.8): |
| -// the claim's prepared_authority names P, its install stamp and the |
| -// digest of P's prepared files manifest (sequence 0); the claim's |
| -// local_install receipt names this box and the digest of its install |
| -// journal; that journal, read whole and held to the receipt's digest, |
| -// names the host, the op, this box, the stamp and P, and its |
| -// files_manifest step binds P's key, the op, the stamp, the digest and the |
| -// exact version the install read. The step's digest must be the |
| -// authority's. The probe's admission keeps the install stamp, so the |
| -// holder's stamp is that stamp too. Anything missing or different refuses. |
| +// relocationPreparedManifest is a relocation's binding (region HA §5.8). |
| +// It holds two stamps apart, each exactly. P's build stamp is the one the |
| +// claim's prepared_authority binds (install_stamp when ToInstalled ran): |
| +// the authority names P, that stamp and the digest of P's prepared files |
| +// manifest (sequence 0), and the install journal's files_manifest step |
| +// binds P's key, the op, that build stamp, the digest and the exact |
| +// version. This box's install stamp is the claim's current install_stamp, |
| +// which a replacement target issues anew while keeping P and its |
| +// authority (hostclaim ReplaceTarget at installed, or in probing before |
| +// P's first admission): the holder's stamp (the probe's admission keeps |
| +// it), the claim's local_install receipt (this box, that stamp, the |
| +// digest of its install journal) and the journal itself (read whole, held |
| +// to the receipt's digest, naming the host, the op, this box, that stamp |
| +// and P) all carry it. The build precedes every install, so the build |
| +// stamp is never past the install stamp. The step's digest must be the |
| +// authority's. Anything missing or different refuses; neither stamp |
| +// stands in for the other. |
| func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen, key string) (preparedManifest, error) { |
| c := o.Claim |
| auth, err := preparedAuthority(c) |
| @@ -731,8 +738,8 @@ func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen, |
| if err != nil { |
| return preparedManifest{}, err |
| } |
| - if auth.Generation != gen || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) || auth.InstallStamp != stamp || c.Holder.Stamp != stamp { |
| - return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v is not P %s's at the install stamp %d", hostbox.ErrChanged, auth, gen, stamp) |
| + if auth.Generation != gen || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) || auth.InstallStamp == 0 || auth.InstallStamp > stamp || c.Holder.Stamp != stamp { |
| + return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v is not a build of P %s at or before the install stamp %d", hostbox.ErrChanged, auth, gen, stamp) |
| } |
| var receipt hostclaim.LocalInstall |
| if json.Unmarshal([]byte(c.Op.Journal["local_install"]), &receipt) != nil || receipt.Box != box || receipt.InstallStamp != stamp || !claimHexDigest(receipt.JournalSHA256) { |
| @@ -760,7 +767,7 @@ func relocationPreparedManifest(dataDir, box string, o hostclaim.Observed, gen, |
| if !ok { |
| return preparedManifest{}, fmt.Errorf("%w: the install journal of %s has no prepared files manifest", errLandingChanged, c.Op.ID) |
| } |
| - pm, err := decodePreparedManifest(step, key, c.Op.ID, stamp) |
| + pm, err := decodePreparedManifest(step, key, c.Op.ID, auth.InstallStamp) |
| if err != nil { |
| return preparedManifest{}, err |
| } |
diff --git a/internal/host/relocationinstall.go b/internal/host/relocationinstall.go| index d6835c4d2..4b25b3503 100644 |
| --- a/internal/host/relocationinstall.go |
| +++ b/internal/host/relocationinstall.go |
| @@ -190,20 +190,29 @@ type relocationInstallJournal struct { |
| // prepared files manifest (JSON preparedManifest: key, sha256, version, |
| // op, stamp): what the probe's C8 establishment consumes as its baseline, |
| // read by exactly that version (relocationPreparedManifest), as a move's |
| -// op journal binds its own (landStepFilesManifest). |
| +// op journal binds its own (landStepFilesManifest). Its stamp is P's build |
| +// stamp, the one the claim's prepared_authority binds (install_stamp when |
| +// ToInstalled ran), not this box's install stamp: a replacement target |
| +// keeps P and its authority under a new install stamp (hostclaim |
| +// ReplaceTarget at installed, or in probing before P's first admission), |
| +// and its own install journal records its own stamp beside this binding. |
| const relocationStepFilesManifest = "files_manifest" |
| // relocationPreparedBinding binds P's prepared files manifest for the |
| // probe's establishment: the current version of P's manifest key as the |
| // versioned listing names it, read by that exact version, which must be a |
| // prepared manifest (sequence 0) with exactly the entries the install |
| -// judged (loaded), bound to the op and the install stamp with the digest |
| -// of its bytes. A missing, deleted or unversioned manifest, or one that is |
| -// not what the install judged, refuses. |
| -func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, bucket, prefix, op string, stamp uint64, loaded hostfiles.Manifest) (preparedManifest, error) { |
| +// judged (loaded) and the digest the claim's prepared authority (auth) |
| +// verified, bound to the op and P's build stamp (auth.InstallStamp). A |
| +// missing, deleted or unversioned manifest, one that is not what the |
| +// install judged, or an authority that is not P's, refuses. |
| +func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, bucket, prefix, op string, auth hostclaim.PreparedAuthority, loaded hostfiles.Manifest) (preparedManifest, error) { |
| if data == nil || bucket == "" { |
| return preparedManifest{}, ErrClaimStopUnavailable |
| } |
| + if auth.InstallStamp == 0 || auth.FilesSeq != 0 || !claimHexDigest(auth.FilesManifestSHA256) { |
| + return preparedManifest{}, fmt.Errorf("%w: the prepared authority %+v binds no build of P's files manifest", hostrelocate.ErrRefused, auth) |
| + } |
| key := hostfiles.ManifestKey(prefix) |
| vs, err := data.ListVersions(ctx, bucket, key) |
| if err != nil { |
| @@ -230,7 +239,11 @@ func relocationPreparedBinding(ctx context.Context, data lease.VersionedStore, b |
| if err := json.Unmarshal(raw, &m); err != nil || m.V != hostfiles.ManifestVersion || m.Seq != 0 || loaded.Seq != 0 || !sameEntries(m, loaded) { |
| return preparedManifest{}, errors.Join(fmt.Errorf("%w: P's files manifest %s@%s is not the prepared manifest the install judged", hostrelocate.ErrRefused, key, version), err) |
| } |
| - return preparedManifest{Key: key, SHA256: hostfilesSum(raw), Version: version, Op: op, Stamp: stamp}, nil |
| + sum := hostfilesSum(raw) |
| + if sum != auth.FilesManifestSHA256 { |
| + return preparedManifest{}, fmt.Errorf("%w: P's files manifest %s@%s is not the prepared authority's", hostrelocate.ErrRefused, key, version) |
| + } |
| + return preparedManifest{Key: key, SHA256: sum, Version: version, Op: op, Stamp: auth.InstallStamp}, nil |
| } |
| // relocationOpDir is the operation's local journal directory (ruling 44): |
| @@ -470,7 +483,11 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont |
| } |
| // The binding the probe's C8 establishment reads P's prepared |
| // manifest by: its exact version, journalled below. |
| - binding, err := relocationPreparedBinding(ctx, cfg.Versions, cfg.Bucket, prefix, c.Op.ID, stamp, pm) |
| + auth, err := preparedAuthority(c) |
| + if err != nil { |
| + return hostclaim.LocalInstall{}, err |
| + } |
| + binding, err := relocationPreparedBinding(ctx, cfg.Versions, cfg.Bucket, prefix, c.Op.ID, auth, pm) |
| if err != nil { |
| return hostclaim.LocalInstall{}, err |
| } |
diff --git a/internal/host/relocationinstall_linux_test.go b/internal/host/relocationinstall_linux_test.go| index c9c18fc12..31601027a 100644 |
| --- a/internal/host/relocationinstall_linux_test.go |
| +++ b/internal/host/relocationinstall_linux_test.go |
| @@ -95,8 +95,16 @@ func newInstallFixtureWith(t *testing.T, opts buildOptions) *installFixture { |
| 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 |
| @@ -105,34 +113,54 @@ func newInstallFixtureWith(t *testing.T, opts buildOptions) *installFixture { |
| 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) |
| } |
| - t.Cleanup(func() { f.reg.Close() }) |
| + reg := f.reg |
| + t.Cleanup(func() { reg.Close() }) |
| if f.inv, err = hostbox.OpenStopInventoryDB(filepath.Join(r.DataDir, ".hostclaims", "inventory.db")); err != nil { |
| t.Fatal(err) |
| } |
| - t.Cleanup(func() { f.inv.Close() }) |
| + 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: "dst", Enrollment: f.actor.Enrollment, DirectoryRevision: "4", |
| + 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: "dst", Enrollment: f.actor.Enrollment, DirectoryRevision: "4", |
| - Reconciler: hostbox.Reconciler{Claims: b.claims, Guard: guard, Box: "dst", Region: "ie", Local: park.Local{Guard: guard, Claims: b.claims, DataDir: r.DataDir, Box: "dst"}}} |
| - b.a.Wake = &ClaimWakeAssembly{Roles: &ClaimRoleAssembly{Inventory: f.inv, Identity: handoverRecoveryIdentity(t, f.actor), ManagedRoots: map[string]string{"data": r.DataDir}}} |
| + 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: f.actor, BootID: "boot", ControllerID: "ctl", PID: 100, ProcessStart: "5000"}, nil |
| + return claimStopLocal{Actor: actor, BootID: "boot", ControllerID: "ctl", PID: 100, ProcessStart: "5000"}, nil |
| } |
| - b.cfg.Registry, b.cfg.OptDir = f.reg, r.OptDir |
| - b.cfg.Source.Guard, b.cfg.Source.Now = guard, time.Now |
| - b.cfg.render = f.render |
| - b.cfg.materialize = f.materialize |
| - b.cfg.verifyLive = f.verifyLive |
| - return f |
| + 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 |
| @@ -145,7 +173,7 @@ func (f *installFixture) render(ctx context.Context, _ release.Store, _ *ecdh.Pr |
| if err != nil { |
| return appconfig.HeldRender{}, err |
| } |
| - if rt.Status != "relocating" || rt.HoldOp != op || box != "dst" { |
| + if rt.Status != "relocating" || rt.HoldOp != op || box != f.actor.Box { |
| return appconfig.HeldRender{}, appconfig.ErrNotHeld |
| } |
| var body strings.Builder |
diff --git a/internal/host/relocationprobefiles_linux_test.go b/internal/host/relocationprobefiles_linux_test.go| index d7476f6f2..8bd741de1 100644 |
| --- a/internal/host/relocationprobefiles_linux_test.go |
| +++ b/internal/host/relocationprobefiles_linux_test.go |
| @@ -7,6 +7,7 @@ import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| + "strconv" |
| "strings" |
| "testing" |
| "time" |
| @@ -31,34 +32,112 @@ type probeFilesFixture struct { |
| *installFixture |
| receipt hostclaim.LocalInstall |
| inputs *RelocationWakeInputs |
| + // replaced is what the replaced target left when the fixture replaced |
| + // it (newReplacedProbeFilesFixture): its receipt and its install |
| + // journal's bytes, at P's build stamp. |
| + replaced struct { |
| + receipt hostclaim.LocalInstall |
| + journal []byte |
| + } |
| } |
| func newProbeFilesFixture(t *testing.T, opts buildOptions) *probeFilesFixture { |
| t.Helper() |
| - ctx := t.Context() |
| - // Claim Plan 15's origin record is on, as the provider's origin proof |
| - // (ClaimOriginProof) needs: every admission through the claim's CAS, |
| - // the source's g1 and the probe's P, is recorded. |
| + f := newProbeFilesInstalled(t, opts) |
| + f.installAndProbe() |
| + f.bindProvider() |
| + return f |
| +} |
| + |
| +// newProbeFilesInstalled is the fixture at installed, P built and its |
| +// prepared authority journalled, before any local install. Claim Plan |
| +// 15's origin record is on, as the provider's origin proof |
| +// (ClaimOriginProof) needs: every admission through the claim's CAS, the |
| +// source's g1 and the probe's P, is recorded. |
| +func newProbeFilesInstalled(t *testing.T, opts buildOptions) *probeFilesFixture { |
| + t.Helper() |
| t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1") |
| - f := &probeFilesFixture{installFixture: newInstallFixtureWith(t, opts)} |
| - auth, done, err := f.verifier.Verify(ctx, f.read()) |
| - if err != nil || !done { |
| - t.Fatalf("P is not verified: done=%v %v", done, err) |
| + return &probeFilesFixture{installFixture: newInstallFixtureWith(t, opts)} |
| +} |
| + |
| +// installAndProbe is the executor's local install at the current install |
| +// stamp, its receipt journalled, and its entry into probing. |
| +func (f *probeFilesFixture) installAndProbe() { |
| + f.t.Helper() |
| + var err error |
| + if f.receipt, err = f.install(); err != nil { |
| + f.t.Fatal(err) |
| + } |
| + f.update(hostclaim.RecordLocalInstall("rel-1", f.receipt)) |
| + f.update(hostclaim.ToProbing("rel-1", time.Now())) |
| +} |
| + |
| +// newReplacedProbeFilesFixture replaces a terminated target that had made |
| +// its local install, at phase (installed, or probing before P's first |
| +// admission), as hostclaim.ReplaceTarget does for both: install(c, next, |
| +// P, installed) names the replacement box as the executor, keeps P and its |
| +// prepared authority, issues a new install stamp (the CAS's generation) |
| +// and drops the receipt; ToInstalled does not run again. The replacement, |
| +// dst2, is a fresh box: its guard is activated at the new stamp, and it |
| +// makes its own local install, records it and enters probing. The replaced |
| +// target's receipt and journal bytes are kept for the negatives. |
| +func newReplacedProbeFilesFixture(t *testing.T, opts buildOptions, phase string) *probeFilesFixture { |
| + t.Helper() |
| + f := newProbeFilesInstalled(t, opts) |
| + var err error |
| + if f.replaced.receipt, err = f.install(); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.update(hostclaim.RecordLocalInstall("rel-1", f.replaced.receipt)) |
| + if phase == hostclaim.PhaseRelocatingProbing { |
| + f.update(hostclaim.ToProbing("rel-1", time.Now())) |
| + } |
| + if f.read().Claim.Op.Phase != phase { |
| + t.Fatalf("the replaced target is at %s, not %s", f.read().Claim.Op.Phase, phase) |
| + } |
| + path, _ := relocationInstallPath(f.r.DataDir, "rel-1") |
| + if f.replaced.journal, err = os.ReadFile(path); err != nil { |
| + t.Fatal(err) |
| } |
| + build := f.read().Claim.Holder.Stamp |
| + next := hostbox.Actor{Box: "dst2", Enrollment: strings.Repeat("b", 32), Region: "ie", Revision: 4} |
| f.update(func(c *hostclaim.Claim) error { |
| - auth.InstallStamp = c.Holder.Stamp |
| - c.Op.Journal["prepared_authority"] = string(mustJSON(t, auth)) |
| + if hostclaim.PreparedOpened(c) { |
| + return errors.New("P was opened") |
| + } |
| + stamp := c.Gen + 1 // hostclaim install(): the CAS's resulting gen |
| + c.Op.Exec, c.Op.Phase = next.Box, hostclaim.PhaseInstalled |
| + c.Op.Intent["target_box"], c.Op.Intent["target_enrollment"] = next.Box, next.Enrollment |
| + c.Op.Participants = []string{"src", next.Box} |
| + c.Op.Journal["exec_ack."+next.Box] = `{"op":"rel-1"}` |
| + c.Holder.Stamp = stamp |
| + c.Op.Journal["install_stamp"] = strconv.FormatUint(stamp, 10) |
| + delete(c.Op.Journal, "local_install") |
| return nil |
| }) |
| - if f.receipt, err = f.install(); err != nil { |
| + c := f.read().Claim |
| + auth, err := preparedAuthority(c) |
| + if err != nil || auth.InstallStamp != build || c.Holder.Stamp == build { |
| + t.Fatalf("the replacement keeps P's authority at the build stamp %d under a new stamp %d: %+v %v", build, c.Holder.Stamp, auth, err) |
| + } |
| + f.onBox(claimCensusBox(t), next) |
| + if err := f.a.Guard.Activate(t.Context(), buildHost, next.Box, nil); err != nil { |
| t.Fatal(err) |
| } |
| - f.update(hostclaim.RecordLocalInstall("rel-1", f.receipt)) |
| - f.update(hostclaim.ToProbing("rel-1", time.Now())) |
| + f.installAndProbe() |
| + f.bindProvider() |
| + return f |
| +} |
| + |
| +// bindProvider opens the real C8 provider the production source opens |
| +// and binds the relocation's wake inputs to it. |
| +func (f *probeFilesFixture) bindProvider() { |
| + t := f.t |
| + ctx := t.Context() |
| src := &movesFilesSource{DataDir: f.r.DataDir, Routes: f.reg, newFiles: func(context.Context, string, string) (release.Store, error) { |
| return bucketStore{b: f.bucket, bucket: "data"}, nil |
| }} |
| - c8, err := src.Open(ctx, f.claims, f.bucket, hostbox.Actor{Box: "dst", Region: "ie", Bucket: "data", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}}) |
| + c8, err := src.Open(ctx, f.claims, f.bucket, hostbox.Actor{Box: f.actor.Box, Region: "ie", Bucket: "data", Cloud: hostbox.CloudIdentity{Region: "eu-west-1"}}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| @@ -67,9 +146,8 @@ func newProbeFilesFixture(t *testing.T, opts buildOptions) *probeFilesFixture { |
| Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) { |
| return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/opt/carlos/live/" + buildHost, ExecutableSHA256: strings.Repeat("b", 64), Version: "v1"}, nil |
| }, |
| - FilesManifest: c8, Markers: plan9D5Markers{f.bucket}, Claims: f.claims, FilesStore: f.files, Data: f.bucket, Bucket: "data", Region: "ie", Box: "dst", |
| + FilesManifest: c8, Markers: plan9D5Markers{f.bucket}, Claims: f.claims, FilesStore: f.files, Data: f.bucket, Bucket: "data", Region: "ie", Box: f.actor.Box, |
| NewID: func() string { seq++; return fmt.Sprintf("fresh%d", seq) }, Now: time.Now, Releases: &roReleases{}} |
| - return f |
| } |
| func (f *probeFilesFixture) update(tr func(*hostclaim.Claim) error) { |
| @@ -99,7 +177,7 @@ func (f *probeFilesFixture) probe(env string) (ClaimLaunchInputs, error) { |
| if err != nil { |
| return in, err |
| } |
| - counted, err := hostclaim.Update(ctx, f.claims, o, hostclaim.BeginProbeAttempt("rel-1", "dst")) |
| + counted, err := hostclaim.Update(ctx, f.claims, o, hostclaim.BeginProbeAttempt("rel-1", f.actor.Box)) |
| if err != nil { |
| f.t.Fatal(err) |
| } |
| @@ -275,3 +353,97 @@ func TestRelocationPreparedBindingIsRelocationsOwn(t *testing.T) { |
| } |
| }) |
| } |
| + |
| +// Merge round 7: a replacement target keeps P and its prepared authority |
| +// at P's build stamp while it installs under a new install stamp (at |
| +// installed, or in probing before P's first admission). Its first probe |
| +// establishes P's files through the real provider: P's manifest bound at |
| +// the build stamp, its own receipt and install journal at the new one. |
| +// The replacement's install journal records P's build stamp in its |
| +// files_manifest step and its own stamp as the journal's install stamp. |
| +func TestRelocationReplacementProbeEstablishesPsFiles(t *testing.T) { |
| + opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}} |
| + for _, phase := range []string{hostclaim.PhaseInstalled, hostclaim.PhaseRelocatingProbing} { |
| + t.Run("replaced at "+phase, func(t *testing.T) { |
| + f := newReplacedProbeFilesFixture(t, opts, phase) |
| + c := f.read().Claim |
| + auth, _ := preparedAuthority(c) |
| + if f.receipt.InstallStamp != c.Holder.Stamp || f.receipt.InstallStamp == auth.InstallStamp { |
| + t.Fatalf("the replacement's receipt %+v is not at its own install stamp %d", f.receipt, c.Holder.Stamp) |
| + } |
| + if _, err := f.probe(firstProbeEnv); err != nil { |
| + t.Fatalf("the replacement's first probe: %v", err) |
| + } |
| + m, pm := f.established() |
| + if pm.Stamp != auth.InstallStamp || m.Platform[hostfiles.EnvItem].SHA256 != hostfilesSum([]byte(firstProbeEnv)) || m.Entries["public/index.html"].SHA256 == "" { |
| + t.Fatalf("binding %+v (build stamp %d), manifest %+v", pm, auth.InstallStamp, m) |
| + } |
| + path, _ := relocationInstallPath(f.r.DataDir, "rel-1") |
| + raw, _ := os.ReadFile(path) |
| + var j relocationInstallJournal |
| + if err := json.Unmarshal(raw, &j); err != nil || j.InstallStamp != c.Holder.Stamp { |
| + t.Fatalf("the replacement's journal is at %d, not its install stamp %d: %v", j.InstallStamp, c.Holder.Stamp, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// Merge round 7: neither stamp stands in for the other. The replaced |
| +// target's install journal (P's build stamp), or its receipt, under the |
| +// replacement's install stamp refuses; a prepared authority whose stamp |
| +// is not the build stamp the install bound, or is past the install |
| +// stamp, refuses; and nothing is established. |
| +func TestRelocationReplacementRefusesTheWrongStamp(t *testing.T) { |
| + opts := buildOptions{DB: true, Files: map[string]string{"public/index.html": "<h1>shop</h1>"}, Platform: map[string]string{hostfiles.EnvItem: installEnv, hostfiles.WakeItem: installWake()}} |
| + for name, tamper := range map[string]func(f *probeFilesFixture){ |
| + "the replaced target's install journal": func(f *probeFilesFixture) { |
| + path, _ := relocationInstallPath(f.r.DataDir, "rel-1") |
| + if err := os.WriteFile(path, f.replaced.journal, 0o600); err != nil { |
| + f.t.Fatal(err) |
| + } |
| + }, |
| + "the replaced target's install journal and receipt": func(f *probeFilesFixture) { |
| + path, _ := relocationInstallPath(f.r.DataDir, "rel-1") |
| + if err := os.WriteFile(path, f.replaced.journal, 0o600); err != nil { |
| + f.t.Fatal(err) |
| + } |
| + f.update(func(c *hostclaim.Claim) error { |
| + c.Op.Journal["local_install"] = string(mustJSON(f.t, f.replaced.receipt)) |
| + return nil |
| + }) |
| + }, |
| + "an authority at the install stamp": func(f *probeFilesFixture) { |
| + f.update(func(c *hostclaim.Claim) error { |
| + auth, err := preparedAuthority(c) |
| + if err != nil { |
| + return err |
| + } |
| + auth.InstallStamp = c.Holder.Stamp |
| + c.Op.Journal["prepared_authority"] = string(mustJSON(f.t, auth)) |
| + return nil |
| + }) |
| + }, |
| + "an authority past the install stamp": func(f *probeFilesFixture) { |
| + f.update(func(c *hostclaim.Claim) error { |
| + auth, err := preparedAuthority(c) |
| + if err != nil { |
| + return err |
| + } |
| + auth.InstallStamp = c.Holder.Stamp + 1 |
| + c.Op.Journal["prepared_authority"] = string(mustJSON(f.t, auth)) |
| + return nil |
| + }) |
| + }, |
| + } { |
| + t.Run(name, func(t *testing.T) { |
| + f := newReplacedProbeFilesFixture(t, opts, hostclaim.PhaseRelocatingProbing) |
| + tamper(f) |
| + if _, err := f.probe(firstProbeEnv); err == nil { |
| + t.Fatal("established under the wrong stamp") |
| + } |
| + if m, _, _ := hostfiles.Load(t.Context(), f.files, movesFilesPrefix(f.read().Claim.Holder.Location, "p1")); m.Seq != 0 { |
| + t.Fatalf("P's manifest was replaced: %+v", m) |
| + } |
| + }) |
| + } |
| +} |