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
3import (
4 "context"
5 "crypto/sha256"
6 "encoding/hex"
7 "encoding/json"
8 "path/filepath"
9 "sort"
10 "strings"
11 "sync"
12
13 "github.com/carlosframework/platform/internal/hostbox"
14 "github.com/carlosframework/platform/internal/hostclaim"
15 "github.com/carlosframework/platform/internal/hostguard"
16 "github.com/carlosframework/platform/internal/hostwrite"
17 "github.com/carlosframework/platform/internal/provision"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21// These selectors contain no local paths. Root must independently resolve the
22// installation from its state inventory before returning an authorization.
23type servingArtifactIdentity = hostbox.ServingArtifactIdentity
24
25type servingLegacyPredecessor = hostbox.ServingLegacyPredecessor
26
27type servingParkAuthorization = hostbox.ServingParkResult
28
29// A zero predecessor asks Park to resume an existing record for successor;
30// it must never authorize a new retirement. Complete independently rereads
31// the claim's paired tags. Neither call may run while Work is held.
32type servingDatabaseParker interface {
33 Park(context.Context, servingArtifactIdentity, servingArtifactIdentity) (servingParkAuthorization, error)
34 ParkLegacy(context.Context, servingArtifactIdentity, servingLegacyPredecessor) (servingParkAuthorization, error)
35 Complete(context.Context, string, servingArtifactIdentity) error
36}
37
38type claimServingArtifactRetirement struct {
39 Claims release.Store // The assembly's authorized claim writer, never a new bucket client.
40 Guard hostguard.Guard
41 DataDir, Box, Region string
42 Root servingDatabaseParker
43 mu sync.Mutex
44 completed map[string]servingParkAuthorization
45}
46
47func servingRetirementID(prior, original servingArtifactIdentity, legacy bool) string {
48 body, _ := json.Marshal(struct {
49 Prior, Successor servingArtifactIdentity
50 Legacy bool
51 }{prior, original, legacy})
52 sum := sha256.Sum256(body)
53 return hex.EncodeToString(sum[:])
54}
55
56func servingWriterIdentity(o hostclaim.Observed, box, region string) (servingArtifactIdentity, error) {
57 if o.Absent || o.Host == "" || o.Claim == nil || o.Claim.Host != o.Host || o.Claim.Holder == nil || box == "" || region == "" {
58 return servingArtifactIdentity{}, hostbox.ErrChanged
59 }
60 c, h := o.Claim, o.Claim.Holder
61 if c.Op.Kind != hostclaim.KindServing || c.Op.ID != "" || c.Op.Phase != "" || c.Op.Exec != box || h.WriterBox != box || h.Incarnation == "" || h.Location == "" || h.Generation == "" || h.Session == 0 || h.Stamp != h.Session {
62 return servingArtifactIdentity{}, hostbox.ErrChanged
63 }
64 found := 0
65 for _, a := range h.Admissions {
66 if a.Session != h.Session {
67 continue
68 }
69 if a.Box != box || a.Region != region || a.Generation != h.Generation || a.Mode != "cold" {
70 return servingArtifactIdentity{}, hostbox.ErrChanged
71 }
72 found++
73 }
74 if found != 1 {
75 return servingArtifactIdentity{}, hostbox.ErrChanged
76 }
77 return servingArtifactIdentity{Host: o.Host, Box: box, Region: region, Incarnation: h.Incarnation, Location: h.Location, Generation: h.Generation, Stamp: h.Stamp, Session: h.Session}, nil
78}
79
80func (r *claimServingArtifactRetirement) snapshot(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) (hostclaim.Observed, servingArtifactIdentity, string, uint64, error) {
81 fail := func(err error) (hostclaim.Observed, servingArtifactIdentity, string, uint64, error) {
82 return hostclaim.Observed{}, servingArtifactIdentity{}, "", 0, err
83 }
84 if ctx == nil || r == nil || r.Claims == nil || r.Root == nil || !filepath.IsAbs(r.DataDir) || r.Guard.DataDir != r.DataDir {
85 return fail(hostbox.ErrUnsupported)
86 }
87 expected, err := servingWriterIdentity(admitted, r.Box, r.Region)
88 if err != nil {
89 return fail(err)
90 }
91 if in.DBPath != provision.NestedDBPath(r.DataDir, admitted.Host) {
92 return fail(hostbox.ErrChanged)
93 }
94 db, err := filepath.Rel(r.DataDir, in.DBPath)
95 if err != nil || !filepath.IsLocal(db) {
96 return fail(hostbox.ErrChanged)
97 }
98 db = filepath.ToSlash(db)
99 fresh, err := hostclaim.Read(ctx, r.Claims, admitted.Host)
100 if err != nil {
101 return fail(err)
102 }
103 actual, err := servingWriterIdentity(fresh, r.Box, r.Region)
104 if err != nil || actual != expected {
105 return fail(hostbox.ErrChanged)
106 }
107 a, err := servingInventoryPair(fresh.Claim.Inventory[r.Box], db)
108 if err != nil || a > actual.Stamp {
109 return fail(hostbox.ErrChanged)
110 }
111 return fresh, actual, db, a, nil
112}
113
114func servingPredecessor(c *hostclaim.Claim, current servingArtifactIdentity, stamp uint64, generation string) (servingArtifactIdentity, error) {
115 if stamp == 0 || stamp >= current.Stamp {
116 return servingArtifactIdentity{}, hostbox.ErrChanged
117 }
118 var out servingArtifactIdentity
119 count := 0
120 for _, a := range c.Holder.Admissions {
121 if a.Session != stamp {
122 continue
123 }
124 if a.Mode != "cold" || a.Box != current.Box || a.Region != current.Region || a.Generation == "" || a.Generation == current.Generation || (generation != "" && a.Generation != generation) {
125 return out, hostbox.ErrChanged
126 }
127 out = current
128 out.Session, out.Stamp, out.Generation = a.Session, stamp, a.Generation
129 count++
130 }
131 if count != 1 {
132 return servingArtifactIdentity{}, hostbox.ErrChanged
133 }
134 return out, nil
135}
136
137func (r *claimServingArtifactRetirement) stopped(ctx context.Context, prior servingArtifactIdentity) error {
138 receipts, err := r.Guard.CompletedServingStops(ctx, prior.Host, hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp}, prior.Session, prior.Box)
139 if err != nil {
140 return err
141 }
142 if len(receipts) == 0 {
143 return hostguard.ErrNoFenceReceipt
144 }
145 for _, receipt := range receipts {
146 if receipt.Target.Region != prior.Region || verifyRetirementReceipt(ctx, receipt) != nil {
147 return ErrClaimStopReceipt
148 }
149 }
150 return nil
151}
152
153func (r *claimServingArtifactRetirement) Retire(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, generation string) error {
154 if generation == "" {
155 return hostbox.ErrChanged
156 }
157 return r.recover(ctx, admitted, in, generation)
158}
159
160func (r *claimServingArtifactRetirement) Recover(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) error {
161 return r.recover(ctx, admitted, in, "")
162}
163
164func (r *claimServingArtifactRetirement) recover(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, generation string) error {
165 r.forgetCompletion(admitted.Host)
166 fresh, current, _, stamp, err := r.snapshot(ctx, admitted, in)
167 if err != nil {
168 return err
169 }
170 if stamp == 0 {
171 // Legacy authority is available only after PrepareState. In particular,
172 // missing tags never authorize deleting an existing generation marker.
173 if generation != "" {
174 return hostbox.ErrChanged
175 }
176 return nil
177 }
178 var prior servingArtifactIdentity
179 if stamp != current.Stamp {
180 prior, err = servingPredecessor(fresh.Claim, current, stamp, generation)
181 if err != nil {
182 return err
183 }
184 if err = r.stopped(ctx, prior); err != nil {
185 return err
186 }
187 }
188 local, err := servingAgentDirectory(in.DBPath)
189 if err != nil {
190 return err
191 }
192 authorization, err := r.Root.Park(ctx, current, prior)
193 if err != nil {
194 return err
195 }
196 if generation != "" && authorization.Predecessor.Generation != generation {
197 return hostbox.ErrChanged
198 }
199 return r.finish(ctx, admitted, in, authorization, local, nil)
200}
201
202func (r *claimServingArtifactRetirement) BeforeLegacyInstall(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, work hostwrite.Work) error {
203 r.forgetCompletion(admitted.Host)
204 _, current, _, stamp, err := r.snapshot(ctx, admitted, in)
205 if err != nil {
206 return err
207 }
208 if work == nil {
209 return hostbox.ErrUnsupported
210 }
211 if stamp != 0 {
212 return r.Recover(ctx, admitted, in)
213 }
214 prior := servingLegacyPredecessor{Session: in.Admission.ObservedSession, Generation: in.Admission.ObservedGeneration, Empty: in.Empty && in.Admission.ObservedSession == 0}
215 if (prior.Session == 0) != (prior.Generation == "") || prior.Session >= current.Session || (prior.Empty && prior.Session != 0) {
216 return hostbox.ErrChanged
217 }
218 local, err := servingAgentDirectory(in.DBPath)
219 if err != nil {
220 return err
221 }
222 authorization, err := r.Root.ParkLegacy(ctx, current, prior)
223 if err != nil {
224 return err
225 }
226 if !authorization.Legacy {
227 return hostbox.ErrChanged
228 }
229 return r.finish(ctx, admitted, in, authorization, local, work)
230}
231
232func (r *claimServingArtifactRetirement) validateAuthorization(ctx context.Context, fresh hostclaim.Observed, current servingArtifactIdentity, a servingParkAuthorization) error {
233 original := a.OriginalSuccessor
234 if a.TagsFrom > current.Stamp {
235 return hostbox.ErrChanged
236 }
237 if a.TagsFrom != 0 && a.TagsFrom != current.Stamp && a.TagsFrom != a.Predecessor.Stamp {
238 if a.TagsFrom < original.Stamp {
239 return hostbox.ErrChanged
240 }
241 if _, err := servingPredecessor(fresh.Claim, current, a.TagsFrom, ""); err != nil {
242 return err
243 }
244 }
245 if !a.Legacy && a.TagsFrom == 0 {
246 return hostbox.ErrChanged
247 }
248 if a.Successor != current || original.Host != current.Host || original.Box != current.Box || original.Region != current.Region || original.Incarnation != current.Incarnation || original.Location != current.Location || original.Stamp == 0 || original.Session != original.Stamp || original.Generation == "" || original.Session > current.Session || a.Directory.Device == 0 || a.Directory.Inode == 0 || a.Directory.Mount == 0 || a.ID != servingRetirementID(a.Predecessor, original, a.Legacy) {
249 return hostbox.ErrChanged
250 }
251 if original.Session == current.Session && original != current {
252 return hostbox.ErrChanged
253 }
254 // A continuation preserves the original record identity; it cannot rename
255 // that record to the next admission. Root authenticates its own continuation.
256 if original.Session < current.Session {
257 if _, err := servingPredecessor(fresh.Claim, current, original.Stamp, original.Generation); err != nil {
258 return err
259 }
260 }
261 if !a.Legacy {
262 prior, err := servingPredecessor(fresh.Claim, current, a.Predecessor.Stamp, a.Predecessor.Generation)
263 if err != nil || prior != a.Predecessor || prior.Stamp >= original.Stamp {
264 return hostbox.ErrChanged
265 }
266 return r.stopped(ctx, prior)
267 }
268 if a.Predecessor != (servingArtifactIdentity{}) {
269 return hostbox.ErrChanged
270 }
271 return nil
272}
273
274func (r *claimServingArtifactRetirement) finish(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, a servingParkAuthorization, local servingArtifactDirectory, work hostwrite.Work) error {
275 fresh, current, _, _, err := r.snapshot(ctx, admitted, in)
276 if err != nil {
277 return err
278 }
279 if err = r.validateAuthorization(ctx, fresh, current, a); err != nil {
280 return err
281 }
282 // Mount IDs belong to a namespace. Root keeps its original mount identity;
283 // the hardened agent pins its own mount before the RPC and checks it again
284 // during enrollment. Both views must still name the same device and inode.
285 if a.Directory.Device != local.Device || a.Directory.Inode != local.Inode || local.Mount == 0 {
286 return hostbox.ErrChanged
287 }
288 a.Directory = local
289 enroll := func() error { return r.enrollPairLocked(ctx, admitted, in, a) }
290 if work != nil {
291 err = work.With(ctx, in.Identity, enroll)
292 } else {
293 err = r.Guard.With(ctx, current.Host, hostguard.Captured{Incarnation: current.Incarnation, Stamp: current.Stamp}, func(c *hostclaim.Claim) error {
294 identity, err := servingWriterIdentity(hostclaim.Observed{Host: current.Host, Claim: c}, r.Box, r.Region)
295 if err != nil || identity != current {
296 return hostbox.ErrChanged
297 }
298 return nil
299 }, enroll)
300 }
301 if err != nil {
302 return err
303 }
304 if err = r.Root.Complete(ctx, a.ID, current); err != nil {
305 return err
306 }
307 r.mu.Lock()
308 if r.completed == nil {
309 r.completed = make(map[string]servingParkAuthorization)
310 }
311 r.completed[current.Host] = a
312 r.mu.Unlock()
313 return nil
314}
315
316// enrollPairLocked runs only under the selected Work or the matching Guard.
317// The root authorization proves the old sources have been consumed. The
318// descriptor check prevents a replacement inode from reaching purgeDB.
319func (r *claimServingArtifactRetirement) enrollPairLocked(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs, a servingParkAuthorization) error {
320 fresh, current, db, stamp, err := r.snapshot(ctx, admitted, in)
321 if err != nil {
322 return err
323 }
324 if a.Successor != current {
325 return hostbox.ErrChanged
326 }
327 prior := a.TagsFrom
328 if stamp != prior && stamp != current.Stamp {
329 return hostbox.ErrChanged
330 }
331 if err = checkServingArtifactAbsence(in.DBPath, a.Directory); err != nil {
332 return err
333 }
334 transition := servingInventoryTransition(current, db, prior)
335 if stamp == current.Stamp {
336 return transition(fresh.Claim)
337 }
338 written, updateErr := hostclaim.Update(ctx, r.Claims, fresh, transition)
339 if updateErr == nil {
340 if err := in.observeClaimWrite(fresh, written); err != nil {
341 return err
342 }
343 } else if in.claimWrite != nil {
344 return updateErr
345 }
346 // An uncertain CAS is resolved only by a fresh, exact pair and the same
347 // writer. A refusal never permits a database or marker write.
348 after, same, _, tagged, readErr := r.snapshot(ctx, admitted, in)
349 if readErr != nil {
350 return readErr
351 }
352 if same != current || tagged != current.Stamp {
353 if updateErr != nil {
354 return updateErr
355 }
356 return hostbox.ErrChanged
357 }
358 if err = transition(after.Claim); err != nil {
359 return err
360 }
361 return checkServingArtifactAbsence(in.DBPath, a.Directory)
362}
363
364func servingInventoryTransition(current servingArtifactIdentity, db string, prior uint64) func(*hostclaim.Claim) error {
365 return func(c *hostclaim.Claim) error {
366 actual, err := servingWriterIdentity(hostclaim.Observed{Host: current.Host, Claim: c}, current.Box, current.Region)
367 if err != nil || actual != current || !filepath.IsLocal(db) || filepath.ToSlash(filepath.Clean(db)) != db || strings.ContainsAny(db, "\\\x00") {
368 return hostbox.ErrChanged
369 }
370 entry := c.Inventory[current.Box]
371 stamp, err := servingInventoryPair(entry, db)
372 if err != nil || (stamp != prior && stamp != current.Stamp) {
373 return hostbox.ErrChanged
374 }
375 // Copy before editing: a rejected transition cannot mutate its observation.
376 paths := make(map[string]uint64, len(entry.Paths)+2)
377 for p, s := range entry.Paths {
378 paths[p] = s
379 }
380 paths[db], paths[db+".gen"] = current.Stamp, current.Stamp
381 if c.Inventory == nil {
382 c.Inventory = make(map[string]hostclaim.InventoryEntry)
383 }
384 c.Inventory[current.Box] = hostclaim.InventoryEntry{Layout: "nested", Paths: paths}
385 return nil
386 }
387}
388
389func (r *claimServingArtifactRetirement) EnrollLocked(ctx context.Context, admitted hostclaim.Observed, in ClaimLaunchInputs) error {
390 if r == nil {
391 return hostbox.ErrUnsupported
392 }
393 r.mu.Lock()
394 a, ok := r.completed[admitted.Host]
395 r.mu.Unlock()
396 if !ok {
397 return hostbox.ErrChanged
398 }
399 // This final hook can only revalidate a pair completed outside Work. It
400 // cannot finish an incomplete root record or enroll an unparked legacy DB.
401 _, current, _, stamp, err := r.snapshot(ctx, admitted, in)
402 if err != nil {
403 return err
404 }
405 if a.Successor != current || stamp != current.Stamp {
406 return hostbox.ErrChanged
407 }
408 return r.enrollPairLocked(ctx, admitted, in, a)
409}
410
411// servingInventoryPair preserves RecordInventory's normalized-path and
412// ancestor rules without taking its lock again inside Work.
413func servingInventoryPair(entry hostclaim.InventoryEntry, db string) (uint64, error) {
414 if !filepath.IsLocal(db) || filepath.ToSlash(filepath.Clean(db)) != db || strings.ContainsAny(db, "\\\x00") {
415 return 0, hostbox.ErrChanged
416 }
417 if len(entry.Paths) != 0 && entry.Layout != "nested" {
418 return 0, hostbox.ErrChanged
419 }
420 left, leftOK := entry.Paths[db]
421 right, rightOK := entry.Paths[db+".gen"]
422 if leftOK != rightOK || left != right || (leftOK && left == 0) {
423 return 0, hostbox.ErrChanged
424 }
425 all := make([]string, 0, len(entry.Paths)+2)
426 for p, stamp := range entry.Paths {
427 if !filepath.IsLocal(p) || filepath.ToSlash(filepath.Clean(p)) != p || strings.ContainsAny(p, "\\\x00") || stamp == 0 {
428 return 0, hostbox.ErrChanged
429 }
430 all = append(all, p)
431 }
432 if !leftOK {
433 all = append(all, db, db+".gen")
434 }
435 sort.Strings(all)
436 for i, p := range all {
437 for _, next := range all[i+1:] {
438 if strings.HasPrefix(next, p+"/") {
439 return 0, hostbox.ErrChanged
440 }
441 }
442 }
443 return left, nil
444}
445
446// bindServingArtifactRetirement composes the serving contract with whichever
447// operation owners the assembly already enabled. A missing root client leaves
448// the existing production refusal in place.
449func bindServingArtifactRetirement(w *ClaimWakeAssembly, r *claimServingArtifactRetirement) error {
450 if w == nil || r == nil || r.Root == nil || r.Claims == nil {
451 return hostbox.ErrUnsupported
452 }
453 operationRetire, operationRecover := w.RetireArtifacts, w.RecoverArtifacts
454 w.RetireArtifacts = func(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs, generation string) error {
455 if o.Claim != nil && o.Claim.Op.Kind == hostclaim.KindServing {
456 return r.Retire(ctx, o, in, generation)
457 }
458 if operationRetire == nil {
459 return hostbox.ErrChanged
460 }
461 return operationRetire(ctx, o, in, generation)
462 }
463 w.RecoverArtifacts = func(ctx context.Context, o hostclaim.Observed, in ClaimLaunchInputs) error {
464 if o.Claim != nil && o.Claim.Op.Kind == hostclaim.KindServing {
465 return r.Recover(ctx, o, in)
466 }
467 if operationRecover == nil {
468 return nil
469 }
470 return operationRecover(ctx, o, in)
471 }
472 w.BeforeLegacyInstall, w.BeforeInstallLocked = r.BeforeLegacyInstall, r.EnrollLocked
473 w.RequireServingEnrollment = true
474 return nil
475}
476
477func (r *claimServingArtifactRetirement) forgetCompletion(host string) {
478 if r == nil {
479 return
480 }
481 r.mu.Lock()
482 delete(r.completed, host)
483 r.mu.Unlock()
484}
485