carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostbox
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "database/sql"
8 "encoding/hex"
9 "encoding/json"
10 "errors"
11 "fmt"
12 "io/fs"
13 "os"
14 "path/filepath"
15 "strings"
16
17 "github.com/carlosframework/platform/internal/hostclaim"
18 "github.com/carlosframework/platform/internal/hostguard"
19 "github.com/carlosframework/platform/internal/registry"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23type ColdSuccessor struct {
24 Host, Incarnation, Box, Enrollment, DirectoryRevision, ETag, Generation, Op, Phase string
25 Stamp, Session uint64
26}
27type ColdHandover struct {
28 OperationsOnly bool
29 checkpoint func(string)
30 afterIntent func() error
31 afterPark func() error
32 Guard hostguard.Guard
33 Claims release.Store
34 Registry *registry.Registry
35 Inventory *StopInventoryDB
36 Launches *LaunchJournal
37 Roots map[string]string
38 JournalDir string
39 VerifyReceipt hostguard.CleanupVerify
40 // Admit, when set, is root's fresh successor admission (roster binding
41 // N5, B1). Run calls it before any other step, for a new handover and
42 // for every replay of a journaled one, startup recovery included, so a
43 // successor is started or resumed only under the binding the roster
44 // gives this box now.
45 Admit func(context.Context, ColdSuccessor) error
46 // Obsolete, when set, reports a journaled successor stamped under a
47 // binding other than root's startup binding (roster binding B3).
48 // Recover never resumes such a handover; it settles it (settleObsolete).
49 Obsolete func(ColdSuccessor) bool
50}
51type coldJournal struct {
52 ServingInstallation *hostguard.RootInstallation `json:",omitempty"`
53 V int
54 IntendedAbsent bool
55 ID, Phase string
56 Prior hostguard.FenceTarget
57 Barrier hostguard.FenceTarget
58 Successor ColdSuccessor
59 Inventory StopInventory
60 Receipt hostguard.FenceReceipt
61 Before, After registry.ColdRows
62}
63
64func ownershipFor(host, incarnation string, stamp uint64, box string) string {
65 sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", host, incarnation, stamp, box)))
66 return hex.EncodeToString(sum[:])
67}
68func rowIDFor(owner, kind, name string) string {
69 sum := sha256.Sum256([]byte(owner + "\x00" + kind + "\x00" + name))
70 return hex.EncodeToString(sum[:])
71}
72func (h ColdHandover) Run(ctx context.Context, prior hostguard.FenceTarget, next ColdSuccessor) error {
73 if h.OperationsOnly && (next.Op == "" || next.Phase == "") {
74 return ErrUnsupported
75 }
76 if h.Claims == nil || h.Registry == nil || h.Inventory == nil || h.Launches == nil || h.VerifyReceipt == nil || !filepath.IsAbs(h.JournalDir) || len(h.Roots) == 0 || next.Host != prior.Host || next.Incarnation != prior.Incarnation || next.Stamp <= prior.Stamp || next.Session <= prior.Session || next.Stamp != next.Session || next.Box != prior.Box || next.Enrollment != prior.Enrollment || next.ETag == "" || next.Generation == "" {
77 return ErrUnsupported
78 }
79 if h.Admit != nil {
80 if err := h.Admit(ctx, next); err != nil {
81 return err
82 }
83 }
84 check := func(c *hostclaim.Claim) error {
85 if c == nil || c.Host != next.Host || c.Holder == nil || c.Holder.Family != "instance" || c.Holder.Incarnation != next.Incarnation || c.Holder.Stamp != next.Stamp || c.Holder.Session != next.Session || c.Holder.Generation != next.Generation || c.Holder.WriterBox != next.Box || c.Op.ID != next.Op || c.Op.Phase != next.Phase || c.Op.Exec != next.Box {
86 return ErrChanged
87 }
88 found := 0
89 for _, admission := range c.Holder.Admissions {
90 if admission.Session == next.Session && admission.Box == next.Box && admission.Generation == next.Generation && admission.Mode == "cold" {
91 found++
92 }
93 }
94 if found != 1 {
95 return ErrChanged
96 }
97 return nil
98 }
99 observed, err := hostclaim.Read(ctx, h.Claims, next.Host)
100 if err != nil {
101 return err
102 }
103 if err = check(observed.Claim); err != nil {
104 return err
105 }
106 id := "handover-" + ownershipFor(next.Host, next.Incarnation, next.Stamp, next.Box)
107 path := filepath.Join(h.JournalDir, id+".json")
108 var j coldJournal
109 body, err := readTrustedJournal(path)
110 if err == nil {
111 if decodeStrict(body, &j) != nil || j.V != 1 || j.ID != id || j.Prior != prior || j.Successor != next || (j.Phase != "prepared" && j.Phase != "parked" && j.Phase != "complete") {
112 return ErrTypedJournal
113 }
114 if j.Phase == "complete" {
115 return h.Guard.CompleteColdHandover(ctx, j.Barrier, true, check, func() error { return nil })
116 }
117 } else if !errors.Is(err, os.ErrNotExist) {
118 return err
119 } else {
120 if observed.ETag != next.ETag {
121 return ErrChanged
122 }
123 receipt, err := h.Guard.FenceReceiptFor(ctx, prior)
124 if err != nil {
125 return err
126 }
127 if err = h.VerifyReceipt(ctx, receipt); err != nil {
128 return err
129 }
130 barrier := prior
131 barrier.FenceID = id
132 barrier.Stamp = next.Stamp
133 barrier.Session = next.Session
134 barrier.Op = next.Op
135 barrier.Phase = next.Phase
136 barrier.ClaimETag = next.ETag
137 barrier.DirectoryRevision = next.DirectoryRevision
138
139 err = h.Guard.CleanupWith(ctx, prior, h.VerifyReceipt, func() error {
140 saved, loadErr := readTrustedJournal(path)
141 if loadErr == nil {
142 var existing coldJournal
143 if decodeStrict(saved, &existing) != nil || existing.ID != id || existing.Prior != prior || existing.Successor != next {
144 return ErrTypedJournal
145 }
146 j = existing
147 return nil
148 }
149 if !errors.Is(loadErr, os.ErrNotExist) {
150 return loadErr
151 }
152 in, err := h.Inventory.ReadExactStopInventory(ctx, prior)
153 if err != nil {
154 return err
155 }
156 if !in.Complete || in.Host != prior.Host || in.Incarnation != prior.Incarnation || in.InstallationStamp != prior.Stamp || in.Box != prior.Box || in.Enrollment != prior.Enrollment || in.DirectoryRevision != prior.DirectoryRevision {
157 return ErrTypedInventory
158 }
159 row, err := h.Registry.Get(prior.Host)
160 if err != nil {
161 return err
162 }
163 if row == nil || !row.ClaimOwnership.Valid() || row.ClaimIncarnation != prior.Incarnation || row.ClaimStamp != prior.Stamp || row.Kind != "instance" || row.Unit != "" || row.Addr != "" || row.Addrs != "" || row.Backing != "" || row.Tunnel != "" || row.Platform || row.Canary || row.Replica {
164 return ErrUnsupported
165 }
166 if err := ValidateAgentInventory(in, row, h.Roots["data"]); err != nil {
167 return err
168 }
169 timers, err := h.Registry.TimersFor(prior.Host)
170 if err != nil {
171 return err
172 }
173 for _, timer := range timers {
174 if timer.ClaimIncarnation != prior.Incarnation || timer.ClaimStamp != prior.Stamp || timer.ClaimOwner != row.ClaimOwner || timer.ClaimRowID == "" || timer.Registration == "" {
175 return ErrTypedInventory
176 }
177 }
178 before := registry.ColdRows{Route: *row, Timers: timers}
179 after := registry.ColdRows{Route: *row, Timers: append([]registry.Timer(nil), timers...)}
180 owner := ownershipFor(next.Host, next.Incarnation, next.Stamp, next.Box)
181 after.Route.ClaimOwnership = registry.ClaimOwnership{ClaimIncarnation: next.Incarnation, ClaimStamp: next.Stamp, ClaimOwner: owner, ClaimRowID: rowIDFor(owner, "route", next.Host)}
182 for i := range after.Timers {
183 after.Timers[i].ClaimOwnership = after.Route.ClaimOwnership
184 after.Timers[i].ClaimRowID = rowIDFor(owner, "timer", after.Timers[i].Name)
185 }
186 j = coldJournal{V: 1, ID: id, Phase: "prepared", Prior: prior, Barrier: barrier, Successor: next, Inventory: in, Receipt: receipt, Before: before, After: after}
187 return writeColdJournal(path, j)
188 })
189 if err != nil {
190 return err
191 }
192 }
193 if j.Phase == "complete" {
194 return h.Guard.CompleteColdHandover(ctx, j.Barrier, true, check, func() error { return nil })
195 }
196 if err := ValidateAgentInventory(j.Inventory, &j.Before.Route, h.Roots["data"]); err != nil {
197 return err
198 }
199 if j.Phase == "prepared" {
200 h.atCheckpoint("intent")
201 }
202 if h.afterIntent != nil && j.Phase == "prepared" {
203 if err := h.afterIntent(); err != nil {
204 return err
205 }
206 }
207 if err = h.Guard.BeginColdHandover(ctx, prior, j.Barrier, check); err != nil {
208 return err
209 }
210 if err = h.VerifyReceipt(ctx, j.Receipt); err != nil {
211 return err
212 }
213 if effects, err := h.Launches.HasInstallationEffects(ctx, next.Host, next.Incarnation, next.Stamp); err != nil {
214 return err
215 } else if effects {
216 return ErrChanged
217 }
218 if j.Phase == "prepared" {
219 inventory := RetirementInventory{Host: prior.Host, Captured: hostguard.Captured{Incarnation: prior.Incarnation, Stamp: prior.Stamp}, Ownership: j.Before.Route.ClaimOwner}
220 var intended []StopInventoryEntry
221 names := map[string]int{}
222 for _, entry := range j.Inventory.Entries {
223 if entry.Kind != "route" && entry.Kind != "timer" {
224 names[entry.ManagedRoot+"\x00"+entry.RelativePath]++
225 }
226 if entry.Ownership != inventory.Ownership || entry.Kind == "unit-state" {
227 return ErrTypedInventory
228 }
229 // An intended path is an intent PrepareState persisted before its
230 // mkdir and never finalized. It carries no captured identity, so it
231 // is handled only by retireIntended below, never as an artifact.
232 if entry.Kind == "timer" && (entry.Materialization == "intended" || entry.TimerRetired) {
233 if entry.Device != 0 || entry.Inode != 0 || entry.Type != 0 || entry.ManagedRoot != "registry" || entry.RowIdentity == "" {
234 return ErrTypedInventory
235 }
236 present := false
237 for _, timer := range j.Before.Timers {
238 if entry.RelativePath != timer.Host+"/"+timer.Name {
239 continue
240 }
241 if entry.TimerRetired || timer.Registration != entry.RowIdentity || timer.ClaimRowID != entry.RowIdentity || timer.ClaimOwner != entry.Ownership {
242 return ErrTypedInventory
243 }
244 present = true
245 }
246 if !present {
247 inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactTimer, Root: entry.ManagedRoot, Name: entry.RelativePath, Owner: entry.Ownership, RowID: entry.RowIdentity + "~" + entry.RowIdentity, AllowAbsent: true})
248 }
249 continue
250 }
251 if entry.Materialization == "intended" {
252 if entry.Kind != "path" || entry.Device != 0 || entry.Inode != 0 || !managedName(entry.RelativePath) || h.Roots[entry.ManagedRoot] == "" {
253 return ErrTypedInventory
254 }
255 intended = append(intended, entry)
256 continue
257 }
258 if entry.Materialization != "materialized" {
259 return ErrTypedInventory
260 }
261 if entry.Kind == "timer" {
262 continue
263 }
264 inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactKind(entry.Kind), Root: entry.ManagedRoot, Name: entry.RelativePath, Owner: entry.Ownership, RowID: entry.RowIdentity, Identity: entry.Identity, Dev: entry.Device, Ino: entry.Inode, Type: entry.Type})
265 }
266 for _, timer := range j.Before.Timers {
267 inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactTimer, Root: "registry", Name: timer.Host + "/" + timer.Name, Owner: timer.ClaimOwner, RowID: timer.ClaimRowID + "~" + timer.Registration})
268 }
269 for _, entry := range intended {
270 if names[entry.ManagedRoot+"\x00"+entry.RelativePath] != 1 {
271 return ErrTypedInventory
272 }
273 }
274 intent, err := NewRetirementJournal(j.ID+"-old", inventory)
275 if err != nil {
276 return err
277 }
278 intent.Target = &j.Prior
279 intent.StopReceipt = append([]byte(nil), j.Receipt.Bytes...)
280 if err = h.retireIntended(ctx, j, intended); err != nil {
281 return err
282 }
283 retire := TypedRetirement{JournalDir: filepath.Join(h.JournalDir, "retired"), Authority: InstallationCleanup{Guard: h.Guard, Verify: h.VerifyReceipt}, Filesystem: TypedParker{CapturePersistent: h.Guard.Root != nil, Roots: h.Roots, ParkedRoots: h.Roots, Incarnation: prior.Incarnation, Host: prior.Host, Retirement: intent.ID}, Rows: RegistryRows{Registry: h.Registry, Inventory: inventory}, Ack: func(context.Context, RetirementJournal) error { return nil }}
284 if err = retire.Run(ctx, prior, intent); err != nil {
285 return err
286 }
287
288 if err = h.Guard.CleanupWith(ctx, prior, h.VerifyReceipt, func() error {
289 current, err := readTrustedJournal(path)
290 if err != nil {
291 return err
292 }
293 var saved coldJournal
294 if decodeStrict(current, &saved) != nil || saved.ID != j.ID || saved.Prior != j.Prior || saved.Successor != j.Successor {
295 return ErrTypedJournal
296 }
297 if saved.Phase == "complete" || saved.Phase == "parked" {
298 j = saved
299 return nil
300 }
301 if saved.Phase != "prepared" {
302 return ErrTypedJournal
303 }
304 saved.Phase = "parked"
305 j = saved
306 return writeColdJournal(path, j)
307 }); err != nil {
308 return err
309 }
310 }
311 if j.Phase == "complete" {
312 return h.Guard.CompleteColdHandover(ctx, j.Barrier, true, check, func() error { return nil })
313 }
314 h.atCheckpoint("parked")
315 if h.afterPark != nil {
316 if err := h.afterPark(); err != nil {
317 return err
318 }
319 }
320 return h.Guard.CompleteColdHandover(ctx, j.Barrier, false, check, func() error {
321 if effects, err := h.Launches.HasInstallationEffects(ctx, next.Host, next.Incarnation, next.Stamp); err != nil {
322 return err
323 } else if effects {
324 return ErrChanged
325 }
326 for _, entry := range j.Inventory.Entries {
327 if entry.Kind == "route" || entry.Kind == "timer" {
328 continue
329 }
330 rootPath := h.Roots[entry.ManagedRoot]
331 if rootPath == "" {
332 return ErrTypedInventory
333 }
334 root, err := os.OpenRoot(rootPath)
335 if err != nil {
336 return err
337 }
338 _, statErr := root.Lstat(entry.RelativePath)
339 root.Close()
340 if !errors.Is(statErr, os.ErrNotExist) {
341 if statErr != nil {
342 return statErr
343 }
344 return ErrChanged
345 }
346 }
347 if err := coldNamespaceEmpty(h.Roots, next.Host, h.Guard.LockDir, filepath.Join(h.Guard.DataDir, "hostguard"), h.JournalDir); err != nil {
348 return err
349 }
350 owner := j.After.Route.ClaimOwnership
351 successor := StopInventory{Complete: true, Host: next.Host, Incarnation: next.Incarnation, InstallationStamp: next.Stamp, Box: next.Box, Enrollment: next.Enrollment, DirectoryRevision: next.DirectoryRevision, Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: next.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "intended"}}}
352 existing, err := h.Inventory.ReadInstallation(ctx, next.Host, next.Incarnation, next.Stamp)
353 if err == nil {
354 if len(existing.Entries) != 1 || existing.Entries[0].Kind != "route" {
355 return ErrTypedInventory
356 }
357 existing.Entries[0].Materialization = "intended"
358 a, _ := json.Marshal(existing)
359 b, _ := json.Marshal(successor)
360 if !bytes.Equal(a, b) {
361 return ErrTypedInventory
362 }
363 } else if !errors.Is(err, sql.ErrNoRows) {
364 return err
365 }
366 if err = h.Inventory.WriteStopInventory(ctx, successor); err != nil {
367 return err
368 }
369 h.atCheckpoint("inventory-intent")
370 if err = h.Registry.RestoreClaimRows(ctx, j.ID, j.ID+"-old", j.Before, j.After); err != nil {
371 return err
372 }
373 h.atCheckpoint("rows")
374 successor.Entries[0].Materialization = "materialized"
375 if err = h.Inventory.WriteStopInventory(ctx, successor); err != nil {
376 return err
377 }
378 h.atCheckpoint("inventory-materialized")
379 j.Phase = "complete"
380 if err := writeColdJournal(path, j); err != nil {
381 return err
382 }
383 h.atCheckpoint("complete-journal")
384 return nil
385 })
386}
387
388// retireIntended settles prior state paths whose intent is durable but whose
389// materialization is not. PrepareState verifies absence before persisting the
390// intent and refuses an intended name it finds present, so with the prior
391// controller fenced dead, the successor barred and without effects or path
392// inventory, the only writer of such a name is the prior's own mkdir. An absent
393// name moves nothing. A present one must still be an empty directory of the
394// recorded type; it is captured into a typed retirement journal and parked by
395// inode under exact cleanup authority, so it is preserved, never unlinked.
396// Anything else refuses before any artifact moves.
397func (h ColdHandover) retireIntended(ctx context.Context, j coldJournal, intended []StopInventoryEntry) error {
398 if len(intended) == 0 {
399 return nil
400 }
401 if successor, err := h.Inventory.ReadInstallation(ctx, j.Successor.Host, j.Successor.Incarnation, j.Successor.Stamp); err == nil {
402 for _, entry := range successor.Entries {
403 if entry.Kind != "route" {
404 return ErrChanged
405 }
406 }
407 } else if !errors.Is(err, sql.ErrNoRows) {
408 return err
409 }
410 id := j.ID + "-intended"
411 dir := filepath.Join(h.JournalDir, "retired")
412 parker := TypedParker{CapturePersistent: h.Guard.Root != nil, Roots: h.Roots, ParkedRoots: h.Roots, Incarnation: j.Prior.Incarnation, Host: j.Prior.Host, Retirement: id}
413 want := RetirementInventory{Host: j.Prior.Host, Captured: hostguard.Captured{Incarnation: j.Prior.Incarnation, Stamp: j.Prior.Stamp}, Ownership: j.Before.Route.ClaimOwner}
414 body, err := readTrustedJournal(filepath.Join(dir, id+".json"))
415 resumed := err == nil
416 if resumed {
417 // The captured identities were decided once, durably, by this
418 // controller. Never recapture: the parked inode must be the journalled one.
419 var saved RetirementJournal
420 if decodeStrict(body, &saved) != nil || saved.ID != id || saved.Inventory.Host != want.Host || saved.Inventory.Captured != want.Captured || saved.Inventory.Ownership != want.Ownership {
421 return ErrTypedJournal
422 }
423 want.Artifacts = saved.Inventory.Artifacts
424 for _, a := range want.Artifacts {
425 found := 0
426 for _, entry := range intended {
427 if a.Root == entry.ManagedRoot && a.Name == entry.RelativePath {
428 found++
429 }
430 }
431 if found != 1 {
432 return ErrTypedJournal
433 }
434 }
435 } else if !errors.Is(err, os.ErrNotExist) {
436 return err
437 }
438 resumed = resumed || j.IntendedAbsent
439 for _, entry := range intended {
440 journalled := false
441 for _, a := range want.Artifacts {
442 if a.Root == entry.ManagedRoot && a.Name == entry.RelativePath {
443 if a.Kind != ArtifactPath || a.Owner != entry.Ownership || entry.Type == 0 || a.Type != entry.Type {
444 return ErrTypedJournal
445 }
446 journalled = true
447 }
448 }
449 if journalled {
450 continue
451 }
452 // Absence is read below the configured root only; a missing root is
453 // not evidence that the name was never materialized.
454 if info, err := os.Lstat(h.Roots[entry.ManagedRoot]); err != nil || !info.IsDir() {
455 return ErrTypedInventory
456 }
457 a, err := parker.CaptureArtifact(entry.ManagedRoot, entry.RelativePath, entry.Ownership, ArtifactPath)
458 if errors.Is(err, fs.ErrNotExist) {
459 continue
460 }
461 if err != nil {
462 return err
463 }
464 // A name that was absent when the journal was written has no recorded
465 // decision; its later appearance is unknown ownership.
466 if resumed || entry.Type == 0 || a.Type != entry.Type {
467 return ErrChanged
468 }
469 if err = parker.VerifyEmptyDirectory(a); err != nil {
470 return err
471 }
472 again, err := parker.CaptureArtifact(entry.ManagedRoot, entry.RelativePath, entry.Ownership, ArtifactPath)
473 if err != nil || again != a {
474 return ErrChanged
475 }
476 want.Artifacts = append(want.Artifacts, a)
477 }
478 if len(want.Artifacts) == 0 {
479 if j.IntendedAbsent {
480 return nil
481 }
482 // Persist the all-absent decision before the main retirement can move
483 // anything. Replay must not attribute a newly appearing name to prior.
484 return h.Guard.CleanupWith(ctx, j.Prior, h.VerifyReceipt, func() error {
485 path := filepath.Join(h.JournalDir, j.ID+".json")
486 body, err := readTrustedJournal(path)
487 if err != nil {
488 return err
489 }
490 var saved coldJournal
491 if decodeStrict(body, &saved) != nil || saved.ID != j.ID || saved.Prior != j.Prior || saved.Successor != j.Successor || saved.Phase != "prepared" {
492 return ErrTypedJournal
493 }
494 saved.IntendedAbsent = true
495 return writeColdJournal(path, saved)
496 })
497 }
498 intent, err := NewRetirementJournal(id, want)
499 if err != nil {
500 return err
501 }
502 target := j.Prior
503 intent.Target = &target
504 intent.StopReceipt = append([]byte(nil), j.Receipt.Bytes...)
505 retire := TypedRetirement{JournalDir: dir, Authority: InstallationCleanup{Guard: h.Guard, Verify: h.VerifyReceipt}, Filesystem: parker, Rows: RegistryRows{Registry: h.Registry, Inventory: want}, Ack: func(context.Context, RetirementJournal) error { return nil }}
506 if err = retire.Run(ctx, j.Prior, intent); err != nil {
507 return err
508 }
509 // Every captured name is parked now; one present again is not the prior's.
510 for _, a := range want.Artifacts {
511 if _, err := parker.CaptureArtifact(a.Root, a.Name, a.Owner, ArtifactPath); !errors.Is(err, fs.ErrNotExist) {
512 if err != nil {
513 return err
514 }
515 return ErrChanged
516 }
517 }
518 return nil
519}
520
521func writeColdJournal(path string, j coldJournal) error {
522 if err := checkTrustedJournalDir(filepath.Dir(path), true); err != nil {
523 return err
524 }
525 body, err := json.Marshal(j)
526 if err != nil {
527 return err
528 }
529 file, err := os.CreateTemp(filepath.Dir(path), ".handover-*")
530 if err != nil {
531 return err
532 }
533 defer os.Remove(file.Name())
534 if _, err = file.Write(body); err == nil {
535 err = file.Sync()
536 }
537 if closeErr := file.Close(); err == nil {
538 err = closeErr
539 }
540 if err != nil {
541 return err
542 }
543 if err = os.Rename(file.Name(), path); err != nil {
544 return err
545 }
546 dir, err := os.Open(filepath.Dir(path))
547 if err != nil {
548 return err
549 }
550 defer dir.Close()
551 return dir.Sync()
552}
553func (j *LaunchJournal) HasInstallationEffects(ctx context.Context, host, incarnation string, stamp uint64) (bool, error) {
554 if j == nil {
555 return false, ErrUnsupported
556 }
557 if j.remote != nil {
558 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "has-effects", Target: hostguard.FenceTarget{Host: host, Incarnation: incarnation, Stamp: stamp}})
559 return out.Found, err
560 }
561
562 if j == nil || j.db == nil {
563 return false, ErrUnsupported
564 }
565 rows, err := journalExecutorFor(ctx, j.db).QueryContext(ctx, "SELECT body FROM launches")
566 if err != nil {
567 return false, err
568 }
569 defer rows.Close()
570 for rows.Next() {
571 var body []byte
572 if err = rows.Scan(&body); err != nil {
573 return false, err
574 }
575 var in LaunchInventory
576 if err = json.Unmarshal(body, &in); err != nil {
577 return false, err
578 }
579 if !in.Completed.Valid() {
580 return false, ErrTypedInventory
581 }
582 if in.Target.Host == host && in.Target.Incarnation == incarnation && in.Target.Stamp == stamp && (in.Completed.Count > 0 || len(in.Reservations) > 0 || len(in.Files) > 0 || len(in.ProducerIDs) > 0) {
583 return true, nil
584 }
585 }
586 return false, rows.Err()
587}
588
589func coldNamespaceEmpty(roots map[string]string, host string, operational ...string) error {
590 for name, root := range roots {
591 if !component(name) || !filepath.IsAbs(root) {
592 return ErrTypedInventory
593 }
594 info, err := os.Lstat(root)
595 if err != nil {
596 return err
597 }
598 if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
599 return ErrTypedInventory
600 }
601 err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
602 if walkErr != nil {
603 return walkErr
604 }
605 rel, err := filepath.Rel(root, path)
606 if err != nil {
607 return err
608 }
609 if rel == "." {
610 return nil
611 }
612 for _, dir := range operational {
613 if dir != "" && path == filepath.Clean(dir) && entry.IsDir() {
614 return filepath.SkipDir
615 }
616 }
617 if entry.IsDir() && (rel == ".parked" || rel == "hostguard" || rel == ".hostclaims" || (name == "opt" && rel == "releases")) {
618 return filepath.SkipDir
619 }
620 if strings.Contains(rel, host) || strings.Contains(rel, strings.ReplaceAll(host, "-", `\x2d`)) {
621 return ErrTypedInventory
622 }
623 return nil
624 })
625 if err != nil {
626 return err
627 }
628 }
629 return nil
630}
631
632func (h ColdHandover) Recover(ctx context.Context) error {
633 entries, err := readTrustedJournalDir(h.JournalDir)
634 if err != nil {
635 return err
636 }
637 if len(entries) == 0 {
638 return nil
639 }
640 pending, err := h.Guard.PendingFences(ctx)
641 if err != nil {
642 return err
643 }
644 var settleErr error
645 for _, entry := range entries {
646 if entry.IsDir() || filepath.Ext(entry.Name()) != ".json" {
647 continue
648 }
649 body, err := readTrustedJournal(filepath.Join(h.JournalDir, entry.Name()))
650 if err != nil {
651 return err
652 }
653 var j coldJournal
654 if decodeStrict(body, &j) != nil || j.ID+".json" != entry.Name() || j.V != 1 {
655 return ErrTypedJournal
656 }
657 if j.Phase == "complete" {
658 needsFinish := false
659 for _, target := range pending {
660 if target == j.Barrier {
661 needsFinish = true
662 }
663 }
664 if !needsFinish {
665 continue
666 }
667 }
668 if j.Phase == "settled" {
669 continue
670 }
671 if h.Obsolete != nil && h.Obsolete(j.Successor) {
672 // A failed settlement leaves the handover as it was, for the
673 // next startup; the other journals are still recovered.
674 settleErr = errors.Join(settleErr, h.settleObsolete(ctx, filepath.Join(h.JournalDir, entry.Name()), j))
675 continue
676 }
677 if err = h.Run(ctx, j.Prior, j.Successor); err != nil {
678 if h.Guard.Root != nil && h.Guard.AbandonColdHandover(ctx, j.Barrier) == nil {
679 continue
680 }
681 return errors.Join(settleErr, err)
682 }
683 }
684 return settleErr
685}
686
687// settleObsolete settles a journaled cold handover whose successor binding
688// is obsolete (roster binding B3), whatever phase it reached short of
689// complete. The guard clears the barrier and retires the successor's stamp
690// under the host lock, then the journal is rewritten as "settled": Recover
691// skips it from then on and Run refuses it, so the successor is never
692// resumed or reused. The predecessor is untouched: its receipt and active
693// stamp stay, so its stop and retirement still complete, and a new
694// successor admitted under today's binding can take over from it. If the
695// journal rewrite fails, the next startup settles again; the guard step is
696// idempotent.
697func (h ColdHandover) settleObsolete(ctx context.Context, path string, j coldJournal) error {
698 if j.Phase != "prepared" && j.Phase != "parked" && j.Phase != "complete" {
699 return ErrTypedJournal
700 }
701 return h.Guard.SettleObsoleteColdHandover(ctx, j.Prior, j.Barrier, func() error {
702 j.Phase = "settled"
703 return writeColdJournal(path, j)
704 })
705}
706
707func (h ColdHandover) atCheckpoint(point string) {
708 if h.checkpoint != nil {
709 h.checkpoint(point)
710 }
711}
712