carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "os"
9 "sort"
10 "sync"
11
12 "github.com/carlosframework/platform/internal/activator"
13 "github.com/carlosframework/platform/internal/hostbox"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/moves"
18 "github.com/carlosframework/platform/internal/registry"
19)
20
21// ClaimMoveReadAccess is retained by Open and becomes usable after Build.
22// Selection reads authority; effects still require the returned Work.With.
23type ClaimMoveReadAccess interface {
24 HeldWork(context.Context, string, string, hostguard.Captured) (hostwrite.Work, error)
25 Route(context.Context, string) (*registry.Route, error)
26 Launch(context.Context, string) (ClaimServingLaunch, moves.EnvReport, error)
27}
28
29type claimMoveReadAccess struct {
30 mu sync.RWMutex
31 bound bool
32 check func(context.Context) error
33 actor hostbox.Actor
34 source hostbox.WorkSource
35 route func(string) (*registry.Route, error)
36 launch func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error)
37}
38
39func (a *claimMoveReadAccess) bind(ctx context.Context, source hostbox.WorkSource) error {
40 if a == nil {
41 return hostwrite.ErrUnavailable
42 }
43 a.mu.Lock()
44 defer a.mu.Unlock()
45 if a.bound || a.check == nil || a.route == nil || a.launch == nil || source.Box != a.actor.Box || source.Region != a.actor.Region || source.Guard.Claims == nil {
46 return hostwrite.ErrUnavailable
47 }
48 if err := a.check(ctx); err != nil {
49 return err
50 }
51 // SelectAt calls Prepare. Clearing it prevents participation writes while
52 // retaining the same guard, policy, local checks and continuation table.
53 source.Prepare = nil
54 a.source, a.bound = source, true
55 return nil
56}
57
58func (a *claimMoveReadAccess) selected(ctx context.Context, host string) (hostbox.WorkSource, error) {
59 if a == nil || ctx == nil {
60 return hostbox.WorkSource{}, hostwrite.ErrUnavailable
61 }
62 a.mu.RLock()
63 source, bound := a.source, a.bound
64 a.mu.RUnlock()
65 if !bound {
66 return hostbox.WorkSource{}, hostwrite.ErrUnavailable
67 }
68 if err := a.check(ctx); err != nil {
69 return hostbox.WorkSource{}, err
70 }
71 o, err := source.Guard.Claims.Read(ctx, host)
72 if err != nil {
73 return hostbox.WorkSource{}, err
74 }
75 if o.Absent || o.Host != host || o.Claim == nil || o.Claim.Host != host || o.Claim.Holder == nil || o.Claim.Holder.Incarnation == "" || o.Claim.Op.Exec != a.actor.Box || !claimMovesKind(o.Claim.Op.Kind) {
76 return hostbox.WorkSource{}, hostbox.ErrChanged
77 }
78 for _, lost := range o.Claim.Lost {
79 if lost == a.actor.Box {
80 return hostbox.WorkSource{}, hostbox.ErrChanged
81 }
82 }
83 return source, nil
84}
85
86func (a *claimMoveReadAccess) Route(ctx context.Context, host string) (*registry.Route, error) {
87 if _, err := a.selected(ctx, host); err != nil {
88 return nil, err
89 }
90 rt, err := a.route(host)
91 if err != nil {
92 return nil, err
93 }
94 if rt == nil || rt.Host != host {
95 return nil, hostbox.ErrChanged
96 }
97 // Deep copy future reference fields as well as today's scalar fields.
98 raw, err := json.Marshal(rt)
99 if err != nil {
100 return nil, err
101 }
102 var out registry.Route
103 if err := json.Unmarshal(raw, &out); err != nil {
104 return nil, err
105 }
106 return &out, nil
107}
108
109func (a *claimMoveReadAccess) HeldWork(ctx context.Context, host, action string, captured hostguard.Captured) (hostwrite.Work, error) {
110 switch action {
111 case movesActionHeldConfig, movesActionHeldRelease:
112 default:
113 return nil, hostbox.ErrUnsupported
114 }
115 source, err := a.selected(ctx, host)
116 if err != nil {
117 return nil, err
118 }
119 work, err := source.SelectAt(ctx, host, action, captured)
120 if err != nil {
121 return nil, err
122 }
123 return claimMoveReadWork{access: a, host: host, work: work}, nil
124}
125
126type claimMoveReadWork struct {
127 access *claimMoveReadAccess
128 host string
129 work hostwrite.Work
130}
131
132func (w claimMoveReadWork) With(ctx context.Context, id hostwrite.Identity, fn func() error) error {
133 if _, err := w.access.selected(ctx, w.host); err != nil {
134 return err
135 }
136 return w.work.With(ctx, id, fn)
137}
138
139func (a *claimMoveReadAccess) Launch(ctx context.Context, host string) (ClaimServingLaunch, moves.EnvReport, error) {
140 rt, err := a.Route(ctx, host)
141 if err != nil {
142 return ClaimServingLaunch{}, moves.EnvReport{}, err
143 }
144 launch, report, err := a.launch(ctx, *rt)
145 if err != nil {
146 return ClaimServingLaunch{}, moves.EnvReport{}, err
147 }
148 launch.Args = append([]string(nil), launch.Args...)
149 launch.Env = append([]string(nil), launch.Env...)
150 vars := make(map[string]string, len(report.Vars))
151 for k, v := range report.Vars {
152 vars[k] = v
153 }
154 report.Vars = vars
155 return launch, report, nil
156}
157
158func claimMoveLaunchSource(backend *activator.ExecBackend) func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
159 return func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, moves.EnvReport, error) {
160 // The selection refuses a missing token, and gates a replica. This
161 // path must never mint one.
162 snapshot, err := backend.SelectClaimLaunch(ctx, rt)
163 if err != nil {
164 return ClaimServingLaunch{}, moves.EnvReport{}, err
165 }
166 // A declined box's launch gate, as for a claims-backed wake (Task 9
167 // fix round 1, ruling 4c): a moves launch execs the snapshot's bytes
168 // too. Nil, and nothing read, on every other box.
169 if err := declinedClaimGate(backend.OptDir, rt, snapshot); err != nil {
170 return ClaimServingLaunch{}, moves.EnvReport{}, err
171 }
172 env, supplied, _, err := backend.LaunchLayers(rt)
173 if err != nil {
174 return ClaimServingLaunch{}, moves.EnvReport{}, err
175 }
176 key, err := moves.ReportKeyFromSecrets(os.Getenv("CARLOS_SECRETS_KEY"))
177 if err != nil {
178 return ClaimServingLaunch{}, moves.EnvReport{}, err
179 }
180 delete(env, "CARLOS_MOVE_PROBE")
181 entries := make([]string, 0, len(env))
182 for k, v := range env {
183 entries = append(entries, k+"="+v)
184 }
185 sort.Strings(entries)
186 raw, err := json.Marshal(entries)
187 if err != nil {
188 return ClaimServingLaunch{}, moves.EnvReport{}, err
189 }
190 digest := sha256.Sum256(raw)
191 snapshot.Identity.Config = hex.EncodeToString(digest[:])
192 // The report is made from this same composition, so its pin and the
193 // launch's env can never come from different reads (fix round 4).
194 return ClaimServingLaunch{Identity: snapshot.Identity, Executable: snapshot.Executable, ExecutableSHA256: snapshot.SHA256, Args: snapshot.Args, Env: entries, Version: snapshot.Version, Staged: snapshot.Staged, Limit: snapshot.Limit}, moves.ReportLaunch(key, env, supplied), nil
195 }
196}
197
198// ClaimInputPolicyOwner is implemented by (*movesProbeOwners).InputPolicy.
199// The moves owner chooses each kind's record pins; serving has no fallback.
200type ClaimInputPolicyOwner interface {
201 InputPolicy(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error
202}
203
204func (d ClaimOwnerDispatch) InputPolicy(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error {
205 if c == nil || c.Holder == nil || s.Host != c.Host || s.Kind != c.Op.Kind || s.Captured.Incarnation != c.Holder.Incarnation || s.Captured.Stamp != c.Holder.Stamp {
206 return hostbox.ErrChanged
207 }
208 switch {
209 case c.Op.Kind == hostclaim.KindServing:
210 if d.Serving.InputPolicy == nil {
211 return hostwrite.ErrUnavailable
212 }
213 return d.Serving.InputPolicy(ctx, c, s, id)
214 case claimMovesKind(c.Op.Kind):
215 if d.Moves == nil {
216 return claimOperationRefusal()
217 }
218 owner, ok := d.Moves.Inputs.(ClaimInputPolicyOwner)
219 if !ok {
220 return claimOperationRefusal()
221 }
222 return owner.InputPolicy(ctx, c, s, id)
223 default:
224 return hostbox.ErrUnsupported
225 }
226}
227
228func bindProductionClaimInputPolicy(getenv func(string) string, out *ClaimOwnerBindings, read MovesRecordReader) {
229 if !hostwrite.Enabled(getenv) || !moves.Enabled(getenv) {
230 return
231 }
232 dispatch := ClaimOwnerDispatch{Serving: ClaimKindOwners{InputPolicy: out.InputPolicy}, Moves: &out.Moves.ClaimKindOwners}
233 out.InputPolicy = dispatch.InputPolicy
234 // Policy.Check dispatches move actions to Moves, never Inputs directly.
235 out.MovesPolicy = MovesClaimPolicyWithInputs(read, dispatch.InputPolicy)
236}
237