WIP claim Plan 14 track E checkpoint 35 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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") |
| + } |
| +} |