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
3// The moves park providers in production (moves Plan 5 Task 9a; the delta's
4// findings M5, M6, M9, M10 and M17, the Task 9a rulings, and the rulings
5// after claim Plan 12: Q1, Q10). Two things are built here and registered
6// with the claim at agent start, before newClaimWiring:
7//
8// - the park view (movesParkView): the spec's authoritative-region table
9// (§5.1), the exact-version generation marker, and the pins;
10// - Task 5's retirement evidence (movesRetirementEvidence), which also
11// answers VerifyMovesSweep.
12//
13// The lease set for moves scopes (M6) is the claim's own,
14// hostbox.ReplicaParkLeaseSet, which the claim's MoveParks engine resolves
15// through hostbox.AggregateLeases; moves registers none.
16//
17// The moves arms then take the claim's own regional engine through
18// ClaimProviders.MoveParks (armMovesParks): its leases, view and authorizer,
19// after checking that its bucket and region are this box's own (M17), with
20// park.Engine.RunHeld over those leases (M5) and the MoveParks local
21// authorizer (M9).
22//
23// Registration and arming happen only with both CARLOS_MOVES_PHASE1 and
24// CARLOS_HOST_CLAIM on, and both default to off. Even then one gate,
25// movesWakeSourceRegistered, stands in front of both: it opens only once
26// this box's moves source (the wake owners, Plan 5 Task 9b,
27// moveswakesource.go) is registered with the claim through
28// registerMovesWakeSource, which the agent does right after the parks. The
29// conditions production arming still needs are listed in
30// moveswakesource.go. While the gate is shut:
31//
32// - the registered view refuses every call with a *ClaimDependencyError
33// naming Task 9b, so the claim's regional worker and its at-rest sweep
34// queue and run nothing through it;
35// - armMovesParks leaves every park hook unset, so every moves arm stays
36// unarmed and logs its standing notice.
37
38import (
39 "context"
40 "encoding/json"
41 "errors"
42 "fmt"
43 "os"
44 "sort"
45 "strings"
46 "sync"
47 "time"
48
49 "github.com/carlosframework/platform/internal/activator"
50 "github.com/carlosframework/platform/internal/genselect"
51 "github.com/carlosframework/platform/internal/hostbox"
52 "github.com/carlosframework/platform/internal/hostclaim"
53 "github.com/carlosframework/platform/internal/hostfiles"
54 "github.com/carlosframework/platform/internal/hostwrite"
55 "github.com/carlosframework/platform/internal/lease"
56 "github.com/carlosframework/platform/internal/moves"
57 "github.com/carlosframework/platform/internal/park"
58 "github.com/carlosframework/platform/internal/provision"
59 "github.com/carlosframework/platform/internal/release"
60)
61
62// movesNoLiveRegion is the authoritative region the view names when the
63// spec's table says none: a claim with no holder, or a creating, deleting,
64// parking or purging op. The "@" keeps it outside the placement-code
65// alphabet (provision.ValidRegion; "none" itself is a valid code) and apart
66// from park.HomeRegion, so no box's region is ever it and the engine protects
67// nothing in any region, which is what "nothing is live" means.
68const movesNoLiveRegion = "@none"
69
70// parkRegion is this box's engine region: its placement code through
71// park.PlacementRegion, so the home's "" is park.HomeRegion (home-region
72// parks, part 1). Every park engine a moves arm builds, every chore it looks
73// for or queues in its own region, and the region its claim's engine must
74// have go through it. An invalid code maps to "", which arms nothing and
75// which every engine entry refuses.
76func (c moveCfg) parkRegion() string { return park.PlacementRegion(c.Region) }
77
78// movesBoxRegion refuses a placement that disagrees with the verified roster.
79// productionRosterRegion maps home only after checking the signed designation.
80func movesBoxRegion(placement string, resolve func(context.Context) (string, error)) func(context.Context) (string, error) {
81 return func(ctx context.Context) (string, error) {
82 want := park.PlacementRegion(placement)
83 if want == "" {
84 return "", fmt.Errorf("%w: this box's placement %q is not a placement code", park.ErrUnsafe, placement)
85 }
86 got, err := resolve(ctx)
87 if err != nil {
88 return "", err
89 }
90 if got != want {
91 return "", fmt.Errorf("%w: the claim names this box's region %q, its placement's engine region is %q", park.ErrUnsafe, got, want)
92 }
93 return got, nil
94 }
95}
96
97// movesWakeSourceContract is what the gate names while it is shut.
98const movesWakeSourceContract = "moves wake owners and C8 files manifest provider (Plan 5 Task 9b)"
99
100// movesWakeSourceRegistration is this box's moves source as registered with
101// the claim: a pointer of moves' own, so the gate can tell the claim's
102// registered source is this one and not another.
103type movesWakeSourceRegistration struct{ ClaimProductionMoveSources }
104
105// movesWakeSource is what registerMovesWakeSource registered, nil before.
106var movesWakeSource struct {
107 sync.Mutex
108 registered *movesWakeSourceRegistration
109}
110
111// registerMovesWakeSource registers src, this box's moves source (the wake
112// owners and the C8 files manifest provider, Plan 5 Task 9b), with the claim
113// (RegisterProductionClaimMoveSources), and records it for the gate. It is
114// one-shot, as the claim's slot is; any refusal records nothing. In
115// production registerProductionMovesWakeSource calls it, with both
116// switches on, and so arms the parks (Plan 5 Task 9b).
117func registerMovesWakeSource(src ClaimProductionMoveSources) error {
118 if src == nil {
119 return hostwrite.ErrUnavailable
120 }
121 movesWakeSource.Lock()
122 defer movesWakeSource.Unlock()
123 if movesWakeSource.registered != nil {
124 return hostwrite.ErrUnavailable
125 }
126 reg := &movesWakeSourceRegistration{src}
127 if err := RegisterProductionClaimMoveSources(reg); err != nil {
128 return err
129 }
130 movesWakeSource.registered = reg
131 return nil
132}
133
134// movesWakeSourceRegistered is the one gate between the moves parks and
135// production (Q1): nil only once registerMovesWakeSource succeeded and the
136// claim's registered move source is still that registration; otherwise a
137// *ClaimDependencyError naming Task 9b. The parks then arm in the same agent
138// start as the wake owners, never before, and until then the registered
139// view refuses, so the claim's sweep is not widened to every host.
140func movesWakeSourceRegistered() error {
141 movesWakeSource.Lock()
142 reg := movesWakeSource.registered
143 movesWakeSource.Unlock()
144 if reg != nil {
145 claimMoveSources.Lock()
146 ours := claimMoveSources.Source == ClaimProductionMoveSources(reg)
147 claimMoveSources.Unlock()
148 if ours {
149 return nil
150 }
151 }
152 return &ClaimDependencyError{Contracts: []string{movesWakeSourceContract}}
153}
154
155// movesRunHeld is the moves arms' RunHeld (M5): park.Engine.RunHeld, which
156// runs one chore under a lease the caller already holds, the build's
157// own-location lease, which a landing keeps from checking through published
158// (spec §7.6). The engine's leases must lend it (AcquireHeld, the claim's
159// hostbox.AggregateLeases); Engine.Run would take the same lock again and
160// wait on the build itself.
161func movesRunHeld(ctx context.Context, e park.Engine, w park.Work, held park.HeldLease) error {
162 return e.RunHeld(ctx, w, held)
163}
164
165// heldLender is what park.Engine.RunHeld needs of an engine's leases to
166// borrow the caller's held lease (hostbox.AggregateLeases).
167type heldLender interface {
168 AcquireHeld(ctx context.Context, bucket, scope string, own park.HeldLease) (park.HeldLease, error)
169}
170
171// movesParkView is the park view the claim's engine and the moves arms read
172// (park.View): bound to the observation's ETag, seen from region (in
173// production the claim's own region, movesParkViewAt), naming
174// the authoritative region by the spec's table (§5.1, movesAuthoritative).
175// In the authoritative region it reads the location's generation.json by
176// exact version (MarkerKnown, MarkerGeneration, Marker; absent is the origin)
177// and pins the snapshot version the marker names, the installed root.
178// GenerationOf is the landing's classifier over the holder's location.
179// Anything it cannot read or place is an error, never a smaller live set.
180func movesParkView(records MovesRecordReader, data genselect.Reader, bucket, region string) func(context.Context, hostclaim.Observed) (park.View, error) {
181 return movesParkViewAt(records, data, bucket, fixedRegion(region))
182}
183
184// fixedRegion is a region resolver that always answers region.
185func fixedRegion(region string) func(context.Context) (string, error) {
186 return func(context.Context) (string, error) { return region, nil }
187}
188
189// cachedRegion resolves once and keeps the first success; a failure is
190// returned and asked again next time. A box's roster region does not change
191// under a running agent; if it did, the engine's own check (the view's
192// region against its own) refuses.
193func cachedRegion(resolve func(context.Context) (string, error)) func(context.Context) (string, error) {
194 var mu sync.Mutex
195 var cached *string
196 return func(ctx context.Context) (string, error) {
197 mu.Lock()
198 defer mu.Unlock()
199 if cached != nil {
200 return *cached, nil
201 }
202 r, err := resolve(ctx)
203 if err != nil {
204 return "", err
205 }
206 cached = &r
207 return r, nil
208 }
209}
210
211// movesParkViewAt is movesParkView seen from the region regionOf answers.
212// Production's is the claim's verified roster row's region
213// (productionRosterRegion), the region the claim's engine runs in, so the
214// view's region is the claim's by construction (M17; fix round 1), held to
215// this box's placement by movesBoxRegion. Every region it names is an engine
216// region: the home is park.HomeRegion, never "", and a region of "" (unset)
217// refuses. The record's placement codes are mapped once, in
218// movesAuthoritative.
219func movesParkViewAt(records MovesRecordReader, data genselect.Reader, bucket string, regionOf func(context.Context) (string, error)) func(context.Context, hostclaim.Observed) (park.View, error) {
220 return func(ctx context.Context, obs hostclaim.Observed) (park.View, error) {
221 if ctx == nil || records == nil || data == nil || bucket == "" || regionOf == nil {
222 return park.View{}, hostwrite.ErrUnavailable
223 }
224 if obs.Absent || obs.Claim == nil || obs.ETag == "" || obs.Host == "" || obs.Claim.Host != obs.Host {
225 return park.View{}, park.ErrUnsafe
226 }
227 region, err := regionOf(ctx)
228 if err != nil {
229 return park.View{}, fmt.Errorf("this box's region: %w", err)
230 }
231 if region == "" {
232 return park.View{}, fmt.Errorf("%w: this box's region is unset", park.ErrUnsafe)
233 }
234 auth, err := movesAuthoritative(ctx, records, obs, region)
235 if err != nil {
236 return park.View{}, err
237 }
238 v := park.View{HomeRegion: obs.Claim.Op.Intent["home_region"], ClaimETag: obs.ETag, Region: region, AuthoritativeRegion: auth}
239 h := obs.Claim.Holder
240 if h == nil {
241 return v, nil
242 }
243 location := strings.TrimSuffix(h.Location, "/")
244 v.GenerationOf = func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }
245 if !park.SameRegion(v.HomeRegion, auth, region) {
246 return v, nil
247 }
248 listing, err := data.ListVersions(ctx, bucket, location+"/")
249 if err != nil {
250 return park.View{}, fmt.Errorf("list %s: %w", location, err)
251 }
252 if err := readMarker(ctx, data, bucket, obs.Host, location, *h, listing, &v); err != nil {
253 return park.View{}, err
254 }
255 if err := pinFilesLiveSet(ctx, data, bucket, location, listing, &v, liveGenerations(obs.Claim, v)...); err != nil {
256 return park.View{}, err
257 }
258 return v, nil
259 }
260}
261
262// liveGenerations are the generations whose files manifests are live
263// (fix round 1, I2): the holder's and the marker's, and every generation
264// park.LiveGenerations, the engine's own protected set, names for the
265// holder's location, the unfinished admission chain included. When the
266// engine refuses that walk it refuses every park of the location too, so
267// the holder's and the marker's are enough then. Sorted, for a stable read
268// order.
269func liveGenerations(c *hostclaim.Claim, v park.View) []string {
270 seen := map[string]bool{c.Holder.Generation: true, v.MarkerGeneration: true}
271 if live, err := park.LiveGenerations(c, v, c.Holder.Location); err == nil {
272 for g := range live {
273 seen[g] = true
274 }
275 }
276 out := make([]string, 0, len(seen))
277 for g := range seen {
278 out = append(out, g)
279 }
280 sort.Strings(out)
281 return out
282}
283
284// pinFilesLiveSet pins the files live set (J2/F4, Paul's #236 ruling; M10):
285// every exact version an object a live generation's files manifest names,
286// read through hostfiles.Load by the version the listing names. The live
287// generations are liveGenerations'. The rule is M10's: spec
288// §6.6 P6-D3 says a target's manifest never references a source-region
289// object, so a manifest in this region's bucket names objects in this
290// region's bucket, and a region pins only what its own live manifests name.
291// With no manifest, nothing more is pinned. A manifest that names an object
292// outside the location, or one the listing does not hold, refuses.
293func pinFilesLiveSet(ctx context.Context, data genselect.Reader, bucket, location string, listing []lease.ObjectVersion, v *park.View, generations ...string) error {
294 seen := map[string]bool{}
295 for _, gen := range generations {
296 if gen == "" || gen == hostclaim.RootGeneration || gen == landingMarkerGeneration || seen[gen] {
297 continue
298 }
299 seen[gen] = true
300 prefix := movesFilesPrefix(location, gen)
301 key := hostfiles.ManifestKey(prefix)
302 var current *lease.ObjectVersion
303 for i := range listing {
304 if listing[i].Key == key && listing[i].IsLatest {
305 current = &listing[i]
306 }
307 }
308 if current == nil {
309 continue
310 }
311 if current.DeleteMarker || current.VersionID == "" || current.VersionID == "null" {
312 return fmt.Errorf("%w: %s is not a readable version", park.ErrUnsafe, key)
313 }
314 m, found, err := hostfiles.Load(ctx, versionStore{data: data, bucket: bucket, key: key, version: current.VersionID}, prefix)
315 if err != nil || !found {
316 return fmt.Errorf("%w: the files manifest %s: %v", park.ErrUnsafe, key, err)
317 }
318 for _, entries := range []map[string]hostfiles.Entry{m.Entries, m.Platform} {
319 for path, e := range entries {
320 if e.Object == "" || e.Deleted {
321 continue
322 }
323 if !strings.HasPrefix(e.Object, location+"/") {
324 return fmt.Errorf("%w: %s names %s for %s, outside %s", park.ErrUnsafe, key, e.Object, path, location)
325 }
326 if _, ok := v.GenerationOf(park.Ref{Key: e.Object}); !ok {
327 return fmt.Errorf("%w: %s names %s, which belongs to no generation", park.ErrUnsafe, key, e.Object)
328 }
329 pinned := false
330 for _, x := range listing {
331 if x.Key == e.Object && !x.DeleteMarker && x.VersionID != "" {
332 if v.Pinned == nil {
333 v.Pinned = map[park.Ref]bool{}
334 }
335 v.Pinned[park.Ref{Key: x.Key, VersionID: x.VersionID}] = true
336 pinned = true
337 }
338 }
339 if !pinned {
340 return fmt.Errorf("%w: %s names %s, which the bucket does not hold", park.ErrUnsafe, key, e.Object)
341 }
342 }
343 }
344 }
345 return nil
346}
347
348// versionStore is a read-only release.Store over one object read by the
349// exact version a listing named: hostfiles.Load's reader for a manifest.
350type versionStore struct {
351 data genselect.Reader
352 bucket, key, version string
353}
354
355func (s versionStore) Get(ctx context.Context, key string) ([]byte, string, error) {
356 if key != s.key {
357 return nil, "", os.ErrNotExist
358 }
359 body, err := s.data.GetVersion(ctx, s.bucket, key, s.version)
360 return body, s.version, err
361}
362func (versionStore) Put(context.Context, string, []byte, string) (string, error) {
363 return "", errors.New("host: the park view reads only")
364}
365func (versionStore) List(context.Context, string) ([]string, error) {
366 return nil, errors.New("host: the park view reads only")
367}
368func (versionStore) ListDirs(context.Context, string) ([]string, error) {
369 return nil, errors.New("host: the park view reads only")
370}
371func (versionStore) Delete(context.Context, string) error {
372 return errors.New("host: the park view reads only")
373}
374
375// readMarker fills the marker and the installed root's pin from the
376// location's generation.json, read by the exact version the listing names.
377func readMarker(ctx context.Context, data genselect.Reader, bucket, host, location string, h hostclaim.Holder, listing []lease.ObjectVersion, v *park.View) error {
378 key := location + "/generation.json"
379 var latest *lease.ObjectVersion
380 for i := range listing {
381 if listing[i].Key == key && listing[i].IsLatest {
382 if latest != nil {
383 return fmt.Errorf("%w: two current versions of %s", park.ErrUnsafe, key)
384 }
385 latest = &listing[i]
386 }
387 }
388 if latest == nil {
389 // Absent everywhere means origin (§5.1).
390 v.MarkerKnown = true
391 return nil
392 }
393 if latest.DeleteMarker || latest.VersionID == "" || latest.VersionID == "null" {
394 return fmt.Errorf("%w: %s is not a readable version", park.ErrUnsafe, key)
395 }
396 raw, err := data.GetVersion(ctx, bucket, key, latest.VersionID)
397 if err != nil {
398 return fmt.Errorf("read %s: %w", key, err)
399 }
400 var m genselect.Marker
401 if err := json.Unmarshal(raw, &m); err != nil {
402 return fmt.Errorf("%w: %s: %v", park.ErrUnsafe, key, err)
403 }
404 if m.Op == "" || m.Host != host || m.Incarnation != h.Incarnation {
405 return fmt.Errorf("%w: %s names %s/%s generation %q, not the holder's", park.ErrUnsafe, key, m.Host, m.Incarnation, m.Op)
406 }
407 v.MarkerKnown, v.MarkerGeneration, v.Marker = true, m.Op, park.Ref{Key: key, VersionID: latest.VersionID}
408 if m.SnapshotKey != "" || m.SnapshotVersion != "" {
409 root := park.Ref{Key: m.SnapshotKey, VersionID: m.SnapshotVersion}
410 if root.Key == "" || root.VersionID == "" {
411 return fmt.Errorf("%w: %s pins %q at %q", park.ErrUnsafe, key, root.Key, root.VersionID)
412 }
413 if _, ok := v.GenerationOf(root); !ok {
414 return fmt.Errorf("%w: %s pins %s outside %s", park.ErrUnsafe, key, root.Key, location)
415 }
416 v.Pinned = map[park.Ref]bool{root: true}
417 }
418 return nil
419}
420
421// movesAuthoritative is the spec's authoritative-region table (§5.1, claim
422// rev 6.5 §6.2a, R4-3): the region of the holder's current installation.
423//
424// - no holder, creating, deleting, parking, purging: none (nothing live);
425// - serving, pushing: op.exec's region, which is the region of the newest
426// admission into holder.generation, or, for a holder with no such
427// admission (a legacy installation), the record's region;
428// - moving (before the flip), reverting: the source's, move.from;
429// - landing: the source's until the target's probe is admitted into
430// holder.generation in move.to, the target's from then on, committed or
431// not; a committed landing with no such admission refuses. Seen from
432// the source's own region, the source stays authoritative until the
433// record commits (J2/F4: its files live set is protected on the source
434// until the commit);
435// - restoring: the region of the newest admission into holder.generation,
436// the source's or the target's (a clean return's CAS names the source's
437// generation again, a dirty return keeps the target's until the source's
438// probe is admitted); no admission is the source's pre-move generation.
439// Seen from the source's own region, the source stays authoritative
440// while the target releases (releasing, released), until the source's
441// return (J2/F4, L18 finding 1: a clean return restores the source's
442// retained generation, so its files live set stays protected there).
443// A dirty return's install parks the pre-move generations (D6), so
444// from its return CAS on the newest admission decides again.
445//
446// For a move kind the record must carry the op's move, or have closed it
447// (the record-first close, M4), when the newest admission decides alone. A
448// region the claim's history contradicts refuses. So does any other kind
449// (placing, relocating): it has no row in the table.
450//
451// The move's From and To are placement codes; they are compared and
452// answered as engine regions (park.PlacementRegion), so a move out of home
453// names park.HomeRegion, which no named region equals, and a code that is no
454// placement refuses. region is already an engine region. A serving holder
455// with no admission still takes the record's region as it is, and a record
456// with the home's "" there still refuses: that row is the claim's sweep of
457// every legacy home host, not a move's, and is left as it was.
458func movesAuthoritative(ctx context.Context, records MovesRecordReader, obs hostclaim.Observed, region string) (string, error) {
459 c := obs.Claim
460 h := c.Holder
461 if h == nil {
462 return movesNoLiveRegion, nil
463 }
464 switch c.Op.Kind {
465 case hostclaim.KindCreating, hostclaim.KindDeleting, hostclaim.KindParking, hostclaim.KindPurging:
466 return movesNoLiveRegion, nil
467 }
468 admitted, known := admittedRegion(*h)
469 switch c.Op.Kind {
470 case hostclaim.KindServing, hostclaim.KindPushing:
471 if known {
472 return admitted, nil
473 }
474 rec, err := holderRecord(ctx, records, obs)
475 if err != nil {
476 return "", err
477 }
478 if rec.Region == "" {
479 return "", fmt.Errorf("%w: %s's record names no region and the claim no admission", park.ErrUnsafe, obs.Host)
480 }
481 return rec.Region, nil
482 case hostclaim.KindMoving, hostclaim.KindReverting, hostclaim.KindLanding, hostclaim.KindRestoring:
483 default:
484 return "", fmt.Errorf("%w: the authoritative-region table has no row for a %s op", park.ErrUnsafe, c.Op.Kind)
485 }
486 rec, err := holderRecord(ctx, records, obs)
487 if err != nil {
488 return "", err
489 }
490 m := rec.Move
491 if m == nil || m.ID != c.Op.ID || m.Legacy() || m.From == m.To {
492 if l := rec.LastMove; m == nil && l != nil && l.ID == c.Op.ID && known {
493 return admitted, nil // the record closed first (M4); the claim's history decides
494 }
495 return "", fmt.Errorf("%w: the record does not carry move %s", park.ErrUnsafe, c.Op.ID)
496 }
497 from, to := park.PlacementRegion(m.From), park.PlacementRegion(m.To)
498 if from == "" || to == "" {
499 return "", fmt.Errorf("%w: move %s names %q and %q, which are not both placement codes", park.ErrUnsafe, m.ID, m.From, m.To)
500 }
501 contradicts := func(want ...string) error {
502 if !known {
503 return nil
504 }
505 for _, w := range want {
506 if park.SameRegion(c.Op.Intent["home_region"], admitted, w) {
507 return nil
508 }
509 }
510 return fmt.Errorf("%w: holder.generation was admitted in %s, which a %s op of %s cannot have", park.ErrUnsafe, admitted, c.Op.Kind, m.ID)
511 }
512 switch c.Op.Kind {
513 case hostclaim.KindMoving, hostclaim.KindReverting:
514 if err := contradicts(from); err != nil {
515 return "", err
516 }
517 return from, nil
518 case hostclaim.KindLanding:
519 if err := contradicts(from, to); err != nil {
520 return "", err
521 }
522 if known && park.SameRegion(c.Op.Intent["home_region"], admitted, to) {
523 if park.SameRegion(c.Op.Intent["home_region"], region, from) && c.Op.Phase != hostclaim.PhaseCommitted && m.State != provision.MoveCommitted {
524 // J2/F4 (Paul's #236 ruling): the source's live set, its
525 // files live set among it, stays protected on the source
526 // until the record commits. Seen from the source's region the
527 // source stays authoritative, so the engine protects there
528 // too (and refuses a park of the location, whose current
529 // generation the source's marker does not name).
530 return from, nil
531 }
532 return to, nil
533 }
534 if c.Op.Phase == hostclaim.PhaseCommitted || m.State == provision.MoveCommitted {
535 return "", fmt.Errorf("%w: move %s committed with no admission in %s", park.ErrUnsafe, m.ID, to)
536 }
537 return from, nil
538 default: // restoring
539 if err := contradicts(from, to); err != nil {
540 return "", err
541 }
542 if known {
543 returned := c.Op.Phase != hostclaim.PhaseReleasing && c.Op.Phase != hostclaim.PhaseReleased
544 if park.SameRegion(c.Op.Intent["home_region"], admitted, to) && park.SameRegion(c.Op.Intent["home_region"], region, from) && !returned {
545 // J2/F4 (Paul's #236 ruling; L18 finding 1): the
546 // newest admission is still the target's, but until
547 // the source takes the host back a clean return may
548 // name the source's retained generation again (a
549 // dirty release declared lost becomes clean,
550 // MoveRollbackLossCommitted), so seen from the
551 // source's region the source stays authoritative, as
552 // at landing before the commit. Once the return is
553 // written, a clean one is admitted in the source
554 // already, and a dirty one's install parks the
555 // pre-move generations (D6).
556 return from, nil
557 }
558 return admitted, nil
559 }
560 return from, nil
561 }
562}
563
564// admittedRegion is the region of the newest admission into
565// holder.generation, the origin included.
566func admittedRegion(h hostclaim.Holder) (string, bool) {
567 want := h.Generation
568 if want == "" {
569 want = hostclaim.RootGeneration
570 }
571 for i := len(h.Admissions) - 1; i >= 0; i-- {
572 g := h.Admissions[i].Generation
573 if g == "" {
574 g = hostclaim.RootGeneration
575 }
576 if g == want {
577 r := h.Admissions[i].Region
578 return r, r != ""
579 }
580 }
581 return "", false
582}
583
584// holderRecord reads the holder's own record: an instance holder's key, the
585// same account, app and host.
586func holderRecord(ctx context.Context, records MovesRecordReader, obs hostclaim.Observed) (*provision.Record, error) {
587 h := obs.Claim.Holder
588 if h.Family != "instance" || h.Key != provision.RecordKey(h.Account, h.App, obs.Host) {
589 return nil, fmt.Errorf("%w: the holder of %s is not an instance record", park.ErrUnsafe, obs.Host)
590 }
591 rec, err := records(ctx, h.Account, h.App, obs.Host)
592 if err != nil {
593 return nil, fmt.Errorf("read %s's record: %w", obs.Host, err)
594 }
595 if rec == nil || rec.Account != h.Account || rec.App != h.App || rec.Host != obs.Host {
596 return nil, fmt.Errorf("%w: %s's record is not the holder's", park.ErrUnsafe, obs.Host)
597 }
598 return rec, nil
599}
600
601// movesParkSources is what registerMovesParks registers: the view behind
602// the wake-source gate, and Task 5's evidence.
603type movesParkSources struct {
604 View func(context.Context, hostclaim.Observed) (park.View, error)
605 Evidence hostbox.RegionalEvidence
606}
607
608// newMovesParkSources builds the two providers. While the gate
609// (movesWakeSourceRegistered) is shut the view refuses with its error before
610// it reads anything, so nothing is queued or run through it.
611func newMovesParkSources(records MovesRecordReader, data genselect.Reader, bucket string, regionOf func(context.Context) (string, error), evidence hostbox.RegionalEvidence) (movesParkSources, error) {
612 if records == nil || data == nil || bucket == "" || regionOf == nil || evidence == nil {
613 return movesParkSources{}, hostwrite.ErrUnavailable
614 }
615 view := movesParkViewAt(records, data, bucket, regionOf)
616 return movesParkSources{
617 View: func(ctx context.Context, obs hostclaim.Observed) (park.View, error) {
618 if err := movesWakeSourceRegistered(); err != nil {
619 return park.View{}, err
620 }
621 return view(ctx, obs)
622 },
623 Evidence: evidence,
624 }, nil
625}
626
627// registerMovesParks registers the moves park providers with the claim
628// (RegisterProductionClaimMoveParks), at agent start before newClaimWiring.
629func registerMovesParks(src movesParkSources) error {
630 return RegisterProductionClaimMoveParks(src.View, src.Evidence)
631}
632
633// armMovesParks sets the moves arms' park hooks from the claim's MoveParks
634// capability (M17): its engine's leases, view and authorizer, only when the
635// engine's bucket and region are this box's own bucket and its placement's
636// engine region (parkRegion, compared through the engine's OwnsRegion; a home
637// box only when the claim grants the @home alias, claim Plan 13); RunHeld
638// over those leases, which must lend a held lease (M5); and the MoveParks
639// local authorizer (M9). An engine region of "" never arms, and nothing
640// arms while the wake-source gate is shut (movesWakeSourceRegistered). Any
641// refusal leaves every hook unset, so every arm stays unarmed.
642func armMovesParks(ctx context.Context, cfg *moveCfg, providers ClaimProviders) error {
643 if ctx == nil || cfg == nil {
644 return hostwrite.ErrUnavailable
645 }
646 engine, _, local, err := providers.MoveParks(ctx)
647 if err != nil {
648 return fmt.Errorf("the claim's MoveParks: %w", err)
649 }
650 if want := cfg.parkRegion(); want == "" || engine.Bucket != cfg.OwnBucket || !engine.OwnsRegion(want) {
651 return fmt.Errorf("%w: the claim's park engine is %s in %q, this box's moves use %s in %q (placement %q)", hostbox.ErrChanged, engine.Bucket, engine.Region, cfg.OwnBucket, want, cfg.Region)
652 }
653 if cfg.parkRegion() == park.HomeRegion && engine.HomeRegion == "" {
654 // A home box arms only when the claim grants it the @home alias
655 // (claim Plan 13: its signed roster region is the fleet's signed home
656 // designation); any other engine, one naming itself @home included,
657 // leaves it unarmed.
658 return fmt.Errorf("%w: the claim grants this home box no @home alias (engine %s in %q)", hostbox.ErrChanged, engine.Bucket, engine.Region)
659 }
660 if engine.Leases == nil || engine.View == nil || engine.Authorize == nil {
661 return errors.Join(hostwrite.ErrUnavailable, errors.New("the claim's park engine lacks its leases, view or authorizer"))
662 }
663 if _, ok := engine.Leases.(heldLender); !ok {
664 return errors.Join(hostwrite.ErrUnavailable, errors.New("the claim's park leases cannot lend a held lease to RunHeld"))
665 }
666 if local == nil {
667 return errors.Join(hostwrite.ErrUnavailable, errors.New("the claim's MoveParks has no local authorizer"))
668 }
669 if err := movesWakeSourceRegistered(); err != nil {
670 return err
671 }
672 cfg.ParkHomeRegion = engine.HomeRegion
673 cfg.ParkLeases, cfg.ParkView, cfg.ParkAuthorize = engine.Leases, engine.View, engine.Authorize
674 cfg.ParkRunHeld = movesRunHeld
675 cfg.ParkLocalAuthorize = local
676 return nil
677}
678
679// registerProductionMovesParks is the agent's registration, before
680// newClaimWiring: only with both CARLOS_HOST_CLAIM and CARLOS_MOVES_PHASE1
681// on (ruling H1), over the deployment store's records, this box's data
682// bucket read by exact version (lease.NewVersionedS3, through rootaws), and
683// the region regionOf answers, behind the wake-source gate
684// (movesWakeSourceRegistered): the registered view refuses every host until
685// the moves wake source registers right after it
686// (registerProductionMovesWakeSource, Plan 5 Task 9b), to which it returns
687// the sources it registered. Production's
688// regionOf is productionRosterRegion: the claim's verified roster row (fix
689// round 1), held by movesBoxRegion to the engine region of this box's own
690// placement, CARLOS_REGION. The roster refuses a row with region "", so on
691// a home box it names home otherwise than park.HomeRegion, and the view
692// refuses there until the claim names home as the engine does (home-region
693// parks, part 1). Off, it registers nothing, returns no sources, and the
694// claim keeps its named refusals. Task 5's evidence goes through refusals,
695// which pages its anomalies once (movesrefusals.go).
696func registerProductionMovesParks(ctx context.Context, getenv func(string) string, store release.Store, hib activator.Config, regionOf func(context.Context) (string, error), refusals *movesRefusals) (movesParkSources, error) {
697 if !hostwrite.Enabled(getenv) || !moves.Enabled(getenv) {
698 return movesParkSources{}, nil
699 }
700 if ctx == nil || store == nil || hib.Bucket == "" || hib.Region == "" || regionOf == nil || refusals == nil {
701 return movesParkSources{}, errors.Join(hostwrite.ErrUnavailable, errors.New("the moves park view needs the deployment store, the data bucket and this box's roster region"))
702 }
703 data, err := lease.NewVersionedS3(ctx, map[string]string{hib.Bucket: hib.Region})
704 if err != nil {
705 return movesParkSources{}, err
706 }
707 records := MovesRecordsFrom(store)
708 evidence := refusals.evidence(movesRetirementEvidence{Records: records, Now: time.Now})
709 region := movesBoxRegion(strings.TrimSpace(getenv("CARLOS_REGION")), cachedRegion(regionOf))
710 src, err := newMovesParkSources(records, data, hib.Bucket, region, evidence)
711 if err != nil {
712 return movesParkSources{}, err
713 }
714 if err := registerMovesParks(src); err != nil {
715 return movesParkSources{}, err
716 }
717 return src, nil
718}
719
720// productionRosterRegion is this box's region as the claim knows it: the
721// region of its verified roster row (readProductionRosterRow), the same row
722// OpenClaimOwners builds the claim's identity, and so its engine's region,
723// from.
724func productionRosterRegion(store release.Store, getenv func(string) string) func(context.Context) (string, error) {
725 return func(ctx context.Context) (string, error) {
726 rr, err := readProductionRosterRow(ctx, store, getenv)
727 if err != nil {
728 return "", err
729 }
730 engine, home, err := claimRegionBinding(rr.row.Region, getenv("CARLOS_REGION"), rr.home)
731 if err != nil {
732 return "", err
733 }
734 return park.PlacementRegion(park.HomePlacementCode(home, engine)), nil
735 }
736}
737