package host import ( "context" "errors" "fmt" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/rootstate" "net/http" "os" "path/filepath" "strings" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/controlsock" "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/provision" "github.com/carlosframework/platform/internal/release" ) type ClaimDependencyError struct{ Contracts []string } func (e *ClaimDependencyError) Error() string { return "host claims: missing provider: " + strings.Join(e.Contracts, ", ") } func (e *ClaimDependencyError) Unwrap() error { return hostwrite.ErrUnavailable } type claimEntryKey struct{} type claimEntry struct { Resources ClaimRuntimeResources Enabled, FlagRead bool Store release.Store Assembly *ClaimAssembly } func claimStore(ctx context.Context) release.Store { v, _ := ctx.Value(claimEntryKey{}).(claimEntry) return v.Store } func claimEntryContext(ctx context.Context, getenv func(string) string, open func(context.Context) (release.Store, error)) (context.Context, error) { entry, _ := ctx.Value(claimEntryKey{}).(claimEntry) if !entry.FlagRead { entry.Enabled = hostwrite.Enabled(getenv) entry.FlagRead = true } ctx = context.WithValue(ctx, claimEntryKey{}, entry) if !entry.Enabled || entry.Store != nil { return ctx, nil } if open == nil { return ctx, hostwrite.ErrUnavailable } startup, cancel := context.WithTimeout(ctx, 2*time.Minute) defer cancel() store, err := open(startup) if err != nil { return ctx, err } if store == nil { return ctx, hostwrite.ErrUnavailable } entry.Store = store return context.WithValue(ctx, claimEntryKey{}, entry), nil } func claimEntryEnv(ctx context.Context) func(string) string { entry, _ := ctx.Value(claimEntryKey{}).(claimEntry) return func(key string) string { if key == "CARLOS_HOST_CLAIM" { if entry.FlagRead && entry.Enabled { return "1" } return "" } return os.Getenv(key) } } // WithClaimAssembly is used by the deployment composition after it constructs // the Plan 8/8b and moves adapters. It does not activate any host itself. func WithClaimAssembly(ctx context.Context, a *ClaimAssembly) context.Context { v, _ := ctx.Value(claimEntryKey{}).(claimEntry) v.Assembly = a return context.WithValue(ctx, claimEntryKey{}, v) } // ClaimAssembly owns the concrete wiring. Dependencies retain their contract // names so a missing implementation cannot silently become permissive policy. type ClaimAssembly struct { LossAdmission *hostbox.LossAdmission moveRead *claimMoveReadAccess wakeUnavailable error Journals *hostbox.JournalService cleanupEnrollment *claimCleanupEnrollment RetirementAuthority *hostops.ProductionAuthority Mode string Producers *ClaimProducerSet Launches *hostbox.LaunchJournal SharedPaths map[string][]string ManagedRoots map[string]string Reconcile *hostbox.ReconcileAdapter RootJournal hostbox.StopKernelJournalReader Controllers StopControllerReader Claims release.Store Guard hostguard.Guard Identity hostbox.IdentityProvider Local hostbox.LocalAuthority Policy hostbox.Policy Data lease.VersionedStore Wake *ClaimWakeAssembly Stop *ClaimStopCoordinator Evidence hostbox.RegionalEvidence LeaseSet hostbox.LeaseSet View func(context.Context, hostclaim.Observed) (park.View, error) Authorize func(context.Context, hostclaim.Observed, hostclaim.Obligation) error AdmissionEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error PublicationEvidence func(context.Context, hostclaim.Observed) error NewID func() string Provision func(*provision.ReconcileConfig) Close func(context.Context) error // Test seams for the local stop identity and the root transport. Nil uses // the verified identity and the instance-control socket. stopLocal func(context.Context) (claimStopLocal, error) collectStop func(context.Context, StopRequest, StopDrainer) (StopResult, error) // acquireReplicaLease is the relocation build's replica lease, a test // seam for its loss. Nil uses lease.Acquire. acquireReplicaLease func(ctx context.Context, bucket, prefix, region string) (replicaLease, error) } func (a ClaimAssembly) Build(ctx context.Context) (ClaimProviders, error) { if a.Reconcile != nil && a.Reconcile.Registry != nil { a.Policy.Held = hostbox.RegistryHoldPolicy(a.Reconcile.Registry) } if ctx == nil || a.RetirementAuthority == nil || a.Claims == nil || a.Guard.DataDir == "" || a.Data == nil || a.Local == nil || a.Policy.Held == nil || a.Wake == nil || a.Stop == nil || a.Evidence == nil || a.LeaseSet == nil || a.View == nil || a.Authorize == nil || a.NewID == nil || a.AdmissionEvidence == nil || a.PublicationEvidence == nil || a.Reconcile == nil || a.Close == nil { return ClaimProviders{}, hostwrite.ErrUnavailable } if a.Controllers == nil || a.Launches == nil || a.Reconcile.Registry == nil || a.Reconcile.Inventory == nil || !filepath.IsAbs(a.Reconcile.Retirement.JournalDir) { return ClaimProviders{}, hostwrite.ErrUnavailable } for _, name := range []string{"data", "opt", "units"} { if !filepath.IsAbs(a.ManagedRoots[name]) { return ClaimProviders{}, hostwrite.ErrUnavailable } } if filepath.Clean(a.ManagedRoots["data"]) != filepath.Clean(a.Guard.DataDir) { return ClaimProviders{}, hostbox.ErrChanged } // Build owns these values. Do not mutate a caller's shared adapter or accept // zero-value write configuration that would reopen the legacy path. reconcile := *a.Reconcile a.Reconcile = &reconcile actor, err := a.Identity.Verify(ctx) if err != nil { return ClaimProviders{}, err } a.Guard.Claims = hostguard.StoreReader{Store: a.Claims} // Continue is the moves C1 session table, nil unless both flags are on (D1). source := hostbox.WorkSource{FleetHomeRegion: actor.FleetHomeRegion, HomeRegion: actor.HomeRegion, Guard: a.Guard, Box: actor.Box, Region: park.PlacementRegion(actor.Region), Now: time.Now, Policy: a.Policy.Check, Local: a.Identity.Local(a.Local), Continue: a.Wake.Continue} source.Timer = func(ctx context.Context, work hostwrite.Work, timer registry.Timer, expected string, remove bool) (*registry.Timer, error) { return a.Reconcile.MutateTimer(ctx, work, timer, expected, remove) } a.Reconcile.Reconciler.Claims = a.Claims a.Reconcile.Reconciler.Guard = a.Guard a.Reconcile.Reconciler.Writes = hostwrite.Config{Enabled: true, Selector: source} a.Reconcile.Reconciler.Box = actor.Box a.Reconcile.Reconciler.Region = park.PlacementRegion(actor.Region) a.Reconcile.Reconciler.HomeRegion = actor.HomeRegion a.Reconcile.Reconciler.Now = time.Now a.Reconcile.Reconciler.Local = park.Local{Guard: a.Guard, Claims: a.Claims, DataDir: a.Guard.DataDir, Box: actor.Box} a.Reconcile.Box = actor.Box a.Reconcile.Enrollment = actor.Enrollment a.Reconcile.DirectoryRevision = actor.Binding census := &hostbox.EmptyInstallationCensus{Inventory: a.Reconcile.Inventory, Registry: a.Reconcile.Registry, Launches: a.Launches, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: actor.Binding, Roots: a.ManagedRoots, SharedPaths: a.SharedPaths} a.Reconcile.Census = census a.Reconcile.Reconciler.CheckLocal = census.CheckLocal a.Reconcile.Stop = a.collectRetirementStop a.Reconcile.RemoteRetire = func(ctx context.Context, target hostguard.FenceTarget) error { return RetireClaimOverSocket(ctx, controlsock.Path, target) } check := claimBoundIdentity(a.Identity.Verify, actor) data := hostbox.RequestStore{Store: a.Data, Check: check} writer := hostbox.Writer{HomeRegion: actor.HomeRegion, Loss: a.LossAdmission, Claims: a.Claims, Data: data, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Box: actor.Box, AdmissionEvidence: a.AdmissionEvidence, PublicationEvidence: a.PublicationEvidence, NewID: a.NewID} a.bindClaimWakeStop() if local, ok := a.Local.(*productionClaimLocal); ok { if err := local.bind(ctx, &a); err != nil { return ClaimProviders{}, err } a.Reconcile.Reconciler.Prepare = local.Prepare source.Prepare = local.Prepare a.Reconcile.Reconciler.Writes = hostwrite.Config{Enabled: true, Selector: source} } a.Provision = a.Reconcile.Configure if a.moveRead != nil { if err := a.moveRead.bind(ctx, source); err != nil { return ClaimProviders{}, err } } a.Wake.LeaseRegion = actor.Cloud.Region wake := refusingRootWake(actor.Box, a.Claims) if (a.Mode == "agent" || a.Mode == "") && a.wakeUnavailable == nil { wake, err = a.Wake.Build(ctx, source, writer) } if a.wakeUnavailable != nil && (a.Mode == "agent" || a.Mode == "") { wake = refusingClaimMoveWake(actor.Box, a.Claims, a.wakeUnavailable) } if err != nil { return ClaimProviders{}, err } if wake == nil || wake.Read == nil || wake.Prerequisites == nil || wake.Admit == nil || wake.Prepare == nil || wake.StopForeign == nil || wake.FailedAdmission == nil { return ClaimProviders{}, hostwrite.ErrUnavailable } if a.Mode == "agent" && a.wakeUnavailable == nil { a.Wake.seedFactory = func(call context.Context, op *filesSeedOperation, exclusion *activator.SeedExclusion) (*activator.ClaimRuntime, *ClaimWakeAssembly, error) { return newFilesSeedRuntime(call, a, source, writer, op, exclusion) } } // Where claimed wakes lease (writer.Bucket, a.Wake.LeaseRegion above): // the agent's build-state store reads and writes beside that lease. wake.Bucket, wake.LeaseRegion = actor.Bucket, actor.Cloud.Region enrollment := claimRegionalEnrollment{identity: a.Identity, actor: actor} authorize := func(ctx context.Context, o hostclaim.Observed, ob hostclaim.Obligation) error { if err := enrollment.VerifyRegional(ctx, actor.Region, actor.Bucket, hostbox.RequestLimit); err != nil { return err } return a.Authorize(ctx, o, ob) } leases := hostbox.LeaseSet(func(ctx context.Context, bucket, scope string) ([]string, error) { if err := check(ctx); err != nil { return nil, err } if bucket != actor.Bucket { return nil, hostbox.ErrChanged } return a.LeaseSet(ctx, bucket, scope) }) view := func(ctx context.Context, o hostclaim.Observed) (park.View, error) { if err := check(ctx); err != nil { return park.View{}, err } return a.View(ctx, o) } engine := park.Engine{HomeRegion: actor.HomeRegion, Store: data, Claims: a.Claims, Bucket: actor.Bucket, Region: park.PlacementRegion(actor.Region), Leases: hostbox.AggregateLeases{Leases: hostbox.RenewingLeases{Region: actor.Cloud.Region}, Resolve: leases}, Now: time.Now, NewID: a.NewID, View: view, Authorize: authorize} engine.VerifyMovesSweep = func(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (park.MoveSweepProof, error) { if err := check(ctx); err != nil { return park.MoveSweepProof{}, err } verifier, ok := a.Evidence.(park.MoveSweepVerifier) if !ok { return park.MoveSweepProof{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 newest retired move fence or release evidence"}} } return verifier.VerifyMovesSweep(ctx, o, versions) } regional := hostbox.RegionalWorker{Regional: hostbox.Regional{Engine: engine, Owner: func(ctx context.Context, _ hostclaim.Observed) error { return enrollment.VerifyRegional(ctx, actor.Region, actor.Bucket, hostbox.RequestLimit) }}, Enrollment: enrollment, Evidence: a.Evidence} regional, err = hostops.NewClaimRegionalWorker(regional.Regional, enrollment, *a.RetirementAuthority, a.Evidence) if err != nil { return ClaimProviders{}, err } if local, ok := a.Local.(*productionClaimLocal); ok { local.start(ctx) } if err := a.startWakeResetWorker(ctx); err != nil { return ClaimProviders{}, err } return ClaimProviders{guard: a.Guard, registerLanding: a.Reconcile.RegisterLanding, moveTargetFence: claimBoxFenceVerifier(check, a.Claims, a.RetirementAuthority), moveParks: &claimMoveParks{local: claimMoveLocalAuthorizer(check, a.Authorize), engine: regional.Regional.Engine, leases: leases, check: check}, Delivery: ClaimTimerDelivery(a.Producers, a.NewID), Selector: source, Wake: wake, Regional: regional, Stop: a.Stop, Provision: a.Provision, Close: a.Close}, nil } type claimRegionalEnrollment struct { identity hostbox.IdentityProvider actor hostbox.Actor } func (e claimRegionalEnrollment) VerifyRegional(ctx context.Context, region, bucket string, limit time.Duration) error { if limit != hostbox.RequestLimit { return hostbox.ErrUnsupported } a, err := e.identity.Verify(ctx) if err != nil { return err } if !hostbox.SameBinding(a, e.actor) || !park.SameRegion(a.HomeRegion, park.PlacementRegion(a.Region), region) || a.Bucket != bucket { return hostbox.ErrChanged } return nil } func productionClaimFactory(ctx context.Context) (ClaimProviders, error) { entry, _ := ctx.Value(claimEntryKey{}).(claimEntry) if entry.Store == nil { return ClaimProviders{}, fmt.Errorf("deployment client must precede claim assembly: %w", hostwrite.ErrUnavailable) } if entry.Assembly == nil { var err error ctx, err = ensureProductionClaimAssembly(ctx) if err != nil { return ClaimProviders{}, err } entry, _ = ctx.Value(claimEntryKey{}).(claimEntry) } if entry.Assembly.Claims != entry.Store { return ClaimProviders{}, errors.New("host claims: deployment store mismatch") } return entry.Assembly.Build(ctx) } func configuredClaimProvision(cfg provision.ReconcileConfig, configure []func(*provision.ReconcileConfig)) provision.ReconcileConfig { for _, f := range configure { if f != nil { f(&cfg) } } return cfg } func NewProductionRootClaimStop(ctx context.Context) (http.Handler, error) { entry, _ := ctx.Value(claimEntryKey{}).(claimEntry) if entry.Store == nil { var err error ctx, err = claimEntryContext(ctx, os.Getenv, openDeployStore) if err != nil { return nil, err } entry, _ = ctx.Value(claimEntryKey{}).(claimEntry) } if entry.Assembly == nil || entry.Assembly.Stop == nil || entry.Assembly.RootJournal == nil || entry.Assembly.Controllers == nil { return nil, &ClaimDependencyError{Contracts: []string{"Plan 8/8b verified controller, roster, floor and quarantine root bindings"}} } assembly := entry.Assembly if assembly.Claims != entry.Store || assembly.Guard.DataDir == "" || assembly.Reconcile == nil || assembly.Reconcile.Inventory == nil { return nil, hostwrite.ErrUnavailable } for _, name := range []string{"data", "opt", "units"} { if !filepath.IsAbs(assembly.ManagedRoots[name]) { return nil, hostwrite.ErrUnavailable } } actor, err := assembly.actorForRoot(ctx) if err != nil { return nil, err } if actor.Box == "" || actor.Enrollment == "" { return nil, hostbox.ErrChanged } handler, err := NewProductionClaimStopHandler(assembly.rootStopCoordinator(entry.Store), assembly.RootJournal, assembly.Controllers, assembly.ManagedRoots) if err != nil { return nil, err } stop, ok := handler.(ClaimStopHTTP) if !ok { return nil, ErrClaimStopUnavailable } root := assembly.claimRootHTTP(actor, stop) state, err := openClaimExecutionAuthority() if err != nil { return nil, err } if assembly.Wake == nil || assembly.Wake.Inputs == nil { state.Close() return nil, hostbox.ErrUnsupported } root, err = withServingParkRoot(root, assembly.Wake.Inputs) if err != nil { state.Close() return nil, err } runtime, err := newClaimExecutionRuntime(root, state) if err != nil { root.servingParks.Close(ctx) state.Close() return nil, err } runtime.parks = root.servingParks runtime.root = &root root.Execution, root.executionRuntime = runtime.broker, runtime return root, nil } // rootStopCoordinator is root's stop coordinator over the assembly and the // deployment store, before NewProductionClaimStopHandler binds its peer // authentication, kernel and identities. Its numeral bound is the cleanup // enrollment's fresh roster read. func (assembly *ClaimAssembly) rootStopCoordinator(store release.Store) ClaimStopCoordinator { coordinator := *assembly.Stop coordinator.Guard = assembly.Guard coordinator.Guard.Claims = hostguard.StoreReader{Store: store} coordinator.Claims = hostguard.StoreReader{Store: store} coordinator.Inventory = assembly.Reconcile.Inventory coordinator.Evidence = store // The numeral bound is root's own fresh verified roster read (fix // round 1, finding 3): the cleanup enrollment's, which still reads a // quarantined box's roster. Production instance-control always has one; // without it the bound stays nil and every stop refuses. if assembly.cleanupEnrollment != nil { coordinator.VerifiedRosterRevision = assembly.cleanupEnrollment.rosterRevision } return coordinator } // claimRootHTTP is instance-control's root handler over the assembly. Two // identities, kept apart (roster binding R2): actor is the historical // cleanup identity of root's startup read, which pins the census and // stamps what the cleanup service itself constructs, and keeps stops, // retirements and settlements of historical installations working while // the box is quarantined. InventoryActor is the fresh verified identity on // every call: every comparison that admits new work (an inventory write, // a serving handover, a recovery rebind, a reboot preparation) is against // the roster as it is now, so a quarantine refuses at once and a // readmission admits only the new binding, without a restart. func (assembly *ClaimAssembly) claimRootHTTP(actor hostbox.Actor, stop ClaimStopHTTP) ClaimRootHTTP { reconcile := *assembly.Reconcile reconcile.Box, reconcile.Enrollment = actor.Box, actor.Enrollment reconcile.Census = &hostbox.EmptyInstallationCensus{Inventory: assembly.Reconcile.Inventory, Registry: assembly.Reconcile.Registry, Launches: assembly.Launches, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: actor.Binding, Roots: assembly.ManagedRoots, SharedPaths: assembly.SharedPaths} return ClaimRootHTTP{inventoryIdentity: rootstate.ObjectIdentity, InventoryActor: assembly.Identity.Verify, startupBinding: actor.Binding, Journals: assembly.Journals, RequireNewWork: func(ctx context.Context) error { _, err := assembly.Identity.Verify(ctx); return err }, Launches: assembly.Launches, Stop: stop, Reconcile: reconcile, Roots: assembly.ManagedRoots} } func claimMoveLocalAuthorizer(check func(context.Context) error, owner func(context.Context, hostclaim.Observed, hostclaim.Obligation) error) ClaimLocalParkAuthorizer { return func(ctx context.Context, c *hostclaim.Claim, ob hostclaim.Obligation) error { if check == nil || owner == nil || c == nil || ob.Kind != "park-local" { return hostwrite.ErrUnavailable } if err := check(ctx); err != nil { return err } // Preserve the unwrapped owner used by the assembly's own park.Local. return owner(ctx, hostclaim.Observed{Host: c.Host, Claim: c}, ob) } }