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 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "io"
11 "log"
12 "os"
13 "reflect"
14 "time"
15
16 "github.com/carlosframework/platform/internal/hostbox"
17 "github.com/carlosframework/platform/internal/hostguard"
18 "github.com/carlosframework/platform/internal/rootstate"
19)
20
21// servingParkLedger runs behind root authorization and the journal transport.
22// Its callbacks are private implementation seams; requests cannot supply a
23// source, disposition, timestamp or completion.
24type servingParkLedger struct {
25 historical bool
26 state rootstate.Store
27 fs servingParkFilesystem
28 journal *hostbox.LaunchJournal
29 now func() time.Time
30}
31
32type servingParkFile struct {
33 Persistent servingParkPersistentFile `json:",omitzero"`
34 Name string
35 Present bool
36 Device, Inode, Mount, Bytes uint64
37 Mode, Links uint32
38 Moved, DeleteIntent, Deleted bool
39}
40
41type servingParkSource struct {
42 ParentPersistent servingParkPersistentDirectory `json:",omitzero"`
43 Volume string `json:",omitempty"`
44 Persistent servingParkPersistentDirectory `json:",omitzero"`
45 BootID string `json:",omitempty"`
46 Directory servingArtifactDirectory
47 Parent servingArtifactDirectory
48 Files [4]servingParkFile
49}
50
51type servingParkRecord struct {
52 EventPersistent servingParkPersistentDirectory `json:",omitzero"`
53 CollectAfter time.Time `json:",omitempty"`
54 Namespace servingArtifactDirectory `json:",omitempty"`
55 LegacyEvidence *servingLegacyEvidence `json:",omitempty"`
56 Continuations []servingParkContinuation `json:",omitempty"`
57 EventRemoveIntent, EventRemoved bool `json:",omitempty"`
58 Version int
59 Authorization servingParkAuthorization
60 Source servingParkSource
61 Receipts []hostguard.FenceReceipt
62 Disposition servingParkDisposition
63 Event servingArtifactDirectory
64 Prepared, Parked, Complete, Deleting, Closed bool
65 ParkedAt, ReapAfter time.Time
66 Cause string
67}
68
69type servingParkContinuation struct {
70 Authorization servingParkAuthorization
71 Receipts []hostguard.FenceReceipt
72}
73
74func (r servingParkRecord) activeAuthorization() servingParkAuthorization {
75 if len(r.Continuations) != 0 {
76 return r.Continuations[len(r.Continuations)-1].Authorization
77 }
78 return r.Authorization
79}
80
81type servingLegacyConsumption struct {
82 Collected bool `json:",omitempty"`
83 LegacyEvidence *servingLegacyEvidence `json:",omitempty"`
84 Receipts []hostguard.FenceReceipt `json:",omitempty"`
85 Authorization servingParkAuthorization
86 Source servingParkSource
87}
88
89type servingParkSlot struct {
90 Version int
91 Box, Host, ID, Pending, Last string
92}
93
94type servingParkFilesystem interface {
95 // Capture validates a root-authenticated source before any slot is changed.
96 Capture(servingParkAuthorization) (servingParkSource, error)
97 Prepare(*servingParkRecord, func() error) error
98 Move(*servingParkRecord, func() error) error
99 Delete(*servingParkRecord, func() error) error
100 CheckParked(*servingParkRecord) error
101}
102
103func servingParkKey(box, host string) string {
104 sum := sha256.Sum256([]byte(box + "\x00" + host))
105 return hex.EncodeToString(sum[:])
106}
107
108func servingParkDecode(body []byte, out any) error {
109 d := json.NewDecoder(bytes.NewReader(body))
110 d.DisallowUnknownFields()
111 if d.Decode(out) != nil || d.Decode(new(any)) != io.EOF {
112 return hostbox.ErrChanged
113 }
114 return nil
115}
116
117func (l *servingParkLedger) write(name string, value any) error {
118 body, err := json.Marshal(value)
119 if err != nil {
120 return err
121 }
122 return l.state.Write(name, body)
123}
124
125func (l *servingParkLedger) read(id string) (servingParkRecord, error) {
126 r, err := l.readHistorical(id)
127 if err == nil {
128 err = l.bindRecord(&r, false)
129 }
130 if err == nil {
131 err = validateServingParkRecord(r)
132 }
133 return r, err
134}
135
136func (l *servingParkLedger) readHistorical(id string) (servingParkRecord, error) {
137 var r servingParkRecord
138 if len(id) != 64 {
139 return r, hostbox.ErrChanged
140 }
141 if _, err := hex.DecodeString(id); err != nil {
142 return r, hostbox.ErrChanged
143 }
144 body, err := l.state.Read("retirement-" + id + ".json")
145 if err != nil {
146 return r, err
147 }
148 if err = servingParkDecode(body, &r); err != nil {
149 return r, err
150 }
151 if err = validateServingParkRecord(r); err != nil || r.Authorization.ID != id {
152 return r, hostbox.ErrChanged
153 }
154 return r, nil
155}
156
157func validateServingParkRecord(r servingParkRecord) error {
158 if err := validateServingParkPersistentRecord(r); err != nil {
159 return err
160 }
161 a := r.Authorization
162 s := a.OriginalSuccessor
163 if r.Version != 1 || a.ID != servingRetirementID(a.Predecessor, s, a.Legacy) || s.Host == "" || s.Box == "" || s.Incarnation == "" || s.Generation == "" || s.Region == "" || s.Location == "" || s.Stamp == 0 || s.Stamp != s.Session || a.Successor != s || a.Directory != r.Source.Directory || r.Source.Directory.Inode == 0 || r.Source.Directory.Device == 0 || r.Source.Directory.Mount == 0 || r.Source.Parent.Inode == 0 || r.Source.Parent.Device != r.Source.Directory.Device || r.Source.Parent.Mount != r.Source.Directory.Mount {
164 return hostbox.ErrChanged
165 }
166 if a.Legacy {
167 if a.Predecessor != (servingArtifactIdentity{}) || a.TagsFrom != 0 || r.Source.Files[1].Present {
168 return hostbox.ErrChanged
169 }
170 } else {
171 p := a.Predecessor
172 if p.Host != s.Host || p.Box != s.Box || p.Region != s.Region || p.Incarnation != s.Incarnation || p.Location != s.Location || p.Generation == "" || p.Generation == s.Generation || p.Stamp == 0 || p.Stamp != p.Session || p.Stamp >= s.Stamp || a.TagsFrom != p.Stamp {
173 return hostbox.ErrChanged
174 }
175 }
176 if r.LegacyEvidence != nil && validateServingLegacyEvidence(a, r.LegacyEvidence) != nil {
177 return hostbox.ErrChanged
178 }
179 noProcess := a.Legacy && r.LegacyEvidence != nil && r.LegacyEvidence.NoProcess != nil
180 if (len(r.Receipts) == 0) != noProcess {
181 return hostbox.ErrChanged
182 }
183 seen := make(map[string]bool)
184 for _, receipt := range r.Receipts {
185 t := receipt.Target
186 if t.Host != s.Host || t.Box != s.Box || t.Region != s.Region || t.Incarnation != s.Incarnation || t.Stamp == 0 || t.Stamp >= s.Stamp || t.Session >= s.Session || t.Op != "" || t.Phase != "" || t.ObligationID != "" || t.FenceID == "" || seen[t.FenceID] || verifyRetirementReceipt(context.Background(), receipt) != nil {
187 return hostbox.ErrChanged
188 }
189 if a.Legacy && r.LegacyEvidence != nil && t.Session != r.LegacyEvidence.Selector.Session {
190 return hostbox.ErrChanged
191 }
192 if !a.Legacy && (t.Stamp != a.Predecessor.Stamp || t.Session != a.Predecessor.Session) {
193 return hostbox.ErrChanged
194 }
195 seen[t.FenceID] = true
196 }
197 for i, suffix := range []string{"", ".gen", "-wal", "-shm"} {
198 f := r.Source.Files[i]
199 if f.Name != s.Host+".db"+suffix {
200 return hostbox.ErrChanged
201 }
202 if f.Present {
203 if f.Device != r.Source.Directory.Device || f.Mount != r.Source.Directory.Mount || f.Inode == 0 || f.Mode != 0100000 || f.Links != 1 {
204 return hostbox.ErrChanged
205 }
206 } else if f.Device != 0 || f.Inode != 0 || f.Mount != 0 || f.Mode != 0 || f.Links != 0 || f.Bytes != 0 || f.Moved || f.DeleteIntent || f.Deleted {
207 return hostbox.ErrChanged
208 }
209 if f.Deleted && (!f.DeleteIntent || !r.Deleting) || f.DeleteIntent && !f.Moved {
210 return hostbox.ErrChanged
211 }
212 }
213 if r.Disposition != servingParkRetain && r.Disposition != servingParkDiscard && r.Disposition != servingParkArtifactFree {
214 return hostbox.ErrChanged
215 }
216 if (r.Disposition == servingParkArtifactFree) == r.Source.Files[0].Present {
217 return hostbox.ErrChanged
218 }
219 if r.Prepared && (r.Event.Inode == 0 || r.Event.Device != r.Source.Directory.Device || r.Event.Mount != r.Source.Directory.Mount) {
220 return hostbox.ErrChanged
221 }
222 if r.Namespace != (servingArtifactDirectory{}) && (!r.Prepared || r.Namespace.Inode == 0 || r.Namespace.Device != r.Source.Parent.Device || r.Namespace.Mount != r.Source.Parent.Mount || r.Namespace == r.Source.Parent || r.Namespace == r.Event) {
223 return hostbox.ErrChanged
224 }
225 if r.Parked {
226 if !r.Prepared || r.ParkedAt.IsZero() || !r.ReapAfter.Equal(r.ParkedAt.Add(7*24*time.Hour)) {
227 return hostbox.ErrChanged
228 }
229 for _, f := range r.Source.Files {
230 if f.Present && !f.Moved {
231 return hostbox.ErrChanged
232 }
233 }
234 } else if !r.ParkedAt.IsZero() || !r.ReapAfter.IsZero() || r.Complete {
235 return hostbox.ErrChanged
236 }
237 if r.Deleting && (r.Cause != "discard" && r.Cause != "replacement" && r.Cause != "expiry" && r.Cause != "pressure") {
238 return hostbox.ErrChanged
239 }
240 if r.Deleting && !r.Parked {
241 return hostbox.ErrChanged
242 }
243 if r.EventRemoved && !r.EventRemoveIntent || r.EventRemoveIntent && !r.Deleting {
244 return hostbox.ErrChanged
245 }
246 if r.EventRemoveIntent {
247 for _, item := range r.Source.Files {
248 if item.Present && !item.Deleted {
249 return hostbox.ErrChanged
250 }
251 }
252 }
253 if r.Closed && !r.Deleting {
254 return hostbox.ErrChanged
255 }
256 if !r.CollectAfter.IsZero() && (!r.Complete || !r.Closed || !r.EventRemoved || r.Namespace == (servingArtifactDirectory{}) || r.CollectAfter.Before(r.ParkedAt.Add(hostbox.JournalReceiptRetention))) {
257 return hostbox.ErrChanged
258 }
259 if r.Closed {
260 for _, item := range r.Source.Files {
261 if item.Present && !item.Deleted {
262 return hostbox.ErrChanged
263 }
264 }
265 }
266 if r.Complete && r.Disposition != servingParkRetain && !r.Closed {
267 return hostbox.ErrChanged
268 }
269 previous := a
270 if len(r.Continuations) > 32 {
271 return hostbox.ErrChanged
272 }
273 for _, continuation := range r.Continuations {
274 next := continuation.Authorization
275 want := previous
276 want.Successor = next.Successor
277 want.TagsFrom = next.TagsFrom
278 identity := previous.Successor
279 identity.Stamp, identity.Session, identity.Generation = next.Successor.Stamp, next.Successor.Session, next.Successor.Generation
280 if next != want || next.Successor != identity || identity.Stamp <= previous.Successor.Stamp || identity.Session != identity.Stamp || identity.Generation == "" || identity.Generation == previous.Successor.Generation || (next.TagsFrom != previous.TagsFrom && next.TagsFrom != previous.Successor.Stamp && next.TagsFrom != next.Successor.Stamp) || len(continuation.Receipts) == 0 || !r.Parked {
281 return hostbox.ErrChanged
282 }
283 for _, receipt := range continuation.Receipts {
284 t := receipt.Target
285 p := previous.Successor
286 if t.Host != p.Host || t.Box != p.Box || t.Region != p.Region || t.Incarnation != p.Incarnation || t.Stamp != p.Stamp || t.Session != p.Session || t.Op != "" || t.Phase != "" || t.ObligationID != "" || verifyRetirementReceipt(context.Background(), receipt) != nil {
287 return hostbox.ErrChanged
288 }
289 }
290 previous = next
291 }
292 return nil
293}
294
295func (l *servingParkLedger) save(r servingParkRecord) error {
296 if err := l.bindRecord(&r, true); err != nil {
297 return err
298 }
299 if err := validateServingParkRecord(r); err != nil {
300 return err
301 }
302 return l.write("retirement-"+r.Authorization.ID+".json", r)
303}
304
305func (l *servingParkLedger) slot(a servingParkAuthorization) (servingParkSlot, error) {
306 s := servingParkSlot{Version: 1, Box: a.Successor.Box, Host: a.Successor.Host}
307 b, err := l.state.Read("slot-" + servingParkKey(s.Box, s.Host) + ".json")
308 if errors.Is(err, os.ErrNotExist) {
309 return s, nil
310 }
311 if err != nil {
312 return s, err
313 }
314 var saved servingParkSlot
315 if servingParkDecode(b, &saved) != nil || saved.Version != 1 || saved.Box != s.Box || saved.Host != s.Host {
316 return s, hostbox.ErrChanged
317 }
318 if saved.ID != "" {
319 r, err := l.readHistorical(saved.ID)
320 if err != nil || r.Disposition != servingParkRetain || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host {
321 return s, hostbox.ErrChanged
322 }
323 }
324 if saved.Pending != "" {
325 r, err := l.read(saved.Pending)
326 if err != nil || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host {
327 return s, hostbox.ErrChanged
328 }
329 }
330 if saved.Last != "" {
331 r, err := l.readHistorical(saved.Last)
332 if err != nil || r.Authorization.Successor.Box != s.Box || r.Authorization.Successor.Host != s.Host {
333 return s, hostbox.ErrChanged
334 }
335 }
336 return saved, nil
337}
338
339// withNamespace precedes the root host guard, slot lock and record lock. The
340// authority callback must acquire the host guard and independently authenticate
341// the claim, receipts, state directory and launch history while it is held.
342func (l *servingParkLedger) withNamespace(ctx context.Context, fn func() error) error {
343 if ctx == nil || l == nil || l.state == nil || l.fs == nil || l.now == nil || fn == nil {
344 return hostbox.ErrUnsupported
345 }
346 unlock, err := l.state.Lock(ctx, "namespace.lock")
347 if err != nil {
348 return err
349 }
350 defer unlock()
351 if err = ctx.Err(); err != nil {
352 return err
353 }
354 return fn()
355}
356
357// parkLocked runs inside independently verified root host authority. Fresh
358// capture is never substituted for the immutable source on a replay.
359func (l *servingParkLedger) parkLocked(ctx context.Context, a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) {
360 return l.parkWithEvidenceLocked(ctx, a, receipts, nil)
361}
362
363// capture validates the full source before namespace recovery may write any
364// authority. The ledger repeats it before saving a fresh retirement intent.
365func (l *servingParkLedger) capture(ctx context.Context, a servingParkAuthorization, receipts []hostguard.FenceReceipt, legacy *servingLegacyEvidence) (servingParkRecord, error) {
366 var zero servingParkRecord
367 source, e := l.fs.Capture(a)
368 if e != nil {
369 return zero, e
370 }
371 disposition := servingParkArtifactFree
372 if a.Legacy {
373 if source.Files[1].Present {
374 return zero, hostbox.ErrChanged
375 }
376 if source.Files[0].Present {
377 disposition = servingParkRetain
378 }
379 } else {
380 disposition, e = servingGenerationParkDisposition(ctx, l.journal, a.Predecessor, receipts, source.Files[0].Present)
381 if e != nil {
382 return zero, e
383 }
384 }
385 r := servingParkRecord{Version: 1, Authorization: a, Source: source, Receipts: receipts, LegacyEvidence: legacy, Disposition: disposition}
386 if legacy != nil && legacy.Selector.Empty {
387 for _, f := range source.Files {
388 if f.Present {
389 return zero, hostbox.ErrChanged
390 }
391 }
392 }
393 if e = validateServingParkRecord(r); e != nil {
394 return zero, e
395 }
396 return r, nil
397}
398
399func (l *servingParkLedger) parkWithEvidenceLocked(ctx context.Context, a servingParkAuthorization, receipts []hostguard.FenceReceipt, legacy *servingLegacyEvidence) (servingParkRecord, error) {
400 var zero servingParkRecord
401 if err := ctx.Err(); err != nil {
402 return zero, err
403 }
404 unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock")
405 if err != nil {
406 return zero, err
407 }
408 defer unlock()
409 slot, err := l.slot(a)
410 if err != nil {
411 return zero, err
412 }
413 if slot.Pending != "" && slot.Pending != a.ID {
414 return zero, hostbox.ErrChanged
415 }
416 unlockRecord, err := l.state.Lock(ctx, "retirement-"+a.ID+".lock")
417 if err != nil {
418 return zero, err
419 }
420 defer unlockRecord()
421 r, err := l.read(a.ID)
422 if errors.Is(err, os.ErrNotExist) {
423 // Last survives collection. An old request cannot reconstruct a removed
424 // intent from a restored source in the same incarnation. The outer root
425 // guard independently requires the current incarnation and successor.
426 if slot.Last != "" {
427 last, e := l.readHistorical(slot.Last)
428 if e != nil {
429 return zero, e
430 }
431 prior := last.activeAuthorization().Successor
432 if prior.Incarnation == a.Successor.Incarnation && a.Successor.Stamp <= prior.Stamp {
433 return zero, hostbox.ErrChanged
434 }
435 }
436 captured, e := l.capture(ctx, a, receipts, legacy)
437 if e != nil {
438 return zero, e
439 }
440 r = captured
441 source := r.Source
442 // Consumption survives slot replacement, expiry and park tombstones.
443 if a.Legacy {
444 key := "legacy-" + servingParkKey(a.Successor.Box, a.Successor.Host+"\x00"+a.Successor.Incarnation) + ".json"
445 var consumed servingLegacyConsumption
446 want := servingLegacyConsumption{Authorization: a, Source: source, Receipts: receipts, LegacyEvidence: legacy}
447 body, e := l.state.Read(key)
448 if e == nil {
449 if servingParkDecode(body, &consumed) != nil || !sameServingParkSource(consumed.Source, want.Source) {
450 return zero, hostbox.ErrChanged
451 }
452 // A lost old-format intent keeps its original consumption
453 // evidence. Never infer historical handles from current bytes.
454 want.Source = consumed.Source
455 if !reflect.DeepEqual(consumed, want) {
456 return zero, hostbox.ErrChanged
457 }
458 r.Source = consumed.Source
459 } else if !errors.Is(e, os.ErrNotExist) {
460 return zero, e
461 }
462 if e = l.write(key, want); e != nil {
463 return zero, e
464 }
465 }
466 if e = l.save(r); e != nil {
467 return zero, e
468 }
469 } else if err != nil {
470 return zero, err
471 }
472 if r.Authorization != a || !sameServingLegacyEvidence(r.LegacyEvidence, legacy) || !sameServingRootStops(r.Receipts, receipts) {
473 return zero, hostbox.ErrChanged
474 }
475 if r.Closed || r.Complete {
476 return r, nil
477 }
478 if slot.Pending == "" {
479 slot.Pending = a.ID
480 slot.Last = a.ID
481 if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
482 return zero, err
483 }
484 }
485 if r.Deleting {
486 if r.Disposition == servingParkRetain || r.Cause != "discard" {
487 return zero, hostbox.ErrChanged
488 }
489 if err = l.deleteLocked(ctx, &r, "discard"); err != nil {
490 return zero, err
491 }
492 return r, nil
493 }
494 if r.Disposition == servingParkRetain && slot.ID != a.ID {
495 // A completed replay can never reacquire a slot it has lost.
496 if r.Parked || r.Complete {
497 return zero, hostbox.ErrChanged
498 }
499 fresh, e := l.fs.Capture(a)
500 if e != nil || !sameServingParkSource(r.Source, fresh) {
501 return zero, hostbox.ErrChanged
502 }
503 if slot.ID != "" {
504 if e = l.replaceLocked(ctx, slot.ID); e != nil {
505 return zero, e
506 }
507 }
508 slot.ID = a.ID
509 if err = l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot); err != nil {
510 return zero, err
511 }
512 }
513 save := func() error {
514 if err := ctx.Err(); err != nil {
515 return err
516 }
517 return l.save(r)
518 }
519 if err = l.fs.Prepare(&r, save); err != nil {
520 return zero, err
521 }
522 if err = l.fs.Move(&r, save); err != nil {
523 return zero, err
524 }
525 if !r.Parked {
526 r.Parked, r.ParkedAt = true, l.now().UTC()
527 r.ReapAfter = r.ParkedAt.Add(7 * 24 * time.Hour)
528 if err = save(); err != nil {
529 return zero, err
530 }
531 }
532 // Even discarded restores are first isolated by recorded rename into the
533 // root-owned event. A raced source replacement is never unlinked in place.
534 if r.Disposition != servingParkRetain && !r.Closed {
535 if err = l.deleteLocked(ctx, &r, "discard"); err != nil {
536 return zero, err
537 }
538 }
539 return r, nil
540}
541
542// completeLocked follows a fresh root check of the successor's paired tags.
543// Its caller holds namespace and host authority, never an agent Work lock.
544func (l *servingParkLedger) completeLocked(ctx context.Context, id string, a servingArtifactIdentity) error {
545 unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Box, a.Host)+".lock")
546 if err != nil {
547 return err
548 }
549 defer unlock()
550 r, err := l.read(id)
551 if err != nil {
552 return err
553 }
554 if r.activeAuthorization().Successor != a || !r.Parked {
555 return hostbox.ErrChanged
556 }
557 unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
558 if err != nil {
559 return err
560 }
561 defer unlockRecord()
562 slot, err := l.slot(r.Authorization)
563 if err != nil {
564 return err
565 }
566 if !r.Complete {
567 if slot.Pending != id {
568 return hostbox.ErrChanged
569 }
570 if err = l.fs.CheckParked(&r); err != nil {
571 return err
572 }
573 if r.Disposition == servingParkRetain {
574 if slot.ID != id || r.Closed || r.Deleting {
575 return hostbox.ErrChanged
576 }
577 } else if !r.Closed {
578 return hostbox.ErrChanged
579 }
580 if err = ctx.Err(); err != nil {
581 return err
582 }
583 r.Complete = true
584 if err = l.save(r); err != nil {
585 return err
586 }
587 }
588 if slot.Pending == id {
589 slot.Pending = ""
590 return l.write("slot-"+servingParkKey(slot.Box, slot.Host)+".json", slot)
591 }
592 return nil
593}
594
595// continueLocked records permission from a later stopped successor without
596// changing the original retirement, source identities or retention deadline.
597// Root has already rechecked the original and every continuation stop.
598func (l *servingParkLedger) continueLocked(ctx context.Context, id string, a servingParkAuthorization, receipts []hostguard.FenceReceipt) (servingParkRecord, error) {
599 var zero servingParkRecord
600 unlock, err := l.state.Lock(ctx, "slot-"+servingParkKey(a.Successor.Box, a.Successor.Host)+".lock")
601 if err != nil {
602 return zero, err
603 }
604 defer unlock()
605 unlockRecord, err := l.state.Lock(ctx, "retirement-"+id+".lock")
606 if err != nil {
607 return zero, err
608 }
609 defer unlockRecord()
610 r, err := l.read(id)
611 if err != nil {
612 return zero, err
613 }
614 if r.activeAuthorization() == a {
615 return r, nil
616 }
617 slot, err := l.slot(a)
618 if err != nil || slot.Pending != id || r.Complete {
619 return zero, hostbox.ErrChanged
620 }
621 if err = l.fs.CheckParked(&r); err != nil {
622 return zero, err
623 }
624 r.Continuations = append(r.Continuations, servingParkContinuation{Authorization: a, Receipts: receipts})
625 if err = ctx.Err(); err != nil {
626 return zero, err
627 }
628 if err = l.save(r); err != nil {
629 return zero, err
630 }
631 return r, nil
632}
633
634func (l *servingParkLedger) deleteLocked(ctx context.Context, r *servingParkRecord, cause string) error {
635 if r.Closed {
636 return nil
637 }
638 if cause != "discard" && (!r.Complete || r.Disposition != servingParkRetain) {
639 return hostbox.ErrChanged
640 }
641 if cause == "discard" && r.Disposition == servingParkRetain {
642 return hostbox.ErrChanged
643 }
644 if r.Deleting && r.Cause != cause {
645 return hostbox.ErrChanged
646 }
647 save := func() error {
648 if err := ctx.Err(); err != nil {
649 return err
650 }
651 return l.save(*r)
652 }
653 r.Deleting, r.Cause = true, cause
654 if err := save(); err != nil {
655 return err
656 }
657 if err := l.fs.Delete(r, save); err != nil {
658 return err
659 }
660 r.Closed = true
661 return save()
662}
663
664func (l *servingParkLedger) replaceLocked(ctx context.Context, id string) error {
665 unlock, err := l.state.Lock(ctx, "retirement-"+id+".lock")
666 if err != nil {
667 return err
668 }
669 defer unlock()
670 old, err := l.readHistorical(id)
671 if err != nil || !old.Complete {
672 return hostbox.ErrChanged
673 }
674 if old.Closed {
675 return nil
676 }
677 cause := "replacement"
678 if old.Deleting {
679 cause = old.Cause
680 }
681 current, err := l.read(id)
682 if err == nil {
683 err = l.deleteLocked(ctx, &current, cause)
684 }
685 if err != nil {
686 log.Printf("Host %q: serving database retirement %q retained: %s.", old.Authorization.Successor.Host, id, err)
687 }
688 return ctx.Err()
689}
690
691func sameServingRootStops(a, b []hostguard.FenceReceipt) bool {
692 if len(a) != len(b) {
693 return false
694 }
695 for i := range a {
696 if a[i].Target != b[i].Target || !a[i].EvidenceAt.Equal(b[i].EvidenceAt) || !bytes.Equal(a[i].Bytes, b[i].Bytes) {
697 return false
698 }
699 }
700 return true
701}
702