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 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io/fs"
12 "log"
13 "os"
14 "path/filepath"
15 "reflect"
16 "strings"
17 "sync"
18 "time"
19
20 "github.com/carlosframework/platform/internal/activator"
21 "github.com/carlosframework/platform/internal/appconfig"
22 "github.com/carlosframework/platform/internal/genselect"
23 "github.com/carlosframework/platform/internal/hostbox"
24 "github.com/carlosframework/platform/internal/hostclaim"
25 "github.com/carlosframework/platform/internal/hostguard"
26 "github.com/carlosframework/platform/internal/hostops"
27 "github.com/carlosframework/platform/internal/hostwrite"
28 "github.com/carlosframework/platform/internal/moves"
29 "github.com/carlosframework/platform/internal/moves/archive"
30 "github.com/carlosframework/platform/internal/park"
31 "github.com/carlosframework/platform/internal/provision"
32 "github.com/carlosframework/platform/internal/registry"
33 "github.com/carlosframework/platform/internal/release"
34)
35
36// The moves owners for a landing probe (moves Phase 1 Plan 3 Task 5c; claim
37// Plan 9 Group D, D5; rulings H2, H4, H13, H14, H16, H18). The claim's
38// dispatcher (ClaimOwnerDispatch) hands this owner set only the move kinds,
39// and it dispatches them again by the observed op's kind (ruling Q7): the
40// restoring and reverting kinds go to the recovery owners
41// (movesRecoveryOwners, movesrecoveryowners.go; Plan 4 Task 9), the moving
42// kind is refused, and the landing is served here. Of the landing it serves
43// one shape, a landing probe: the claim at landing/probing with this box
44// executing as the move's target, the policy a probe, the registry row held
45// by the move (and the committed wake, Task 7's, below). Everything else is
46// refused with hostbox.ErrUnsupported or hostbox.ErrChanged, before any
47// lease, stop or launch.
48//
49// The claim owns the admission, the restore, the launch, readiness,
50// replication and the publication sequence (claimwakeassembly.go). This
51// owner set supplies what moves knows:
52//
53// - Select (read-only, before admission): the landing is still
54// admissible (its deadline, the record attempt within what T11
55// admitted, the install the record vouches for); the launch-input pin
56// at spawn (§7.7, round-2 finding 14): the live release and the report
57// of the selected launch's own environment are the pinned ones, else
58// ProbeInputsChanged
59// with the recorded sentence "configuration changed during the move",
60// which the landing turns into its rollback (Task 6); then the
61// admission per §7.14. The first probe is a cold admission into the
62// prepared generation, the move id, after PreparedCheck (.files-gen and
63// <dbPath>.gen name it and the holder's incarnation, and the journalled
64// install manifest is the seal's N); its baseline is the build's pin
65// (genselect.SelectPrepared, never an earlier stay's marker, H14); it
66// keeps the flip's stamp and claims sealed coverage when the seal was
67// built from the stopped database. A restarted attempt, once the move
68// id is admitted, is a cold admission into a fresh generation: no kept
69// stamp, no coverage, its baseline the prepared pin while the move id
70// has published nothing and the certified remote (genselect.Select)
71// once it has (H14). The launch identity is the pinned config and
72// release (H4), CARLOS_MOVE_PROBE=1 is in the child's environment (H18)
73// and the readiness path is the app's moves.ready_path (H12).
74// - AdmissionEvidence: the same decisions and the same admissibility,
75// re-derived and compared, pin included, against a baseline selected
76// again, immediately before the CAS.
77// - Materialize, before every start, under the admitted spawn selection,
78// in the order ruled for C1 (fix round 1, finding 8): the held render
79// and the held release, each under its own claim selection, held-config
80// and held-release, through the existing held paths
81// (appconfig.RenderHeldUnder, release.MaterializeHeldUnder) and
82// MovesClaimPolicy's probing rows (H13), the render's etags compared
83// with the pins before anything is selected or written; then the
84// verified prepared marker, published under the admitted writer's
85// fence; then the §5.1 start check (moves.StartCheck, not weakened)
86// against the claim after the admission's CAS; only then the spawn.
87// - Publish: Group D's call before replication finds the prepared marker
88// Materialize published and records it; it writes only under the fence
89// Materialize recorded for the session. The marker names the move id
90// with the build's pin and install.json's digest, superseding the
91// current marker (an earlier stay's, which Plan 2 keeps) by its exact
92// ETag, or created when there is none. PublishFenced: a restarted
93// attempt's fresh generation, after its own first root, as serving
94// publishes it. PublicationEvidence: this process's receipt for the
95// admitted session, read back.
96//
97// Production: the moves wake source (movesWakeSources, Plan 5 Task 9b)
98// opens this owner set for the claim with both switches on, and its
99// registration opens the gate that arms the parks.
100type movesProbeOwners struct {
101 HomeRegion, FleetHomeRegion string
102 // Claims is the claim store (the deployment store in production).
103 Claims release.Store
104 Records MovesRecordReader
105 // ReadyPath is the app manifest's moves.ready_path, or "/".
106 ReadyPath func(ctx context.Context, account, app string) (string, error)
107 Data genselect.Reader
108 Bucket, Region, Box string
109 // DataDir is the root of the op journals (moves.JournalDir).
110 DataDir string
111 FilesDir func(host string) (dir, envPath string)
112 Markers ClaimMarkerStore
113 Route func(host string) (*registry.Route, error)
114 // Launch is the held row's launch: the live executable, its digest,
115 // arguments, environment and version (the claim's launch snapshot), and
116 // the moves env report of that same environment, computed at capture
117 // from the one composition that produced the env (claimMoveLaunchSource:
118 // ExecBackend.LaunchLayers, moves.ReportLaunch). The report's Digest is
119 // the launch's move-portable pin. It is the moves owners' own value:
120 // never persisted, and never digested by the claim (fix round 4).
121 Launch func(context.Context, registry.Route) (ClaimServingLaunch, moves.EnvReport, error)
122 // EnvKey is the env report's HMAC key (moves.ReportKeyFromSecrets), the
123 // key Launch's report is made with. The pin is checked against the
124 // report of the selected launch's own environment, so no report can be
125 // decoupled from what runs (fix round 1, finding 3). Normalisation
126 // contract: the report covers the launch's K=V list, last value wins
127 // (childEnv's dedupe), with the probe-only variables (probeOnlyVars:
128 // CARLOS_MOVE_PROBE) removed, since the source never ran with them and
129 // the probe adds CARLOS_MOVE_PROBE=1 after the pin, and its digest is the
130 // move-portable one (moves.PortableDigest). The production Launch is this
131 // box's own composition (claim Plan 10's launch accessor).
132 EnvKey []byte
133 // Admitted is the number of probe attempts T11 admitted (H10).
134 Admitted func(context.Context, *provision.Record) (int, error)
135 // Works selects a held action's claim work at the admitted capture
136 // (hostbox.WorkSource.SelectAt).
137 Works func(ctx context.Context, host, action string, captured hostguard.Captured) (hostwrite.Work, error)
138 Render func(ctx context.Context, rt registry.Route, op string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error)
139 // Release makes version live for the held row before a start. On a box
140 // whose agent cannot write /opt/carlos it is heldReleaseProbeRelease
141 // (Task 7b): root publishes, inside root's own held-release selection.
142 Release func(ctx context.Context, rt registry.Route, version string, work hostwrite.Work) error
143 NewID func() string
144 Now func() time.Time
145 // Page pages an operator for a refused start check, once per op
146 // (movespaging.go, §13): PageOperator once claim Plan 10 registers this
147 // owner set; nil, nothing pages.
148 Page func(ctx context.Context, level, subject, body string) error
149 // Files is the moves C8 files manifest provider (claim §6.2a P6-D3;
150 // Task 6 builds it). The recovery owners' Materialize establishes the
151 // admitted generation's files manifest through it before every start;
152 // without it every recovery kind is refused, typed, before anything is
153 // read (movesrecoveryowners.go).
154 Files ClaimFilesManifest
155 // pageWarned is the pages warned unsent for want of a pager.
156 pageWarned sync.Map
157
158 mu sync.Mutex
159 published map[string]ClaimPublicationReceipt
160 // envPins are the move-portable pins Select captured, by host, each
161 // bound to the exact launch environment it was computed for
162 // (captureEnvPin): what Materialize checks the same inputs against.
163 // Memory only, never persisted, never digested by the claim. Each lives
164 // as long as its wake (holdEnvPin) and never past its deadline.
165 envPins map[string]capturedEnvPin
166 envPinSeq uint64
167 // fences are the admitted spawn selections Materialize ran under, by
168 // host and session: the live fence for the session's marker writes.
169 fences map[string]hostwrite.Work
170 // lossValidated is each host's last LossSources answer, which the
171 // recovery's AdmissionEvidence of the same admission consumes
172 // (movesloss.go).
173 lossValidated map[string]lossValidation
174}
175
176var (
177 _ ClaimWakeInputs = (*movesProbeOwners)(nil)
178 _ ClaimFencedPublication = (*movesProbeOwners)(nil)
179)
180
181// ProbeInputsChanged is a probe refused because a launch input is not the
182// one pinned at readiness. Reason is the landing's recorded rollback
183// sentence (provision.RollbackConfigChanged); the landing records it.
184type ProbeInputsChanged struct{ Reason string }
185
186func (e *ProbeInputsChanged) Error() string {
187 return "host: the probe's launch inputs are not the pinned ones: " + e.Reason
188}
189
190func probeInputsChanged() error {
191 return &ProbeInputsChanged{Reason: provision.RollbackConfigChanged}
192}
193
194// owners is the owner set's ClaimKindOwners.
195func (m *movesProbeOwners) owners() ClaimKindOwners {
196 return ClaimKindOwners{Inputs: m, AdmissionEvidence: m.AdmissionEvidence, PublicationEvidence: m.PublicationEvidence}
197}
198
199// MovesOwners is ClaimMovesOwners for the probe: this owner set and ops,
200// with the moves loss owner (the resolve owner's signed-intent policy,
201// hostops.MoveLossRecoveryOwner) and the loss baseline validator
202// (LossSources, movesloss.go) the claim's writer admits a declared or fenced
203// return through. ProbeReady stays nil, so the claim binds its own probe
204// readiness check (claimProbeSocketReady: the declared path, 2xx only, a
205// peer in the started role's cgroup; H12).
206func (m *movesProbeOwners) MovesOwners(ops OperationAttempts) ClaimMovesOwners {
207 return ClaimMovesOwners{LossRecovery: hostops.MoveLossRecoveryOwner(), LossSources: m.LossSources, ClaimKindOwners: m.owners(), Operations: ops}
208}
209
210func (m *movesProbeOwners) valid() bool {
211 return m != nil && m.Claims != nil && m.Records != nil && m.ReadyPath != nil && m.Data != nil && m.Bucket != "" && m.Region != "" && m.Box != "" &&
212 m.DataDir != "" && m.FilesDir != nil && m.Markers != nil && m.Route != nil && m.Launch != nil && len(m.EnvKey) != 0 && m.Admitted != nil && m.Works != nil &&
213 m.Render != nil && m.Release != nil && m.NewID != nil && m.Now != nil
214}
215
216// probeLanding is what one landing probe's checks read.
217type probeLanding struct {
218 homeRegion string
219 // committed is a landing wake after the commit (Plan 3 Task 7): the
220 // claim at landing/committed, the record committed.
221 committed bool
222 op string
223 holder *hostclaim.Holder
224 rec *provision.Record
225 seal moves.Seal
226 manifest moves.InstallManifest
227}
228
229// landing checks that o is a landing probe this box may run and reads the
230// record, the seal and the journalled install manifest.
231func (m *movesProbeOwners) landing(ctx context.Context, o hostclaim.Observed) (probeLanding, error) {
232 var v probeLanding
233 if !m.valid() {
234 return v, hostwrite.ErrUnavailable
235 }
236 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Op.Kind != hostclaim.KindLanding {
237 return v, hostbox.ErrUnsupported
238 }
239 c := o.Claim
240 var bindingErr error
241 v.homeRegion, bindingErr = park.VerifiedHome(m.FleetHomeRegion, m.HomeRegion, m.Region, c.Op.Intent["home_region"])
242 if bindingErr != nil {
243 return v, bindingErr
244 }
245 switch c.Op.Phase {
246 case hostclaim.PhaseLandingProbing:
247 case hostclaim.PhaseLandingCommitted:
248 v.committed = true
249 default:
250 return v, hostbox.ErrChanged
251 }
252 if c.Host != o.Host || c.Op.ID == "" || c.Op.Exec != m.Box || c.Op.Intent["target"] != m.Box ||
253 c.Op.Intent["source"] == m.Box || !park.SameRegion(m.HomeRegion, c.Op.Intent["to"], m.Region) || len(c.Op.Participants) != 2 || c.Op.Participants[1] != m.Box {
254 return v, hostbox.ErrChanged
255 }
256 for _, lost := range c.Lost {
257 if lost == m.Box {
258 return v, hostbox.ErrChanged
259 }
260 }
261 v.op, v.holder = c.Op.ID, c.Holder
262 rec, err := m.Records(ctx, v.holder.Account, v.holder.App, o.Host)
263 if err != nil {
264 return v, err
265 }
266 if rec == nil || rec.Host != o.Host || rec.Move == nil {
267 return v, hostbox.ErrChanged
268 }
269 mv := rec.Move
270 if park.PlacementRegion(mv.From) == "" || mv.ID != v.op || mv.Incarnation != v.holder.Incarnation || !park.PlacementMatches(m.HomeRegion, mv.To, m.Region) || mv.Inputs == nil || mv.Inputs.Release == "" {
271 return v, hostbox.ErrChanged
272 }
273 if v.committed {
274 if mv.State != provision.MoveCommitted {
275 return v, hostbox.ErrChanged
276 }
277 } else if mv.State != provision.MoveLanding || mv.LandingPhase != provision.LandProbing || mv.Reason != "" {
278 return v, hostbox.ErrChanged
279 }
280 v.rec = rec
281 if v.seal, err = journalledSeal(c); err != nil {
282 return v, err
283 }
284 j, err := moves.ReadJournal(m.DataDir, o.Host, v.op)
285 if err != nil {
286 return v, err
287 }
288 raw, ok := j.Steps[landStepInstall]
289 if !ok {
290 return v, fmt.Errorf("%w: the landing journalled no install manifest", hostbox.ErrChanged)
291 }
292 if err := json.Unmarshal([]byte(raw), &v.manifest); err != nil {
293 return v, fmt.Errorf("the journalled install manifest: %w", err)
294 }
295 if v.manifest.Op != v.op || v.manifest.Host != o.Host || v.manifest.N != v.seal.N || v.manifest.N == 0 || v.manifest.SnapshotKey == "" || v.manifest.SnapshotVersion == "" {
296 return v, fmt.Errorf("%w: the install manifest is not this move's build at the seal", hostbox.ErrChanged)
297 }
298 return v, nil
299}
300
301// heldRoute is the registry row the probe runs: this host, held by the move.
302func heldRoute(rt registry.Route, host, op string) error {
303 if rt.Host != host || !registry.IsHold(rt.Status) || rt.HoldOp != op || !filepath.IsAbs(rt.DBPath) {
304 return hostbox.ErrChanged
305 }
306 return nil
307}
308
309// firstAdmission reports whether the holder's writer is still the move's
310// source: the flip leaves the source's session in place, and every
311// admission on this box makes it the writer. A first admission is the
312// prepared one; any later one is a restarted attempt into a fresh
313// generation. It reads no admission history, so trimming cannot change it.
314func firstAdmission(h *hostclaim.Holder, box string) bool {
315 return h.WriterBox != box
316}
317
318// admissible is the landing's own word that an admission may happen now
319// (fix round 1, finding 1): before the landing's deadline (the earlier of
320// cut_deadline and landing_deadline, H8), at a record attempt within what
321// T11 admitted (H10), over the install the record vouches for.
322func (m *movesProbeOwners) admissible(ctx context.Context, v probeLanding) error {
323 mv := v.rec.Move
324 d, _ := landingDeadlineOf(mv)
325 if d == nil || !m.Now().Before(*d) {
326 return fmt.Errorf("%w: the landing is past its deadline", hostbox.ErrChanged)
327 }
328 admitted, err := m.Admitted(ctx, v.rec)
329 if err != nil {
330 return err
331 }
332 if mv.Landing == nil || mv.Landing.Attempts < 1 || mv.Landing.Attempts > moves.AttemptsFor(moves.ProbeLanding, admitted) {
333 return fmt.Errorf("%w: the record's probe attempt is not one T11 admitted", hostbox.ErrChanged)
334 }
335 if mv.Landing.InstalledDBSHA256 != "sha256:"+v.manifest.DBSHA256 {
336 return fmt.Errorf("%w: the record does not vouch for this install", hostbox.ErrChanged)
337 }
338 return nil
339}
340
341// probeOnlyVars are the variables the probe adds to the launch and the
342// readiness pin never saw.
343var probeOnlyVars = map[string]bool{"CARLOS_MOVE_PROBE": true}
344
345// capturedEnvPin is a launch environment's move-portable pin as Select
346// computed it at capture (Launch's report), bound to that environment by
347// its fingerprint. seq names this capture, so the end of an earlier wake
348// never drops a later one; until is the wake's deadline, after which the
349// entry is gone whether or not the wake ever reached Materialize; held is
350// set once its wake's start loop holds it (holdEnvPin).
351type capturedEnvPin struct {
352 env, pin string
353 seq uint64
354 until time.Time
355 held bool
356}
357
358// envPinHold bounds a captured pin whose wake names no deadline (the
359// committed wake's policy).
360const envPinHold = 15 * time.Minute
361
362// envFingerprint identifies a launch environment exactly: every entry, in
363// order.
364func envFingerprint(env []string) string {
365 sum := sha256.Sum256([]byte(strings.Join(env, "\x00")))
366 return hex.EncodeToString(sum[:])
367}
368
369// captureEnvPin records the pin Select computed for the inputs it returns
370// (env is ClaimLaunchInputs.Env), replacing any earlier one for host. The
371// entry lasts until its wake ends (holdEnvPin) and never past deadline (the
372// wake's StartPolicy.Deadline, or envPinHold when it names none): a wake
373// abandoned before Materialize leaves nothing behind longer than that.
374// Every capture first drops the expired entries.
375func (m *movesProbeOwners) captureEnvPin(host string, env []string, pin string, deadline time.Time) {
376 now := m.now()
377 if deadline.IsZero() {
378 deadline = now.Add(envPinHold)
379 }
380 m.mu.Lock()
381 defer m.mu.Unlock()
382 if m.envPins == nil {
383 m.envPins = map[string]capturedEnvPin{}
384 }
385 m.pruneEnvPins(now)
386 m.envPinSeq++
387 m.envPins[host] = capturedEnvPin{env: envFingerprint(env), pin: pin, seq: m.envPinSeq, until: deadline}
388}
389
390// now is the owners' clock, the wall clock when none is set.
391func (m *movesProbeOwners) now() time.Time {
392 if m.Now == nil {
393 return time.Now()
394 }
395 return m.Now()
396}
397
398// pruneEnvPins drops the entries past their deadline. m.mu is held.
399func (m *movesProbeOwners) pruneEnvPins(now time.Time) {
400 for host, c := range m.envPins {
401 if !now.Before(c.until) {
402 delete(m.envPins, host)
403 }
404 }
405}
406
407// capturedPin is the pin Select captured for exactly env on host; false
408// when Select captured none for it in this process, or it has expired
409// (Materialize then refuses, fail closed).
410func (m *movesProbeOwners) capturedPin(host string, env []string) (string, bool) {
411 m.mu.Lock()
412 defer m.mu.Unlock()
413 m.pruneEnvPins(m.now())
414 c, ok := m.envPins[host]
415 if !ok || c.env != envFingerprint(env) {
416 return "", false
417 }
418 return c.pin, true
419}
420
421// holdEnvPin ties host's captured pin to its wake: ctx is the context the
422// activator's start loop passes Materialize, which it cancels when the
423// wake ends, completed, failed or abandoned. Every retry of the same
424// launch runs under the same context and finds the entry; the end of the
425// wake drops it, and never a later capture for the same host (seq). Held
426// once per capture.
427func (m *movesProbeOwners) holdEnvPin(ctx context.Context, host string) {
428 m.mu.Lock()
429 c, ok := m.envPins[host]
430 if !ok || c.held {
431 m.mu.Unlock()
432 return
433 }
434 c.held = true
435 m.envPins[host] = c
436 m.mu.Unlock()
437 seq := c.seq
438 context.AfterFunc(ctx, func() { m.dropEnvPin(host, seq) })
439}
440
441// dropEnvPin removes host's captured pin if it is still capture seq.
442func (m *movesProbeOwners) dropEnvPin(host string, seq uint64) {
443 m.mu.Lock()
444 defer m.mu.Unlock()
445 if c, ok := m.envPins[host]; ok && c.seq == seq {
446 delete(m.envPins, host)
447 }
448}
449
450// localEnvDigest is the digest of the whole launch environment, every
451// variable but the probe-only ones (moves.LocalDigest): the recovery's
452// check against inputs this same box recorded just before the wake (fix
453// round 2, platform-1e's same-box rule).
454func localEnvDigest(key []byte, env []string) string {
455 return moves.LocalDigest(key, launchVars(env))
456}
457
458// launchVars is a launch's K=V list as a map, last value wins, the
459// probe-only variables dropped.
460func launchVars(env []string) map[string]string {
461 vars := map[string]string{}
462 for _, kv := range env {
463 k, v, ok := strings.Cut(kv, "=")
464 if !ok || probeOnlyVars[k] {
465 continue
466 }
467 vars[k] = v
468 }
469 return vars
470}
471
472// manifestDigest is install.json's digest: the build writes the manifest
473// as json.Marshal of the journalled value (moves.BuildGeneration).
474func manifestDigest(m moves.InstallManifest) (string, error) {
475 raw, err := json.Marshal(m)
476 if err != nil {
477 return "", err
478 }
479 sum := sha256.Sum256(raw)
480 return hex.EncodeToString(sum[:]), nil
481}
482
483func manifestPin(m moves.InstallManifest) genselect.Pin {
484 return genselect.Pin{SnapshotKey: m.SnapshotKey, SnapshotVersion: m.SnapshotVersion, SnapshotTXID: m.N, SnapshotCRC64: m.SnapshotCRC64}
485}
486
487// landingMarker reports whether the location's current marker already
488// names this landing: the move id's verified prepared marker, or a fresh
489// generation proved to descend from the move's admission (descendsFromMove).
490// Anything else (an earlier stay's, though its cold admission on this box
491// and region is still in the history; another incarnation's) is superseded
492// by the prepared one, never taken for a restart (fix round 2, finding 8).
493func (m *movesProbeOwners) landingMarker(marker *genselect.Marker, host string, v probeLanding, digest string) bool {
494 h := v.holder
495 if marker == nil || marker.Incarnation != h.Incarnation || marker.Host != host || marker.Box != m.Box {
496 return false
497 }
498 if marker.Op == v.op {
499 return marker.Pin == manifestPin(v.manifest) && marker.ManifestSHA256 == digest
500 }
501 return m.descendsFromMove(h, marker.Op, v.op)
502}
503
504// descendsFromMove is positive proof that generation g is a restart of this
505// landing: walking back from g, each step a cold admission on this box and
506// region into the current generation, its predecessor chain reaches the
507// move's prepared generation (the admission into the move id itself, which
508// names the op). A chain that leaves this box or region, loops, or is
509// trimmed before the move id is no proof.
510func (m *movesProbeOwners) descendsFromMove(h *hostclaim.Holder, g, op string) bool {
511 seen := map[string]bool{}
512 for g != "" && g != op && !seen[g] {
513 seen[g] = true
514 found := false
515 for i := len(h.Admissions) - 1; i >= 0; i-- {
516 a := h.Admissions[i]
517 if a.Generation != g || a.Mode != "cold" {
518 continue
519 }
520 if a.Box != m.Box || !park.SameRegion(m.HomeRegion, a.Region, m.Region) {
521 return false
522 }
523 g, found = a.Predecessor, true
524 break
525 }
526 if !found {
527 return false
528 }
529 }
530 if g != op {
531 return false
532 }
533 for _, a := range h.Admissions {
534 if a.Generation == op && a.Box == m.Box && park.SameRegion(m.HomeRegion, a.Region, m.Region) && a.Mode == "cold" {
535 return true
536 }
537 }
538 return false
539}
540
541// predecessorRegion is AdmitWriter's predecessor region: the last
542// admission's, or the move's source region before any.
543func predecessorRegion(h *hostclaim.Holder, from string) (string, error) {
544 if !provision.ValidRegion(from) {
545 return "", hostbox.ErrChanged
546 }
547 if n := len(h.Admissions); n > 0 {
548 return h.Admissions[n-1].Region, nil
549 }
550 return park.PlacementRegion(from), nil
551}
552
553func (m *movesProbeOwners) request(host string, h *hostclaim.Holder) genselect.Request {
554 return genselect.Request{Bucket: m.Bucket, Region: m.Region, Host: host, Holder: park.HolderRegionView(*h, m.HomeRegion, m.Region)}
555}
556
557func preparedPin(v probeLanding) genselect.PreparedPin {
558 // The build's own pin, from the journalled manifest.
559 return genselect.PreparedPin{Generation: v.op, SnapshotKey: v.manifest.SnapshotKey, SnapshotVersion: v.manifest.SnapshotVersion, SnapshotTXID: v.manifest.N, SnapshotCRC64: v.manifest.SnapshotCRC64}
560}
561
562func markerKey(h *hostclaim.Holder) string {
563 return strings.TrimSuffix(h.Location, "/") + "/generation.json"
564}
565
566// currentMarker is the location's current generation.json version, or nil.
567func (m *movesProbeOwners) currentMarker(ctx context.Context, h *hostclaim.Holder) (*claimMarkerVersion, error) {
568 return currentMarker(ctx, m.Data, m.Bucket, h)
569}
570
571// currentMarker is the location's current generation.json version, or
572// nil when the key has none; a delete marker, or two latest versions, is
573// ErrChanged. The moves probe owners and region HA's relocation inputs
574// share it (8d fix round 1, M3).
575func currentMarker(ctx context.Context, data genselect.Reader, bucket string, h *hostclaim.Holder) (*claimMarkerVersion, error) {
576 key := markerKey(h)
577 versions, err := data.ListVersions(ctx, bucket, key)
578 if err != nil {
579 return nil, err
580 }
581 var current *claimMarkerVersion
582 for _, v := range versions {
583 if v.Key == key && v.IsLatest {
584 if current != nil || v.DeleteMarker {
585 return nil, hostbox.ErrChanged
586 }
587 current = &claimMarkerVersion{ID: v.VersionID, ETag: v.ETag}
588 }
589 }
590 return current, nil
591}
592
593// baseline is the admission's baseline (H14): the build's pin for the
594// prepared generation; for a restarted attempt, the prepared pin while the
595// location's marker is not this landing's (the move id published nothing
596// beyond the build; an earlier stay's pin is never a baseline), and the
597// certified remote once it is.
598func (m *movesProbeOwners) baseline(ctx context.Context, host string, v probeLanding, prepared bool) (*genselect.Plan, error) {
599 if prepared {
600 return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder), preparedPin(v))
601 }
602 digest, err := manifestDigest(v.manifest)
603 if err != nil {
604 return nil, err
605 }
606 marker, err := m.readMarker(ctx, v.holder)
607 if err != nil {
608 return nil, err
609 }
610 if !m.landingMarker(marker, host, v, digest) {
611 return genselect.SelectPrepared(ctx, m.Data, m.request(host, v.holder), preparedPin(v))
612 }
613 return genselect.Select(ctx, m.Data, m.request(host, v.holder))
614}
615
616// localMarkers reads .files-gen (nil when absent) and <dbPath>.gen ("" when
617// absent).
618func (m *movesProbeOwners) localMarkers(host, dbPath string) (*moves.FilesGen, string, error) {
619 dir, _ := m.FilesDir(host)
620 if !filepath.IsAbs(dir) {
621 return nil, "", hostwrite.ErrUnavailable
622 }
623 var files *moves.FilesGen
624 raw, err := os.ReadFile(filepath.Join(dir, archive.MarkerName))
625 switch {
626 case errors.Is(err, fs.ErrNotExist):
627 case err != nil:
628 return nil, "", err
629 default:
630 var fg moves.FilesGen
631 dec := json.NewDecoder(bytes.NewReader(raw))
632 dec.DisallowUnknownFields()
633 if err := dec.Decode(&fg); err != nil {
634 return nil, "", fmt.Errorf("%w: %v", moves.ErrStartFiles, err)
635 }
636 files = &fg
637 }
638 db, err := hostbox.ReadGenerationMarker(dbPath + ".gen")
639 if err != nil {
640 return nil, "", err
641 }
642 return files, db, nil
643}
644
645// launchIdentity is the probe's spawn identity (H4): the pinned config and
646// release, bound to the record by MovesClaimPolicy.
647func launchIdentity(in *provision.MoveInputs) hostwrite.Identity {
648 return hostwrite.Identity{Config: HeldConfigIdentity(in.BundleETags, in.OverlayETag).Config, Release: in.Release}
649}
650
651// probeEnv is the launch environment with CARLOS_MOVE_PROBE=1 (H18), the
652// one entry for that name.
653func probeEnv(env []string) []string {
654 out := make([]string, 0, len(env)+1)
655 for _, kv := range env {
656 if !strings.HasPrefix(kv, "CARLOS_MOVE_PROBE=") {
657 out = append(out, kv)
658 }
659 }
660 return append(out, "CARLOS_MOVE_PROBE=1")
661}
662
663// Select chooses the probe's launch inputs and admission. It is read-only.
664func (m *movesProbeOwners) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, p activator.StartPolicy) (ClaimLaunchInputs, error) {
665 if recoveryKind(o) {
666 return m.recovery().Select(ctx, rt, o, p)
667 }
668 v, err := m.landing(ctx, o)
669 if err != nil {
670 return ClaimLaunchInputs{}, err
671 }
672 if v.committed {
673 if p.Probe || p.Prepared != nil || p.Inputs != nil {
674 return ClaimLaunchInputs{}, hostbox.ErrUnsupported
675 }
676 return m.selectCommitted(ctx, rt, o, v, p.Deadline)
677 }
678 if !p.Probe {
679 return ClaimLaunchInputs{}, hostbox.ErrUnsupported
680 }
681 pins := v.rec.Move.Inputs
682 if (p.Inputs != nil && !reflect.DeepEqual(*p.Inputs, *pins)) || (p.Prepared != nil && *p.Prepared != v.manifest) {
683 return ClaimLaunchInputs{}, hostbox.ErrChanged
684 }
685 if err := heldRoute(rt, o.Host, v.op); err != nil {
686 return ClaimLaunchInputs{}, err
687 }
688 if err := m.admissible(ctx, v); err != nil {
689 return ClaimLaunchInputs{}, err
690 }
691 h := v.holder
692 prepared := firstAdmission(h, m.Box)
693 predecessor, err := predecessorRegion(h, v.rec.Move.From)
694 if err != nil {
695 return ClaimLaunchInputs{}, err
696 }
697 admission := hostclaim.Admission{ObservedOpID: v.op, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: m.Box, Mode: "cold",
698 KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessor}
699 if prepared {
700 files, db, err := m.localMarkers(o.Host, rt.DBPath)
701 if err != nil {
702 return ClaimLaunchInputs{}, err
703 }
704 if err := moves.PreparedCheck(v.op, *h, files, db); err != nil {
705 return ClaimLaunchInputs{}, err
706 }
707 admission.NewGeneration = v.op
708 if v.seal.FromDB {
709 admission.Coverage = "sealed"
710 }
711 } else {
712 admission.NewGeneration = m.NewID()
713 if admission.NewGeneration == v.op || !claimFreshGeneration(h, admission.NewGeneration) {
714 return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
715 }
716 }
717 plan, err := m.baseline(ctx, o.Host, v, prepared)
718 if err != nil {
719 return ClaimLaunchInputs{}, err
720 }
721 admission.RemoteMaxTXID = plan.MaxTXID
722 ready, err := m.ReadyPath(ctx, h.Account, h.App)
723 if err != nil {
724 return ClaimLaunchInputs{}, err
725 }
726 // The launch-input pin at spawn: the authoritative check, after
727 // readiness's (Plan 2's configured), since the inputs can change between.
728 l, report, err := m.Launch(ctx, rt)
729 if err != nil {
730 return ClaimLaunchInputs{}, err
731 }
732 if !filepath.IsAbs(l.Executable) || !claimHexDigest(l.ExecutableSHA256) || l.Version == "" {
733 return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
734 }
735 if l.Version != pins.Release {
736 return ClaimLaunchInputs{}, probeInputsChanged()
737 }
738 // A v1 pin is refused whatever it hashes: it cannot tell an instance's
739 // box variable from the box's (moves.EnvReportVersion). The launch's own
740 // pin is its report's, computed at capture with its env.
741 if !moves.CurrentEnvPin(pins.EnvReport) || report.Digest != pins.EnvReport {
742 return ClaimLaunchInputs{}, probeInputsChanged()
743 }
744 in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: launchIdentity(pins), Baseline: plan, Admission: admission,
745 Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: probeEnv(l.Env),
746 DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, Prepared: prepared, ReadyPath: ready}
747 m.captureEnvPin(rt.Host, in.Env, report.Digest, p.Deadline)
748 return in, nil
749}
750
751// AdmissionEvidence re-derives Select's admission against the observation
752// and a baseline selected again, immediately before the admission's CAS.
753func (m *movesProbeOwners) AdmissionEvidence(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, baseline *genselect.Plan) error {
754 if recoveryKind(o) {
755 return m.recovery().AdmissionEvidence(ctx, o, in, baseline)
756 }
757 v, err := m.landing(ctx, o)
758 if err != nil {
759 return err
760 }
761 if v.committed {
762 return m.committedEvidence(ctx, o, v, in, baseline)
763 }
764 if err := m.admissible(ctx, v); err != nil {
765 return err
766 }
767 h := v.holder
768 predecessor, err := predecessorRegion(h, v.rec.Move.From)
769 if err != nil {
770 return err
771 }
772 if in.ObservedOpID != v.op || in.ObservedPhase != o.Claim.Op.Phase || in.ObservedSession != h.Session || in.ObservedGeneration != h.Generation || in.Box != m.Box || !park.SameRegion(m.HomeRegion, in.Region, m.Region) ||
773 in.Mode != "cold" || in.PredecessorCertified || in.LossID != "" || len(in.AdditionalWritesLost) != 0 || in.KeepStamp != (h.Stamp > h.Session) ||
774 !park.SameRegion(v.homeRegion, in.PredecessorRegion, predecessor) || baseline == nil || in.RemoteMaxTXID != baseline.MaxTXID {
775 return hostbox.ErrChanged
776 }
777 prepared := firstAdmission(h, m.Box)
778 switch {
779 case prepared:
780 coverage := ""
781 if v.seal.FromDB {
782 coverage = "sealed"
783 }
784 pin := manifestPin(v.manifest)
785 if in.NewGeneration != v.op || in.Coverage != coverage || baseline.Generation != v.op || baseline.Pin == nil || *baseline.Pin != pin ||
786 baseline.MaxTXID != v.manifest.N || v.manifest.N != v.seal.N {
787 return hostbox.ErrChanged
788 }
789 rt, err := m.route(o.Host, v.op)
790 if err != nil {
791 return err
792 }
793 files, db, err := m.localMarkers(o.Host, rt.DBPath)
794 if err != nil {
795 return err
796 }
797 if err := moves.PreparedCheck(v.op, *h, files, db); err != nil {
798 return err
799 }
800 default:
801 if in.NewGeneration == v.op || !claimFreshGeneration(h, in.NewGeneration) || in.Coverage != "" {
802 return hostbox.ErrChanged
803 }
804 }
805 fresh, err := m.baseline(ctx, o.Host, v, prepared)
806 if err != nil {
807 return err
808 }
809 if fresh.Generation != baseline.Generation || fresh.MaxTXID != baseline.MaxTXID || !reflect.DeepEqual(fresh.Files, baseline.Files) || !reflect.DeepEqual(fresh.Pin, baseline.Pin) {
810 return hostbox.ErrChanged
811 }
812 return nil
813}
814
815func (m *movesProbeOwners) route(host, op string) (registry.Route, error) {
816 rt, err := m.Route(host)
817 if err != nil {
818 return registry.Route{}, err
819 }
820 if rt == nil {
821 return registry.Route{}, hostbox.ErrChanged
822 }
823 if err := heldRoute(*rt, host, op); err != nil {
824 return registry.Route{}, err
825 }
826 return *rt, nil
827}
828
829// admittedProbe checks that o is the admitted probe session in's admission
830// made: this box writes the admission's new generation.
831func admittedProbe(o hostclaim.Observed, in ClaimLaunchInputs, box string) error {
832 h := o.Claim.Holder
833 if h.WriterBox != box || h.Generation != in.Admission.NewGeneration || h.Generation == "" {
834 return hostbox.ErrChanged
835 }
836 return nil
837}
838
839// heldIdentityWork runs its work under the held write's own identity.
840type heldIdentityWork struct {
841 Work hostwrite.Work
842 ID hostwrite.Identity
843}
844
845func (w heldIdentityWork) With(ctx context.Context, _ hostwrite.Identity, fn func() error) error {
846 if w.Work == nil {
847 return hostwrite.ErrUnavailable
848 }
849 return w.Work.With(ctx, w.ID, fn)
850}
851
852// Materialize runs before every start of the admitted probe, in the ruled
853// order: the held render (its etags compared with the pins before anything
854// is selected or written) and the held release under their own selections,
855// the launch environment's pin again over the exact env that will run, the
856// verified prepared marker under the admitted writer's fence, then the
857// start check. The spawn follows only if all of it passed.
858func (m *movesProbeOwners) Materialize(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
859 if recoveryKind(o) {
860 return m.recovery().Materialize(ctx, o, in, work)
861 }
862 // The captured pin lives as long as this wake: every start attempt
863 // materializes under the start loop's context, which ends with the
864 // wake, completed, failed or abandoned.
865 m.holdEnvPin(ctx, o.Host)
866 v, err := m.landing(ctx, o)
867 if err != nil {
868 return err
869 }
870 if err := admittedProbe(o, in, m.Box); err != nil {
871 return err
872 }
873 if work == nil {
874 return hostwrite.ErrUnavailable
875 }
876 // Without the C8 provider every landing wake is refused by name before
877 // anything is rendered or released (whole-branch fixes, item 1), as the
878 // recovery owners refuse theirs.
879 if m.Files == nil {
880 return claimFilesRefusal()
881 }
882 if v.committed {
883 return m.materializeCommitted(ctx, o, v, in, work)
884 }
885 pins := v.rec.Move.Inputs
886 if in.Identity != launchIdentity(pins) {
887 return hostbox.ErrChanged
888 }
889 fence := writerFence(work, in.Identity)
890 // The admitted spawn selection still holds under the launch identity.
891 if err := fence(ctx); err != nil {
892 return err
893 }
894 rt, err := m.route(o.Host, v.op)
895 if err != nil {
896 return err
897 }
898 if rt.DBPath != in.DBPath {
899 return hostbox.ErrChanged
900 }
901 h := v.holder
902 captured := hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}
903 hr, err := m.Render(ctx, rt, v.op, func(pre appconfig.HeldRender) (hostwrite.Work, error) {
904 // The pin decides before any selection or write (fix round 1,
905 // finding 4): a changed configuration is the landing's rollback
906 // sentence, never the policy's refusal of the write.
907 if !sameBundle(pre.BundleETags, pins.BundleETags) || pre.OverlayETag != pins.OverlayETag {
908 return nil, probeInputsChanged()
909 }
910 w, err := m.Works(ctx, o.Host, movesActionHeldConfig, captured)
911 if err != nil {
912 return nil, err
913 }
914 return heldIdentityWork{Work: w, ID: HeldConfigIdentity(pre.BundleETags, pre.OverlayETag)}, nil
915 })
916 if err != nil {
917 return err
918 }
919 if !sameBundle(hr.BundleETags, pins.BundleETags) || hr.OverlayETag != pins.OverlayETag {
920 return probeInputsChanged()
921 }
922 released, err := m.Works(ctx, o.Host, movesActionHeldRelease, captured)
923 if err != nil {
924 return err
925 }
926 if err := m.Release(ctx, rt, pins.Release, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: pins.Release}}); err != nil {
927 return err
928 }
929 // The launch Materialize prepares is the one Select pinned: its env's
930 // pin as captured (captureEnvPin), never recomputed from another read.
931 if pin, ok := m.capturedPin(o.Host, in.Env); !ok || !moves.CurrentEnvPin(pins.EnvReport) || pin != pins.EnvReport {
932 return probeInputsChanged()
933 }
934 m.recordFence(o.Host, h.Session, work)
935 receipt, err := m.preparedMarker(ctx, o.Host, v, fence)
936 if err != nil {
937 return err
938 }
939 if in.Prepared {
940 m.record(o.Host, h.Session, receipt)
941 }
942 if err := m.repairFilesGen(ctx, o, v, work, in.Identity); err != nil {
943 return err
944 }
945 // The admitted generation's files manifest through C8 before the start
946 // check (whole-branch fixes, item 1): the probe's takes P's prepared
947 // manifest at the journalled version, stages the env file on disk and
948 // drops the adoption record; a refusal stops the start.
949 if err := m.Files.Establish(ctx, o, in, work); err != nil {
950 return err
951 }
952 return m.startCheck(ctx, o, in)
953}
954
955// writerFence is the admitted writer's live fence over its spawn selection:
956// the attempt's lease, the session, stamp and generation, and the policy,
957// checked afresh on every call (claimLeaseWork over hostbox's selection).
958func writerFence(work hostwrite.Work, id hostwrite.Identity) func(context.Context) error {
959 return func(ctx context.Context) error {
960 if err := ctx.Err(); err != nil {
961 return err
962 }
963 return work.With(ctx, id, func() error { return nil })
964 }
965}
966
967// preparedMarker establishes the verified prepared marker under fence: the
968// install.json the build wrote is the journalled manifest, and the location's
969// generation.json names this landing (left as it is) or is superseded by the
970// move id's marker with the build's pin and that manifest's digest, by
971// If-Match on the current version or If-None-Match when there is none. The
972// store runs fence before every request attempt. It returns the receipt for
973// the move id's marker, or the zero receipt when a later landing generation's
974// marker is current.
975func (m *movesProbeOwners) preparedMarker(ctx context.Context, host string, v probeLanding, fence func(context.Context) error) (ClaimPublicationReceipt, error) {
976 h := v.holder
977 digest, err := manifestDigest(v.manifest)
978 if err != nil {
979 return ClaimPublicationReceipt{}, err
980 }
981 bounded, cancel := context.WithTimeout(ctx, hostbox.RequestLimit)
982 defer cancel()
983 if err := m.verifyInstall(bounded, h, v, digest); err != nil {
984 return ClaimPublicationReceipt{}, err
985 }
986 pin := manifestPin(v.manifest)
987 root := genselect.File{Key: v.manifest.SnapshotKey, VersionID: v.manifest.SnapshotVersion, Level: 9, MinTXID: 1, MaxTXID: v.manifest.N}
988 current, err := m.currentMarker(bounded, h)
989 if err != nil {
990 return ClaimPublicationReceipt{}, err
991 }
992 if current != nil {
993 marker, err := m.readMarker(bounded, h)
994 if err != nil {
995 return ClaimPublicationReceipt{}, err
996 }
997 if m.landingMarker(marker, host, v, digest) {
998 if marker.Op != v.op {
999 return ClaimPublicationReceipt{}, nil
1000 }
1001 return ClaimPublicationReceipt{MarkerVersion: current.ID, Root: root}, nil
1002 }
1003 }
1004 body, err := json.Marshal(genselect.Marker{Op: v.op, Incarnation: h.Incarnation, Host: host, Box: m.Box, InstalledAt: m.Now().UTC(), ManifestSHA256: digest, Pin: pin})
1005 if err != nil {
1006 return ClaimPublicationReceipt{}, err
1007 }
1008 expected := ""
1009 if current != nil {
1010 expected = current.ETag
1011 }
1012 if err := fence(bounded); err != nil {
1013 return ClaimPublicationReceipt{}, err
1014 }
1015 version, err := m.Markers.PutMarker(bounded, m.Bucket, markerKey(h), body, expected, fence)
1016 if err != nil {
1017 return ClaimPublicationReceipt{}, err
1018 }
1019 if version == "" {
1020 return ClaimPublicationReceipt{}, hostwrite.ErrUnavailable
1021 }
1022 return ClaimPublicationReceipt{MarkerVersion: version, Root: root}, nil
1023}
1024
1025// verifyInstall requires the generation's current install.json to be the
1026// journalled manifest, byte for byte (its digest is the marker's).
1027func (m *movesProbeOwners) verifyInstall(ctx context.Context, h *hostclaim.Holder, v probeLanding, digest string) error {
1028 key := moves.InstallManifestKey(moves.GenerationPrefix(h.Location, v.op))
1029 versions, err := m.Data.ListVersions(ctx, m.Bucket, key)
1030 if err != nil {
1031 return err
1032 }
1033 id := ""
1034 for _, ver := range versions {
1035 if ver.Key == key && ver.IsLatest && !ver.DeleteMarker {
1036 id = ver.VersionID
1037 }
1038 }
1039 if id == "" {
1040 return fmt.Errorf("%w: the generation has no install.json", hostbox.ErrChanged)
1041 }
1042 raw, err := m.Data.GetVersion(ctx, m.Bucket, key, id)
1043 if err != nil {
1044 return err
1045 }
1046 sum := sha256.Sum256(raw)
1047 if hex.EncodeToString(sum[:]) != digest {
1048 return fmt.Errorf("%w: install.json is not the journalled manifest", hostbox.ErrChanged)
1049 }
1050 return nil
1051}
1052
1053// startCheck is §5.1's check against the claim after the admission's CAS,
1054// read fresh: generation.json from this region's location, .files-gen and
1055// <dbPath>.gen from this box.
1056func (m *movesProbeOwners) startCheck(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error {
1057 fresh, err := hostclaim.Read(ctx, m.Claims, o.Host)
1058 if err != nil {
1059 return err
1060 }
1061 if !claimSameWriter(o, fresh, hostclaim.SessionContinues) {
1062 return hostbox.ErrChanged
1063 }
1064 marker, err := m.readMarker(ctx, fresh.Claim.Holder)
1065 if err != nil {
1066 return err
1067 }
1068 files, db, err := m.localMarkers(o.Host, in.DBPath)
1069 if err != nil {
1070 return err
1071 }
1072 if err := moves.StartCheck(fresh.Claim, marker, files, db); err != nil {
1073 // A refused start check pages an operator once per op (§13,
1074 // movespaging.go), and the agent's log says what became of the page.
1075 op := fresh.Claim.Op.ID
1076 pagingLogf("moves: %s: %s refused to start %s for %s (%s): %v", o.Host, m.Box, o.Host, op, m.pageStartRefused(ctx, o.Host, op), err)
1077 return err
1078 }
1079 return nil
1080}
1081
1082// readMarker is the location's current generation.json, nil when absent.
1083func (m *movesProbeOwners) readMarker(ctx context.Context, h *hostclaim.Holder) (*genselect.Marker, error) {
1084 current, err := m.currentMarker(ctx, h)
1085 if err != nil || current == nil {
1086 return nil, err
1087 }
1088 raw, err := m.Data.GetVersion(ctx, m.Bucket, markerKey(h), current.ID)
1089 if err != nil {
1090 return nil, err
1091 }
1092 var marker genselect.Marker
1093 if err := json.Unmarshal(raw, &marker); err != nil {
1094 return nil, fmt.Errorf("%w: %v", moves.ErrStartMarker, err)
1095 }
1096 return &marker, nil
1097}
1098
1099// EmptyAuthority: a move never admits the empty bootstrap, a recovery
1100// included.
1101func (m *movesProbeOwners) EmptyAuthority(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error {
1102 if recoveryKind(o) {
1103 return m.recovery().EmptyAuthority(ctx, o, in)
1104 }
1105 return hostbox.ErrUnsupported
1106}
1107
1108// Publish is Group D's call for a prepared admission, before replication.
1109// Materialize has already published the verified prepared marker; this
1110// finds it and records the receipt. It writes only under the fence
1111// Materialize recorded for the admitted session, and refuses without one.
1112func (m *movesProbeOwners) Publish(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error {
1113 if recoveryKind(o) {
1114 return m.recovery().Publish(ctx, o, in)
1115 }
1116 v, err := m.landing(ctx, o)
1117 if err != nil {
1118 return err
1119 }
1120 if err := admittedProbe(o, in, m.Box); err != nil {
1121 return err
1122 }
1123 h := v.holder
1124 if !in.Prepared || in.Admission.NewGeneration != v.op {
1125 return hostbox.ErrChanged
1126 }
1127 work := m.fence(o.Host, h.Session)
1128 if work == nil {
1129 return fmt.Errorf("%w: no admitted writer's fence for session %d", hostbox.ErrChanged, h.Session)
1130 }
1131 receipt, err := m.preparedMarker(ctx, o.Host, v, writerFence(work, in.Identity))
1132 if err != nil {
1133 return err
1134 }
1135 if receipt.MarkerVersion == "" {
1136 return hostbox.ErrChanged
1137 }
1138 m.record(o.Host, h.Session, receipt)
1139 return nil
1140}
1141
1142// PublishFenced writes a restarted attempt's fresh generation marker after
1143// the session's own first root, under the publisher's fence, as the claim's
1144// serving publisher does (claimServingOwners.PublishFenced).
1145func (m *movesProbeOwners) PublishFenced(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, pub ClaimPublication) (ClaimPublicationReceipt, error) {
1146 if recoveryKind(o) {
1147 return m.recovery().PublishFenced(ctx, o, in, pub)
1148 }
1149 v, err := m.landing(ctx, o)
1150 if err != nil {
1151 return ClaimPublicationReceipt{}, err
1152 }
1153 if err := admittedProbe(o, in, m.Box); err != nil {
1154 return ClaimPublicationReceipt{}, err
1155 }
1156 if pub.Sync == nil || pub.Check == nil {
1157 return ClaimPublicationReceipt{}, hostwrite.ErrUnavailable
1158 }
1159 h := v.holder
1160 if in.Prepared || h.Generation == v.op {
1161 return ClaimPublicationReceipt{}, hostbox.ErrChanged
1162 }
1163 // The admitted writer's spawn selection, recorded by Materialize: the
1164 // fence the local files marker is refreshed under (fix round 1).
1165 work := m.fence(o.Host, h.Session)
1166 if work == nil {
1167 return ClaimPublicationReceipt{}, fmt.Errorf("%w: no admitted writer's fence for session %d", hostbox.ErrChanged, h.Session)
1168 }
1169 // The files on disk must be the landing's before anything is published,
1170 // and their content is recorded as this generation's install record
1171 // (fix round 3): the digest the refresh, and any later repair, compare.
1172 var digest string
1173 if err := work.With(ctx, in.Identity, func() error {
1174 if err := m.verifyFiles(o.Host, v); err != nil {
1175 return err
1176 }
1177 var err error
1178 digest, err = m.filesDigest(o.Host)
1179 return err
1180 }); err != nil {
1181 return ClaimPublicationReceipt{}, err
1182 }
1183 if err := m.recordFilesDigest(o.Host, v.op, h.Generation, digest); err != nil {
1184 return ClaimPublicationReceipt{}, err
1185 }
1186 return m.publishFresh(ctx, o.Host, h, pub, func() error { return m.refreshFilesGen(ctx, o.Host, v, work, in.Identity, digest) })
1187}
1188
1189// publishFresh is the fenced publication of the holder's fresh generation,
1190// shared by the landing's restarted attempt and the recovery owners: the
1191// session's replicator synced, the generation's own first root selected,
1192// then generation.json naming the generation superseding the current marker
1193// by its exact ETag (created when there is none), pub.Check immediately
1194// before the write and before every attempt. A replay that finds this
1195// generation's marker from this box records it. refresh runs after the
1196// marker, before the receipt is recorded (the local files marker).
1197func (m *movesProbeOwners) publishFresh(ctx context.Context, host string, h *hostclaim.Holder, pub ClaimPublication, refresh func() error) (ClaimPublicationReceipt, error) {
1198 if err := pub.Sync(ctx); err != nil {
1199 return ClaimPublicationReceipt{}, err
1200 }
1201 bounded, cancel := context.WithTimeout(ctx, hostbox.RequestLimit)
1202 defer cancel()
1203 plan, err := genselect.Select(bounded, m.Data, m.request(host, h))
1204 if err != nil {
1205 return ClaimPublicationReceipt{}, err
1206 }
1207 // Only this session's bound replica client writes under the fresh
1208 // generation, so its root is the session's own acknowledged upload.
1209 if plan.Generation != h.Generation || plan.Root == nil || plan.Root.MinTXID != 1 || plan.Root.VersionID == "" {
1210 return ClaimPublicationReceipt{}, hostbox.ErrChanged
1211 }
1212 root := *plan.Root
1213 current, err := m.currentMarker(bounded, h)
1214 if err != nil {
1215 return ClaimPublicationReceipt{}, err
1216 }
1217 if current != nil && plan.Marker != nil && plan.MarkerVersion == current.ID && plan.Marker.Op == h.Generation {
1218 if plan.Marker.Box != m.Box || plan.Marker.Incarnation != h.Incarnation {
1219 return ClaimPublicationReceipt{}, hostbox.ErrChanged
1220 }
1221 receipt := ClaimPublicationReceipt{MarkerVersion: current.ID, Root: root}
1222 if err := refresh(); err != nil {
1223 return ClaimPublicationReceipt{}, err
1224 }
1225 m.record(host, h.Session, receipt)
1226 return receipt, nil
1227 }
1228 body, err := json.Marshal(genselect.Marker{Op: h.Generation, Incarnation: h.Incarnation, Host: host, Box: m.Box, InstalledAt: m.Now().UTC()})
1229 if err != nil {
1230 return ClaimPublicationReceipt{}, err
1231 }
1232 expected := ""
1233 if current != nil {
1234 expected = current.ETag
1235 }
1236 if err := pub.Check(bounded); err != nil {
1237 return ClaimPublicationReceipt{}, err
1238 }
1239 version, err := m.Markers.PutMarker(bounded, m.Bucket, markerKey(h), body, expected, pub.Check)
1240 if err != nil {
1241 return ClaimPublicationReceipt{}, err
1242 }
1243 if version == "" {
1244 return ClaimPublicationReceipt{}, hostwrite.ErrUnavailable
1245 }
1246 receipt := ClaimPublicationReceipt{MarkerVersion: version, Root: root}
1247 if err := refresh(); err != nil {
1248 return ClaimPublicationReceipt{}, err
1249 }
1250 m.record(host, h.Session, receipt)
1251 return receipt, nil
1252}
1253
1254// recordFence keeps the admitted spawn selection Materialize ran under, by
1255// host and session: the writer's fence for that session's marker writes and
1256// its files marker refresh.
1257func (m *movesProbeOwners) recordFence(host string, session uint64, work hostwrite.Work) {
1258 m.mu.Lock()
1259 defer m.mu.Unlock()
1260 if m.fences == nil {
1261 m.fences = map[string]hostwrite.Work{}
1262 }
1263 m.fences[claimPublishedKey(host, session)] = work
1264}
1265
1266// fence is the admitted spawn selection Materialize recorded for the
1267// session, or nil.
1268func (m *movesProbeOwners) fence(host string, session uint64) hostwrite.Work {
1269 m.mu.Lock()
1270 defer m.mu.Unlock()
1271 return m.fences[claimPublishedKey(host, session)]
1272}
1273
1274// recovery is the recovery owners over this owner set's sources.
1275func (m *movesProbeOwners) recovery() movesRecoveryOwners { return movesRecoveryOwners{m: m} }
1276
1277// verifyFiles requires the files directory and <host>.env on disk to be the
1278// ones the landing placed: the install receipt's identities
1279// (landStepFilesPlaced). Anything else fails the publication.
1280func (m *movesProbeOwners) verifyFiles(host string, v probeLanding) error {
1281 j, err := moves.ReadJournal(m.DataDir, host, v.op)
1282 if err != nil {
1283 return err
1284 }
1285 placed, err := journalledStaged(j, landStepFilesPlaced)
1286 if err != nil {
1287 return fmt.Errorf("%w: %v", hostbox.ErrChanged, err)
1288 }
1289 dir, env := m.FilesDir(host)
1290 if ok, err := sameIdentity(dir, placed.Files); err != nil || !ok {
1291 return fmt.Errorf("%w: %s is not the files directory the landing placed (%v)", hostbox.ErrChanged, dir, err)
1292 }
1293 if placed.Env != "" {
1294 if ok, err := sameIdentity(env, placed.Env); err != nil || !ok {
1295 return fmt.Errorf("%w: %s is not the environment file the landing placed (%v)", hostbox.ErrChanged, env, err)
1296 }
1297 }
1298 return nil
1299}
1300
1301// refreshFilesGen points .files-gen at the published generation (fix round
1302// 1, finding 1), inside the admitted writer's fence, after the files on disk
1303// are proved the landing's again: the files are the same bytes the move
1304// installed, and the generation that now owns them is the one just
1305// published. Task 4's start check is unchanged: the next admission's cold
1306// predecessor is this generation. <dbPath>.gen needs no refresh: every cold
1307// restore rewrites it to the admitted generation (H11).
1308func (m *movesProbeOwners) refreshFilesGen(ctx context.Context, host string, v probeLanding, work hostwrite.Work, id hostwrite.Identity, digest string) error {
1309 h := v.holder
1310 marker, err := json.Marshal(moves.FilesGen{Op: h.Generation, Incarnation: h.Incarnation})
1311 if err != nil {
1312 return err
1313 }
1314 dir, _ := m.FilesDir(host)
1315 return work.With(ctx, id, func() error {
1316 if err := m.verifyFilesContent(host, v, digest); err != nil {
1317 return err
1318 }
1319 if err := syncPath(dir); err != nil {
1320 return err
1321 }
1322 return writeFileAtomic(filepath.Join(dir, archive.MarkerName), marker, 0o644)
1323 })
1324}
1325
1326// filesPlainStep is the op journal step a published generation's files
1327// digest is recorded under: its install record (fix round 3).
1328const filesPlainStep = "files_plain_"
1329
1330// filesDigest is the files tree's plain digest, the landing's own measure
1331// (archive.PlainDigest over the files directory and <host>.env; .files-gen
1332// is not part of it). The tree is capped at 256 MiB.
1333func (m *movesProbeOwners) filesDigest(host string) (string, error) {
1334 dir, env := m.FilesDir(host)
1335 plain, _, _, err := archivePlainDigest(dir, env)
1336 if err != nil {
1337 return "", err
1338 }
1339 if plain == "" {
1340 return "", fmt.Errorf("%w: the files tree has no digest", hostbox.ErrChanged)
1341 }
1342 return plain, nil
1343}
1344
1345// recordFilesDigest journals generation g's files digest, the install record
1346// a later repair compares. A different digest already recorded for g is a
1347// conflict.
1348func (m *movesProbeOwners) recordFilesDigest(host, op, g, digest string) error {
1349 m.mu.Lock()
1350 defer m.mu.Unlock()
1351 // Under the journal's one lock (moves.UpdateJournal): the landing's pass
1352 // and the paging rules write this journal too.
1353 err := moves.UpdateJournal(m.DataDir, host, op, func(j *moves.Journal) error {
1354 if have, ok := j.Steps[filesPlainStep+g]; ok {
1355 if have != digest {
1356 return fmt.Errorf("%w: generation %s recorded other files", hostbox.ErrChanged, g)
1357 }
1358 return errJournalUnchanged
1359 }
1360 j.Mark(filesPlainStep+g, digest)
1361 return nil
1362 })
1363 if errors.Is(err, errJournalUnchanged) {
1364 return nil
1365 }
1366 return err
1367}
1368
1369// errJournalUnchanged ends a moves.UpdateJournal that has nothing to write;
1370// its caller treats it as success.
1371var errJournalUnchanged = errors.New("moves journal: nothing to write")
1372
1373// recordedFilesDigest is generation g's recorded files digest: the move id's
1374// is the source's stopped baseline the landing installed (Move.Source.Files);
1375// a later generation's is the one its publication journalled. false when
1376// none is recorded.
1377func (m *movesProbeOwners) recordedFilesDigest(host string, v probeLanding, g string) (string, bool) {
1378 if g == v.op {
1379 src := v.rec.Move.Source
1380 if src == nil || src.Files == nil || src.Files.Plain == "" {
1381 return "", false
1382 }
1383 return src.Files.Plain, true
1384 }
1385 j, err := moves.ReadJournal(m.DataDir, host, v.op)
1386 if err != nil {
1387 return "", false
1388 }
1389 d, ok := j.Steps[filesPlainStep+g]
1390 return d, ok && d != ""
1391}
1392
1393// verifyFilesContent requires both the landing's placement (identity) and
1394// the recorded content (the plain digest): never identity alone.
1395func (m *movesProbeOwners) verifyFilesContent(host string, v probeLanding, want string) error {
1396 if want == "" {
1397 return fmt.Errorf("%w: no recorded files digest", hostbox.ErrChanged)
1398 }
1399 if err := m.verifyFiles(host, v); err != nil {
1400 return err
1401 }
1402 got, err := m.filesDigest(host)
1403 if err != nil {
1404 return err
1405 }
1406 if got != want {
1407 return fmt.Errorf("%w: the files on disk are not the recorded ones", hostbox.ErrChanged)
1408 }
1409 return nil
1410}
1411
1412func (m *movesProbeOwners) record(host string, session uint64, receipt ClaimPublicationReceipt) {
1413 m.mu.Lock()
1414 defer m.mu.Unlock()
1415 if m.published == nil {
1416 m.published = map[string]ClaimPublicationReceipt{}
1417 }
1418 m.published[claimPublishedKey(host, session)] = receipt
1419}
1420
1421// PublicationEvidence requires this process's receipt for the admitted
1422// session: the current marker is exactly the version it wrote, naming the
1423// holder's generation, incarnation, host and this box (and, for the move
1424// id, the build's pin), and the root it named still exists, current.
1425func (m *movesProbeOwners) PublicationEvidence(ctx context.Context, o hostclaim.Observed) error {
1426 if recoveryKind(o) {
1427 return m.recovery().PublicationEvidence(ctx, o)
1428 }
1429 v, err := m.landing(ctx, o)
1430 if err != nil {
1431 return err
1432 }
1433 return m.publicationEvidence(ctx, o.Host, v.holder, v.op, v.manifest)
1434}
1435
1436// publicationEvidence is PublicationEvidence's check of the admitted
1437// session's receipt, shared with the recovery owners. prepared names the
1438// prepared generation whose marker also carries manifest's pin and digest
1439// ("" for none).
1440func (m *movesProbeOwners) publicationEvidence(ctx context.Context, host string, h *hostclaim.Holder, prepared string, manifest moves.InstallManifest) error {
1441 if h.WriterBox != m.Box {
1442 return hostbox.ErrChanged
1443 }
1444 m.mu.Lock()
1445 receipt, ok := m.published[claimPublishedKey(host, h.Session)]
1446 m.mu.Unlock()
1447 prefix, err := hostbox.GenerationPrefix(*h)
1448 if !ok || err != nil || receipt.MarkerVersion == "" || receipt.Root.VersionID == "" || !strings.HasPrefix(receipt.Root.Key, prefix) {
1449 return hostbox.ErrChanged
1450 }
1451 plan, err := genselect.Select(ctx, m.Data, m.request(host, h))
1452 if err != nil {
1453 return err
1454 }
1455 if plan.Generation != h.Generation || plan.Marker == nil || plan.MarkerVersion != receipt.MarkerVersion || plan.Marker.Op != h.Generation || plan.Marker.Box != m.Box || plan.Marker.Incarnation != h.Incarnation || plan.Marker.Host != host {
1456 return hostbox.ErrChanged
1457 }
1458 if prepared != "" && h.Generation == prepared {
1459 digest, err := manifestDigest(manifest)
1460 if err != nil {
1461 return err
1462 }
1463 if plan.Marker.Pin != manifestPin(manifest) || plan.Marker.ManifestSHA256 != digest {
1464 return hostbox.ErrChanged
1465 }
1466 }
1467 versions, err := m.Data.ListVersions(ctx, m.Bucket, receipt.Root.Key)
1468 if err != nil {
1469 return err
1470 }
1471 exact, current := false, false
1472 for _, ver := range versions {
1473 if ver.Key != receipt.Root.Key || ver.DeleteMarker {
1474 continue
1475 }
1476 exact = exact || ver.VersionID == receipt.Root.VersionID
1477 current = current || ver.IsLatest
1478 }
1479 if !exact || !current {
1480 return hostbox.ErrChanged
1481 }
1482 return nil
1483}
1484
1485// The committed wake (Plan 3 Task 7; Task 5's concern). After the commit the
1486// claim stays landing/committed until the source's finalization closes the
1487// move, and Group D admits a landing wake there (K6's committed row). A wake
1488// happens only when no process serves the host: the probe's own entry, while
1489// it lives, is the serving process and is never restarted. The wake is a
1490// cold admission into a fresh generation from the certified remote (the
1491// landing's published marker, genselect.Select), continuing the admitted
1492// session's generation: no kept stamp, no coverage. It launches the row's
1493// ordinary launch (the claim's launch snapshot, its own identity, which
1494// MovesClaimPolicyWithInputs's committed check accepts), never the probe
1495// variable, and the held inputs are not rendered again: after the open the
1496// row is config sync's.
1497
1498// committedRoute is the row a committed wake runs: this host's, held by the
1499// move until the open, then ready.
1500func committedRoute(rt registry.Route, host, op string) error {
1501 if rt.Host != host || !filepath.IsAbs(rt.DBPath) {
1502 return hostbox.ErrChanged
1503 }
1504 if (registry.IsHold(rt.Status) && rt.HoldOp == op) || (rt.Status == "ready" && rt.HoldOp == "") {
1505 return nil
1506 }
1507 return hostbox.ErrChanged
1508}
1509
1510// withoutProbeVars is a launch environment with the probe-only variables
1511// removed.
1512func withoutProbeVars(env []string) []string {
1513 out := make([]string, 0, len(env))
1514 for _, kv := range env {
1515 if k, _, _ := strings.Cut(kv, "="); !probeOnlyVars[k] {
1516 out = append(out, kv)
1517 }
1518 }
1519 return out
1520}
1521
1522func (m *movesProbeOwners) committedAdmission(o hostclaim.Observed, v probeLanding, gen string, plan *genselect.Plan) (hostclaim.Admission, error) {
1523 h := v.holder
1524 predecessor, err := predecessorRegion(h, v.rec.Move.From)
1525 if err != nil {
1526 return hostclaim.Admission{}, err
1527 }
1528 return hostclaim.Admission{ObservedOpID: v.op, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: m.Box, Mode: "cold",
1529 NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: m.Region, PredecessorRegion: predecessor, RemoteMaxTXID: plan.MaxTXID}, nil
1530}
1531
1532func (m *movesProbeOwners) selectCommitted(ctx context.Context, rt registry.Route, o hostclaim.Observed, v probeLanding, deadline time.Time) (ClaimLaunchInputs, error) {
1533 if err := committedRoute(rt, o.Host, v.op); err != nil {
1534 return ClaimLaunchInputs{}, err
1535 }
1536 h := v.holder
1537 if h.WriterBox != m.Box {
1538 return ClaimLaunchInputs{}, hostbox.ErrChanged
1539 }
1540 gen := m.NewID()
1541 if gen == v.op || !claimFreshGeneration(h, gen) {
1542 return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
1543 }
1544 plan, err := genselect.Select(ctx, m.Data, m.request(o.Host, h))
1545 if err != nil {
1546 return ClaimLaunchInputs{}, err
1547 }
1548 l, report, err := m.Launch(ctx, rt)
1549 if err != nil {
1550 return ClaimLaunchInputs{}, err
1551 }
1552 if !filepath.IsAbs(l.Executable) || !claimHexDigest(l.ExecutableSHA256) || l.Version == "" {
1553 return ClaimLaunchInputs{}, hostwrite.ErrUnavailable
1554 }
1555 // The committed wake runs the move's pinned inputs (InputPolicy), judged
1556 // on the launch itself: its environment's report and its release.
1557 pins := v.rec.Move.Inputs
1558 id := committedLaunchIdentity(report.Digest, l.Version)
1559 if !moves.CurrentEnvPin(pins.EnvReport) || id != pinnedLaunchIdentity(pins) {
1560 return ClaimLaunchInputs{}, fmt.Errorf("%w: the launch is not the move's pinned inputs", hostbox.ErrChanged)
1561 }
1562 admission, err := m.committedAdmission(o, v, gen, plan)
1563 if err != nil {
1564 return ClaimLaunchInputs{}, err
1565 }
1566 in := ClaimLaunchInputs{Roles: []string{"main"}, Identity: id, Baseline: plan, Admission: admission,
1567 Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: withoutProbeVars(l.Env),
1568 DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit}
1569 m.captureEnvPin(rt.Host, in.Env, report.Digest, deadline)
1570 return in, nil
1571}
1572
1573func (m *movesProbeOwners) committedEvidence(ctx context.Context, o hostclaim.Observed, v probeLanding, in hostclaim.Admission, baseline *genselect.Plan) error {
1574 h := v.holder
1575 if baseline == nil || h.WriterBox != m.Box || in.NewGeneration == v.op || !claimFreshGeneration(h, in.NewGeneration) || in.PredecessorCertified || in.LossID != "" || len(in.AdditionalWritesLost) != 0 {
1576 return hostbox.ErrChanged
1577 }
1578 want, err := m.committedAdmission(o, v, in.NewGeneration, baseline)
1579 if err != nil {
1580 return err
1581 }
1582 if !reflect.DeepEqual(in, want) {
1583 return hostbox.ErrChanged
1584 }
1585 fresh, err := genselect.Select(ctx, m.Data, m.request(o.Host, h))
1586 if err != nil {
1587 return err
1588 }
1589 if fresh.Generation != baseline.Generation || fresh.MaxTXID != baseline.MaxTXID || !reflect.DeepEqual(fresh.Files, baseline.Files) || !reflect.DeepEqual(fresh.Pin, baseline.Pin) {
1590 return hostbox.ErrChanged
1591 }
1592 return nil
1593}
1594
1595func (m *movesProbeOwners) materializeCommitted(ctx context.Context, o hostclaim.Observed, v probeLanding, in ClaimLaunchInputs, work hostwrite.Work) error {
1596 // The identity is the launch that will run (in.Env, in.Version), and it
1597 // is the pins'.
1598 pin, ok := m.capturedPin(o.Host, in.Env)
1599 if !ok || in.Identity != committedLaunchIdentity(pin, in.Version) || in.Identity != pinnedLaunchIdentity(v.rec.Move.Inputs) {
1600 return hostbox.ErrChanged
1601 }
1602 if err := writerFence(work, in.Identity)(ctx); err != nil {
1603 return err
1604 }
1605 m.recordFence(o.Host, v.holder.Session, work)
1606 rt, err := m.Route(o.Host)
1607 if err != nil {
1608 return err
1609 }
1610 if rt == nil || rt.DBPath != in.DBPath {
1611 return hostbox.ErrChanged
1612 }
1613 if err := committedRoute(*rt, o.Host, v.op); err != nil {
1614 return err
1615 }
1616 if err := m.repairFilesGen(ctx, o, v, work, in.Identity); err != nil {
1617 return err
1618 }
1619 // The fresh generation's files manifest through C8, copied from the
1620 // admitted predecessor's, before the start check (item 1).
1621 if err := m.Files.Establish(ctx, o, in, work); err != nil {
1622 return err
1623 }
1624 return m.startCheck(ctx, o, in)
1625}
1626
1627// InputPolicy is the moves owner's input policy: the entry point claim Plan
1628// 10 rev 5's ClaimOwnerDispatch routes a move kind's input check to (the
1629// shape of ClaimOwnerBindings.InputPolicy; a move kind never falls back to
1630// serving's inputs). It answers the committed wake's spawn (Plan 3 Task 7):
1631// at landing/committed the launch identity, computed by the owner from the
1632// launch that will run (committedLaunchIdentity: its environment's report
1633// digest and its release), must be the move's pins' (Move.Inputs.EnvReport
1634// and Move.Inputs.Release), over a committed record of this move. The
1635// other move-kind inputs are MovesClaimPolicy's own rows; anything else is
1636// refused here.
1637func (m *movesProbeOwners) InputPolicy(ctx context.Context, c *hostclaim.Claim, s hostbox.Selection, id hostwrite.Identity) error {
1638 if m == nil || m.Records == nil || ctx == nil {
1639 return hostwrite.ErrUnavailable
1640 }
1641 if c == nil || c.Holder == nil || c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseLandingCommitted ||
1642 s.Phase != hostclaim.PhaseLandingCommitted || s.Action != movesActionSpawn || s.Op != c.Op.ID || s.Host != c.Host {
1643 return hostbox.ErrChanged
1644 }
1645 rec, err := m.Records(ctx, c.Holder.Account, c.Holder.App, c.Host)
1646 if err != nil {
1647 return err
1648 }
1649 if rec == nil || rec.Host != c.Host || rec.Move == nil || rec.Move.ID != c.Op.ID || rec.Move.Incarnation != c.Holder.Incarnation ||
1650 rec.Move.State != provision.MoveCommitted || rec.Move.Inputs == nil || rec.Move.Inputs.Release == "" {
1651 return hostbox.ErrChanged
1652 }
1653 if !moves.CurrentEnvPin(rec.Move.Inputs.EnvReport) || id != pinnedLaunchIdentity(rec.Move.Inputs) {
1654 return fmt.Errorf("%w: the committed launch is not the move's pinned inputs", hostbox.ErrChanged)
1655 }
1656 return nil
1657}
1658
1659// committedEnvPrefix marks a committed launch's config identity: the report
1660// digest of its environment.
1661const committedEnvPrefix = "launch-env:"
1662
1663// committedLaunchIdentity is the committed wake's identity, computed from
1664// the launch that will run (fix round 2, item 2): its environment's
1665// move-portable pin as captured with it (Launch's report, CARLOS_MOVE_PROBE
1666// excluded) and its release. The input policy compares it with the pins.
1667func committedLaunchIdentity(pin, version string) hostwrite.Identity {
1668 return hostwrite.Identity{Config: committedEnvPrefix + pin, Release: version}
1669}
1670
1671// pinnedLaunchIdentity is the identity a launch matching the move's pins
1672// carries.
1673func pinnedLaunchIdentity(in *provision.MoveInputs) hostwrite.Identity {
1674 return hostwrite.Identity{Config: committedEnvPrefix + in.EnvReport, Release: in.Release}
1675}
1676
1677// repairFilesGen brings .files-gen to the admitted generation's cold
1678// predecessor T before the start check, inside the admitted writer's fence,
1679// in two cases, each verified by content (the files tree's plain digest
1680// against a recorded one, with the landing's placement), never by identity
1681// alone:
1682//
1683// - an interrupted refresh (Task 7 fix round 2): the published marker
1684// names T itself, and .files-gen names an older generation of T's
1685// ancestry; the files must match T's recorded digest.
1686// - unpublished cold admissions (final review, ruling 1): the published
1687// marker names an older generation P, and T reaches P through cold
1688// admissions on this box and region, none of which published (a
1689// publication would have superseded P's marker); .files-gen names P or
1690// one of those admissions. The files must match P's recorded digest:
1691// every admission since P restored from P's data and its files are P's.
1692//
1693// Anything else is left alone, for the unchanged start check to decide.
1694func (m *movesProbeOwners) repairFilesGen(ctx context.Context, o hostclaim.Observed, v probeLanding, work hostwrite.Work, id hostwrite.Identity) error {
1695 h := o.Claim.Holder
1696 marker, err := m.readMarker(ctx, h)
1697 if err != nil || marker == nil {
1698 return err
1699 }
1700 published := marker.Op
1701 if published == "" || marker.Incarnation != h.Incarnation || marker.Host != o.Host {
1702 return nil
1703 }
1704 t, ok := coldPredecessor(h, h.Generation)
1705 if !ok || t == "" {
1706 return nil
1707 }
1708 dir, _ := m.FilesDir(o.Host)
1709 files, _, err := m.localMarkers(o.Host, filepath.Join(dir, ".none"))
1710 if err != nil || files == nil || files.Op == t || files.Incarnation != h.Incarnation {
1711 return nil
1712 }
1713 var vouch string // the published generation whose recorded digest the files must match
1714 switch {
1715 case published == t:
1716 if !ancestorOf(h, t, files.Op) {
1717 return nil
1718 }
1719 vouch = t
1720 default:
1721 path, ok := m.unpublishedPath(h, t, published)
1722 if !ok || !containsString(path[1:], files.Op) {
1723 return nil
1724 }
1725 vouch = published
1726 }
1727 want, ok := m.recordedFilesDigest(o.Host, v, vouch)
1728 if !ok {
1729 return nil // nothing recorded: the start check decides
1730 }
1731 body, err := json.Marshal(moves.FilesGen{Op: t, Incarnation: h.Incarnation})
1732 if err != nil {
1733 return err
1734 }
1735 return work.With(ctx, id, func() error {
1736 if m.verifyFilesContent(o.Host, v, want) != nil {
1737 return nil // not the vouching generation's files: the start check decides
1738 }
1739 if err := syncPath(dir); err != nil {
1740 return err
1741 }
1742 log.Printf("moves: %s: .files-gen repaired from %s to %s (vouched by the published %s)", o.Host, files.Op, t, vouch)
1743 return writeFileAtomic(filepath.Join(dir, archive.MarkerName), body, 0o644)
1744 })
1745}
1746
1747// unpublishedPath walks from back to the published generation to through
1748// cold admissions, each into the generation walked and on this box and
1749// region. It returns the generations from from to to, inclusive, or false
1750// when the chain leaves this box or region, breaks, loops, or never
1751// reaches to.
1752func (m *movesProbeOwners) unpublishedPath(h *hostclaim.Holder, from, to string) ([]string, bool) {
1753 path := []string{from}
1754 for g := from; g != to; {
1755 if len(path) > len(h.Admissions)+1 {
1756 return nil, false
1757 }
1758 var found *hostclaim.AdmissionRecord
1759 for i := len(h.Admissions) - 1; i >= 0; i-- {
1760 if a := h.Admissions[i]; a.Generation == g && a.Mode == "cold" {
1761 found = &h.Admissions[i]
1762 break
1763 }
1764 }
1765 if found == nil || found.Box != m.Box || !park.SameRegion(m.HomeRegion, found.Region, m.Region) || found.Predecessor == "" {
1766 return nil, false
1767 }
1768 g = found.Predecessor
1769 path = append(path, g)
1770 }
1771 return path, true
1772}
1773
1774// coldPredecessor is the predecessor of the newest cold admission into g.
1775func coldPredecessor(h *hostclaim.Holder, g string) (string, bool) {
1776 for i := len(h.Admissions) - 1; i >= 0; i-- {
1777 if a := h.Admissions[i]; a.Generation == g && a.Mode == "cold" {
1778 return a.Predecessor, true
1779 }
1780 }
1781 return "", false
1782}
1783
1784// ancestorOf reports whether old is reached by walking g's cold
1785// predecessors in the holder's admissions.
1786func ancestorOf(h *hostclaim.Holder, g, old string) bool {
1787 for steps := 0; steps <= len(h.Admissions); steps++ {
1788 pred, ok := coldPredecessor(h, g)
1789 if !ok || pred == "" {
1790 return false
1791 }
1792 if pred == old {
1793 return true
1794 }
1795 g = pred
1796 }
1797 return false
1798}
1799