// The fleet's layout intent (isolated-exec design ยง8.2): the append log that // tells a box which storage layout, flat or nested, a route should converge // to. It records a MODE, not a one-shot request, so a row that becomes // eligible after the intent was recorded is migrated on the next pass, and a // box that was away converges when it returns. An old box never reads the // key; an old console never writes it. // // The shape mirrors restart-scope.json (see restartscope.go): a small // envelope carrying a schema and a log of requests, oldest first, so two // requests issued within one poll window cannot collapse into a single etag // move and lose one of them. package release import ( "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "strings" "time" ) const ( LayoutModeNone = "" LayoutModeFlat = "flat" LayoutModeNested = "nested" ) // LayoutScope narrows an intent to one box by label; an empty Box is the // whole fleet. type LayoutScope struct { Box string `json:"box,omitempty"` } // LayoutIntent is one recorded request in control/layout.json: which layout // to converge to, and which box or boxes it addresses. type LayoutIntent struct { ID string `json:"id"` RequestedAt string `json:"requested_at"` Mode string `json:"mode"` Scope LayoutScope `json:"scope"` // Force asks the pass to stop an awake host for its move at once // rather than defer it for the awake grace (layout.Pass.AwakeGrace). // Additive: a box that does not know the field defers as before. Force bool `json:"force,omitempty"` } // LayoutIntents is the object at control/layout.json: the append log of // LayoutIntent requests, oldest first. type LayoutIntents struct { Schema int `json:"schema"` // 1 Requests []LayoutIntent `json:"requests"` } // LayoutIntentKey is the bucket key of the fleet's layout intent log. func LayoutIntentKey() string { return "control/layout.json" } // ValidLayoutMode reports whether m is a mode a box can converge to. The // empty mode is a valid ABSENCE (LayoutModeNone), never a valid request. func ValidLayoutMode(m string) bool { return m == LayoutModeFlat || m == LayoutModeNested } // Applies reports whether the intent addresses the box named boxLabel: every // box when Scope.Box is empty, or only the one box it names. func (in *LayoutIntent) Applies(boxLabel string) bool { return in != nil && (in.Scope.Box == "" || in.Scope.Box == boxLabel) } func parseLayoutIntents(key string, raw []byte) (*LayoutIntents, error) { var g LayoutIntents if err := json.Unmarshal(raw, &g); err != nil { return nil, fmt.Errorf("layout intent at %s: %w", key, err) } if g.Schema > 1 { return nil, fmt.Errorf("layout intent at %s: schema %d is newer than this binary understands; upgrade carlos", key, g.Schema) } return &g, nil } // LoadLayoutIntent reads the log and returns the newest request whose scope // applies to boxLabel. found=false means either the object is absent, which // is every deployment's state until an operator records an intent, or it has // requests but none of them address this box. // // A newer schema is an error rather than a best-effort parse: a box that // cannot be sure what a request means must do nothing, so the request is // picked up whenever the box is upgraded rather than dropped or guessed at. func LoadLayoutIntent(ctx context.Context, s Store, boxLabel string) (*LayoutIntent, bool, error) { key := LayoutIntentKey() raw, _, err := s.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { return nil, false, nil } if err != nil { return nil, false, err } g, err := parseLayoutIntents(key, raw) if err != nil { return nil, false, err } for i := len(g.Requests) - 1; i >= 0; i-- { if g.Requests[i].Applies(boxLabel) { in := g.Requests[i] return &in, true, nil } } return nil, false, nil } // LoadLayoutIntents reads the whole log, every scope. found=false means // the object is absent. The console door's status read uses it to show the // newest request whatever box it addresses; a box reads LoadLayoutIntent, // which answers for its own label. func LoadLayoutIntents(ctx context.Context, s Store) (*LayoutIntents, bool, error) { key := LayoutIntentKey() raw, _, err := s.Get(ctx, key) if errors.Is(err, os.ErrNotExist) { return nil, false, nil } if err != nil { return nil, false, err } g, err := parseLayoutIntents(key, raw) if err != nil { return nil, false, err } return g, true, nil } // AppendLayoutIntent records one request, oldest-first, after every request // already in the log, and returns the stamped request it wrote. ID and // RequestedAt are stamped here when the caller leaves them empty, so a // caller that only cares about Mode and Scope never has to mint either // itself, and returning the stamped value means the console door (plan 5) // never has to read the object back just to learn the id it recorded. // // A CAS conflict is retried, the same shape and for the same reason as // AppendRestartScope: another writer's request names a different mode or // scope, so dropping ours would answer "requested" for a convergence that // will never happen. The retry budget is small on purpose; this is a // human-paced verb, and a contended object means something is wrong that // another lap will not fix. func AppendLayoutIntent(ctx context.Context, s Store, in LayoutIntent) (LayoutIntent, error) { if !ValidLayoutMode(in.Mode) { return LayoutIntent{}, fmt.Errorf("layout intent: mode %q is not flat or nested", in.Mode) } if in.ID == "" { id := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, id); err != nil { return LayoutIntent{}, err } in.ID = hex.EncodeToString(id) } if in.RequestedAt == "" { in.RequestedAt = time.Now().UTC().Format(time.RFC3339) } key := LayoutIntentKey() var lastErr error for attempt := 0; attempt < 3; attempt++ { raw, etag, err := s.Get(ctx, key) if err != nil && !errors.Is(err, os.ErrNotExist) { return LayoutIntent{}, err } g := &LayoutIntents{Schema: 1} if err == nil { if g, err = parseLayoutIntents(key, raw); err != nil { // Refusing beats overwriting: a log this binary cannot read is // one whose entries it would silently drop on the next write. return LayoutIntent{}, err } } else { etag = "" // create-only } g.Schema = 1 g.Requests = append(g.Requests, in) body, merr := json.Marshal(g) if merr != nil { return LayoutIntent{}, merr } if _, err := s.Put(ctx, key, body, etag); err != nil { if errors.Is(err, ErrCASConflict) { lastErr = err continue } return LayoutIntent{}, err } return in, nil } return LayoutIntent{}, fmt.Errorf("layout intent: %s is being written by someone else; nothing was requested; try again: %w", key, lastErr) } // LayoutFloor is the first platform release on which a nested row WAKES: // the one cut from a main that contains plan 4 (7b7d3026), whose edge unit // drops RestrictSUIDSGID= so the agent's openat2 calls work. v0.20.0 to // v0.24.2 carry the instance mount units, so a nested row's data is safe on // them, but the agent cannot open a nested directory there and the row // never wakes, which is an outage; so the floor is the release that works, // not the release that merely keeps the files. No tag contains 7b7d3026 at // the time of writing (latest v0.24.2), so this names the next: v0.25.0 // MUST be cut from a main that contains 7b7d3026. // // Both console system doors refuse a pointer move below this floor while // any box could hold a nested row, and the layout door refuses `migrate` // while any box in scope runs below it. The box-side guard // (host/systemapply.go) judges the candidate's staged unit set, and, when // the box holds nested rows, the candidate's own reported version against // this floor. TestLayoutFloorIsTheFirstReleaseWhereNestedRowsWake pins it. var LayoutFloor = SemVer{Major: 0, Minor: 25, Patch: 0} // ParseReleaseVersion parses a release tag with or without its leading v. func ParseReleaseVersion(version string) (SemVer, bool) { return ParseSemVer(strings.TrimPrefix(version, "v")) } // BelowFloor reports whether version parses and is below floor. A tag that // does not parse is false: the caller decides what an unplaceable version // means for its own rule. func BelowFloor(version string, floor SemVer) bool { v, ok := ParseReleaseVersion(version) return ok && v.Compare(floor) < 0 } // PreLayout reports whether a release tag predates the layout. A tag that // does not parse as a version is treated as pre-layout: refusing is the // safe answer for a release nobody can place against the floor, and the // refusal names the tag so an operator can see why. func PreLayout(version string) bool { v, ok := ParseReleaseVersion(version) if !ok { return true } return v.Compare(LayoutFloor) < 0 } // RecordLayoutIntent is the console door's writer (plan 5): one request, // stamped here, appended under CAS by AppendLayoutIntent. force asks the // boxes in scope to stop an awake host for its move at once rather than // wait out its awake grace (LayoutIntent.Force). actor is recorded by the // console's own audit row, not in the object: the object is read by every // box, and an operator's address has no business in a box's view. func RecordLayoutIntent(ctx context.Context, s Store, mode, box string, force bool) (LayoutIntent, error) { return AppendLayoutIntent(ctx, s, LayoutIntent{Mode: mode, Scope: LayoutScope{Box: box}, Force: force}) }