Plan 14 integrated
lifecycle prototype, copy edit (not for merge):Paul's copy edit applied (em dash to a full stop); lifecycle cold-wake prototype (failing at provisioning, under investigation).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+464
−2
internal/host/plan14_lifecycle_root_linux_test.go+462 −0internal/release/programhistory_test.go+2 −2
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 293.941s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.652s
ok github.com/carlosframework/platform/cmd/moveprobe 93.231s
ok github.com/carlosframework/platform/cmd/zddprobe 0.701s
ok github.com/carlosframework/platform/internal/activator 250.687s
ok github.com/carlosframework/platform/internal/agentsock 0.014s
ok github.com/carlosframework/platform/internal/alerting 0.007s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.435s
ok github.com/carlosframework/platform/internal/apperr 0.076s
ok github.com/carlosframework/platform/internal/applog 0.146s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.899s
? 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.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.762s
ok github.com/carlosframework/platform/internal/changelog 0.309s
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 388.911s
? 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 6.385s
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.002s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.336s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.003s
ok github.com/carlosframework/platform/internal/edge 26.647s
? 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.129s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.014s
ok github.com/carlosframework/platform/internal/geo 0.016s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/historyreadback 0.148s
ok github.com/carlosframework/platform/internal/host 173.399s
ok github.com/carlosframework/platform/internal/hostactors 0.454s
ok github.com/carlosframework/platform/internal/hostbox 2.619s
ok github.com/carlosframework/platform/internal/hostclaim 0.497s
ok github.com/carlosframework/platform/internal/hostevidence 0.005s
ok github.com/carlosframework/platform/internal/hostexec 4.918s
ok github.com/carlosframework/platform/internal/hostfence 0.038s
ok github.com/carlosframework/platform/internal/hostfiles 10.482s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.034s
ok github.com/carlosframework/platform/internal/hostops 1.549s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.440s
ok github.com/carlosframework/platform/internal/hostresolve 0.143s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 5.224s
ok github.com/carlosframework/platform/internal/instanceexec 2.718s
ok github.com/carlosframework/platform/internal/instancefs 0.005s
ok github.com/carlosframework/platform/internal/instancekey 0.004s
ok github.com/carlosframework/platform/internal/instanceupdates 0.009s
ok github.com/carlosframework/platform/internal/layout 1.504s
ok github.com/carlosframework/platform/internal/lease 9.803s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.006s
ok github.com/carlosframework/platform/internal/ledger 0.017s
ok github.com/carlosframework/platform/internal/mail 0.172s
ok github.com/carlosframework/platform/internal/maintenance 0.078s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.515s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.063s
ok github.com/carlosframework/platform/internal/metrics 0.007s
ok github.com/carlosframework/platform/internal/moves 21.047s
ok github.com/carlosframework/platform/internal/moves/archive 0.811s
ok github.com/carlosframework/platform/internal/objstore 0.181s
ok github.com/carlosframework/platform/internal/ops 0.177s
ok github.com/carlosframework/platform/internal/park 0.197s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 35.335s
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 5.436s
ok github.com/carlosframework/platform/internal/relay 7.265s
ok github.com/carlosframework/platform/internal/release 22.369s
ok github.com/carlosframework/platform/internal/rollout 3.759s
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.058s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/runs 0.575s
ok github.com/carlosframework/platform/internal/s3compat 0.007s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.483s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.563s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.545s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.121s
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.002s
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.001s
ok github.com/carlosframework/platform/internal/vet 1.577s
=== 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
linux/arm64 carlos binary: 48496802 bytes (ceiling 52000000)
--- 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, 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.021s
=== RUN TestT4StorageProbeMinio
2026/10/16 13:37:32 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DF06126742D073, 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.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.020s
minio contract: PASS
diff --git a/internal/host/plan14_lifecycle_root_linux_test.go b/internal/host/plan14_lifecycle_root_linux_test.go| new file mode 100644 |
| index 000000000..62386f8c9 |
| --- /dev/null |
| +++ b/internal/host/plan14_lifecycle_root_linux_test.go |
| @@ -0,0 +1,462 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "crypto/ed25519" |
| + "crypto/sha256" |
| + "database/sql" |
| + "encoding/base64" |
| + "encoding/hex" |
| + "encoding/json" |
| + "io" |
| + "net" |
| + "net/http" |
| + "net/http/httptest" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "runtime" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/activator" |
| + "github.com/carlosframework/platform/internal/cgroup" |
| + "github.com/carlosframework/platform/internal/controlsock" |
| + "github.com/carlosframework/platform/internal/hostactors" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostops" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/lease" |
| + "github.com/carlosframework/platform/internal/lease/leasetest" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rollout" |
| + "github.com/carlosframework/platform/internal/rootparticipation" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +const lifecycleHost = "lifecycle.bab.example" |
| +const lifecycleHome = "/var/lib/plan14-lifecycle" |
| + |
| +func init() { |
| + if lifecyclePayloadArgs(os.Args) { |
| + db, err := sql.Open("sqlite", os.Args[4]) |
| + if err != nil { |
| + panic(err) |
| + } |
| + if _, err = db.Exec("PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS lifecycle (value TEXT)"); err != nil { |
| + panic(err) |
| + } |
| + listener, err := net.Listen("unix", os.Args[2]) |
| + if err != nil { |
| + panic(err) |
| + } |
| + if err = http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "lifecycle tenant") })); err != nil { |
| + panic(err) |
| + } |
| + os.Exit(0) |
| + } |
| +} |
| + |
| +func lifecyclePayloadArgs(args []string) bool { |
| + return len(args) == 5 && args[1] == "--socket" && args[2] == filepath.Join(provision.InstanceRunDir(lifecycleHost), "app.sock") && args[3] == "--db" && args[4] == provision.NestedDBPath("/data", lifecycleHost) |
| +} |
| + |
| +func TestPlan14LifecyclePayloadDispatch(t *testing.T) { |
| + ours := []string{"tenant", "--socket", filepath.Join(provision.InstanceRunDir(lifecycleHost), "app.sock"), "--db", provision.NestedDBPath("/data", lifecycleHost)} |
| + if !lifecyclePayloadArgs(ours) { |
| + t.Fatal("lifecycle payload refused") |
| + } |
| + for _, index := range []int{1, 2, 3, 4} { |
| + other := append([]string(nil), ours...) |
| + other[index] = "another-test" |
| + if lifecyclePayloadArgs(other) { |
| + t.Fatalf("another payload intercepted at argument %d", index) |
| + } |
| + } |
| + if lifecyclePayloadArgs(ours[:4]) { |
| + t.Fatal("incomplete payload intercepted") |
| + } |
| +} |
| + |
| +func TestRootRebootPlan14LifecycleRegressionPhase1(t *testing.T) { |
| + cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^(TestPlan14LifecyclePayloadDispatch|TestPlan14RootRuntimeInstalledFailure|TestPlan14BoundNetworkEdgeRootRunner|TestPlan14ConsoleLibraryRootRunner|TestPlan14RealConsoleRootRunner|TestPlan14RealWoodstarRootRunner)$", "-test.v", "-test.timeout=6m") |
| + out, err := cmd.CombinedOutput() |
| + t.Log(string(out)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + TestRootRebootPlan14ReviewBatchPhase1(t) |
| +} |
| + |
| +func TestRootRebootPlan14LifecycleRegressionPhase2(t *testing.T) { |
| + TestRootRebootPlan14ReviewBatchPhase2(t) |
| +} |
| + |
| +func lifecycleCheck(t *testing.T, step string, err error) { |
| + t.Helper() |
| + if err != nil { |
| + t.Fatalf("%s: %v", step, err) |
| + } |
| +} |
| + |
| +// Only the external box enrollment and object service are supplied by this |
| +// harness. Local authority, execution, settings, files and launch wiring use |
| +// the same constructors as instance-control and the edge. |
| +type lifecycleOwners struct{ t *testing.T } |
| + |
| +func (p lifecycleOwners) OpenClaimOwners(ctx context.Context, store release.Store, r ClaimRuntimeResources) (ClaimOwnerBindings, error) { |
| + key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| + identity := handoverRecoveryIdentity(p.t, hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Revision: 1, Region: "ie", ControlBucket: "deployment"}) |
| + actor, err := identity.Verify(ctx) |
| + if err != nil { |
| + return ClaimOwnerBindings{}, err |
| + } |
| + directory := hostactors.Directory{Store: store, PublicKey: key.Public().(ed25519.PublicKey), Bucket: actor.ControlBucket} |
| + data, err := lease.NewVersionedS3(ctx, map[string]string{actor.Bucket: actor.Cloud.Region}) |
| + if err != nil { |
| + return ClaimOwnerBindings{}, err |
| + } |
| + markers, err := newProductionClaimMarkers(ctx, actor.Bucket, actor.Cloud.Region) |
| + if err != nil { |
| + return ClaimOwnerBindings{}, err |
| + } |
| + local := &productionClaimLocal{Resources: r, Identity: identity, Directory: directory, Store: store, slots: map[string]*claimParticipation{}} |
| + backend := claimExecBackend(r, productionInstanceKeyReaders()) |
| + tokens := claimAdminTokens{Guard: hostguard.Guard{DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}}, Claims: store, Verify: identity.Verify, Local: identity.Local(local), Policy: hostbox.Policy{Held: hostbox.RegistryHoldPolicy(r.Registry)}.Check, Now: time.Now} |
| + input := claimUnitInputReader(rootClaimUnitInputs) |
| + if r.Mode == "agent" { |
| + input = NewClaimRoleRootClient(controlsock.Path).UnitInputs |
| + } |
| + regional := productionClaimRegional{Identity: identity, Local: local} |
| + out := ClaimOwnerBindings{Identity: identity, RetirementAuthority: &hostops.ProductionAuthority{Store: store, Directory: directory, Now: time.Now}, EvidenceKey: ed25519.NewKeyFromSeed(bytes.Repeat([]byte{2}, ed25519.SeedSize)), Controllers: productionStopControllers{}, Data: data, Local: local, |
| + Serving: ClaimServingSources{Launch: claimServingLaunchSource(backend, tokens, input), Markers: markers}, |
| + InputPolicy: claimInputPolicy(r.Registry.Get, store, key.Public().(ed25519.PublicKey), backend, input), MovesPolicy: MovesClaimPolicy(MovesRecordsFrom(store)), |
| + Evidence: regional, LeaseSet: regional.LeaseSet, View: regional.View, Authorize: regional.Authorize, Close: local.Close} |
| + source := &movesFilesSource{DataDir: r.DataDir, Routes: r.Registry, newFiles: newProductionFilesStore} |
| + _, err = openClaimFilesManifest(ctx, os.Getenv, source, store, data, actor, &out, claimBoundIdentity(identity.Verify, actor)) |
| + if r.Mode != "instance-control" { |
| + out.EvidenceKey = nil |
| + } |
| + bindClaimLegacyOwners(&out) |
| + return out, err |
| +} |
| + |
| +func lifecycleEnvironment(t *testing.T, endpoint string) { |
| + t.Helper() |
| + for k, v := range map[string]string{"CARLOS_HOST_CLAIM": "1", "CARLOS_HOST_CLAIM_ORIGINS": "1", "CARLOS_MOVES_PHASE1": "", "CARLOS_REGION_HA": "", "CARLOS_DATA_DIR": "/data", "CARLOS_OPT_DIR": "/opt/carlos", "CARLOS_DEPLOYMENT_BUCKET": "", "CARLOS_DEPLOYMENT_DIR": lifecycleHome + "/deployment", "CARLOS_REGION": "ie", "AWS_ACCESS_KEY_ID": "test-key", "AWS_SECRET_ACCESS_KEY": "test-secret", "AWS_SESSION_TOKEN": "", "AWS_EC2_METADATA_DISABLED": "true", "AWS_CONFIG_FILE": "/nonexistent", "AWS_SHARED_CREDENTIALS_FILE": "/nonexistent", "CARLOS_S3_ENDPOINT": endpoint, "CARLOS_RELEASE_PUBKEY": base64.StdEncoding.EncodeToString(ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)).Public().(ed25519.PublicKey))} { |
| + t.Setenv(k, v) |
| + } |
| +} |
| + |
| +func TestPlan14LifecycleColdWakeRootRunner(t *testing.T) { |
| + if os.Getenv("PLAN14_LIFECYCLE_AGENT") == "1" { |
| + lifecycleAgent(t) |
| + return |
| + } |
| + uid, gid := generationRunnerRegistryUser(t) |
| + for _, path := range []string{lifecycleHome, "/data", "/opt/carlos", "/run/carlos", "/run/carlos-control", "/etc/carlos", "/var/lib/carlos-claims", "/var/lib/carlos-release"} { |
| + lifecycleCheck(t, "exclusive fixture "+path, os.Mkdir(path, 0755)) |
| + t.Cleanup(func() { lifecycleCheck(t, "cleanup "+path, os.RemoveAll(path)) }) |
| + } |
| + lifecycleCheck(t, "private authority", os.Chmod("/var/lib/carlos-claims", 0700)) |
| + lifecycleCheck(t, "input service owner", os.Chown(lifecycleHome, uid, gid)) |
| + lifecycleCheck(t, "runtime service owner", os.Chown("/run/carlos", uid, gid)) |
| + lifecycleCheck(t, "data owner", os.Chown("/data", uid, gid)) |
| + for _, path := range []string{lifecycleHome + "/deployment", "/data/hostguard", "/data/.parked", "/data/layout", "/data/instances", "/data/.instances", "/run/carlos/.instances", "/run/carlos/.logs", "/data/.hostexec", "/run/carlos/instances", "/run/carlos/logs", "/run/carlos/hostguard", "/run/carlos/hostexec"} { |
| + lifecycleCheck(t, "directory", os.MkdirAll(path, 0755)) |
| + lifecycleCheck(t, "directory owner", os.Chown(path, uid, gid)) |
| + } |
| + for _, path := range []string{"/data/.instances", "/data/instances", "/data/layout", "/run/carlos/.logs", "/run/carlos/logs"} { |
| + lifecycleCheck(t, "private tree", os.Chmod(path, 0700)) |
| + } |
| + model := leasetest.NewVersionedBucket() |
| + model.AddBucket("data") |
| + handler, _ := leasetest.NewS3Server(model) |
| + server := httptest.NewServer(handler) |
| + t.Cleanup(server.Close) |
| + lifecycleEnvironment(t, server.URL) |
| + store := release.NewDirStore(lifecycleHome + "/deployment") |
| + key := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) |
| + identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Revision: 1, Region: "ie", ControlBucket: "deployment"}) |
| + actor, err := identity.Verify(t.Context()) |
| + lifecycleCheck(t, "identity", err) |
| + roster := hostactors.Roster{V: 1, DeploymentBucket: actor.ControlBucket, Revision: 1, RequestLimitNS: int64(hostbox.RequestLimit), Boxes: map[string]hostactors.Box{actor.Box: {BoxID: actor.Box, Enrollment: actor.Enrollment, Namespace: actor.Namespace, Label: actor.Box, Region: actor.Region, Pool: "default", Cloud: hostactors.Cloud{Provider: actor.Cloud.Provider, Account: actor.Cloud.Account, Region: actor.Cloud.Region, InstanceID: actor.Cloud.InstanceID}, EvidenceSHA256: strings.Repeat("c", 64), CensusPublicKey: ed25519.NewKeyFromSeed(bytes.Repeat([]byte{2}, ed25519.SeedSize)).Public().(ed25519.PublicKey), State: "active"}}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} |
| + raw, err := release.SignRootEnvelope(release.RootSigner(key), roster) |
| + lifecycleCheck(t, "sign roster", err) |
| + _, err = store.Put(t.Context(), hostactors.Key, raw, "") |
| + lifecycleCheck(t, "roster", err) |
| + filesSeedConstructionMigration(t, "/opt/carlos", actor.ControlBucket, actor.Box, actor.Enrollment) |
| + state, err := rootstate.Open(rootparticipation.Namespace, true, false) |
| + lifecycleCheck(t, "census state", err) |
| + census := rootparticipation.Census{Schema: 1, Box: actor.Box, Enrollment: actor.Enrollment, Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true} |
| + raw, err = json.Marshal(census) |
| + lifecycleCheck(t, "census encode", err) |
| + lifecycleCheck(t, "census", state.Write("census.json", raw)) |
| + state.Close() |
| + failures, err := rootstate.Open(rollout.RootFailuresDir, true, false) |
| + lifecycleCheck(t, "failure state", err) |
| + _, err = rollout.InitializeRootFailures(failures, rootUpdatesMigration(census), nil) |
| + lifecycleCheck(t, "initialize migration failures", err) |
| + failures.Close() |
| + pins, err := rootstate.Open(release.RootGenerationsDir, true, false) |
| + lifecycleCheck(t, "generation store", err) |
| + pins.Close() |
| + binary, err := os.Executable() |
| + lifecycleCheck(t, "test binary", err) |
| + image, err := os.ReadFile(binary) |
| + lifecycleCheck(t, "test image", err) |
| + lifecycleCheck(t, "executor", os.WriteFile("/opt/carlos/carlos", image, 0755)) |
| + lifecycleCheck(t, "guard executor", os.WriteFile("/opt/carlos/system/guard/carlos", image, 0755)) |
| + sum := sha256.Sum256(image) |
| + platform := runtime.GOOS + "-" + runtime.GOARCH |
| + pointer := release.SystemPointer{Schema: 1, Component: "router", Channel: "stable", Spread: "0s", PromotedAt: time.Now(), Current: release.SystemRelease{Version: "v2.0.0", ReleaseTag: "v2.0.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{platform: {Key: release.SystemArtifactKey("v2.0.0", platform), SHA256: hex.EncodeToString(sum[:])}}}} |
| + signed, err := release.SignRootEnvelope(release.RootSigner(key), pointer) |
| + lifecycleCheck(t, "sign executor", err) |
| + identities, err := rootstate.Open("/opt/carlos/system/guard/identities", false, true) |
| + lifecycleCheck(t, "executor identities", err) |
| + lifecycleCheck(t, "executor identity", identities.Write("v2.0.0.json", signed)) |
| + identities.Close() |
| + // Create uses the lifecycle transition functions and ordinary record writer; |
| + // it never inserts a fence, launch, settings or installation record. |
| + now := time.Now().UTC() |
| + rec := provision.Record{Schema: 1, Host: lifecycleHost, Account: "bab", App: "lifecycle", Channel: "stable", Region: "ie", CreatedAt: now} |
| + raw, err = json.Marshal(rec) |
| + lifecycleCheck(t, "record input", err) |
| + recordKey := provision.RecordKey(rec.Account, rec.App, rec.Host) |
| + before, err := hostclaim.Read(t.Context(), store, rec.Host) |
| + lifecycleCheck(t, "absent claim", err) |
| + created, err := hostclaim.Update(t.Context(), store, before, hostclaim.BeginCreate(hostclaim.NewHolder{Host: rec.Host, Account: rec.Account, App: rec.App, Family: "instance", Key: recordKey, Region: "ie", RecordRegion: "ie", CreatedAt: now}, raw, "create", "runner", now)) |
| + lifecycleCheck(t, "begin create", err) |
| + _, _, err = provision.WriteRecord(provision.WithRecordSequence(t.Context()), store, recordKey, []byte(`{}`), raw, "") |
| + lifecycleCheck(t, "create record", err) |
| + _, err = hostclaim.Update(t.Context(), store, created, hostclaim.CompleteCreate("create", actor.Box)) |
| + lifecycleCheck(t, "complete create", err) |
| + manifest := release.Manifest{App: rec.App, Version: "v1", Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}} |
| + lifecycleCheck(t, "ship", release.Ship(t.Context(), store, rec.Account, manifest, map[string][]byte{release.AdoptPlatform(): image})) |
| + lifecycleCheck(t, "promote", release.Promote(t.Context(), store, key, rec.Account, rec.App, "stable", "v1")) |
| + // Only deployment inputs are service-owned. Root authority remains private. |
| + lifecycleCheck(t, "deployment owner", filepath.Walk(lifecycleHome+"/deployment", func(path string, info os.FileInfo, err error) error { |
| + if err != nil { |
| + return err |
| + } |
| + return os.Chown(path, uid, gid) |
| + })) |
| + reg, err := openServiceRegistry("/data/registry.db") |
| + lifecycleCheck(t, "registry", err) |
| + t.Cleanup(func() { reg.Close() }) |
| + resources := ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: "/data", OptDir: "/opt/carlos", UnitsDir: "/etc/systemd/system"} |
| + ctx := context.WithValue(t.Context(), claimEntryKey{}, claimEntry{Enabled: true, FlagRead: true, Store: store, Resources: resources}) |
| + assembly, err := OpenProductionClaimAssembly(ctx, store, resources, lifecycleOwners{t}) |
| + lifecycleCheck(t, "root assembly", err) |
| + t.Cleanup(func() { assembly.Close(context.Background()) }) |
| + ctx = WithClaimAssembly(ctx, assembly) |
| + lifecycleCheck(t, "launch verifier", registerProductionLaunchVerifier()) |
| + root, err := NewProductionRootClaimStop(ctx) |
| + lifecycleCheck(t, "root constructor", err) |
| + wrapped, err := WithProductionClaimLaunch(ctx, root) |
| + lifecycleCheck(t, "launch constructor", err) |
| + rootHTTP := wrapped.(ClaimRootWithLaunch) |
| + t.Cleanup(func() { rootHTTP.Root.executionRuntime.Close(context.Background()) }) |
| + listener, err := net.Listen("unix", controlsock.Path) |
| + lifecycleCheck(t, "root socket", err) |
| + lifecycleCheck(t, "root socket mode", os.Chmod(controlsock.Path, 0660)) |
| + lifecycleCheck(t, "root socket group", os.Chown(controlsock.Path, 0, gid)) |
| + controlCtx, cancelControl := context.WithCancel(ctx) |
| + defer cancelControl() |
| + control := &instancectl.Server{ClaimStop: wrapped, PeerOK: func(c net.Conn) error { _, err := rootHTTP.Root.Stop.Authenticate(ctx, c); return err }, Logf: t.Logf} |
| + done := make(chan error, 1) |
| + go func() { done <- control.Serve(controlCtx, listener) }() |
| + defer func() { cancelControl(); lifecycleCheck(t, "control shutdown", <-done) }() |
| + lifecycleCheck(t, "root recovery", rootHTTP.RecoverPending(ctx)) |
| + rootHTTP.Start(ctx) |
| + lifecycleInstallUnits(t) |
| + lifecycleStartAgent(t, server.URL, func() { |
| + _, _, err = release.StageSignedHead(ctx, store, key.Public().(ed25519.PublicKey), nil, "/opt/carlos", registry.Route{Host: rec.Host, Account: rec.Account, App: rec.App, Channel: "stable"}) |
| + lifecycleCheck(t, "signed stage", err) |
| + lifecycleCheck(t, "point initial version", release.PointLive("/opt/carlos", rec.Host, rec.Account, "v1", rec.App)) |
| + }) |
| +} |
| + |
| +func lifecycleStartAgent(t *testing.T, endpoint string, stage func()) { |
| + t.Helper() |
| + unit, err := unitsFS.ReadFile("units/carlos-edge.service") |
| + lifecycleCheck(t, "edge template", err) |
| + var lines []string |
| + for _, line := range strings.Split(string(unit), "\n") { |
| + key, _, _ := strings.Cut(line, "=") |
| + switch key { |
| + case "ExecStart", "ExecReload", "EnvironmentFile", "Restart": |
| + continue |
| + } |
| + lines = append(lines, line) |
| + } |
| + lines = append(lines, "[Service]", "Restart=no", "ExecStart=/opt/carlos/carlos -test.run=^TestPlan14LifecycleColdWakeRootRunner$ -test.v -test.timeout=4m", "Environment=PLAN14_LIFECYCLE_AGENT=1", "Environment=CARLOS_S3_ENDPOINT="+endpoint, "Environment=TMPDIR=/run/carlos", "ReadWritePaths="+lifecycleHome) |
| + path := "/run/systemd/system/carlos-edge.service" |
| + f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| + lifecycleCheck(t, "edge exclusive", err) |
| + _, err = f.WriteString(strings.Join(lines, "\n")) |
| + f.Close() |
| + lifecycleCheck(t, "edge write", err) |
| + t.Cleanup(func() { |
| + exec.Command("systemctl", "stop", "carlos-edge.service").Run() |
| + os.Remove(path) |
| + exec.Command("systemctl", "daemon-reload").Run() |
| + }) |
| + out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput() |
| + lifecycleCheck(t, string(out), err) |
| + out, err = exec.Command("systemctl", "start", "carlos-edge.service").CombinedOutput() |
| + lifecycleCheck(t, string(out), err) |
| + deadline := time.Now().Add(3 * time.Minute) |
| + staged := false |
| + for time.Now().Before(deadline) { |
| + if !staged { |
| + if _, err := os.Stat("/run/carlos/lifecycle-stage"); err == nil { |
| + stage() |
| + lifecycleCheck(t, "stage ack", os.WriteFile("/run/carlos/lifecycle-staged", []byte("ready"), 0644)) |
| + staged = true |
| + } |
| + } |
| + raw, err := os.ReadFile("/run/carlos/lifecycle-result") |
| + if err == nil { |
| + if string(raw) != "pass" { |
| + log, _ := exec.Command("journalctl", "-u", "carlos-edge.service", "--no-pager", "-n", "100").CombinedOutput() |
| + t.Fatalf("%s: %s", raw, log) |
| + } |
| + return |
| + } |
| + status, _ := exec.Command("systemctl", "show", "carlos-edge.service", "-p", "ActiveState", "--value").Output() |
| + if strings.TrimSpace(string(status)) == "failed" { |
| + break |
| + } |
| + time.Sleep(100 * time.Millisecond) |
| + } |
| + log, _ := exec.Command("journalctl", "-u", "carlos-edge.service", "--no-pager", "-n", "100").CombinedOutput() |
| + t.Fatalf("agent did not complete: %s", log) |
| +} |
| + |
| +func lifecycleAgent(t *testing.T) { |
| + lifecycleEnvironment(t, os.Getenv("CARLOS_S3_ENDPOINT")) |
| + defer func() { |
| + result := "pass" |
| + if t.Failed() { |
| + result = "agent failed; inspect journal" |
| + } |
| + os.WriteFile("/run/carlos/lifecycle-result", []byte(result), 0600) |
| + }() |
| + store := release.NewDirStore(lifecycleHome + "/deployment") |
| + reg, err := registry.Open("/data/registry.db") |
| + lifecycleCheck(t, "agent registry", err) |
| + defer reg.Close() |
| + manager := cgroup.SetupRetaining(t.Logf) |
| + resources := ClaimRuntimeResources{Mode: "agent", Registry: reg, Manager: manager, DataDir: "/data", OptDir: "/opt/carlos", UnitsDir: "/etc/systemd/system"} |
| + ctx := context.WithValue(t.Context(), claimEntryKey{}, claimEntry{Enabled: true, FlagRead: true, Store: store, Resources: resources}) |
| + assembly, err := OpenProductionClaimAssembly(ctx, store, resources, lifecycleOwners{t}) |
| + lifecycleCheck(t, "agent assembly", err) |
| + defer assembly.Close(context.Background()) |
| + providers, err := assembly.Build(ctx) |
| + lifecycleCheck(t, "agent build", err) |
| + cfg := provision.ReconcileConfig{Region: "ie", DataDir: "/data", Layout: func(context.Context, release.Store) (string, error) { return release.LayoutModeNested, nil }} |
| + providers.Provision(&cfg) |
| + lifecycleCheck(t, "provision", provision.Reconcile(ctx, store, reg, cfg)) |
| + route, err := reg.Get(lifecycleHost) |
| + lifecycleCheck(t, "route", err) |
| + if route == nil { |
| + t.Fatal("create did not install route") |
| + } |
| + lifecycleCheck(t, "request staging", os.WriteFile("/run/carlos/lifecycle-stage", []byte("ready"), 0600)) |
| + stageDeadline := time.Now().Add(time.Minute) |
| + for { |
| + if _, err := os.Stat("/run/carlos/lifecycle-staged"); err == nil { |
| + break |
| + } |
| + if time.Now().After(stageDeadline) { |
| + t.Fatal("stage timeout") |
| + } |
| + time.Sleep(20 * time.Millisecond) |
| + } |
| + for wake := 0; wake < 2; wake++ { |
| + lifecycleWake(t, ctx, providers.Wake, *route) |
| + t.Logf("completed cold wake %d", wake+1) |
| + } |
| +} |
| + |
| +func lifecycleWake(t *testing.T, ctx context.Context, runtime *activator.ClaimRuntime, route registry.Route) { |
| + t.Helper() |
| + observed, err := runtime.Read(ctx, lifecycleHost) |
| + lifecycleCheck(t, "read for wake", err) |
| + policy := activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second} |
| + lifecycleCheck(t, "wake prerequisites", runtime.Prerequisites(ctx, route, observed, policy)) |
| + admitted, err := runtime.Admit(ctx, route, observed, policy) |
| + lifecycleCheck(t, "cold admission", err) |
| + start, err := runtime.Prepare(ctx, route, admitted, policy) |
| + lifecycleCheck(t, "wake prepare", err) |
| + lifecycleCheck(t, "restore", start.Restore(ctx)) |
| + lifecycleCheck(t, "reserve", start.Reserve(ctx, 0)) |
| + lifecycleCheck(t, "materialize", start.Materialize(ctx)) |
| + launch, err := start.PrepareLaunch(ctx) |
| + lifecycleCheck(t, "launch preparation", err) |
| + cmd, _, settle, _, err := launch.Start(ctx, "main", start.CurrentAttempt(), start.Build, func() error { return ctx.Err() }) |
| + lifecycleCheck(t, "root launch", err) |
| + defer settle(context.Background()) |
| + lifecycleCheck(t, "tenant ready", start.Ready(ctx, cmd)) |
| + prefix, err := hostbox.GenerationPrefix(*admitted.Claim.Holder) |
| + lifecycleCheck(t, "generation prefix", err) |
| + rep, err := start.Replicate(ctx, prefix) |
| + lifecycleCheck(t, "replicate", err) |
| + defer rep.Close(context.Background()) |
| + current, err := runtime.Read(ctx, lifecycleHost) |
| + lifecycleCheck(t, "read publication", err) |
| + _, err = start.Publish(ctx, current) |
| + lifecycleCheck(t, "publish", err) |
| + lifecycleCheck(t, "healthy", start.Healthy(ctx, 0, cmd)) |
| + transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| + return (&net.Dialer{}).DialContext(ctx, "unix", route.Socket) |
| + }} |
| + defer transport.CloseIdleConnections() |
| + response, err := (&http.Client{Transport: transport, Timeout: time.Second}).Get("http://tenant/") |
| + lifecycleCheck(t, "serve", err) |
| + raw, err := io.ReadAll(response.Body) |
| + response.Body.Close() |
| + lifecycleCheck(t, "read tenant", err) |
| + if response.StatusCode != 200 || string(raw) != "lifecycle tenant" { |
| + t.Fatalf("tenant response %d %q", response.StatusCode, raw) |
| + } |
| + lifecycleCheck(t, "stop tenant", settle(ctx)) |
| + lifecycleCheck(t, "stop replication", rep.Close(ctx)) |
| + lifecycleCheck(t, "close admission", runtime.FailedAdmission(ctx, admitted, hostclaim.ErrWrongState)) |
| +} |
| + |
| +func lifecycleInstallUnits(t *testing.T) { |
| + t.Helper() |
| + for _, name := range []string{"carlos.slice", "carlos-instances.slice", "data-instances.mount", "run-carlos-instances.mount", "run-carlos-logs.mount", "carlos-instance@.service"} { |
| + raw, err := unitsFS.ReadFile("units/" + name) |
| + lifecycleCheck(t, "unit template", err) |
| + path := filepath.Join("/run/systemd/system", name) |
| + f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| + lifecycleCheck(t, "exclusive unit", err) |
| + _, err = f.Write(raw) |
| + f.Close() |
| + lifecycleCheck(t, "install unit", err) |
| + t.Cleanup(func() { |
| + exec.Command("systemctl", "stop", name).Run() |
| + os.Remove(path) |
| + exec.Command("systemctl", "daemon-reload").Run() |
| + }) |
| + } |
| + t.Cleanup(func() { |
| + exec.Command("systemctl", "stop", instancectl.UnitName(instancectl.RoleRun, lifecycleHost)).Run() |
| + exec.Command("systemctl", "stop", instancectl.SliceName(lifecycleHost)).Run() |
| + for _, name := range []string{instancectl.UnitName(instancectl.RoleRun, lifecycleHost), instancectl.SliceName(lifecycleHost)} { |
| + os.RemoveAll(filepath.Join("/run/systemd/system", name+".d")) |
| + os.RemoveAll(filepath.Join("/run/systemd/system.control", name+".d")) |
| + } |
| + }) |
| +} |
diff --git a/internal/release/programhistory_test.go b/internal/release/programhistory_test.go| index 41fb1dc2f..d82f73802 100644 |
| --- a/internal/release/programhistory_test.go |
| +++ b/internal/release/programhistory_test.go |
| @@ -46,8 +46,8 @@ func TestBoxRefusesWrongProgramWithNoManifestField(t *testing.T) { |
| if v, _ := LiveVersion(opt, host); v != "old" { |
| t.Fatalf("live = %q, want old", v) |
| } |
| - if !strings.Contains(out.String(), "example.com/carlos") || !strings.Contains(out.String(), "not pointed live") || |
| - !strings.Contains(out.String(), "ship a new version with carlos ship --program-change and promote it, if the change is intended") { |
| + want := " console.example.com: new is example.com/carlos, but this route has only run example.com/console. not pointed live (ship a new version with carlos ship --program-change and promote it, if the change is intended)\n" |
| + if !strings.Contains(out.String(), want) { |
| t.Fatalf("printed %q, want the program refusal and its way out", out.String()) |
| } |
| r, _ := ReadRefusal(bake, host) |