carlos / platform Public

WIP claim Plan 14 track E checkpoint 35 (not for merge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 4588e23694b2301ead1eea72cf193240f715ae61 parent c3d0435
1 file changed, +307 −0
  • internal/host/plan14_generation_exact_retirement_root_linux_test.go +307 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	353.717s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.244s
ok  	github.com/carlosframework/platform/cmd/moveprobe	92.279s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.711s
ok  	github.com/carlosframework/platform/internal/activator	233.866s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.007s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.360s
ok  	github.com/carlosframework/platform/internal/apperr	0.081s
ok  	github.com/carlosframework/platform/internal/applog	0.156s
ok  	github.com/carlosframework/platform/internal/awsinv	0.005s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.894s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.716s
ok  	github.com/carlosframework/platform/internal/changelog	0.311s
ok  	github.com/carlosframework/platform/internal/clamd	0.055s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	367.321s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.591s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.219s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.102s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.124s
ok  	github.com/carlosframework/platform/internal/gate	1.654s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.013s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.153s
ok  	github.com/carlosframework/platform/internal/host	135.690s
ok  	github.com/carlosframework/platform/internal/hostactors	0.357s
ok  	github.com/carlosframework/platform/internal/hostbox	2.262s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.519s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.024s
ok  	github.com/carlosframework/platform/internal/hostexec	4.918s
ok  	github.com/carlosframework/platform/internal/hostfence	0.029s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.583s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.024s
ok  	github.com/carlosframework/platform/internal/hostguard	1.006s
ok  	github.com/carlosframework/platform/internal/hostops	1.452s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.024s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.024s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.457s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.133s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.024s
ok  	github.com/carlosframework/platform/internal/infraview	0.024s
ok  	github.com/carlosframework/platform/internal/instancectl	1.816s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.732s
ok  	github.com/carlosframework/platform/internal/instancefs	0.029s
ok  	github.com/carlosframework/platform/internal/instancekey	0.024s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.029s
ok  	github.com/carlosframework/platform/internal/layout	1.466s
ok  	github.com/carlosframework/platform/internal/lease	9.439s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.006s
ok  	github.com/carlosframework/platform/internal/ledger	0.013s
ok  	github.com/carlosframework/platform/internal/mail	0.165s
ok  	github.com/carlosframework/platform/internal/maintenance	0.086s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.525s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.076s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.110s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.746s
ok  	github.com/carlosframework/platform/internal/objstore	0.181s
ok  	github.com/carlosframework/platform/internal/ops	0.163s
ok  	github.com/carlosframework/platform/internal/park	0.199s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	14.065s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.482s
ok  	github.com/carlosframework/platform/internal/relay	7.245s
ok  	github.com/carlosframework/platform/internal/release	22.062s
ok  	github.com/carlosframework/platform/internal/rollout	3.801s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.298s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.965s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.253s
ok  	github.com/carlosframework/platform/internal/status	0.041s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.198s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.006s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.564s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.024s
=== RUN   TestT4StorageProbeMinio
2026/10/14 19:55:44 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7D8CA7C011AD, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.022s
minio contract: PASS
diff --git a/internal/host/plan14_generation_exact_retirement_root_linux_test.go b/internal/host/plan14_generation_exact_retirement_root_linux_test.go
new file mode 100644
index 000000000..838435859
--- /dev/null
+++ b/internal/host/plan14_generation_exact_retirement_root_linux_test.go
@@ -0,0 +1,307 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/rootparticipation"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func TestPlan14GenerationExactRetirementRootRunner(t *testing.T) {
+ const childKey = "PLAN14_E35_EXACT_RETIREMENT_CHILD"
+ dir := os.Getenv(childKey)
+ if dir == "" {
+ dir = generationRootRunnerDir(t)
+ generationRunnerRegistryUser(t)
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14GenerationExactRetirementRootRunner$", "-test.v", "-test.timeout=2m")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), childKey+"="+dir)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("isolated exact retirement: %v\n%s", err, out)
+ } else {
+ t.Logf("%s", out)
+ }
+ return
+ }
+ if os.Geteuid() != 0 {
+ t.Fatal("designated runner must execute as root")
+ }
+ t.Setenv("CARLOS_OPT_DIR", dir)
+ t.Setenv("CARLOS_DATA_DIR", filepath.Join(dir, "data"))
+ for _, path := range []string{dataDir(), filepath.Join(dir, "host-locks")} {
+ if err := os.Mkdir(path, 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ ctx := t.Context()
+ open := func(name string) rootstate.Store {
+ t.Helper()
+ s, err := rootstate.Open(filepath.Join(dir, name), true, false)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { s.Close() })
+ return s
+ }
+ fences, census, generations, exclusion := open("fences"), open("census"), open("generations"), open("exclusion")
+ write := func(s rootstate.Store, name string, value any) {
+ t.Helper()
+ raw, err := json.Marshal(value)
+ if err == nil {
+ err = s.Write(name, raw)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ identity := func(path string) rootstate.FileIdentity {
+ t.Helper()
+ f, err := os.Open(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ id, err := rootstate.Identity(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return id
+ }
+ start := func(path string) (int, string, func()) {
+ t.Helper()
+ cmd := exec.CommandContext(ctx, path, "90")
+ cmd.Dir = dir
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ joined := false
+ join := func() {
+ if !joined {
+ _ = cmd.Process.Kill()
+ _ = cmd.Wait()
+ joined = true
+ }
+ }
+ t.Cleanup(join)
+ raw, err := generationRead(filepath.Join("/proc", strconv.Itoa(cmd.Process.Pid), "stat"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ stamp, err := generationStart(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return cmd.Process.Pid, stamp, join
+ }
+ boot, err := (generationKernel{proc: "/proc"}).boot()
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec := provision.Record{Schema: 1, Host: filepath.Base(dir) + ".example", Account: "bab", App: "shop", Kind: "service", Addr: "127.0.0.1:8080", Channel: "stable", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)}
+ store := release.NewDirStore(filepath.Join(dir, "deployment"))
+ plan14PutRecord(t, store, rec)
+ inc, err := hostclaim.Incarnation(rec.Account, rec.App, rec.Host, rec.CreatedAt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ in := hostguard.RootInstallation{Host: rec.Host, Account: rec.Account, App: rec.App, Record: provision.RecordKey(rec.Account, rec.App, rec.Host), Incarnation: inc, Stamp: 1, Box: "runner", Enrollment: "en", Migration: 1, InventoryRevision: 1}
+ // Enrollment, installation, journal closure and group identities are fixtures.
+ // Kernel references, private stores, reconciliation and sweep are production.
+ write(fences, in.Host+".json", map[string]any{"Schema": 1, "Installation": in, "State": map[string]any{"v": 1, "host": in.Host, "incarnation": inc, "active_stamp": in.Stamp}})
+ write(census, "census.json", rootparticipation.Census{Schema: 1, Box: in.Box, Enrollment: in.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true})
+ journalPath := filepath.Join(dir, "journals.db")
+ journal, err := hostbox.OpenLaunchJournal(journalPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer journal.Close()
+ service, err := hostbox.NewJournalService(journal)
+ if err != nil {
+ t.Fatal(err)
+ }
+ payload := hostbox.StopInventory{Complete: true, Host: in.Host, Incarnation: inc, InstallationStamp: 1, Box: in.Box, Enrollment: in.Enrollment, DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: in.Host, RowIdentity: "row", Ownership: "owner", Materialization: "materialized"}}}
+ if err := service.Inventory.WriteStopInventory(ctx, payload); err != nil {
+ t.Fatal(err)
+ }
+ uid, gid := generationRunnerRegistryUser(t)
+ if err := os.Chown(dataDir(), uid, gid); err != nil {
+ t.Fatal(err)
+ }
+ reg, err := openRegistryChild(filepath.Join(dataDir(), "registry.db"), uid, gid)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer reg.Close()
+ row := registry.Route{Host: in.Host, Account: in.Account, App: in.App, Version: "v2", ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: inc, ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "row"}}
+ if err := reg.Upsert(row); err != nil {
+ t.Fatal(err)
+ }
+ if err := reg.Close(); err != nil {
+ t.Fatal(err)
+ }
+ rows := fakeLister{routes: []registry.Route{row}}
+ body, err := os.ReadFile("/bin/sleep")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sum := sha256.Sum256(body)
+ for _, version := range []string{"v1", "v2", "v3"} {
+ if err := release.StageArtifact(dir, in.Account, version, in.App, body); err != nil {
+ t.Fatal(err)
+ }
+ when := time.Now().Add(-48 * time.Hour)
+ if err := os.Chtimes(release.VersionPath(dir, in.Account, version, in.App), when, when); err != nil {
+ t.Fatal(err)
+ }
+ }
+ controllerPID, controllerStart, joinController := start("/bin/sleep")
+ ledger := &release.RootGenerations{Store: generations}
+ reserve := func(version string) (release.GenerationReservation, hostbox.LaunchReservation) {
+ t.Helper()
+ target := hostguard.FenceTarget{Host: in.Host, Incarnation: inc, Stamp: 1, Session: 1, Box: in.Box, Enrollment: in.Enrollment, BootID: boot, ControllerID: version}
+ path := release.VersionPath(dir, in.Account, version, in.App)
+ parent := hostbox.LaunchGroup{RelativePath: "fixture-prelaunch", Device: 1, Inode: 1}
+ if err := journal.Prelaunch(ctx, hostbox.LaunchInventory{Target: target, ControllerPID: controllerPID, ProcessStart: controllerStart, Groups: []hostbox.LaunchGroup{parent}, NoState: true}); err != nil {
+ t.Fatal(err)
+ }
+ b := release.GenerationBinding{Binding: release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}, Target: generationOwner(target), Installation: rootHistoryInstallation(in), Record: in.Record, RecordETag: "fixture", Attempt: "main", Inputs: "fixture", Invocation: version, ControllerPID: controllerPID, ControllerStart: controllerStart, Version: version, Digest: hex.EncodeToString(sum[:]), Path: path, Tree: filepath.Dir(path), Executable: identity(path), TreeIdentity: identity(filepath.Dir(path))}
+ var r release.GenerationReservation
+ if err := rootstate.WithRelease(ctx, exclusion, func() error { var err error; r, err = ledger.Reserve(b); return err }); err != nil {
+ t.Fatal(err)
+ }
+ unit := hostbox.LaunchReservation{ID: b.Attempt, Role: "main", Target: target, Inputs: b.Inputs, InvocationID: b.Invocation, Executable: hostbox.LaunchFile{Device: b.Executable.Device, Inode: b.Executable.Inode}, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(in.Host)}}
+ return r, unit
+ }
+ v1, unit := reserve("v1")
+ v3, _ := reserve("v3")
+ if err := journal.IntendUnit(ctx, unit); err != nil {
+ t.Fatal(err)
+ }
+ _, _, joinTenant := start(v1.Binding.Path)
+ // Leave Started absent to exercise a process launched before that commit.
+ verifier := generationRetirement{guard: hostguard.Guard{DataDir: dataDir(), LockDir: filepath.Join(dir, "host-locks"), Root: hostguard.NewRootAuthority(fences)}, journal: journal, kernel: productionGenerationAbsence}
+ source := generationPinSource{open: func() (rootstate.Store, error) {
+ return rootstate.Open(filepath.Join(dir, "generations"), false, false)
+ }, retire: func(ctx context.Context, g *release.RootGenerations) error {
+ records, err := g.Reservations(ctx)
+ if err != nil {
+ return err
+ }
+ return verifier.retireAll(ctx, g, records)
+ }}
+ source.reconcile = (generationReconciler{read: func(ctx context.Context) (generationInventory, error) {
+ snapshot, err := generationInventoryAt(ctx, filepath.Join(dir, "census"), filepath.Join(dir, "fences"), journalPath)
+ if err == nil {
+ snapshot, err = generationRequiredInventory(ctx, store, snapshot, nil)
+ }
+ if err == nil {
+ snapshot, err = productionGenerationRegistryInventory(ctx, snapshot)
+ }
+ return snapshot, err
+ }, kernel: productionGenerationKernelCensus}).reconcile
+ readV1 := func() release.GenerationReservation {
+ t.Helper()
+ r, err := ledger.Read(v1.Binding)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return r
+ }
+ refuse := func(label string) {
+ t.Helper()
+ err := rootstate.WithRelease(ctx, exclusion, func() error {
+ pins, err := source.StagedPins(ctx, dir)
+ if err == nil || pins != nil {
+ t.Fatalf("%s certified: %v %v", label, pins, err)
+ }
+ sweepStagedVersions(rows, dir, filepath.Join(dir, "legacy", "generations.json"), 6*time.Hour, time.Now(), source)
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if readV1().Retirement != "" || !staged(t, dir, "v1") || !staged(t, dir, "v3") {
+ t.Fatalf("%s lost an outstanding pin", label)
+ }
+ raw, err := os.ReadFile(v1.Binding.Path)
+ if err != nil || sha256.Sum256(raw) != sum || identity(v1.Binding.Path) != v1.Binding.Executable {
+ t.Fatalf("%s changed V1's bytes or inode: %v", label, err)
+ }
+ }
+ refuse("live launch")
+ if err := journal.SettleUnit(ctx, unit.Target, unit.ID); err != nil {
+ t.Fatal(err)
+ }
+ if err := journal.CloseWriters(ctx, unit.Target); err != nil {
+ t.Fatal(err)
+ }
+ refuse("closed journal with live controller")
+ joinController()
+ refuse("joined controller with live executable")
+ joinTenant()
+ file, err := os.Open(v1.Binding.Path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer file.Close()
+ refuse("joined processes with open descriptor")
+ if err := file.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if err := rootstate.WithRelease(ctx, exclusion, func() error {
+ pins, err := source.StagedPins(ctx, dir)
+ if err != nil {
+ return err
+ }
+ for path, want := range map[string]bool{v1.Binding.Path: false, v3.Binding.Path: true} {
+ if got, err := pins.Refers(path); err != nil || got != want {
+ t.Fatalf("exact retirement pin %s: %v %v", path, got, err)
+ }
+ }
+ sweepStagedVersions(rows, dir, filepath.Join(dir, "legacy", "generations.json"), 72*time.Hour, time.Now(), source)
+ if !staged(t, dir, "v1") {
+ t.Fatal("retirement bypassed grace")
+ }
+ sweepStagedVersions(rows, dir, filepath.Join(dir, "legacy", "generations.json"), 6*time.Hour, time.Now(), source)
+ return nil
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if staged(t, dir, "v1") || !staged(t, dir, "v2") || !staged(t, dir, "v3") {
+ t.Fatal("sweep did not remove only retired V1")
+ }
+ if got, err := ledger.Read(v3.Binding); err != nil || got != v3 {
+ t.Fatalf("pending V3 changed: %+v %v", got, err)
+ }
+ if readV1().Retirement == "" {
+ t.Fatal("missing retirement tombstone")
+ }
+ if _, err := ledger.Reserve(v1.Binding); err == nil {
+ t.Fatal("retired reservation replayed")
+ }
+ if err := journal.IntendUnit(ctx, unit); err == nil {
+ t.Fatal("closed launch replayed")
+ }
+}