| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "log" |
| 7 | "sync" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/activator" |
| 10 | "github.com/carlosframework/platform/internal/hostbox" |
| 11 | "github.com/carlosframework/platform/internal/hostclaim" |
| 12 | "github.com/carlosframework/platform/internal/hostwrite" |
| 13 | "github.com/carlosframework/platform/internal/lease" |
| 14 | "github.com/carlosframework/platform/internal/park" |
| 15 | "github.com/carlosframework/platform/internal/registry" |
| 16 | "github.com/carlosframework/platform/internal/release" |
| 17 | ) |
| 18 | |
| 19 | type claimMoveParkSources struct { |
| 20 | View func(context.Context, hostclaim.Observed) (park.View, error) |
| 21 | Evidence hostbox.RegionalEvidence |
| 22 | } |
| 23 | |
| 24 | // RegisterProductionClaimMoveParks installs the regional contracts independently |
| 25 | // of the wake owners and C8 files manifest. Registration is atomic and one-shot. |
| 26 | func RegisterProductionClaimMoveParks(view func(context.Context, hostclaim.Observed) (park.View, error), retirementEvidence hostbox.RegionalEvidence) error { |
| 27 | var missing []string |
| 28 | if view == nil { |
| 29 | missing = append(missing, "moves Plan 5 exact-version generation and files-manifest view") |
| 30 | } |
| 31 | if retirementEvidence == nil { |
| 32 | missing = append(missing, "moves Plan 5 retired-move regional evidence") |
| 33 | } |
| 34 | if len(missing) != 0 { |
| 35 | return &ClaimDependencyError{Contracts: missing} |
| 36 | } |
| 37 | claimMoveSources.Lock() |
| 38 | defer claimMoveSources.Unlock() |
| 39 | if claimMoveSources.Parks != nil { |
| 40 | return hostwrite.ErrUnavailable |
| 41 | } |
| 42 | claimMoveSources.Parks = &claimMoveParkSources{View: view, Evidence: retirementEvidence} |
| 43 | return nil |
| 44 | } |
| 45 | |
| 46 | // claimMoveWakeLogf and claimMoveWakeLogOnce log a moves source that did |
| 47 | // not open, once per process (per agent start); variables so the tests can |
| 48 | // read and reset them. |
| 49 | var ( |
| 50 | claimMoveWakeLogf = log.Printf |
| 51 | claimMoveWakeLogOnce = new(sync.Once) |
| 52 | ) |
| 53 | |
| 54 | // openProductionClaimMoveWake opens the registered moves source over the |
| 55 | // claim's data bucket as the serving owners see it (moves Task 8 fix round |
| 56 | // 1): through hostbox.RequestStore, so every bucket request the moves |
| 57 | // owners make is bounded by the claim's request limit, runs under the |
| 58 | // operation's own context (its cancellation still reaches the request), and |
| 59 | // is preceded by check, the box's bound identity (claimBoundIdentity), so a |
| 60 | // refused identity issues no later bucket call. |
| 61 | func openProductionClaimMoveWake(ctx context.Context, store release.Store, actor hostbox.Actor, external ClaimProductionMoveSources, out *ClaimOwnerBindings, check func(context.Context) error) { |
| 62 | if external == nil { |
| 63 | out.wakeUnavailable = &ClaimDependencyError{Contracts: []string{"moves wake owners and C8 origin-qualified files manifest provider"}} |
| 64 | return |
| 65 | } |
| 66 | var data lease.VersionedStore |
| 67 | if out.Data != nil && check != nil { |
| 68 | data = hostbox.RequestStore{Store: out.Data, Check: check} |
| 69 | } |
| 70 | owners, files, err := external.Open(ctx, store, data, actor, out.moveRead) |
| 71 | if err != nil { |
| 72 | out.wakeUnavailable = errors.Join(&ClaimDependencyError{Contracts: []string{"moves wake owners and C8 origin-qualified files manifest provider"}}, err) |
| 73 | // Fail closed (platform-1e): every wake on this box is refused. Say |
| 74 | // why once, so an operator can see it (for example, an unusable |
| 75 | // CARLOS_SECRETS_KEY with CARLOS_MOVES_PHASE1 on). |
| 76 | claimMoveWakeLogOnce.Do(func() { |
| 77 | claimMoveWakeLogf("host claims: the moves wake source did not open, so every wake on this box is refused, serving included: %v", err) |
| 78 | }) |
| 79 | return |
| 80 | } |
| 81 | out.Moves, out.Serving.Files = owners, files |
| 82 | } |
| 83 | |
| 84 | func refusingClaimMoveWake(box string, store release.Store, reason error) *activator.ClaimRuntime { |
| 85 | w := refusingRootWake(box, store) |
| 86 | w.Prerequisites = func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) error { return reason } |
| 87 | w.Admit = func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (hostclaim.Observed, error) { |
| 88 | return hostclaim.Observed{}, reason |
| 89 | } |
| 90 | w.Prepare = func(context.Context, registry.Route, hostclaim.Observed, activator.StartPolicy) (activator.ClaimStart, error) { |
| 91 | return activator.ClaimStart{}, reason |
| 92 | } |
| 93 | return w |
| 94 | } |
| 95 | |
| 96 | // claimBoundIdentity pins the entire verified enrollment, including its cloud |
| 97 | // instance, buckets, namespace and binding, for a built assembly. The roster |
| 98 | // write counter is not pinned: holds and other boxes' rows move it. |
| 99 | func claimBoundIdentity(verify func(context.Context) (hostbox.Actor, error), expected hostbox.Actor) func(context.Context) error { |
| 100 | return func(ctx context.Context) error { |
| 101 | if ctx == nil || verify == nil || expected.Enrollment == "" { |
| 102 | return hostwrite.ErrUnavailable |
| 103 | } |
| 104 | current, err := verify(ctx) |
| 105 | if err != nil { |
| 106 | return err |
| 107 | } |
| 108 | if !hostbox.SameBinding(current, expected) { |
| 109 | return hostbox.ErrChanged |
| 110 | } |
| 111 | return nil |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // ClaimLocalParkAuthorizer is the owner's local check before park.Local effects. |
| 116 | type ClaimLocalParkAuthorizer func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error |
| 117 | |
| 118 | type claimMoveParks struct { |
| 119 | local ClaimLocalParkAuthorizer |
| 120 | engine park.Engine |
| 121 | leases hostbox.LeaseSet |
| 122 | check func(context.Context) error |
| 123 | } |
| 124 | |
| 125 | // MoveParks returns the regional engine, lease inventory and local authorizer. |
| 126 | // Only Build can populate this capability, after Identity.Verify succeeds. |
| 127 | func (p ClaimProviders) MoveParks(ctx context.Context) (park.Engine, hostbox.LeaseSet, ClaimLocalParkAuthorizer, error) { |
| 128 | if p.moveParks == nil || p.moveParks.check == nil || p.moveParks.leases == nil { |
| 129 | return park.Engine{}, nil, nil, hostwrite.ErrUnavailable |
| 130 | } |
| 131 | if err := p.moveParks.check(ctx); err != nil { |
| 132 | return park.Engine{}, nil, nil, err |
| 133 | } |
| 134 | if p.moveParks.local == nil { |
| 135 | return park.Engine{}, nil, nil, hostwrite.ErrUnavailable |
| 136 | } |
| 137 | return p.moveParks.engine, p.moveParks.leases, p.moveParks.local, nil |
| 138 | } |
| 139 | |
| 140 | func (p productionClaimRegional) VerifyMovesSweep(ctx context.Context, o hostclaim.Observed, versions []lease.ObjectVersion) (park.MoveSweepProof, error) { |
| 141 | if _, err := p.Identity.Verify(ctx); err != nil { |
| 142 | return park.MoveSweepProof{}, err |
| 143 | } |
| 144 | if p.Parks == nil { |
| 145 | return park.MoveSweepProof{}, park.ErrUnsafe |
| 146 | } |
| 147 | if o.Claim != nil && o.Claim.Free() { |
| 148 | return claimFreeMoveProof(o) |
| 149 | } |
| 150 | verifier, ok := p.Parks.Evidence.(park.MoveSweepVerifier) |
| 151 | if !ok { |
| 152 | return park.MoveSweepProof{}, &ClaimDependencyError{Contracts: []string{"moves Plan 5 newest retired move fence or release evidence"}} |
| 153 | } |
| 154 | if _, err := p.Identity.Verify(ctx); err != nil { |
| 155 | return park.MoveSweepProof{}, err |
| 156 | } |
| 157 | return verifier.VerifyMovesSweep(ctx, o, versions) |
| 158 | } |
| 159 | |