//go:build linux package host import ( "bytes" "context" "crypto/ed25519" "log" "os" "path/filepath" "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/genselect" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/rootstate" ) type fix4Owners struct{ ClaimOwnerBindings } func (o fix4Owners) OpenClaimOwners(context.Context, release.Store, ClaimRuntimeResources) (ClaimOwnerBindings, error) { return o.ClaimOwnerBindings, nil } type fix4UnusedDependencies struct { hostbox.LocalAuthority StopControllerReader lease.VersionedStore ClaimWakeInputs hostbox.RegionalEvidence } func fix4JournalFixture(t *testing.T) (release.Store, ClaimRuntimeResources, fix4Owners, string) { t.Helper() root := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize)) evidence := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{2}, ed25519.SeedSize)) store := release.NewDirStore(t.TempDir()) identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: strings.Repeat("a", 32), Revision: 1, Binding: "1", Region: "ie", ControlBucket: "deployment"}) actor, err := identity.Verify(t.Context()) if err != nil { t.Fatal(err) } row := hostactors.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: evidence.Public().(ed25519.PublicKey), State: "active"} roster := hostactors.Roster{V: 1, DeploymentBucket: actor.ControlBucket, Revision: 1, RequestLimitNS: int64(hostbox.RequestLimit), Boxes: map[string]hostactors.Box{actor.Box: row}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} body, err := release.SignRootEnvelope(release.RootSigner(root), roster) if err != nil { t.Fatal(err) } if _, err = store.Put(t.Context(), hostactors.Key, body, ""); err != nil { t.Fatal(err) } directory := hostactors.Directory{Store: store, PublicKey: root.Public().(ed25519.PublicKey), Bucket: actor.ControlBucket} unused := fix4UnusedDependencies{} owners := fix4Owners{ClaimOwnerBindings{ Identity: identity, RetirementAuthority: &hostops.ProductionAuthority{Store: store, Directory: directory, Now: time.Now}, EvidenceKey: evidence, Local: unused, Controllers: unused, Data: unused, Inputs: unused, Evidence: unused, InputPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, MovesPolicy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, LeaseSet: func(context.Context, string, string) ([]string, error) { return nil, nil }, View: func(context.Context, hostclaim.Observed) (park.View, error) { return park.View{}, nil }, Authorize: func(context.Context, hostclaim.Observed, hostclaim.Obligation) error { return nil }, AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil }, PublicationEvidence: func(context.Context, hostclaim.Observed) error { return nil }, Close: func(context.Context) error { return nil }, }} reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) resources := ClaimRuntimeResources{Mode: "instance-control", Registry: reg, DataDir: t.TempDir(), OptDir: t.TempDir(), UnitsDir: t.TempDir()} legacy := filepath.Join(resources.DataDir, ".hostclaims") if err = os.Mkdir(legacy, 0700); err != nil { t.Fatal(err) } journalPath := filepath.Join(t.TempDir(), "journals.db") authorityDir := t.TempDir() originalAuthority := openClaimRootAuthority t.Cleanup(func() { openClaimRootAuthority = originalAuthority }) openClaimRootAuthority = func() (rootstate.Store, error) { return &rollbackDisk{dir: authorityDir}, nil } original := openClaimRootJournal t.Cleanup(func() { openClaimRootJournal = original }) openClaimRootJournal = func() (*hostbox.LaunchJournal, error) { return hostbox.OpenLaunchJournal(journalPath) } return store, resources, owners, legacy } func TestPlan9Fix4RootAssemblyIgnoresReplacedLegacyDirectory(t *testing.T) { fixtureWithoutRootMigrationCensus(t) for _, mode := range []string{"instance-control", "adopt"} { for _, replacement := range []string{"missing", "renamed", "symlink", "file"} { t.Run(mode+"/"+replacement, func(t *testing.T) { store, resources, owners, legacy := fix4JournalFixture(t) resources.Mode = mode if mode == "adopt" { owners.EvidenceKey = nil } journal, err := openClaimRootJournal() if err != nil { t.Fatal(err) } service, err := hostbox.NewJournalService(journal) if err != nil { t.Fatal(err) } if alert, err := service.CheckLegacyMigration(t.Context(), legacy); alert != nil || err != nil { t.Fatalf("marker: %v %v", alert, err) } if err = journal.Close(); err != nil { t.Fatal(err) } target := t.TempDir() if replacement == "renamed" { err = os.Rename(legacy, legacy+"-moved") } else { err = os.Remove(legacy) } if err != nil { t.Fatal(err) } switch replacement { case "symlink": err = os.Symlink(target, legacy) case "file": err = os.WriteFile(legacy, []byte("agent replacement"), 0640) } if err != nil { t.Fatal(err) } var logs bytes.Buffer previous := log.Writer() log.SetOutput(&logs) defer log.SetOutput(previous) assembly, err := OpenProductionClaimAssembly(t.Context(), store, resources, owners) if err != nil { t.Fatalf("agent-controlled legacy directory prevented root startup: %v", err) } if err = assembly.Close(t.Context()); err != nil { t.Fatal(err) } if !strings.Contains(logs.String(), "ignored after migration") { t.Fatalf("missing legacy directory alert: %q", logs.String()) } if replacement == "missing" || replacement == "renamed" { if _, err = os.Lstat(legacy); !os.IsNotExist(err) { t.Fatalf("root recreated agent directory: %v", err) } } if replacement == "symlink" { if got, err := os.Readlink(legacy); err != nil || got != target { t.Fatalf("root changed symlink: %q %v", got, err) } } if replacement == "file" { if got, err := os.ReadFile(legacy); err != nil || string(got) != "agent replacement" { t.Fatalf("root changed file: %q %v", got, err) } } }) } } } func TestPlan9Fix4MigrationRetryAfterCommitRecoversOwnedMask(t *testing.T) { for _, removeLegacy := range []bool{false, true} { t.Run(map[bool]string{false: "unchanged", true: "removed"}[removeLegacy], func(t *testing.T) { store, resources, owners, legacy := fix4JournalFixture(t) var calls []string plan9Fix2Systemctl(t, "enabled", &calls) owner := plan9Fix2MaskOwner(t) launch, err := hostbox.OpenLaunchJournal(filepath.Join(legacy, "launches.db")) if err != nil { t.Fatal(err) } if err = launch.Close(); err != nil { t.Fatal(err) } inventory, err := openLegacyInventory(t, filepath.Join(legacy, "inventory.db")) if err != nil { t.Fatal(err) } if err = inventory.Close(); err != nil { t.Fatal(err) } journal, err := openClaimRootJournal() if err != nil { t.Fatal(err) } service, err := hostbox.NewJournalService(journal) if err != nil { t.Fatal(err) } err = service.MigrateLegacy(t.Context(), legacy, "box", strings.Repeat("a", 32), func(ctx context.Context) (func(), error) { _, err := quiesceLegacyClaimJournals(ctx) return func() {}, err }, nil) if err != nil { t.Fatal(err) } if err = journal.Close(); err != nil { t.Fatal(err) } raw, err := os.ReadFile(owner) record, recorded := decodeClaimEdgeMaskRecord(raw, plan9Fix2Boot(t)) if err != nil || !recorded { t.Fatalf("commit lost crash mask identity: %q %v", raw, err) } if ours, identityErr := claimEdgeMaskStillOwned(record); identityErr != nil || !ours { t.Fatalf("commit changed crash mask identity: %v %v", ours, identityErr) } if removeLegacy { if err = os.RemoveAll(legacy); err != nil { t.Fatal(err) } } calls = nil plan9Fix2Systemctl(t, "masked-runtime", &calls) if err = migrateClaimJournals(t.Context(), store, resources, owners); err != nil { t.Fatal(err) } if !slices.Contains(calls, "unmask --runtime carlos-edge.service") { t.Fatalf("committed retry left edge masked: %v", calls) } if _, err = os.Stat(owner); !os.IsNotExist(err) { t.Fatalf("migration mask record remains: %v", err) } for _, call := range calls { if strings.HasPrefix(call, "mask ") { t.Fatalf("completed retry masked edge again: %v", calls) } } }) } } func TestPlan9Fix4CompletedMigrationKeepsOperatorMask(t *testing.T) { for _, test := range []struct{ name, state, record string }{ {"unrecorded-runtime", "masked-runtime", ""}, {"different-boot", "masked-runtime", "another-boot"}, {"persistent", "masked", "same-boot"}, {"unmasked", "enabled", "same-boot"}, } { t.Run(test.name, func(t *testing.T) { store, resources, owners, _ := fix4JournalFixture(t) owner := plan9Fix2MaskOwner(t) var calls []string plan9Fix2Systemctl(t, test.state, &calls) if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil { t.Fatal(err) } record := test.record if record == "same-boot" { record = plan9Fix2Boot(t) } if record != "" { if err := os.WriteFile(owner, []byte(record), 0600); err != nil { t.Fatal(err) } } calls = nil if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil { t.Fatal(err) } for _, call := range calls { if strings.HasPrefix(call, "unmask") || strings.HasPrefix(call, "mask") { t.Fatalf("operator state changed: %v", calls) } } }) } } func TestPlan9Fix4CompletedMigrationRetainsRecordOnUnmaskFailure(t *testing.T) { store, resources, owners, _ := fix4JournalFixture(t) owner := plan9Fix2MaskOwner(t) var calls []string plan9Fix2Systemctl(t, "masked-runtime", &calls) if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil { t.Fatal(err) } record := plan11Rev4WriteOwnedMask(t, owner) systemctl := claimSystemctl claimSystemctl = func(ctx context.Context, args ...string) ([]byte, error) { if args[0] == "unmask" { return nil, os.ErrPermission } return systemctl(ctx, args...) } if err := migrateClaimJournals(t.Context(), store, resources, owners); err == nil { t.Fatal("unmask failure reported success") } if raw, err := os.ReadFile(owner); err != nil { t.Fatalf("failed recovery discarded mask ownership: %q %v", raw, err) } else if got, ok := decodeClaimEdgeMaskRecord(raw, plan9Fix2Boot(t)); !ok || got != record { t.Fatalf("failed recovery changed mask ownership: %q", raw) } claimSystemctl = systemctl if err := migrateClaimJournals(t.Context(), store, resources, owners); err != nil { t.Fatal(err) } if _, err := os.Stat(owner); !os.IsNotExist(err) { t.Fatalf("successful recovery kept ownership record: %v", err) } }