carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "os"
11 "sort"
12 "sync"
13 "time"
14
15 "github.com/carlosframework/platform/internal/domains"
16 "github.com/carlosframework/platform/internal/hostactors"
17 "github.com/carlosframework/platform/internal/hostclaim"
18 "github.com/carlosframework/platform/internal/provision"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22const BackfillKey = "control/hosts-backfill.json"
23const HostsQuarantineKey = "control/hosts-quarantine.json"
24
25type CensusInstallation struct {
26 Placement string `json:"placement"`
27 Host string `json:"host"`
28 Account string `json:"account"`
29 App string `json:"app"`
30 Family string `json:"family"`
31 Key string `json:"key"`
32 Incarnation string `json:"incarnation"`
33 Stamp uint64 `json:"stamp"`
34 Local bool `json:"local"`
35 Route bool `json:"route"`
36}
37type CensusReport struct {
38 Box string `json:"box"`
39 Enrollment string `json:"enrollment"`
40 BootID string `json:"boot_id"`
41 Reservation string `json:"reservation"`
42 DirectoryRevision uint64 `json:"directory_revision"`
43 CapturedAt time.Time `json:"captured_at"`
44 Complete bool `json:"complete"`
45 Installations []CensusInstallation `json:"installations"`
46 // Seeds is the box's own seed state (backfill_seeds.go). Signed with
47 // the rest of the report. Nil is a box that did not say, which
48 // activation reads as unknown and refuses.
49 Seeds *CensusSeeds `json:"seeds,omitempty"`
50}
51type Census struct {
52 Records []Record
53 Reports map[string]CensusReport
54 Roster hostactors.Observed
55}
56type BackfillHost struct {
57 Key string `json:"key"`
58 ETag string `json:"etag"`
59 Acquisition string `json:"acq"`
60 Region string `json:"region"`
61 Box string `json:"box"`
62}
63type BackfillPlan struct {
64 V int `json:"v"`
65 ID string `json:"id"`
66 Phase string `json:"phase"`
67 DirectoryRevision uint64 `json:"directory_revision"`
68 CreatedAt time.Time `json:"created_at"`
69 Reports map[string]string `json:"reports"`
70 Hosts map[string]BackfillHost `json:"hosts"`
71}
72type HostsQuarantine struct {
73 V int `json:"v"`
74 Hosts map[string]string `json:"hosts"`
75}
76type CensusEnvelope struct {
77 Report CensusReport `json:"report"`
78 Signature []byte `json:"signature"`
79}
80
81type Backfill struct {
82 mu sync.Mutex
83 cached *Census
84 cachedTag string
85 Store release.Store
86 Directory hostactors.Directory
87}
88type ProductionAuthority struct {
89 HomeRegion string
90 Store release.Store
91 Control *ControlWriter
92 Directory hostactors.Directory
93 Now func() time.Time
94 ConsoleID, Enrollment, Namespace string
95 Signer release.Signer
96 Evidence hostactors.Evidence
97 Backfill *Backfill
98 // Executors is create's executor selection (region HA §5.2, L8). The
99 // create door sets it only with CARLOS_REGION_HA on; nil keeps today's
100 // create path (claim Plan 5's selectExecutor), so a claim-enabled
101 // deployment with HA off (every one-box region included) creates
102 // exactly as before.
103 Executors *ExecutorChooser
104}
105
106// SelectExecutor is Authority's create-time selection: choose() once,
107// before the create CAS, when a chooser is configured; otherwise no
108// selection, and the create is today's. The chooser chooses within claim
109// Plan 5's scope: this authority's namespace and the record's pool.
110//
111// A door that already chose for this host (WithChosenExecutor) is held to
112// that choice, so the create names the box the door checked (H27).
113func (a ProductionAuthority) SelectExecutor(ctx context.Context, obs hostclaim.Observed, r Record) (hostclaim.ExecutorSelection, bool, error) {
114 if a.Executors == nil {
115 return hostclaim.ExecutorSelection{}, false, nil
116 }
117 if sel, ok := ChosenExecutor(ctx, r.Holder.Host); ok {
118 return sel, true, nil
119 }
120 var shape struct {
121 Pool string `json:"pool"`
122 }
123 if err := json.Unmarshal(r.Body, &shape); err != nil {
124 return hostclaim.ExecutorSelection{}, false, err
125 }
126 x := *a.Executors
127 x.Namespace, x.Pool = a.Namespace, shape.Pool
128 sel, err := x.Choose(ctx, obs, r)
129 return sel, err == nil, err
130}
131
132func reportKey(reservation, box string) (string, error) {
133 if !safeSegment(reservation) || !safeSegment(box) {
134 return "", ErrIdentity
135 }
136 return "control/host-census/" + reservation + "/" + box + ".json", nil
137}
138func safeSegment(s string) bool {
139 if s == "" || s == "." || s == ".." {
140 return false
141 }
142 for _, c := range s {
143 if c == '/' || c == '\\' || c == 0 {
144 return false
145 }
146 }
147 return true
148}
149func CheckHostQuarantine(ctx context.Context, s release.Store, host string) error {
150 raw, _, err := s.Get(ctx, HostsQuarantineKey)
151 if errors.Is(err, os.ErrNotExist) {
152 return nil
153 }
154 if err != nil {
155 return err
156 }
157 var q HostsQuarantine
158 if decode(raw, &q) != nil || q.V != 1 || q.Hosts == nil {
159 return ErrIdentity
160 }
161 if _, ok := q.Hosts[host]; ok {
162 return ErrPending
163 }
164 return nil
165}
166func (b *Backfill) read(ctx context.Context) (BackfillPlan, string, error) {
167 p, tag, err := b.readPlan(ctx)
168 if err != nil {
169 return p, "", err
170 }
171 if p.Phase != "adopting" {
172 return p, "", ErrContract
173 }
174 return p, tag, nil
175}
176func (b *Backfill) readPlan(ctx context.Context) (BackfillPlan, string, error) {
177 var p BackfillPlan
178 raw, tag, err := b.Store.Get(ctx, BackfillKey)
179 if err != nil {
180 return p, "", err
181 }
182 if tag == "" || release.VerifyRootEnvelope(b.Directory.PublicKey, raw, &p) != nil || p.V != 1 || !safeSegment(p.ID) || p.CreatedAt.IsZero() || (p.Phase == "adopting" && (p.Hosts == nil || p.Reports == nil)) {
183 return p, "", ErrContract
184 }
185 return p, tag, nil
186}
187func (a ProductionAuthority) census(ctx context.Context, account, app, reservation string, expectedRevision uint64, digests map[string]string) (Census, error) {
188 out := Census{Reports: map[string]CensusReport{}}
189 roster, err := a.Directory.Read(ctx)
190 if err != nil {
191 return out, err
192 }
193 if expectedRevision != 0 && roster.Roster.Revision != expectedRevision {
194 return out, ErrPending
195 }
196 out.Roster = roster
197 apps, err := Apps(ctx, a.Store)
198 if err != nil {
199 return out, err
200 }
201 allRecords := map[string]Record{}
202 for _, p := range apps {
203 records, err := Records(ctx, a.Store, p.Account, p.Name)
204 if err != nil {
205 return out, err
206 }
207 for _, r := range records {
208 if _, ok := allRecords[r.Holder.Host]; ok {
209 return out, ErrIdentity
210 }
211 allRecords[r.Holder.Host] = r
212 if account == "" || (p.Account == account && p.Name == app) {
213 out.Records = append(out.Records, r)
214 }
215 }
216 }
217 for ref, box := range roster.Roster.Boxes {
218 // Retired membership is retained until its inventory is proven empty.
219 key, err := reportKey(reservation, ref)
220 if err != nil {
221 return out, err
222 }
223 raw, tag, err := a.Store.Get(ctx, key)
224 if err != nil {
225 return out, err
226 }
227 if digests != nil {
228 sum := sha256.Sum256(raw)
229 if digests[ref] != hex.EncodeToString(sum[:]) {
230 return out, ErrIdentity
231 }
232 }
233 var envelope CensusEnvelope
234 if decode(raw, &envelope) != nil {
235 return out, ErrIdentity
236 }
237 report := envelope.Report
238 canonical, e := json.Marshal(report)
239 if e != nil || len(box.CensusPublicKey) != ed25519.PublicKeySize || !ed25519.Verify(box.CensusPublicKey, canonical, envelope.Signature) {
240 return out, ErrIdentity
241 }
242 if tag == "" || !report.Complete || report.Box != ref || report.Enrollment != box.Enrollment || report.Reservation != reservation || report.DirectoryRevision != roster.Roster.Revision || report.BootID == "" || report.CapturedAt.IsZero() || report.Installations == nil {
243 return out, ErrIdentity
244 }
245 seen := map[string]bool{}
246 for _, i := range report.Installations {
247 if i.Host == "" || seen[i.Host] || (!i.Route && !i.Local) {
248 return out, ErrIdentity
249 }
250 seen[i.Host] = true
251 r, ok := allRecords[i.Host]
252 if !ok || i.Account != r.Holder.Account || i.App != r.Holder.App || i.Key != r.Holder.Key || i.Family != r.Holder.Family {
253 return out, ErrIdentity
254 }
255 inc, err := r.Incarnation()
256 if err != nil || (i.Incarnation != "" && i.Incarnation != inc) {
257 return out, ErrIdentity
258 }
259 }
260 out.Reports[ref] = report
261 }
262 if digests != nil && len(digests) != len(out.Reports) {
263 return out, ErrIdentity
264 }
265 fresh, err := a.Directory.Read(ctx)
266 if err != nil {
267 return out, err
268 }
269 if fresh.ETag != roster.ETag {
270 return out, ErrPending
271 }
272 return out, nil
273}
274func (a ProductionAuthority) Census(ctx context.Context, account, app, reservation string) (Census, error) {
275 return a.census(ctx, account, app, reservation, 0, nil)
276}
277func (a ProductionAuthority) Inventory(ctx context.Context, obs hostclaim.Observed) ([]string, []string, error) {
278 roster, err := a.Directory.Read(ctx)
279 if err != nil {
280 return nil, nil, err
281 }
282 if len(roster.Roster.Regions) == 0 {
283 return nil, nil, ErrContract
284 }
285 regions := make([]string, 0, len(roster.Roster.Regions))
286 boxes := make([]string, 0, len(roster.Roster.Boxes))
287 for r := range roster.Roster.Regions {
288 regions = append(regions, r)
289 }
290 for b := range roster.Roster.Boxes {
291 boxes = append(boxes, b)
292 }
293 sort.Strings(regions)
294 sort.Strings(boxes)
295 return regions, boxes, nil
296}
297func (a ProductionAuthority) Adoption(ctx context.Context, r Record) (region, box string, regions, boxes []string, err error) {
298 if a.Backfill == nil {
299 err = ErrContract
300 return
301 }
302 return a.Backfill.Adoption(ctx, r)
303}
304func (b *Backfill) Adoption(ctx context.Context, r Record) (region, box string, regions, boxes []string, err error) {
305 if err = CheckHostQuarantine(ctx, b.Store, r.Holder.Host); err != nil {
306 return
307 }
308 p, tag, e := b.read(ctx)
309 if e != nil {
310 err = e
311 return
312 }
313 selected, ok := p.Hosts[r.Holder.Host]
314 if !ok || selected.Key != r.Holder.Key || selected.ETag != r.ETag || selected.Acquisition == "" || selected.Region == "" || selected.Box == "" {
315 err = ErrIdentity
316 return
317 }
318 a := ProductionAuthority{Store: b.Store, Directory: b.Directory}
319 b.mu.Lock()
320 if b.cachedTag != tag {
321 b.cached = nil
322 }
323 var census Census
324 if b.cached != nil {
325 census = *b.cached
326 } else {
327 census, e = a.census(ctx, "", "", p.ID, p.DirectoryRevision, p.Reports)
328 if e == nil {
329 b.cached = &census
330 b.cachedTag = tag
331 }
332 }
333 b.mu.Unlock()
334 if e != nil {
335 err = e
336 return
337 }
338 current, e := a.Directory.Read(ctx)
339 if e != nil {
340 err = e
341 return
342 }
343 if current.ETag != census.Roster.ETag {
344 err = ErrPending
345 return
346 }
347 owners := map[string]bool{}
348 found := false
349 for _, rec := range census.Records {
350 if rec.Holder.Host == r.Holder.Host {
351 found = rec.ETag == r.ETag && rec.Holder.Key == r.Holder.Key
352 }
353 }
354 for ref, report := range census.Reports {
355 for _, i := range report.Installations {
356 if i.Host == r.Holder.Host {
357 owners[ref] = true
358 }
359 }
360 }
361 if !found || len(owners) != 1 || !owners[selected.Box] {
362 err = ErrIdentity
363 return
364 }
365 enrolled, ok := census.Roster.Roster.Boxes[selected.Box]
366 if !ok || enrolled.Region != selected.Region || enrolled.State != "active" {
367 err = ErrIdentity
368 return
369 }
370 if r.Holder.Family == "instance" {
371 var instance provision.Record
372 if json.Unmarshal(r.Body, &instance) != nil || len(instance.Replicas) != 0 || provision.ClaimEverywhere(instance) {
373 err = ErrIdentity
374 return
375 }
376 }
377 raw, _, e := b.Store.Get(ctx, domains.ClaimKey(r.Holder.Host))
378 if e != nil {
379 err = e
380 return
381 }
382 var ownership domains.Claim
383 inc, e := r.Incarnation()
384 if e != nil {
385 err = e
386 return
387 }
388 if decode(raw, &ownership) != nil || ownership.Host != r.Holder.Host || ownership.Account != r.Holder.Account || ownership.App != r.Holder.App || ownership.Acquisition != selected.Acquisition || ownership.RecordIncarnation != inc || ownership.OwnershipState != "committed" || ownership.Pending {
389 err = ErrIdentity
390 return
391 }
392 if r.Holder.Acquisition != selected.Acquisition {
393 err = ErrIdentity
394 return
395 }
396 _, freshTag, e := b.read(ctx)
397 if e != nil {
398 err = e
399 return
400 }
401 if freshTag != tag {
402 err = ErrPending
403 return
404 }
405 for region := range census.Roster.Roster.Regions {
406 if region != selected.Region {
407 regions = append(regions, region)
408 }
409 }
410 for ref := range census.Roster.Roster.Boxes {
411 if ref != selected.Box {
412 boxes = append(boxes, ref)
413 }
414 }
415 sort.Strings(regions)
416 sort.Strings(boxes)
417 return selected.Region, selected.Box, regions, boxes, nil
418}
419