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 "encoding/json"
6 "errors"
7 "fmt"
8 "strconv"
9
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/park"
14 "github.com/carlosframework/platform/internal/provision"
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17 "github.com/carlosframework/platform/internal/schedule"
18)
19
20// The moves write policy (moves Phase 1 Plan 2 Task 10b; ruling T10b-scope).
21//
22// hostbox.Policy.Check separates ordinary host writes from the moves-owned
23// ones: under a move kind it checks the registry hold and the selection's
24// op and box, and then asks Policy.Moves. MovesClaimPolicy builds that
25// answer, the value for ClaimOwnerBindings.MovesPolicy. It grants a write
26// only when every one of these holds, and refuses everything else with a
27// *MovesPolicyRefusal (fail closed):
28//
29// - the claim's op is the landing of a move (hostclaim.KindLanding), or a
30// recovery (below). A moving op grants nothing: the source writes
31// nothing held while moving;
32// - the selection is the claim's: the same host, op, kind, phase and
33// capture (holder incarnation and stamp);
34// - this box is the target after the flip: the executor, the second
35// participant, intent.target, and not intent.source; in the move's
36// target region (intent.to); and not in lost;
37// - the landing's phase permits the action (movesPhaseRights): held-timers
38// at local; held-config, held-release, restore and install at
39// configured, the phase whose work installs the database before its
40// closing CAS writes restored (G8; the final review's finding 3), so
41// restored, the install complete, grants none of them. probing (Plan 3
42// Task 5) grants the held render, the held release and the spawn the
43// probe's whole admitted sequence runs under; probed grants nothing to
44// a fresh selection, and the probe's own spawn selection continues at
45// probed and committed on the same session (H3). The ordinary wake
46// rights at committed (§19.1) are Task 7's row;
47// - the identity fits the action: a held-config write names the config
48// and nothing else, a held-release write names the release and nothing
49// else, the probe's spawn names the launch identity (the pinned config
50// and release, H4), and every other action carries no identity;
51// - a held write names the move's pinned inputs (§9.1, §9.2). Policy.Inputs
52// is consulted for serving claims only (wakepolicy.go), so this policy
53// reads the record itself, through the reader it is built with: the
54// record must be this landing (the op, the holder's incarnation, state
55// landing), Move.Inputs must be pinned, a held-release identity must
56// equal Inputs.Release, and a held-config identity must equal
57// HeldConfigIdentity(Inputs.BundleETags, Inputs.OverlayETag), the
58// canonical form of the pinned etags (the empty configuration included:
59// Move.Inputs present is the pin). Missing Move.Inputs, a record that
60// is not this landing, a read error or no reader refuse. The record is
61// read for these two actions only.
62//
63// The recoveries (moves Plan 4 Task 3b, ruling J12) are the source's rows,
64// granted only when every one of these holds:
65//
66// - this box is the claim op's executor, as Policy.Check requires; the
67// move's source (the first participant and intent.source, not
68// intent.target); not in the target region (intent.to) but in the
69// record's From; and not in lost;
70// - the record, read for every recovery action, is this move and this
71// holder incarnation: rolling_back for this rollback (Rollback.ID is
72// intent.rollback) under restoring, reverting under reverting;
73// - the row permits the action. restoring is keyed by the record's
74// Rollback.SourcePhase: local grants held-timers, configured held-config,
75// held-release, restore and install (the installer restores inside
76// configured, before the CAS that writes restored, G8; restored grants
77// nothing), each under the claim at claimed, for a dirty rollback, with the holder at the source's
78// pre-move stamp S (op.journal.source_stamp); probing grants the held
79// config render and the release materialization before every probe
80// (§7.7) and spawn, under the claim at probing. reverting is keyed by
81// the claim's rev_phase: probing grants the same three, with op.phase at
82// probing (J1);
83// - the identity fits the action, a spawn carrying the launch identity
84// (Plan 3 H4): the config and the release it launches with;
85// - the held config and release and the launch name the inputs the
86// recovery rendered, Rollback.RecoveryInputs as the CAS that ends
87// configured records them, never Move.Inputs (§7.7, J2-R2-5). Before
88// they are recorded the render is granted on the rows above: configured
89// is the render the record takes them from, and a clean return renders
90// before its probe with no configured. A dirty return's probe, and every
91// spawn, need them recorded. A revert's are Move.RecoveryInputs, which
92// its probe records before it renders (MoveRecordRevertInputs; M11,
93// Q11): its render, its release and its launch need them recorded and
94// are bound to them, never to Move.Inputs.
95//
96// The target's cleaning (ReplaceTimers(host, nil) at cleaning) and the
97// source's revert close are not rows here: their callers use plain registry
98// calls under WithHostHoldLock, as Plan 3 H5(d) ruled for the source's
99// finalization, and this policy refuses the target any write under a
100// recovery.
101//
102// Production registration. RegisterProductionClaimOwners requires every
103// owner, and the Inputs, PublicationEvidence and AdmissionEvidence owners
104// (Plan 3) and View, Authorize, Evidence and LeaseSet (moves Plan 5) do not
105// exist yet, so nothing registers this in production: it is tested and
106// ready to plug in as MovesClaimPolicy(MovesRecordsFrom(store)). Until that
107// registration, the landing keeps refusing at checking while
108// CARLOS_HOST_CLAIM is on (ruling G16, movesland.go).
109
110// ErrMovesRefused is what every MovesPolicyRefusal unwraps to.
111var ErrMovesRefused = errors.New("host: the moves policy refuses the write")
112
113// MovesPolicyRefusal is MovesClaimPolicy's typed refusal: which action was
114// asked under which op, kind and phase, by which box, and why. Cause is
115// the record read's error when that is the reason.
116type MovesPolicyRefusal struct {
117 Host, Op, Action, Kind, Phase, Box, Reason string
118 Cause error
119}
120
121func (e *MovesPolicyRefusal) Error() string {
122 msg := fmt.Sprintf("host: the moves policy refuses %s for %s under %s/%q on %s: %s", e.Action, e.Host, e.Kind, e.Phase, e.Box, e.Reason)
123 if e.Cause != nil {
124 msg += ": " + e.Cause.Error()
125 }
126 return msg
127}
128
129func (e *MovesPolicyRefusal) Unwrap() []error {
130 if e.Cause != nil {
131 return []error{ErrMovesRefused, e.Cause}
132 }
133 return []error{ErrMovesRefused}
134}
135
136// MovesRecordReader reads the host's provisioning record: the move's
137// pinned inputs live there. Production is MovesRecordsFrom(store).
138type MovesRecordReader func(ctx context.Context, acct, app, host string) (*provision.Record, error)
139
140// MovesRecordsFrom is provision.GetRecord over the deployment store.
141func MovesRecordsFrom(s release.Store) MovesRecordReader {
142 return func(ctx context.Context, acct, app, host string) (*provision.Record, error) {
143 if s == nil {
144 return nil, hostwrite.ErrUnavailable
145 }
146 return provision.GetRecord(ctx, s, acct, app, host)
147 }
148}
149
150// heldConfigIdentityPrefix names the encoding HeldConfigIdentity uses, so
151// a later form can be told apart from it.
152const heldConfigIdentityPrefix = "etags-v1:"
153
154// heldConfigPins is what HeldConfigIdentity encodes: the non-empty bundle
155// etags and the overlay etag. encoding/json writes map keys sorted and
156// escapes every delimiter, so two different sets never share an identity.
157type heldConfigPins struct {
158 ETags map[string]string `json:"etags"`
159 Overlay string `json:"overlay"`
160}
161
162// HeldConfigIdentity is the identity a held-config write carries (§9.1):
163// the bundle etags and the overlay etag it rendered from, in one canonical
164// string, so the policy can compare them with the move's pins exactly. An
165// empty etag is the same as an absent one. The empty configuration (an app
166// with no default bundle and no overlay pins exactly that at readiness) has
167// its own identity, the encoding of the empty set; it is never the zero
168// identity.
169func HeldConfigIdentity(bundleETags map[string]string, overlayETag string) hostwrite.Identity {
170 pins := heldConfigPins{ETags: map[string]string{}, Overlay: overlayETag}
171 for k, v := range bundleETags {
172 if k != "" && v != "" {
173 pins.ETags[k] = v
174 }
175 }
176 raw, err := json.Marshal(pins)
177 if err != nil {
178 // Strings always marshal; a failure here would be a Go runtime
179 // fault, and the zero identity is refused everywhere.
180 return hostwrite.Identity{}
181 }
182 return hostwrite.Identity{Config: heldConfigIdentityPrefix + string(raw)}
183}
184
185// The held actions and the landing's rights hostbox.Policy.Check asks
186// Policy.Moves about (wakepolicy.go).
187const (
188 movesActionHeldConfig = "held-config"
189 movesActionHeldRelease = "held-release"
190 movesActionHeldTimers = "held-timers"
191 movesActionRestore = "restore"
192 movesActionSpawn = "spawn"
193 movesActionSeed = "seed"
194 movesActionWriter = "writer"
195 movesActionInstall = "install"
196)
197
198// movesPhaseRights is the landing's table: the actions each phase permits
199// (§7.6), keyed by the phase whose work makes the write. configured renders,
200// gates the release and installs the database; its closing CAS writes
201// restored (G8), so restored grants nothing. A phase or action not listed is
202// refused.
203//
204// probing (Plan 3 Task 5, H4) is the probe's: the held render and the held
205// release before every start (§7.7), and the spawn, which carries the launch
206// identity bound to the pins. The admitted session's restore, launch record,
207// launch and replication all run under that one spawn selection (claim Plan
208// 9 Group D), so restore, seed and writer get no row. probed grants nothing
209// to a fresh selection; committed's ordinary wake is Task 7's row.
210var movesPhaseRights = map[string]map[string]bool{
211 provision.LandLocal: {movesActionHeldTimers: true},
212 provision.LandConfigured: {movesActionHeldConfig: true, movesActionHeldRelease: true, movesActionRestore: true, movesActionInstall: true},
213 provision.LandProbing: {movesActionHeldConfig: true, movesActionHeldRelease: true, movesActionSpawn: true},
214}
215
216// movesSessionContinues reports whether the claim's phase may differ from
217// the selection's (H3, claim Plan 9 D1): only the admitted session's own
218// spawn selection, and only along the landing's session row
219// (hostclaim.SessionObserves: probing, probed, committed), so the probe's
220// replication and process survive the probed and committed CASes. The
221// rights stay the ones of the phase the selection was made at.
222func movesSessionContinues(c *hostclaim.Claim, s hostbox.Selection) bool {
223 if c.Op.Phase == s.Phase {
224 return true
225 }
226 return s.Action == movesActionSpawn && hostclaim.SessionObserves(c.Op.Kind, s.Phase, c.Op.Phase)
227}
228
229// movesPolicyActions is every action the policy knows. Anything else is
230// refused before the table is consulted.
231var movesPolicyActions = map[string]bool{
232 movesActionHeldConfig: true, movesActionHeldRelease: true, movesActionHeldTimers: true,
233 movesActionRestore: true, movesActionSpawn: true, movesActionSeed: true, movesActionWriter: true, movesActionInstall: true,
234}
235
236// MovesClaimPolicy builds hostbox.Policy.Moves for the moves plans over a
237// record reader: see the file comment for what it grants.
238// MovesClaimPolicy is MovesClaimPolicyWithInputs with no committed input
239// check: the committed wake's spawn is refused (production, until the claim
240// binds one; H4).
241func MovesClaimPolicy(read MovesRecordReader) func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
242 return MovesClaimPolicyWithInputs(read, nil)
243}
244
245// MovesClaimPolicyWithInputs builds hostbox.Policy.Moves. committed is the
246// committed wake's input check (Plan 3 Task 7, H4): a fresh spawn selection
247// at landing/committed carries the ordinary launch identity, and committed
248// says whether it is the launch this host's row would run, as the serving
249// InputPolicy does for serving. Nil refuses every committed spawn.
250func MovesClaimPolicyWithInputs(read MovesRecordReader, committed func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error) func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error {
251 return func(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error {
252 refuse := func(reason string, cause error) error {
253 r := &MovesPolicyRefusal{Host: s.Host, Op: s.Op, Action: s.Action, Box: s.Box, Reason: reason, Cause: cause}
254 if c != nil {
255 r.Kind, r.Phase = c.Op.Kind, c.Op.Phase
256 }
257 return r
258 }
259 if ctx == nil {
260 return refuse("no context", nil)
261 }
262 if err := ctx.Err(); err != nil {
263 return err
264 }
265 if c == nil || c.Holder == nil {
266 return refuse("the claim has no holder", nil)
267 }
268 if !movesPolicyActions[s.Action] {
269 return refuse("not a moves action", nil)
270 }
271 if s.Host == "" || s.Box == "" || s.Region == "" || s.Op == "" {
272 return refuse("the selection names no host, box, region or op", nil)
273 }
274 if c.Host != s.Host {
275 return refuse("the claim is another host's", nil)
276 }
277 if c.Op.ID != s.Op {
278 return refuse("the claim is under another op", nil)
279 }
280 if c.Op.Kind != s.Kind || !movesSessionContinues(c, s) {
281 return refuse("the claim's kind or phase is not the selection's", nil)
282 }
283 if s.Captured.Stamp == 0 || c.Holder.Stamp != s.Captured.Stamp || c.Holder.Incarnation != s.Captured.Incarnation {
284 return refuse("the capture is not the holder's", nil)
285 }
286 switch c.Op.Kind {
287 case hostclaim.KindLanding:
288 case hostclaim.KindReverting, hostclaim.KindRestoring:
289 return movesRecoveryPolicy(ctx, read, c, s, id, refuse)
290 case hostclaim.KindMoving:
291 return refuse("a moving op writes nothing held", nil)
292 default:
293 return refuse("not a move", nil)
294 }
295 if c.Op.Exec != s.Box {
296 return refuse("this box is not the executor", nil)
297 }
298 if len(c.Op.Participants) != 2 || c.Op.Participants[1] != s.Box || c.Op.Intent["target"] != s.Box || c.Op.Intent["source"] == s.Box {
299 return refuse("this box is not the move's target", nil)
300 }
301 if _, err := park.VerifiedHome(s.FleetHomeRegion, s.HomeRegion, s.Region, c.Op.Intent["home_region"]); err != nil {
302 return refuse("this region is not the move's target region", err)
303 }
304 if !park.SameRegion(s.HomeRegion, c.Op.Intent["to"], s.Region) {
305 return refuse("this region is not the move's target region", nil)
306 }
307 for _, lost := range c.Lost {
308 if lost == s.Box {
309 return refuse("this box is declared lost", nil)
310 }
311 }
312 if s.Phase == hostclaim.PhaseLandingCommitted && s.Action == movesActionSpawn {
313 return movesCommittedSpawn(ctx, read, committed, c, s, id, refuse)
314 }
315 if !movesPhaseRights[s.Phase][s.Action] {
316 return refuse("the phase does not permit the action", nil)
317 }
318 probeSpawn := s.Phase == provision.LandProbing && s.Action == movesActionSpawn
319 if probeSpawn {
320 if !recoveryIdentityFits(s.Action, id) {
321 return refuse("the identity does not fit the action", nil)
322 }
323 } else if !movesIdentityFits(s.Action, id) {
324 return refuse("the identity does not fit the action", nil)
325 }
326 if s.Action != movesActionHeldConfig && s.Action != movesActionHeldRelease && !probeSpawn {
327 return nil
328 }
329 // The held config and release are the move's pinned inputs (§9.1,
330 // §9.2), read from the record this claim's holder names.
331 if read == nil {
332 return refuse("no record reader", nil)
333 }
334 if c.Holder.Account == "" || c.Holder.App == "" {
335 return refuse("the holder names no account or app", nil)
336 }
337 rec, err := read(ctx, c.Holder.Account, c.Holder.App, c.Host)
338 if err != nil {
339 return refuse("the record cannot be read", err)
340 }
341 if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != s.Op || rec.Move.Incarnation != c.Holder.Incarnation {
342 return refuse("the record is not this landing", nil)
343 }
344 switch {
345 case probeSpawn:
346 // The probe's launch is this landing's, unmarked, or the same
347 // session continuing once the record is committed (H3).
348 if (rec.Move.State != provision.MoveLanding || rec.Move.Reason != "") && rec.Move.State != provision.MoveCommitted {
349 return refuse("the record is not this landing", nil)
350 }
351 case rec.Move.State != provision.MoveLanding:
352 return refuse("the record is not this landing", nil)
353 }
354 in := rec.Move.Inputs
355 if in == nil {
356 return refuse("no pinned inputs", nil)
357 }
358 if probeSpawn {
359 if in.Release == "" || id.Config != HeldConfigIdentity(in.BundleETags, in.OverlayETag).Config || id.Release != in.Release {
360 return refuse("the launch is not the pinned one", nil)
361 }
362 return nil
363 }
364 switch s.Action {
365 case movesActionHeldRelease:
366 if in.Release == "" {
367 return refuse("no pinned release", nil)
368 }
369 if id.Release != in.Release {
370 return refuse("the release is not the pinned one", nil)
371 }
372 case movesActionHeldConfig:
373 // Move.Inputs present is the pin, empty etags included: an app
374 // without a default bundle or an overlay pins the empty
375 // configuration, and the render that names the same empty set
376 // is granted.
377 if id.Config != HeldConfigIdentity(in.BundleETags, in.OverlayETag).Config {
378 return refuse("the config is not the pinned one", nil)
379 }
380 }
381 return nil
382 }
383}
384
385// recoveryRow is one row of a recovery's table: the claim phase it runs
386// under, whether it is the dirty return's install, and the actions it
387// grants.
388type recoveryRow struct {
389 claimPhase string
390 install bool
391 actions map[string]bool
392}
393
394// restoringRights is the source's table during a rollback, keyed by the
395// record's Rollback.SourcePhase (§7.8 source table) at the phase whose work
396// makes the write: the installer restores and installs inside configured,
397// before the CAS that writes restored (G8, J24), so restored grants nothing.
398// The claim stays at claimed through the dirty return's install (Task 3) and
399// mirrors probing (J1). A source phase not listed grants nothing.
400var restoringRights = map[string]recoveryRow{
401 provision.RbSourceLocal: {claimPhase: hostclaim.PhaseClaimed, install: true, actions: map[string]bool{movesActionHeldTimers: true}},
402 provision.RbSourceConfigured: {claimPhase: hostclaim.PhaseClaimed, install: true, actions: map[string]bool{
403 movesActionHeldConfig: true, movesActionHeldRelease: true, movesActionRestore: true, movesActionInstall: true}},
404 provision.RbSourceProbing: {claimPhase: hostclaim.PhaseProbing, actions: map[string]bool{movesActionHeldConfig: true, movesActionHeldRelease: true, movesActionSpawn: true}},
405}
406
407// revertingRights is the source's table during a revert, keyed by the
408// claim's rev_phase (§7.4), which op.phase mirrors from probing on (J1).
409var revertingRights = map[string]recoveryRow{
410 hostclaim.PhaseProbing: {claimPhase: hostclaim.PhaseProbing, actions: map[string]bool{movesActionHeldConfig: true, movesActionHeldRelease: true, movesActionSpawn: true}},
411}
412
413// movesRecoveryPolicy is MovesClaimPolicy's reverting and restoring rows:
414// see the file comment. The claim, the selection and the capture have
415// already been matched.
416func movesRecoveryPolicy(ctx context.Context, read MovesRecordReader, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity, refuse func(string, error) error) error {
417 if c.Op.Exec != s.Box {
418 return refuse("this box is not the executor", nil)
419 }
420 if len(c.Op.Participants) == 0 || c.Op.Participants[0] != s.Box || c.Op.Intent["source"] != s.Box || c.Op.Intent["target"] == s.Box {
421 return refuse("this box is not the move's source", nil)
422 }
423 if _, err := park.VerifiedHome(s.FleetHomeRegion, s.HomeRegion, s.Region, c.Op.Intent["home_region"]); err != nil {
424 return refuse("this region is the move's target region", err)
425 }
426 if park.SameRegion(s.HomeRegion, c.Op.Intent["to"], s.Region) {
427 return refuse("this region is the move's target region", nil)
428 }
429 for _, lost := range c.Lost {
430 if lost == s.Box {
431 return refuse("this box is declared lost", nil)
432 }
433 }
434 if read == nil {
435 return refuse("no record reader", nil)
436 }
437 if c.Holder.Account == "" || c.Holder.App == "" {
438 return refuse("the holder names no account or app", nil)
439 }
440 rec, err := read(ctx, c.Holder.Account, c.Holder.App, c.Host)
441 if err != nil {
442 return refuse("the record cannot be read", err)
443 }
444 if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != s.Op || rec.Move.Incarnation != c.Holder.Incarnation {
445 return refuse("the record is not this move", nil)
446 }
447 if !park.PlacementMatches(s.HomeRegion, rec.Move.From, s.Region) {
448 return refuse("this region is not the move's source region", nil)
449 }
450 var (
451 row recoveryRow
452 ok bool
453 rb *provision.MoveRollback
454 recovery *provision.MoveInputs
455 )
456 switch c.Op.Kind {
457 case hostclaim.KindRestoring:
458 rb = rec.Move.Rollback
459 if rec.Move.State != provision.MoveRollingBack || rb == nil || rb.ID == "" || rb.ID != c.Op.Intent["rollback"] {
460 return refuse("the record is not this rollback", nil)
461 }
462 row, ok = restoringRights[rb.SourcePhase]
463 recovery = rb.RecoveryInputs
464 default: // hostclaim.KindReverting
465 if rec.Move.State != provision.MoveReverting {
466 return refuse("the record is not this revert", nil)
467 }
468 row, ok = revertingRights[c.Op.Journal[hostclaim.JournalRevPhase]]
469 recovery = rec.Move.RecoveryInputs
470 }
471 if !ok || row.claimPhase != c.Op.Phase || !row.actions[s.Action] {
472 return refuse("the phase does not permit the action", nil)
473 }
474 if row.install {
475 if !rb.Dirty {
476 return refuse("only a dirty return installs", nil)
477 }
478 stamp, err := strconv.ParseUint(c.Op.Journal[hostclaim.JournalSourceStamp], 10, 64)
479 if err != nil || stamp == 0 || c.Holder.Stamp != stamp {
480 return refuse("the holder is not at the source's pre-move stamp", nil)
481 }
482 }
483 if !recoveryIdentityFits(s.Action, id) {
484 return refuse("the identity does not fit the action", nil)
485 }
486 switch s.Action {
487 case movesActionHeldConfig, movesActionHeldRelease:
488 if recovery == nil {
489 // configured renders the inputs its closing CAS records, and a
490 // clean return's render before its probe precedes the record. A
491 // revert's probe records before it renders (M11), so a revert
492 // has no render before the record.
493 if rb != nil && (rb.SourcePhase == provision.RbSourceConfigured || !rb.Dirty) {
494 return nil
495 }
496 return refuse("no recorded recovery inputs", nil)
497 }
498 case movesActionSpawn:
499 if recovery == nil {
500 return refuse("no recorded recovery inputs", nil)
501 }
502 default:
503 return nil
504 }
505 switch s.Action {
506 case movesActionHeldConfig:
507 if id.Config != HeldConfigIdentity(recovery.BundleETags, recovery.OverlayETag).Config {
508 return refuse("the config is not the recovery's", nil)
509 }
510 case movesActionHeldRelease:
511 if recovery.Release == "" {
512 return refuse("no recorded recovery release", nil)
513 }
514 if id.Release != recovery.Release {
515 return refuse("the release is not the recovery's", nil)
516 }
517 case movesActionSpawn:
518 if recovery.Release == "" {
519 return refuse("no recorded recovery release", nil)
520 }
521 if id.Config != HeldConfigIdentity(recovery.BundleETags, recovery.OverlayETag).Config || id.Release != recovery.Release {
522 return refuse("the launch is not the recovery's", nil)
523 }
524 }
525 return nil
526}
527
528// recoveryIdentityFits is movesIdentityFits for a recovery, with Plan 3
529// H4's launch identity: a spawn names both the config and the release it
530// launches with.
531func recoveryIdentityFits(action string, id hostwrite.Identity) bool {
532 if action == movesActionSpawn {
533 return id.Config != "" && id.Release != ""
534 }
535 return movesIdentityFits(action, id)
536}
537
538// movesIdentityFits: a held-config write names the config and no release,
539// a held-release write the release and no config, and every other action
540// carries no identity at all. Which config and release is the record's
541// business, checked after this.
542func movesIdentityFits(action string, id hostwrite.Identity) bool {
543 switch action {
544 case movesActionHeldConfig:
545 return id.Config != "" && id.Release == ""
546 case movesActionHeldRelease:
547 return id.Release != "" && id.Config == ""
548 default:
549 return id == hostwrite.Identity{}
550 }
551}
552
553// heldTimersReplacer is the landing's timers import (ruling G16):
554// schedule.SelectedTimers over the registry, its Work selected as
555// held-timers for the host, so under CARLOS_HOST_CLAIM the write runs
556// inside the claim's selection and Policy.Moves is asked first. With the
557// flag off hostwrite.Config.Select answers the legacy pass-through and the
558// registry's own ReplaceTimers runs exactly as before. With the flag on and
559// no selector it refuses (hostwrite.ErrUnavailable) and writes nothing.
560func heldTimersReplacer(writes hostwrite.Config, reg *registry.Registry) func(ctx context.Context, host string, rows []registry.Timer) error {
561 return func(ctx context.Context, host string, rows []registry.Timer) error {
562 if ctx == nil || reg == nil {
563 return hostwrite.ErrUnavailable
564 }
565 work, err := writes.Select(ctx, host, movesActionHeldTimers)
566 if err != nil {
567 return err
568 }
569 return schedule.SelectedTimers{Store: reg, Work: work, Context: ctx, Host: host}.ReplaceTimers(host, rows)
570 }
571}
572
573// movesCommittedSpawn is the committed wake's spawn (Plan 3 Task 7): the
574// ordinary launch identity, over a committed record of this move, accepted by
575// the committed input check.
576func movesCommittedSpawn(ctx context.Context, read MovesRecordReader, committed func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity, refuse func(string, error) error) error {
577 if committed == nil {
578 return refuse("the committed launch has no input check", nil)
579 }
580 if !recoveryIdentityFits(s.Action, id) {
581 return refuse("the identity does not fit the action", nil)
582 }
583 if read == nil {
584 return refuse("no record reader", nil)
585 }
586 rec, err := read(ctx, c.Holder.Account, c.Holder.App, c.Host)
587 if err != nil {
588 return refuse("the record cannot be read", err)
589 }
590 if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != s.Op || rec.Move.Incarnation != c.Holder.Incarnation || rec.Move.State != provision.MoveCommitted {
591 return refuse("the record is not this committed move", nil)
592 }
593 if err := committed(ctx, c, s, id); err != nil {
594 return refuse("the launch is not the committed one", err)
595 }
596 return nil
597}
598