carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The fleet's layout intent (isolated-exec design §8.2): the append log that
2// tells a box which storage layout, flat or nested, a route should converge
3// to. It records a MODE, not a one-shot request, so a row that becomes
4// eligible after the intent was recorded is migrated on the next pass, and a
5// box that was away converges when it returns. An old box never reads the
6// key; an old console never writes it.
7//
8// The shape mirrors restart-scope.json (see restartscope.go): a small
9// envelope carrying a schema and a log of requests, oldest first, so two
10// requests issued within one poll window cannot collapse into a single etag
11// move and lose one of them.
12package release
13
14import (
15 "context"
16 "crypto/rand"
17 "encoding/hex"
18 "encoding/json"
19 "errors"
20 "fmt"
21 "io"
22 "os"
23 "strings"
24 "time"
25)
26
27const (
28 LayoutModeNone = ""
29 LayoutModeFlat = "flat"
30 LayoutModeNested = "nested"
31)
32
33// LayoutScope narrows an intent to one box by label; an empty Box is the
34// whole fleet.
35type LayoutScope struct {
36 Box string `json:"box,omitempty"`
37}
38
39// LayoutIntent is one recorded request in control/layout.json: which layout
40// to converge to, and which box or boxes it addresses.
41type LayoutIntent struct {
42 ID string `json:"id"`
43 RequestedAt string `json:"requested_at"`
44 Mode string `json:"mode"`
45 Scope LayoutScope `json:"scope"`
46 // Force asks the pass to stop an awake host for its move at once
47 // rather than defer it for the awake grace (layout.Pass.AwakeGrace).
48 // Additive: a box that does not know the field defers as before.
49 Force bool `json:"force,omitempty"`
50}
51
52// LayoutIntents is the object at control/layout.json: the append log of
53// LayoutIntent requests, oldest first.
54type LayoutIntents struct {
55 Schema int `json:"schema"` // 1
56 Requests []LayoutIntent `json:"requests"`
57}
58
59// LayoutIntentKey is the bucket key of the fleet's layout intent log.
60func LayoutIntentKey() string { return "control/layout.json" }
61
62// ValidLayoutMode reports whether m is a mode a box can converge to. The
63// empty mode is a valid ABSENCE (LayoutModeNone), never a valid request.
64func ValidLayoutMode(m string) bool { return m == LayoutModeFlat || m == LayoutModeNested }
65
66// Applies reports whether the intent addresses the box named boxLabel: every
67// box when Scope.Box is empty, or only the one box it names.
68func (in *LayoutIntent) Applies(boxLabel string) bool {
69 return in != nil && (in.Scope.Box == "" || in.Scope.Box == boxLabel)
70}
71
72func parseLayoutIntents(key string, raw []byte) (*LayoutIntents, error) {
73 var g LayoutIntents
74 if err := json.Unmarshal(raw, &g); err != nil {
75 return nil, fmt.Errorf("layout intent at %s: %w", key, err)
76 }
77 if g.Schema > 1 {
78 return nil, fmt.Errorf("layout intent at %s: schema %d is newer than this binary understands; upgrade carlos", key, g.Schema)
79 }
80 return &g, nil
81}
82
83// LoadLayoutIntent reads the log and returns the newest request whose scope
84// applies to boxLabel. found=false means either the object is absent, which
85// is every deployment's state until an operator records an intent, or it has
86// requests but none of them address this box.
87//
88// A newer schema is an error rather than a best-effort parse: a box that
89// cannot be sure what a request means must do nothing, so the request is
90// picked up whenever the box is upgraded rather than dropped or guessed at.
91func LoadLayoutIntent(ctx context.Context, s Store, boxLabel string) (*LayoutIntent, bool, error) {
92 key := LayoutIntentKey()
93 raw, _, err := s.Get(ctx, key)
94 if errors.Is(err, os.ErrNotExist) {
95 return nil, false, nil
96 }
97 if err != nil {
98 return nil, false, err
99 }
100 g, err := parseLayoutIntents(key, raw)
101 if err != nil {
102 return nil, false, err
103 }
104 for i := len(g.Requests) - 1; i >= 0; i-- {
105 if g.Requests[i].Applies(boxLabel) {
106 in := g.Requests[i]
107 return &in, true, nil
108 }
109 }
110 return nil, false, nil
111}
112
113// LoadLayoutIntents reads the whole log, every scope. found=false means
114// the object is absent. The console door's status read uses it to show the
115// newest request whatever box it addresses; a box reads LoadLayoutIntent,
116// which answers for its own label.
117func LoadLayoutIntents(ctx context.Context, s Store) (*LayoutIntents, bool, error) {
118 key := LayoutIntentKey()
119 raw, _, err := s.Get(ctx, key)
120 if errors.Is(err, os.ErrNotExist) {
121 return nil, false, nil
122 }
123 if err != nil {
124 return nil, false, err
125 }
126 g, err := parseLayoutIntents(key, raw)
127 if err != nil {
128 return nil, false, err
129 }
130 return g, true, nil
131}
132
133// AppendLayoutIntent records one request, oldest-first, after every request
134// already in the log, and returns the stamped request it wrote. ID and
135// RequestedAt are stamped here when the caller leaves them empty, so a
136// caller that only cares about Mode and Scope never has to mint either
137// itself, and returning the stamped value means the console door (plan 5)
138// never has to read the object back just to learn the id it recorded.
139//
140// A CAS conflict is retried, the same shape and for the same reason as
141// AppendRestartScope: another writer's request names a different mode or
142// scope, so dropping ours would answer "requested" for a convergence that
143// will never happen. The retry budget is small on purpose; this is a
144// human-paced verb, and a contended object means something is wrong that
145// another lap will not fix.
146func AppendLayoutIntent(ctx context.Context, s Store, in LayoutIntent) (LayoutIntent, error) {
147 if !ValidLayoutMode(in.Mode) {
148 return LayoutIntent{}, fmt.Errorf("layout intent: mode %q is not flat or nested", in.Mode)
149 }
150 if in.ID == "" {
151 id := make([]byte, 16)
152 if _, err := io.ReadFull(rand.Reader, id); err != nil {
153 return LayoutIntent{}, err
154 }
155 in.ID = hex.EncodeToString(id)
156 }
157 if in.RequestedAt == "" {
158 in.RequestedAt = time.Now().UTC().Format(time.RFC3339)
159 }
160 key := LayoutIntentKey()
161 var lastErr error
162 for attempt := 0; attempt < 3; attempt++ {
163 raw, etag, err := s.Get(ctx, key)
164 if err != nil && !errors.Is(err, os.ErrNotExist) {
165 return LayoutIntent{}, err
166 }
167 g := &LayoutIntents{Schema: 1}
168 if err == nil {
169 if g, err = parseLayoutIntents(key, raw); err != nil {
170 // Refusing beats overwriting: a log this binary cannot read is
171 // one whose entries it would silently drop on the next write.
172 return LayoutIntent{}, err
173 }
174 } else {
175 etag = "" // create-only
176 }
177 g.Schema = 1
178 g.Requests = append(g.Requests, in)
179 body, merr := json.Marshal(g)
180 if merr != nil {
181 return LayoutIntent{}, merr
182 }
183 if _, err := s.Put(ctx, key, body, etag); err != nil {
184 if errors.Is(err, ErrCASConflict) {
185 lastErr = err
186 continue
187 }
188 return LayoutIntent{}, err
189 }
190 return in, nil
191 }
192 return LayoutIntent{}, fmt.Errorf("layout intent: %s is being written by someone else; nothing was requested; try again: %w", key, lastErr)
193}
194
195// LayoutFloor is the first platform release on which a nested row WAKES:
196// the one cut from a main that contains plan 4 (7b7d3026), whose edge unit
197// drops RestrictSUIDSGID= so the agent's openat2 calls work. v0.20.0 to
198// v0.24.2 carry the instance mount units, so a nested row's data is safe on
199// them, but the agent cannot open a nested directory there and the row
200// never wakes, which is an outage; so the floor is the release that works,
201// not the release that merely keeps the files. No tag contains 7b7d3026 at
202// the time of writing (latest v0.24.2), so this names the next: v0.25.0
203// MUST be cut from a main that contains 7b7d3026.
204//
205// Both console system doors refuse a pointer move below this floor while
206// any box could hold a nested row, and the layout door refuses `migrate`
207// while any box in scope runs below it. The box-side guard
208// (host/systemapply.go) judges the candidate's staged unit set, and, when
209// the box holds nested rows, the candidate's own reported version against
210// this floor. TestLayoutFloorIsTheFirstReleaseWhereNestedRowsWake pins it.
211var LayoutFloor = SemVer{Major: 0, Minor: 25, Patch: 0}
212
213// ParseReleaseVersion parses a release tag with or without its leading v.
214func ParseReleaseVersion(version string) (SemVer, bool) {
215 return ParseSemVer(strings.TrimPrefix(version, "v"))
216}
217
218// BelowFloor reports whether version parses and is below floor. A tag that
219// does not parse is false: the caller decides what an unplaceable version
220// means for its own rule.
221func BelowFloor(version string, floor SemVer) bool {
222 v, ok := ParseReleaseVersion(version)
223 return ok && v.Compare(floor) < 0
224}
225
226// PreLayout reports whether a release tag predates the layout. A tag that
227// does not parse as a version is treated as pre-layout: refusing is the
228// safe answer for a release nobody can place against the floor, and the
229// refusal names the tag so an operator can see why.
230func PreLayout(version string) bool {
231 v, ok := ParseReleaseVersion(version)
232 if !ok {
233 return true
234 }
235 return v.Compare(LayoutFloor) < 0
236}
237
238// RecordLayoutIntent is the console door's writer (plan 5): one request,
239// stamped here, appended under CAS by AppendLayoutIntent. force asks the
240// boxes in scope to stop an awake host for its move at once rather than
241// wait out its awake grace (LayoutIntent.Force). actor is recorded by the
242// console's own audit row, not in the object: the object is read by every
243// box, and an operator's address has no business in a box's view.
244func RecordLayoutIntent(ctx context.Context, s Store, mode, box string, force bool) (LayoutIntent, error) {
245 return AppendLayoutIntent(ctx, s, LayoutIntent{Mode: mode, Scope: LayoutScope{Box: box}, Force: force})
246}
247