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/ecdh"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "io/fs"
11 "log"
12 "os"
13 "path/filepath"
14 "strings"
15
16 "github.com/carlosframework/platform/internal/activator"
17 "github.com/carlosframework/platform/internal/appconfig"
18 "github.com/carlosframework/platform/internal/hostbox"
19 "github.com/carlosframework/platform/internal/hostclaim"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/lease"
22 "github.com/carlosframework/platform/internal/moves"
23 "github.com/carlosframework/platform/internal/moves/archive"
24 "github.com/carlosframework/platform/internal/provision"
25 "github.com/carlosframework/platform/internal/registry"
26)
27
28// The installer's helpers (moves spec §7.6 local, restored; shared by the
29// landing and the dirty return, movesinstall.go): the sealed archive read by
30// exact version, verified and extracted into the install's staging, then
31// placed at the live paths inside the host guard; the sealed timers; the
32// database install from the build scratch with its marker, inside the
33// guard; the build again when the journal lost it; and the fsync and
34// atomic-write helpers. The phases are in movesinstall.go, the landing's
35// checking and pass in movesland.go.
36
37// errLandingChanged is a guarded install's refusal when the claim, the
38// record, the installation record or the destination is no longer this
39// install's (a landing's or a dirty return's): nothing is written, and the
40// pass ends with the error.
41var errLandingChanged = errors.New("the installation is no longer this install's")
42
43// errFilesOccupied is the files install's refusal when the files directory
44// holds anything, or <host>.env exists, that this install did not write:
45// it is not the install's to clear, so the install's refusal is written
46// instead (a landing's rollback marker, a return's stuck mark).
47var errFilesOccupied = errors.New("the files paths hold what this install did not write")
48
49// capture reads the installation record and requires it at the holder's
50// incarnation and stamp: the identity every guarded install runs under.
51func (ir *installRun) capture(holder hostclaim.Holder) (hostguard.Captured, error) {
52 captured, err := hostguard.Capture(ir.cfg().DataDir, ir.rec.Host)
53 if err != nil {
54 return captured, fmt.Errorf("read the installation record: %w", err)
55 }
56 if captured.Incarnation != holder.Incarnation || captured.Stamp != holder.Stamp {
57 return captured, fmt.Errorf("move %s: the installation record is at stamp %d of %s, not this %s's %d", ir.g.ID, captured.Stamp, captured.Incarnation, ir.src.Name, holder.Stamp)
58 }
59 return captured, nil
60}
61
62// guarded runs fn inside the host guard for this install's installation
63// (final review, finding 2; hostbox ColdRestore.Install's pattern): under
64// the host's lock, the installation record still active at the captured
65// stamp, no fence over it, the claim read fresh and still passing the
66// install's claim check at phase (a landing's op at phase with this box
67// executing; a return's restoring/claimed with this box executing) with the
68// holder at the captured stamp, and the record read fresh and still this
69// install at phase (installSource.RecordPhase: a landing with no rollback
70// reason; a dirty return). fn re-checks its destination and makes the
71// install; it must not call anything that takes the guard itself
72// (park.Local's retirement and inventory), nor wait on the network.
73func (ir *installRun) guarded(ctx context.Context, captured hostguard.Captured, phase string, fn func() error) error {
74 return ir.guardedWith(ctx, captured, phase, nil, fn)
75}
76
77// guardedWith is guarded with one more check of the fresh claim (nil for
78// none), run after the install's own.
79func (ir *installRun) guardedWith(ctx context.Context, captured hostguard.Captured, phase string, extra func(*hostclaim.Claim) error, fn func() error) error {
80 check := func(c *hostclaim.Claim) error {
81 if err := ir.src.ClaimCheck(phase)(c); err != nil {
82 return err
83 }
84 if extra != nil {
85 if err := extra(c); err != nil {
86 return err
87 }
88 }
89 if c.Holder == nil || c.Holder.Stamp != captured.Stamp {
90 return fmt.Errorf("%w: the claim's holder is not at the installation stamp %d", errLandingChanged, captured.Stamp)
91 }
92 rec, err := provision.GetRecord(ctx, ir.s, ir.rec.Account, ir.rec.App, ir.rec.Host)
93 if err != nil {
94 return fmt.Errorf("re-read the record: %w", err)
95 }
96 if at, err := ir.src.RecordPhase(rec); err != nil || at != phase {
97 return fmt.Errorf("%w: the record is no longer the %s of %s at %s", errLandingChanged, ir.src.Name, ir.g.ID, phase)
98 }
99 return nil
100 }
101 return ir.guard.With(ctx, ir.rec.Host, captured, check, fn)
102}
103
104// stagedFiles names the staged extraction by identity (device and inode):
105// the files directory's and the env file's, Env "" when the archive
106// carries none. A rename keeps an identity, so the same record, journalled
107// as the install receipt once the tree is placed, is what tells this
108// landing's own tree at the live paths from anything else there.
109type stagedFiles struct {
110 Files string `json:"files"`
111 Env string `json:"env,omitempty"`
112}
113
114// stageDir is the op's own extraction staging, <JournalDir>/extract/: the
115// one place a retry clears (final review, finding 1).
116func (ir *installRun) stageDir() string { return filepath.Join(ir.journalDir(), "extract") }
117
118// journalledStaged reads a stagedFiles journal step.
119func journalledStaged(j *moves.Journal, step string) (stagedFiles, error) {
120 var st stagedFiles
121 raw, ok := j.Steps[step]
122 if !ok {
123 return st, fmt.Errorf("the journal has no %s step", step)
124 }
125 if err := json.Unmarshal([]byte(raw), &st); err != nil || st.Files == "" {
126 return st, fmt.Errorf("the journal's %s step %q does not name the staged files (%v)", step, raw, err)
127 }
128 return st, nil
129}
130
131// installFiles is local's sub-step (3) (final review, finding 1). The
132// archive is extracted into the op's staging, never at the live paths, and
133// its identities journalled as `staged`; a retry before that clears only
134// the staging and extracts again. Then, inside the host guard, the staged
135// tree and env are moved to the files directory and <host>.env when those
136// hold nothing this install did not write (absent, or an empty directory,
137// or this install's own tree by identity after a crash mid-move). Anything
138// else there is not the install's to clear: nothing is moved and the
139// rollback is marked. The install receipt (`files_placed`) is journalled,
140// the empty staging removed, and only then `extracted`.
141func (ir *installRun) installFiles(ctx context.Context, j *moves.Journal, mark func(step, value string) error, captured hostguard.Captured, seal moves.Seal, want provision.MoveFilesDigest, dir, envPath string) (restoreOutcome, error) {
142 stage := ir.stageDir()
143 stFiles, stEnv := filepath.Join(stage, "files"), filepath.Join(stage, "env")
144 var staged stagedFiles
145 if j.Done(landStepStaged) {
146 var err error
147 if staged, err = journalledStaged(j, landStepStaged); err != nil {
148 return restoreWaiting, err
149 }
150 } else {
151 // Only the op's own staging is cleared: nothing at the live paths
152 // is touched before the guarded move.
153 if err := removeTree(stage); err != nil {
154 return restoreWaiting, fmt.Errorf("clear the extraction staging: %w", err)
155 }
156 if err := os.MkdirAll(stage, 0o700); err != nil {
157 return restoreWaiting, err
158 }
159 if err := ir.extractArchive(ctx, seal, want, stFiles, stEnv); err != nil {
160 return restoreWaiting, fmt.Errorf("extract the archive: %w", err)
161 }
162 var err error
163 if staged.Files, err = pathIdentity(stFiles); err != nil {
164 return restoreWaiting, err
165 }
166 if present, err := regularFile(stEnv); err != nil {
167 return restoreWaiting, err
168 } else if present {
169 if staged.Env, err = pathIdentity(stEnv); err != nil {
170 return restoreWaiting, err
171 }
172 }
173 body, err := json.Marshal(staged)
174 if err != nil {
175 return restoreWaiting, err
176 }
177 if err := mark(landStepStaged, string(body)); err != nil {
178 return restoreWaiting, err
179 }
180 }
181 err := ir.guarded(ctx, captured, ir.src.Phases[3], func() error {
182 return ir.placeStaged(staged, stFiles, stEnv, dir, envPath)
183 })
184 if errors.Is(err, errFilesOccupied) {
185 log.Printf("moves: %s: %v; nothing was moved or removed", ir.rec.Host, err)
186 ok, err := ir.refuse(ctx, refuseFilesOccupied)
187 if err != nil || !ok {
188 return restoreWaiting, err
189 }
190 return restoreMarked, nil
191 }
192 if err != nil {
193 return restoreWaiting, fmt.Errorf("install the files: %w", err)
194 }
195 if err := mark(landStepFilesPlaced, j.Steps[landStepStaged]); err != nil {
196 return restoreWaiting, err
197 }
198 if err := removeTree(stage); err != nil {
199 return restoreWaiting, fmt.Errorf("remove the extraction staging: %w", err)
200 }
201 if err := mark(landStepExtracted, ""); err != nil {
202 return restoreWaiting, err
203 }
204 return restoreInstalled, nil
205}
206
207// placeStaged runs inside the guard. Every destination is decided before
208// anything moves, so a refusal changes nothing: the files directory must be
209// this install's tree already (by identity: a crash after its move) or
210// absent or an empty directory, and <host>.env this install's env already,
211// or absent. An empty directory gives its mode to the tree and is replaced,
212// as Extract has always done.
213func (ir *installRun) placeStaged(staged stagedFiles, stFiles, stEnv, dir, envPath string) error {
214 filesDone, err := sameIdentity(dir, staged.Files)
215 if err != nil {
216 return err
217 }
218 var emptyDir bool
219 var mode fs.FileMode
220 if !filesDone {
221 if ok, err := sameIdentity(stFiles, staged.Files); err != nil || !ok {
222 return fmt.Errorf("the staged files at %s are not the ones journalled (%v)", stFiles, err)
223 }
224 if emptyDir, mode, err = emptyOrAbsentDir(dir); err != nil {
225 return err
226 }
227 }
228 envDone := false
229 if staged.Env != "" {
230 if envDone, err = sameIdentity(envPath, staged.Env); err != nil {
231 return err
232 }
233 if !envDone {
234 if ok, err := sameIdentity(stEnv, staged.Env); err != nil || !ok {
235 return fmt.Errorf("the staged env at %s is not the one journalled (%v)", stEnv, err)
236 }
237 }
238 }
239 if !envDone {
240 if _, err := os.Lstat(envPath); err == nil {
241 return fmt.Errorf("%w: %s exists", errFilesOccupied, envPath)
242 } else if !errors.Is(err, os.ErrNotExist) {
243 return err
244 }
245 }
246 if !filesDone {
247 if emptyDir {
248 if err := os.Chmod(stFiles, mode); err != nil {
249 return err
250 }
251 if err := syncPath(stFiles); err != nil {
252 return err
253 }
254 // os.Remove refuses a directory that is no longer empty.
255 if err := os.Remove(dir); err != nil {
256 return err
257 }
258 }
259 if err := os.Rename(stFiles, dir); err != nil {
260 return fmt.Errorf("place the files: %w", err)
261 }
262 if err := syncPath(filepath.Dir(dir)); err != nil {
263 return err
264 }
265 if err := ir.fault("after-files-rename"); err != nil {
266 return err
267 }
268 }
269 if staged.Env != "" && !envDone {
270 if err := os.Rename(stEnv, envPath); err != nil {
271 return fmt.Errorf("place the env file: %w", err)
272 }
273 if err := syncPath(filepath.Dir(envPath)); err != nil {
274 return err
275 }
276 }
277 return nil
278}
279
280// emptyOrAbsentDir reports whether path is an empty directory (and its
281// mode); absent is false with no error; anything else (a file, a symlink,
282// a directory with anything in it) is errFilesOccupied.
283func emptyOrAbsentDir(path string) (bool, fs.FileMode, error) {
284 fi, err := os.Lstat(path)
285 if errors.Is(err, os.ErrNotExist) {
286 return false, 0, nil
287 }
288 if err != nil {
289 return false, 0, err
290 }
291 if !fi.IsDir() {
292 return false, 0, fmt.Errorf("%w: %s is not a directory", errFilesOccupied, path)
293 }
294 f, err := os.Open(path)
295 if err != nil {
296 return false, 0, err
297 }
298 names, err := f.Readdirnames(1)
299 f.Close()
300 if len(names) > 0 {
301 return false, 0, fmt.Errorf("%w: %s holds %s", errFilesOccupied, path, names[0])
302 }
303 if err != nil && err != io.EOF {
304 return false, 0, err
305 }
306 return true, fi.Mode().Perm(), nil
307}
308
309// pathIdentity is the device and inode of path itself (not a symlink's
310// target), "dev:ino". Off unix there is none, and the landing refuses.
311func pathIdentity(path string) (string, error) {
312 fi, err := os.Lstat(path)
313 if err != nil {
314 return "", err
315 }
316 dev, ino, ok := fileInode(fi)
317 if !ok {
318 return "", fmt.Errorf("%s: this platform cannot identify files", path)
319 }
320 return fmt.Sprintf("%d:%d", dev, ino), nil
321}
322
323// sameIdentity reports whether path exists with identity id.
324func sameIdentity(path, id string) (bool, error) {
325 got, err := pathIdentity(path)
326 if errors.Is(err, os.ErrNotExist) {
327 return false, nil
328 }
329 if err != nil {
330 return false, err
331 }
332 return id != "" && got == id, nil
333}
334
335// removeTree removes a tree this install made under its own journal
336// directory, whose directories may carry the archive's restrictive modes:
337// each is made writable before its contents are removed. Absent is nothing
338// to do.
339func removeTree(path string) error {
340 if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
341 return nil
342 }
343 filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
344 if err == nil && d.IsDir() {
345 os.Chmod(p, 0o700)
346 }
347 return nil
348 })
349 return os.RemoveAll(path)
350}
351
352// filesGenMarker is the .files-gen body (§5.1): the op and the incarnation.
353type filesGenMarker struct {
354 Op string `json:"op"`
355 Incarnation string `json:"incarnation"`
356}
357
358// extractArchive reads the seal's archive and manifest by exact version from
359// this box's own bucket, verifies the archive against the manifest and the
360// record's digests (archive.Verify: the sealed digest, every entry), and
361// extracts it into dir and envPath (archive.Open under the box's secrets
362// key, then archive.Extract, which admits nothing it did not read whole).
363func (ir *installRun) extractArchive(ctx context.Context, seal moves.Seal, want provision.MoveFilesDigest, dir, envPath string) error {
364 cfg := ir.cfg()
365 var sealedObj, manifestObj *moves.SealedObject
366 for i := range seal.Files {
367 switch o := &seal.Files[i]; {
368 case strings.HasSuffix(o.Key, ".sealed"):
369 sealedObj = o
370 case strings.HasSuffix(o.Key, ".json"):
371 manifestObj = o
372 }
373 }
374 if sealedObj == nil || manifestObj == nil {
375 return errors.New("the seal does not name the archive and its manifest")
376 }
377 vs, err := ir.versioned(ctx)
378 if err != nil {
379 return err
380 }
381 raw, err := vs.GetVersion(ctx, cfg.OwnBucket, manifestObj.Key, manifestObj.VersionID)
382 if err != nil {
383 return fmt.Errorf("read the archive manifest: %w", err)
384 }
385 var m archive.Manifest
386 if err := json.Unmarshal(raw, &m); err != nil {
387 return fmt.Errorf("the archive manifest: %w", err)
388 }
389 if m.Op != ir.src.OpID || m.Incarnation != ir.rec.Move.Incarnation || m.Plain != want.Plain || m.Sealed != want.Sealed {
390 return fmt.Errorf("the archive manifest is for %s/%s (%s, %s), not this move's %s (%s, %s)", m.Op, m.Incarnation, m.Plain, m.Sealed, ir.src.OpID, want.Plain, want.Sealed)
391 }
392 if err := os.MkdirAll(ir.journalDir(), 0o700); err != nil {
393 return err
394 }
395 sealedPath := filepath.Join(ir.journalDir(), "archive.sealed")
396 if err := os.Remove(sealedPath); err != nil && !errors.Is(err, os.ErrNotExist) {
397 return err
398 }
399 defer os.Remove(sealedPath)
400 if err := downloadVersion(ctx, vs, cfg.OwnBucket, sealedObj.Key, sealedObj.VersionID, sealedPath); err != nil {
401 return fmt.Errorf("read the archive: %w", err)
402 }
403 f, err := os.Open(sealedPath)
404 if err != nil {
405 return err
406 }
407 defer f.Close()
408 bind := appconfig.Binding{Account: ir.rec.Account, App: ir.rec.App}
409 if err := archive.Verify(f, m, cfg.SecretsPriv, bind); err != nil {
410 return fmt.Errorf("verify the archive: %w", err)
411 }
412 return openAndExtract(f, m, cfg.SecretsPriv, bind, dir, envPath)
413}
414
415// openAndExtract pipes the opened archive into Extract, as carriedEnv does.
416func openAndExtract(f *os.File, m archive.Manifest, priv *ecdh.PrivateKey, bind appconfig.Binding, dir, envPath string) error {
417 if _, err := f.Seek(0, io.SeekStart); err != nil {
418 return err
419 }
420 pr, pw := io.Pipe()
421 done := make(chan error, 1)
422 go func() {
423 err := archive.Open(f, m.SealedDataKey, priv, bind, pw)
424 pw.CloseWithError(err)
425 done <- err
426 }()
427 xerr := archive.Extract(pr, dir, envPath)
428 pr.CloseWithError(errors.New("extract stopped reading"))
429 if oerr := <-done; oerr != nil && xerr == nil {
430 xerr = oerr
431 }
432 return xerr
433}
434
435// sealedTimers reads the sealed timers snapshot by exact version and checks
436// its digest against the record's; every row is this host's.
437func (ir *installRun) sealedTimers(ctx context.Context, seal moves.Seal, wantDigest string) ([]registry.Timer, error) {
438 cfg := ir.cfg()
439 if len(seal.Timers) != 1 {
440 return nil, fmt.Errorf("the seal names %d timers objects, want 1", len(seal.Timers))
441 }
442 vs, err := ir.versioned(ctx)
443 if err != nil {
444 return nil, err
445 }
446 raw, err := vs.GetVersion(ctx, cfg.OwnBucket, seal.Timers[0].Key, seal.Timers[0].VersionID)
447 if err != nil {
448 return nil, fmt.Errorf("read the timers snapshot: %w", err)
449 }
450 snap, err := moves.DecodeTimers(raw)
451 if err != nil {
452 return nil, err
453 }
454 if snap.Host != ir.rec.Host {
455 return nil, fmt.Errorf("the timers snapshot is %s's, not %s's", snap.Host, ir.rec.Host)
456 }
457 if got := moves.TimersDigest(snap.Rows); got != wantDigest {
458 return nil, fmt.Errorf("the timers snapshot digests to %s, the record says %s", got, wantDigest)
459 }
460 for _, r := range snap.Rows {
461 if r.Host != ir.rec.Host {
462 return nil, fmt.Errorf("timer %q is %s's, not %s's", r.Name, r.Host, ir.rec.Host)
463 }
464 }
465 return snap.Rows, nil
466}
467
468// sameBundle compares two bundle etag maps.
469func sameBundle(a, b map[string]string) bool {
470 if len(a) != len(b) {
471 return false
472 }
473 for k, v := range a {
474 if bv, ok := b[k]; !ok || bv != v {
475 return false
476 }
477 }
478 return true
479}
480
481// restoreOutcome is what restore (or local's files install) ended with: the
482// database installed and its markers written (or the files placed); the
483// record changed instead (the rollback marker, fix round 1, item 2: a
484// marker written is never read as an install); or the phase waiting for
485// the next pass (the location's lease held elsewhere).
486type restoreOutcome int
487
488const (
489 restoreInstalled restoreOutcome = iota
490 restoreMarked
491 restoreWaiting
492)
493
494// restore is the install of the built database (§7.6 restored, R6-3):
495// its sha256 must be the manifest's and CheckDB must pass, else the
496// rollback marker; then, inside the host guard (final review, finding 2;
497// hostbox ColdRestore.Install's pattern), the destination checked again,
498// the rename into DBPath and the generation marker. The returned sha
499// carries the record's "sha256:" prefix, as Source.DBSHA256 does.
500//
501// The build, the verification and the rollback marker run outside the
502// guard (they read the network or write the record); the observation they
503// start from is only a hint, and the guarded block decides: a database at
504// DBPath that is not this build, a marker naming another generation, or a
505// WAL beside an absent database is a successor's or someone else's, and
506// is never replaced.
507func (ir *installRun) restore(ctx context.Context, obs hostclaim.Observed) (sha string, outcome restoreOutcome, err error) {
508 cfg := ir.cfg()
509 host, id := ir.rec.Host, ir.src.GenerationOp
510 m, waiting, err := ir.installManifest(ctx, obs)
511 if err != nil || waiting {
512 return "", restoreWaiting, err
513 }
514 want := "sha256:" + m.DBSHA256
515 dbPath := ir.dbPath()
516 captured, err := ir.capture(*obs.Claim.Holder)
517 if err != nil {
518 return "", restoreWaiting, err
519 }
520 present, err := regularFile(dbPath)
521 if err != nil {
522 return "", restoreWaiting, err
523 }
524 built := filepath.Join(ir.buildDir(), host+".db")
525 if !present {
526 if ok, err := regularFile(built); err != nil {
527 return "", restoreWaiting, err
528 } else if !ok {
529 // The scratch was lost (a rebuilt data directory): build again
530 // from the pinned set, which reproduces the same bytes, under
531 // the location's lease.
532 if _, waiting, err := ir.rebuild(ctx, obs); err != nil || waiting {
533 return "", restoreWaiting, err
534 }
535 }
536 if err := ir.fault("before-db-verify"); err != nil {
537 return "", restoreWaiting, err
538 }
539 got, err := fileSHA256(built)
540 if err != nil {
541 return "", restoreWaiting, err
542 }
543 if got != want {
544 log.Printf("moves: %s: the built database is %s, the manifest says %s", host, got, want)
545 return ir.refuseInstall(ctx)
546 }
547 if err := lease.CheckDB(built); err != nil {
548 log.Printf("moves: %s: the built database does not pass its check: %v", host, err)
549 return ir.refuseInstall(ctx)
550 }
551 for _, side := range []string{"-wal", "-shm"} {
552 os.Remove(built + side)
553 }
554 }
555 err = ir.guarded(ctx, captured, ir.src.Phases[4], func() error {
556 // Every destination is Lstat-checked, never read through: a symlink
557 // or any other non-regular file at <dbPath>.gen, DBPath or the WAL
558 // paths, dangling or not, is not this install's and is never
559 // replaced (final re-review residual). ReadGenerationMarker follows
560 // a symlink and reads a dangling one as absent.
561 if fi, err := os.Lstat(dbPath + ".gen"); err == nil && !fi.Mode().IsRegular() {
562 return fmt.Errorf("%w: %s.gen is not a regular file (%s)", errLandingChanged, dbPath, fi.Mode())
563 } else if err != nil && !errors.Is(err, os.ErrNotExist) {
564 return err
565 }
566 gen, err := hostbox.ReadGenerationMarker(dbPath + ".gen")
567 if err != nil {
568 return fmt.Errorf("read %s.gen: %w", dbPath, err)
569 }
570 if gen != "" && gen != id {
571 return fmt.Errorf("%w: %s.gen names generation %s", errLandingChanged, dbPath, gen)
572 }
573 now, err := regularFile(dbPath) // Lstat: a non-regular DBPath is refused
574 if err != nil {
575 return fmt.Errorf("%w: %v", errLandingChanged, err)
576 }
577 switch {
578 case now:
579 // This landing's own install (a replay after the rename) is
580 // the build's bytes; anything else is not to be replaced.
581 got, err := fileSHA256(dbPath)
582 if err != nil {
583 return err
584 }
585 if got != want {
586 return fmt.Errorf("%w: %s holds a database that is not this install's build (%s, want %s)", errLandingChanged, dbPath, got, want)
587 }
588 case present:
589 return fmt.Errorf("%w: the database observed at %s is gone", errLandingChanged, dbPath)
590 default:
591 for _, side := range []string{"-wal", "-shm"} {
592 if _, err := os.Lstat(dbPath + side); err == nil {
593 return fmt.Errorf("%w: %s%s exists beside an absent database", errLandingChanged, dbPath, side)
594 } else if !errors.Is(err, os.ErrNotExist) {
595 return err
596 }
597 }
598 // No seed marker may stand beside the database this landing
599 // installs: it would name another seed's leftover for it
600 // (re-review finding 8).
601 if err := os.Remove(dbPath + activator.SeedMarkerSuffix); err != nil && !errors.Is(err, os.ErrNotExist) {
602 return err
603 }
604 if err := ir.notePreInstall(); err != nil {
605 return err
606 }
607 if err := os.Rename(built, dbPath); err != nil {
608 return fmt.Errorf("install the database: %w", err)
609 }
610 if err := syncPath(filepath.Dir(dbPath)); err != nil {
611 return err
612 }
613 }
614 if err := ir.fault("after-db-rename"); err != nil {
615 return err
616 }
617 if err := cfg.WriteGen(dbPath+".gen", id); err != nil {
618 return fmt.Errorf("write the generation marker: %w", err)
619 }
620 return syncPath(filepath.Dir(dbPath))
621 })
622 if err != nil {
623 return "", restoreWaiting, err
624 }
625 log.Printf("moves: %s: database installed at %s (%s), generation %s", host, dbPath, want, id)
626 return want, restoreInstalled, nil
627}
628
629// preInstall is the landStepPreInstall record: which of the database's
630// unchecked siblings (installSiblingPaths) were absent, relative to the
631// data directory, when the install was about to put the database in place.
632// Only a sibling recorded absent can later count as this op's (the target's
633// cleaning, rollbackOwnership; the final re-review's residual): one already
634// there was not made by anything of this install's.
635type preInstall struct {
636 Absent []string `json:"absent"`
637}
638
639// installSiblingPaths are the database's siblings the install does not
640// refuse beside an absent database (it refuses a WAL and shared memory):
641// the quarantine and the litestream shadow, relative to the data directory.
642func installSiblingPaths(host string) []string {
643 db := host + ".db"
644 return []string{db + ".corrupt", ".activator-" + db + "-litestream"}
645}
646
647// notePreInstall journals preInstall inside the database install's guard,
648// right before the rename: each sibling Lstat'ed, never followed. A replay
649// before the rename (a crash after the step) looks again and journals what
650// it sees, so the record is always the look that immediately preceded the
651// rename, under the same guard. A replay that finds the database already
652// installed never reaches here, so a step lost that way is missing, and its
653// siblings are never counted as the op's.
654func (ir *installRun) notePreInstall() error {
655 j, err := ir.journal()
656 if err != nil {
657 return err
658 }
659 raw, replay := j.Steps[landStepPreInstall]
660 rec := preInstall{Absent: []string{}}
661 for _, p := range installSiblingPaths(ir.rec.Host) {
662 _, err := os.Lstat(filepath.Join(ir.cfg().DataDir, p))
663 switch {
664 case errors.Is(err, os.ErrNotExist):
665 rec.Absent = append(rec.Absent, p)
666 case err != nil:
667 return err
668 default:
669 log.Printf("moves: %s: %s is already there before the database is installed; it is not the %s's", ir.rec.Host, p, ir.src.Name)
670 }
671 }
672 body, err := json.Marshal(rec)
673 if err != nil {
674 return err
675 }
676 if replay && string(body) == raw {
677 return nil
678 }
679 j.Mark(landStepPreInstall, string(body))
680 if err := j.Commit(ir.cfg().DataDir); err != nil {
681 return err
682 }
683 return ir.fault("after-" + landStepPreInstall)
684}
685
686// refuseInstall writes the install's refusal for a build that does not
687// verify. A refusal that landed is restoreMarked; one refused (the record
688// moved on) is restoreWaiting, and nothing is installed either way.
689func (ir *installRun) refuseInstall(ctx context.Context) (string, restoreOutcome, error) {
690 ok, err := ir.refuse(ctx, refuseInstallMismatch)
691 if err != nil || !ok {
692 return "", restoreWaiting, err
693 }
694 return "", restoreMarked, nil
695}
696
697// installManifest is the journal's, or the build's again when the journal
698// lost it. waiting is true when the rebuild could not take the lease.
699func (ir *installRun) installManifest(ctx context.Context, obs hostclaim.Observed) (moves.InstallManifest, bool, error) {
700 j, err := ir.journal()
701 if err != nil {
702 return moves.InstallManifest{}, false, err
703 }
704 var m moves.InstallManifest
705 if raw, ok := j.Steps[landStepInstall]; ok && json.Unmarshal([]byte(raw), &m) == nil && m.Op == ir.src.GenerationOp && m.Host == ir.rec.Host && m.DBSHA256 != "" {
706 return m, false, nil
707 }
708 return ir.rebuild(ctx, obs)
709}
710
711// rebuild runs BuildGeneration again over the seal: a replay that reuses
712// every object it finds and restores the same bytes into the scratch. It
713// takes the location's lease first (published released it), runs every
714// generation request under it, checks it again before recording the result
715// and releases it after (fix round 1, item 3). waiting is true when another
716// holder has the lease; nothing is written then.
717func (ir *installRun) rebuild(ctx context.Context, obs hostclaim.Observed) (moves.InstallManifest, bool, error) {
718 cfg := ir.cfg()
719 seal, err := ir.seal(obs.Claim)
720 if err != nil {
721 return moves.InstallManifest{}, false, err
722 }
723 vs, err := ir.versioned(ctx)
724 if err != nil {
725 return moves.InstallManifest{}, false, err
726 }
727 holder := *obs.Claim.Holder
728 held, waiting, err := ir.lease(ctx, holder)
729 if waiting || err != nil {
730 return moves.InstallManifest{}, waiting, err
731 }
732 defer ir.releaseLease()
733 m, err := moves.BuildGeneration(leaseContext(ctx, held), moves.BuildInputs{Store: leasedStore{VersionedStore: vs, check: held.Check}, Bucket: cfg.OwnBucket, Source: seal.Replica, N: seal.N,
734 GenerationPrefix: moves.GenerationPrefix(holder.Location, ir.src.GenerationOp), Op: ir.src.GenerationOp, Host: ir.rec.Host, WorkDir: ir.buildDir(), Now: cfg.Now})
735 if err != nil {
736 return moves.InstallManifest{}, false, fmt.Errorf("rebuild the generation: %w", err)
737 }
738 if err := held.Check(ctx); err != nil {
739 return moves.InstallManifest{}, false, fmt.Errorf("the location's lease after the rebuild: %w", err)
740 }
741 j, err := ir.journal()
742 if err != nil {
743 return m, false, err
744 }
745 body, err := json.Marshal(m)
746 if err != nil {
747 return m, false, err
748 }
749 j.Mark(landStepInstall, string(body))
750 return m, false, j.Commit(cfg.DataDir)
751}
752
753// writeFileAtomic writes body to path through a temporary file in the same
754// directory, synced and renamed, then syncs the directory.
755func writeFileAtomic(path string, body []byte, mode os.FileMode) error {
756 dir := filepath.Dir(path)
757 f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*")
758 if err != nil {
759 return err
760 }
761 tmp := f.Name()
762 if _, err := f.Write(body); err != nil {
763 f.Close()
764 os.Remove(tmp)
765 return err
766 }
767 if err := f.Chmod(mode); err != nil {
768 f.Close()
769 os.Remove(tmp)
770 return err
771 }
772 if err := f.Sync(); err != nil {
773 f.Close()
774 os.Remove(tmp)
775 return err
776 }
777 if err := f.Close(); err != nil {
778 os.Remove(tmp)
779 return err
780 }
781 if err := os.Rename(tmp, path); err != nil {
782 os.Remove(tmp)
783 return err
784 }
785 return syncPath(dir)
786}
787
788// syncPath fsyncs a file or directory.
789func syncPath(path string) error {
790 f, err := os.Open(path)
791 if err != nil {
792 return err
793 }
794 defer f.Close()
795 return f.Sync()
796}
797