package host import ( "context" "errors" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/genselect" "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/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" "path/filepath" "strings" "testing" "time" ) type parkRegistrationEvidence struct{} func (parkRegistrationEvidence) VerifyRetirement(context.Context, hostclaim.Observed, []lease.ObjectVersion) (string, time.Time, error) { return "retired", time.Unix(10, 0), nil } type parkRegistrationWake struct { parkRegistrationEvidence opens int failure error } func (p *parkRegistrationWake) Open(context.Context, release.Store, lease.VersionedStore, hostbox.Actor, ClaimMoveReadAccess) (ClaimMovesOwners, ClaimFilesManifest, error) { p.opens++ return ClaimMovesOwners{}, nil, p.failure } func (*parkRegistrationWake) View(context.Context, hostclaim.Observed) (park.View, error) { return park.View{}, nil } func (*parkRegistrationWake) LeaseSet(context.Context, string, string) ([]string, error) { return []string{"lease"}, nil } func (*parkRegistrationWake) Close(context.Context) error { return nil } func TestMoveParksRegistrationIndependentAndAtomic(t *testing.T) { claimMoveSources.Lock() oldSource, oldParks := claimMoveSources.Source, claimMoveSources.Parks claimMoveSources.Source = nil claimMoveSources.Parks = nil claimMoveSources.Unlock() t.Cleanup(func() { claimMoveSources.Lock() claimMoveSources.Source = oldSource claimMoveSources.Parks = oldParks claimMoveSources.Unlock() }) err := RegisterProductionClaimMoveParks(nil, nil) for _, name := range []string{"view", "regional evidence"} { if err == nil || !strings.Contains(err.Error(), name) { t.Fatalf("missing %s: %v", name, err) } } if claimMoveSources.Parks != nil { t.Fatal("partial registration") } source := &parkRegistrationWake{} if err := RegisterProductionClaimMoveParks(source.View, source); err != nil { t.Fatal(err) } if source.opens != 0 || claimMoveSources.Source != nil { t.Fatal("parks opened or registered wake") } if err := RegisterProductionClaimMoveParks(source.View, source); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("replacement: %v", err) } parks := claimMoveSources.Parks if err := RegisterProductionClaimMoveSources(source); err != nil { t.Fatal(err) } if claimMoveSources.Parks != parks || claimMoveSources.Source != source { t.Fatal("wake registration replaced parks") } if err := RegisterProductionClaimMoveSources(source); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatal("duplicate wake registration accepted") } } func TestMoveParksWakeFailureRemainsWakeRefusal(t *testing.T) { boom := errors.New("wake failed") source := &parkRegistrationWake{failure: boom} out := ClaimOwnerBindings{} openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, source, &out, func(context.Context) error { return nil }) if source.opens != 1 || !errors.Is(out.wakeUnavailable, boom) { t.Fatalf("wake failure lost: %v", out.wakeUnavailable) } w := refusingClaimMoveWake("box", nil, out.wakeUnavailable) if err := w.Prerequisites(t.Context(), registry.Route{}, hostclaim.Observed{}, activator.StartPolicy{}); !errors.Is(err, boom) { t.Fatalf("wake opened: %v", err) } openProductionClaimMoveWake(t.Context(), nil, hostbox.Actor{}, nil, &out, func(context.Context) error { return nil }) if out.wakeUnavailable == nil || !strings.Contains(out.wakeUnavailable.Error(), "C8") { t.Fatal("missing wake not named") } } func TestMoveParksVerifiedIdentityAndRealEngine(t *testing.T) { t.Parallel() identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", ControlBucket: "control", Revision: 1, Binding: "1"}) actor, err := identity.Verify(t.Context()) if err != nil { t.Fatal(err) } check := claimBoundIdentity(identity.Verify, actor) sentinel := errors.New("real view") calls := 0 engine := park.Engine{Bucket: actor.Bucket, Region: actor.Region, View: func(context.Context, hostclaim.Observed) (park.View, error) { calls++; return park.View{}, sentinel }} providers := ClaimProviders{moveParks: &claimMoveParks{local: func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error { return nil }, engine: engine, check: check, leases: func(context.Context, string, string) ([]string, error) { return []string{"real lease"}, nil }}} got, leases, _, err := providers.MoveParks(t.Context()) if err != nil { t.Fatal(err) } if _, err := got.View(t.Context(), hostclaim.Observed{}); !errors.Is(err, sentinel) || calls != 1 { t.Fatalf("engine replaced: %v", err) } if names, err := leases(t.Context(), actor.Bucket, "scope"); err != nil || len(names) != 1 || names[0] != "real lease" { t.Fatal("leases replaced") } mutations := map[string]func(*hostbox.Actor){"enrollment": func(a *hostbox.Actor) { a.Enrollment = "new" }, "instance": func(a *hostbox.Actor) { a.Cloud.InstanceID = "new" }, "control bucket": func(a *hostbox.Actor) { a.ControlBucket = "new" }, "data bucket": func(a *hostbox.Actor) { a.Bucket = "new" }, "namespace": func(a *hostbox.Actor) { a.Namespace = "new" }, "binding": func(a *hostbox.Actor) { a.Binding += "-changed" }, "account": func(a *hostbox.Actor) { a.Cloud.Account = "new" }} for name, mutate := range mutations { t.Run(name, func(t *testing.T) { changed := actor mutate(&changed) providers.moveParks.check = claimBoundIdentity(func(context.Context) (hostbox.Actor, error) { return changed, nil }, actor) if _, _, _, err := providers.MoveParks(t.Context()); !errors.Is(err, hostbox.ErrChanged) { t.Fatalf("identity change accepted: %v", err) } }) } // The roster write counter is not part of the bound identity: holds and // other boxes' rows move it without changing this box's binding. t.Run("revision", func(t *testing.T) { changed := actor changed.Revision++ providers.moveParks.check = claimBoundIdentity(func(context.Context) (hostbox.Actor, error) { return changed, nil }, actor) if _, _, _, err := providers.MoveParks(t.Context()); err != nil { t.Fatalf("roster revision bump refused: %v", err) } }) if _, _, _, err := (ClaimProviders{}).MoveParks(t.Context()); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatal("unbuilt accessor accepted") } } type moveParkNoData struct{ lease.VersionedStore } type moveParkNoLocal struct{ hostbox.LocalAuthority } func TestMoveParksBuildWithFailedWakeUsesRegionalEngine(t *testing.T) { ctx := t.Context() base := t.TempDir() reg, err := registry.Open(filepath.Join(base, "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() inventory, err := openLegacyInventory(t, filepath.Join(base, "inventory.db")) if err != nil { t.Fatal(err) } defer inventory.Close() store := release.NewDirStore(filepath.Join(base, "store")) identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box", Enrollment: "enrolled", Region: "region", ControlBucket: "control", Revision: 1, Binding: "1"}) actor, err := identity.Verify(ctx) if err != nil { t.Fatal(err) } boom := errors.New("configured wake failed") wakeSource := &parkRegistrationWake{failure: boom} owners := ClaimOwnerBindings{} openProductionClaimMoveWake(ctx, store, actor, wakeSource, &owners, claimBoundIdentity(identity.Verify, actor)) viewCalls := 0 a := ClaimAssembly{ wakeUnavailable: owners.wakeUnavailable, Claims: store, Guard: hostguard.Guard{DataDir: base}, Identity: identity, RetirementAuthority: &hostops.ProductionAuthority{Store: store, Now: time.Now}, Local: moveParkNoLocal{}, Data: moveParkNoData{}, Wake: &ClaimWakeAssembly{}, Stop: &ClaimStopCoordinator{}, Controllers: productionStopControllers{}, Launches: &hostbox.LaunchJournal{}, Reconcile: &hostbox.ReconcileAdapter{Registry: reg, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: filepath.Join(base, "retirement")}}, ManagedRoots: map[string]string{"data": base, "opt": filepath.Join(base, "opt"), "units": filepath.Join(base, "units")}, Evidence: parkRegistrationEvidence{}, LeaseSet: func(context.Context, string, string) ([]string, error) { return []string{"actual-inventory"}, nil }, View: func(context.Context, hostclaim.Observed) (park.View, error) { viewCalls++; 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 }, NewID: func() string { return "id" }, Close: func(context.Context) error { return nil }, } providers, err := a.Build(ctx) if err != nil { t.Fatal(err) } engine, leases, local, err := providers.MoveParks(ctx) if local == nil { t.Fatal("missing local authorizer") } if err := local(ctx, &hostclaim.Claim{Host: "app.example"}, hostclaim.Obligation{Kind: "park-local"}); err != nil { t.Fatal("local authorizer went through VerifyBound", err) } if err != nil { t.Fatal(err) } if err := providers.Wake.Prerequisites(ctx, registry.Route{}, hostclaim.Observed{}, activator.StartPolicy{}); !errors.Is(err, boom) { t.Fatalf("wake error lost: %v", err) } if _, err := engine.View(ctx, hostclaim.Observed{}); err != nil || viewCalls != 1 { t.Fatalf("wrong view: %v", err) } names, err := leases(ctx, actor.Bucket, "scope") if err != nil || len(names) != 1 || names[0] != "actual-inventory" { t.Fatalf("wrong inventory: %v %v", names, err) } // NewClaimRegionalWorker strengthens Authorize. The accessor must return // that final engine, not its earlier permissive construction value. if err := engine.Authorize(ctx, hostclaim.Observed{}, hostclaim.Obligation{}); err == nil { t.Fatal("accessor bypassed retirement evidence guard") } regional := providers.Regional.(hostbox.RegionalWorker) if engine.Store.(hostbox.RequestStore).Store != regional.Regional.Engine.Store.(hostbox.RequestStore).Store || engine.Claims != regional.Regional.Engine.Claims { t.Fatal("accessor reconstructed another engine") } if err := providers.Close(ctx); err != nil { t.Fatal(err) } }