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/rand"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "os"
12 "sort"
13 "strconv"
14 "strings"
15
16 "github.com/carlosframework/platform/internal/domains"
17 "github.com/carlosframework/platform/internal/hostactors"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 "github.com/carlosframework/platform/internal/hostfloor"
20 "github.com/carlosframework/platform/internal/park"
21 "github.com/carlosframework/platform/internal/provision"
22)
23
24func (a ProductionAuthority) admission(ctx context.Context, account, app string) (AdmissionObserved, error) {
25 raw, _, err := a.Store.Get(ctx, "apps/"+account+"/"+app+"/admission.json")
26 if err != nil {
27 return AdmissionObserved{}, err
28 }
29 var id struct {
30 AppID string `json:"app_id"`
31 }
32 if err = json.Unmarshal(raw, &id); err != nil {
33 return AdmissionObserved{}, err
34 }
35 return ReadAdmission(ctx, a.Store, account, app, id.AppID)
36}
37func (a ProductionAuthority) Create(ctx context.Context, before hostclaim.Observed, r Record, id string) error {
38 if err := CheckCreateFreeze(ctx, a.Store, a.Directory.PublicKey); err != nil {
39 return err
40 }
41 adm, err := a.admission(ctx, r.Holder.Account, r.Holder.App)
42 if err != nil {
43 return err
44 }
45 reg, ok := adm.Value.Registrations[id]
46 inc, err := r.Incarnation()
47 if err != nil {
48 return err
49 }
50 if !ok || adm.Value.State != "open" || adm.Value.Op != nil || reg.Kind != "create" || reg.Host != r.Holder.Host || reg.AppID != adm.Value.AppID || reg.Epoch != adm.Value.Epoch || reg.Owner.Console != a.ConsoleID || reg.Owner.Enrollment != a.Enrollment || reg.Claim != SavedObservation(before) || reg.Incarnation != inc || !reg.CreatedAt.Equal(r.Holder.CreatedAt) || !RegistrationLive(reg, a.Now()) {
51 return ErrPending
52 }
53 owner, ownerErr := domains.GetClaim(ctx, a.Store, r.Holder.Host)
54 if ownerErr != nil && !errors.Is(ownerErr, os.ErrNotExist) {
55 return ownerErr
56 }
57 if ownerErr == nil && owner != nil {
58 legacy := owner.Acquisition == "" && owner.RecordIncarnation == ""
59 if legacy && owner.Pending && !r.VerifiedOwnership {
60 return ErrPending
61 }
62 bound := owner.Acquisition == r.Holder.Acquisition && owner.RecordIncarnation == inc
63 if owner.Account != r.Holder.Account || owner.App != r.Holder.App || (!legacy && !bound) {
64 return ErrPending
65 }
66 if legacy {
67 if _, _, e := a.Store.Get(ctx, r.Holder.Key); !errors.Is(e, os.ErrNotExist) {
68 if e != nil {
69 return e
70 }
71 return ErrPending
72 }
73 }
74 }
75 _, _, err = a.Store.Get(ctx, "apps/"+r.Holder.Account+"/"+r.Holder.App+"/purge.json")
76 if !errors.Is(err, os.ErrNotExist) {
77 if err != nil {
78 return err
79 }
80 return ErrPending
81 }
82 return nil
83}
84
85func operationToken() (string, error) {
86 var b [16]byte
87 if _, err := rand.Read(b[:]); err != nil {
88 return "", err
89 }
90 return hex.EncodeToString(b[:]), nil
91}
92
93type executorCandidate struct {
94 box, enrollment, hash string
95 sameApp bool
96 hosts int
97}
98
99// selectExecutor is claim Plan 5's create-time selection over this
100// console's namespace, the record's region and pool, active and unfenced.
101// With a chooser (ProductionAuthority.Executors, CARLOS_REGION_HA on) the
102// one choice is the chooser's, over its edges evidence; this scope still
103// binds it, so a chosen box outside it refuses the create.
104func (a ProductionAuthority) selectExecutor(ctx context.Context, before hostclaim.Observed, r Record) (string, string, string, error) {
105 var shape struct {
106 Pool string `json:"pool"`
107 Replicas []string `json:"replicas"`
108 }
109 if err := json.Unmarshal(r.Body, &shape); err != nil {
110 return "", "", "", err
111 }
112 if len(shape.Replicas) != 0 {
113 return "", "", "", ErrContract
114 }
115 everywhere := false
116 if r.Holder.Family == "instance" {
117 var rec provision.Record
118 if err := json.Unmarshal(r.Body, &rec); err != nil {
119 return "", "", "", err
120 }
121 everywhere = provision.ClaimEverywhere(rec)
122 }
123 pool := shape.Pool
124 if pool == "" {
125 pool = "default"
126 }
127 roster, err := a.Directory.Read(ctx)
128 if err != nil {
129 return "", "", "", err
130 }
131 if !roster.Roster.MatchesHome(a.HomeRegion) {
132 return "", "", "", ErrIdentity
133 }
134 candidates := map[string]*executorCandidate{}
135 servingScope := 0
136 for ref, b := range roster.Roster.Boxes {
137 if b.Namespace == a.Namespace && b.Pool == pool && b.State == "active" {
138 servingScope++
139 }
140 if b.Namespace != a.Namespace || !park.SameRegion(a.HomeRegion, b.Region, r.Holder.Region) || b.Pool != pool || b.State != "active" {
141 continue
142 }
143 fenced, err := a.Directory.TerminationFence(ctx, roster, ref, a.Now())
144 if err != nil {
145 return "", "", "", err
146 }
147 if fenced {
148 continue
149 }
150 sum := sha256.Sum256([]byte(r.Holder.Host + "\x00" + ref))
151 candidates[ref] = &executorCandidate{box: ref, enrollment: b.Enrollment, hash: hex.EncodeToString(sum[:])}
152 }
153 if everywhere && servingScope != 1 {
154 return "", "", "", ErrContract
155 }
156 if sel, selected, err := a.SelectExecutor(ctx, before, r); err != nil {
157 return "", "", "", err
158 } else if selected {
159 if c := candidates[sel.Box]; c == nil || c.enrollment != sel.Enrollment {
160 return "", "", "", fmt.Errorf("%w: %s is not in this console's namespace and the record's pool", ErrNoEligibleBox, sel.Box)
161 }
162 return sel.Box, sel.Enrollment, pool, nil
163 }
164 hosts, err := ListClaims(ctx, a.Store)
165 if err != nil {
166 return "", "", "", err
167 }
168 for _, host := range hosts {
169 obs, err := hostclaim.Read(ctx, a.Store, host)
170 if err != nil {
171 return "", "", "", err
172 }
173 if obs.Absent || obs.Claim.Holder == nil {
174 continue
175 }
176 box := obs.Claim.Op.Exec
177 if box == "" {
178 box = obs.Claim.Op.Intent["selected_executor"]
179 }
180 c := candidates[box]
181 if c == nil {
182 continue
183 }
184 c.hosts++
185 h := obs.Claim.Holder
186 if h.Account == r.Holder.Account && h.App == r.Holder.App {
187 c.sameApp = true
188 }
189 }
190 var ordered []*executorCandidate
191 for _, c := range candidates {
192 ordered = append(ordered, c)
193 }
194 sort.Slice(ordered, func(i, j int) bool {
195 x, y := ordered[i], ordered[j]
196 if x.sameApp != y.sameApp {
197 return !x.sameApp
198 }
199 if x.hosts != y.hosts {
200 return x.hosts < y.hosts
201 }
202 if x.hash != y.hash {
203 return x.hash > y.hash
204 }
205 return x.box < y.box
206 })
207 if len(ordered) == 0 {
208 return "", "", "", ErrPending
209 }
210 return ordered[0].box, ordered[0].enrollment, pool, nil
211}
212
213func (a ProductionAuthority) StartCreate(ctx context.Context, before hostclaim.Observed, r Record, id, by string) (hostclaim.Observed, error) {
214 box, enrollment, pool, err := a.selectExecutor(ctx, before, r)
215 if err != nil {
216 return hostclaim.Observed{}, err
217 }
218 adm, err := a.admission(ctx, r.Holder.Account, r.Holder.App)
219 if err != nil {
220 return hostclaim.Observed{}, err
221 }
222 region := r.Holder.Region
223 if region == park.HomeRegion {
224 region = a.HomeRegion
225 }
226 scope := hostactors.Scope{Namespace: a.Namespace, Regions: []string{region}, Pools: []string{pool}, Participants: []string{box}, ConsoleID: a.ConsoleID}
227 // Final admission check is after census/reservation, immediately before
228 // the one CAS against the observation saved before registration.
229 if err = a.Create(ctx, before, r, id); err != nil {
230 return hostclaim.Observed{}, err
231 }
232 ownershipEffect, err := operationToken()
233 if err != nil {
234 return hostclaim.Observed{}, err
235 }
236 recordEffect, err := operationToken()
237 if err != nil {
238 return hostclaim.Observed{}, err
239 }
240 transition := hostclaim.BeginSelectedCreate(r.Holder, r.Body, id, by, a.Now(), box, enrollment, adm.Value.AppID, adm.Value.Epoch, ownershipEffect, recordEffect)
241 return a.startHeldClaim(ctx, before, id, scope, func() error { return a.Create(ctx, before, r, id) }, func(c *hostclaim.Claim) error {
242 if err := transition(c); err != nil {
243 return err
244 }
245 if r.VerifiedOwnership {
246 c.Op.Intent["ownership_verified"] = "true"
247 }
248 return nil
249 })
250}
251
252func (a ProductionAuthority) Executor(ctx context.Context, obs hostclaim.Observed, r Record) (string, error) {
253 if obs.Absent || obs.Claim.Holder == nil || !matches(r, obs.Claim.Holder) {
254 return "", ErrIdentity
255 }
256 op := obs.Claim.Op
257 box, enrollment := op.Intent["selected_executor"], op.Intent["executor_enrollment"]
258 if box == "" || enrollment == "" {
259 return "", ErrContract
260 }
261 if err := a.Directory.CheckHold(ctx, obs.Claim, box, enrollment); err != nil {
262 return "", err
263 }
264 actor, err := a.Directory.Lookup(ctx, box)
265 if err != nil {
266 return "", err
267 }
268 if actor.State != "active" || actor.Enrollment != enrollment {
269 return "", ErrPending
270 }
271 adm, err := ReadAdmission(ctx, a.Store, r.Holder.Account, r.Holder.App, op.Intent["app_id"])
272 if err != nil {
273 return "", err
274 }
275 epoch, err := strconv.ParseUint(op.Intent["admission_epoch"], 10, 64)
276 if err != nil || epoch != adm.Value.Epoch {
277 return "", ErrIdentity
278 }
279 foundRecord := false
280 for _, e := range adm.Value.Effects {
281 if e.OperationID != op.ID {
282 continue
283 }
284 if e.AppID != adm.Value.AppID || e.State != "complete" {
285 return "", ErrPending
286 }
287 if e.Resource == r.Holder.Key && e.Action == "record-put" {
288 foundRecord = true
289 }
290 }
291 if !foundRecord {
292 return "", ErrPending
293 }
294 if err = a.FirstPhase(ctx, obs); err != nil {
295 return "", err
296 }
297 return box, nil
298}
299
300func (a ProductionAuthority) FirstPhase(ctx context.Context, obs hostclaim.Observed) error {
301 if obs.Absent || obs.Claim == nil || obs.Claim.Op.ID == "" {
302 return ErrIdentity
303 }
304 roster, err := a.Directory.Read(ctx)
305 if err != nil {
306 return err
307 }
308 hold, ok := roster.Roster.Holds[obs.Claim.Op.Intent["actors_hold"]]
309 if !ok || hold.Host != obs.Host || hold.OpID != obs.Claim.Op.ID {
310 return ErrPending
311 }
312 floor, err := hostfloor.Read(ctx, a.Store, a.Directory.PublicKey, a.Directory.Bucket)
313 if err != nil {
314 return err
315 }
316 if floor.Digest != hold.FloorSHA256 || roster.Roster.FloorSHA256 != floor.Digest {
317 return ErrPending
318 }
319 for actor, enrollment := range hold.Actors {
320 report, ok := hold.Releases[actor]
321 if !ok {
322 return ErrPending
323 }
324 instance := ""
325 if ref, ok := strings.CutPrefix(actor, "box:"); ok {
326 box := roster.Roster.Boxes[ref]
327 if box.State != "active" || box.Enrollment != enrollment {
328 return ErrPending
329 }
330 fenced, err := a.Directory.TerminationFence(ctx, roster, ref, a.Now())
331 if err != nil {
332 return err
333 }
334 if fenced {
335 return ErrPending
336 }
337 instance = box.Cloud.InstanceID
338 } else if ref, ok := strings.CutPrefix(actor, "console:"); ok {
339 console := roster.Roster.Consoles[ref]
340 if console.Enrollment != enrollment {
341 return ErrPending
342 }
343 instance = console.InstanceID
344 } else {
345 return ErrIdentity
346 }
347 if err = hostactors.VerifyReport(ctx, a.Evidence, a.Directory.PublicKey, floor, report, enrollment, instance, a.Now()); err != nil {
348 return err
349 }
350 }
351 if obs.Claim.Op.Kind == hostclaim.KindPurging {
352 if err := a.StopBeforeTeardown(ctx, obs); err != nil {
353 return err
354 }
355 }
356 for _, box := range obs.Claim.Op.Participants {
357 var ack hostclaim.ExecutionAcknowledgement
358 if json.Unmarshal([]byte(obs.Claim.Op.Journal["exec_ack."+box]), &ack) != nil || ack.Op != obs.Claim.Op.ID || ack.Enrollment != hold.Actors["box:"+box] || ack.BootID == "" || ack.ControllerID == "" || ack.LeaseETag == "" || !a.Now().Before(ack.Expires.Add(-RegistrationSkew)) {
359 return ErrPending
360 }
361 }
362 return nil
363}
364func (a ProductionAuthority) StartDelete(ctx context.Context, obs hostclaim.Observed, r Record) error {
365 if obs.Absent || obs.Claim.Holder == nil || !matches(r, obs.Claim.Holder) {
366 return ErrIdentity
367 }
368 if obs.Claim.Op.Kind != hostclaim.KindServing {
369 return ErrPending
370 }
371 adm, err := a.admission(ctx, r.Holder.Account, r.Holder.App)
372 if err != nil {
373 return err
374 }
375 if adm.Value.State != "closed" || adm.Value.Op == nil || adm.Value.Op.Kind != "deleting" || len(adm.Value.Registrations) != 0 {
376 return ErrPending
377 }
378 for _, effect := range adm.Value.Effects {
379 if effect.State != "complete" {
380 return ErrPending
381 }
382 }
383 return nil
384}
385func (a ProductionAuthority) PurgeRetirement(ctx context.Context, obs hostclaim.Observed) error {
386 if obs.Absent || obs.Claim.Holder == nil || obs.Claim.Op.Kind != hostclaim.KindPurging {
387 return ErrIdentity
388 }
389 h := obs.Claim.Holder
390 journal, _, err := ReadPurge(ctx, a.Store, h.Account, h.App)
391 if err != nil {
392 return err
393 }
394 if journal.ID != obs.Claim.Op.ID || journal.Phase != "torn-down" {
395 return ErrPending
396 }
397 return nil
398}
399