carlos / platform Public

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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev ed1a3dbf138fe9c1d673c2fc992e9d19af9412a9 parent 2b52f33
2 files changed, +464 −2
  • internal/host/plan14_lifecycle_root_linux_test.go +462 −0
  • internal/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)