package host import ( "context" "crypto/ecdh" "encoding/json" "errors" "fmt" "io" "io/fs" "log" "os" "path/filepath" "strings" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/moves/archive" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" ) // The installer's helpers (moves spec §7.6 local, restored; shared by the // landing and the dirty return, movesinstall.go): the sealed archive read by // exact version, verified and extracted into the install's staging, then // placed at the live paths inside the host guard; the sealed timers; the // database install from the build scratch with its marker, inside the // guard; the build again when the journal lost it; and the fsync and // atomic-write helpers. The phases are in movesinstall.go, the landing's // checking and pass in movesland.go. // errLandingChanged is a guarded install's refusal when the claim, the // record, the installation record or the destination is no longer this // install's (a landing's or a dirty return's): nothing is written, and the // pass ends with the error. var errLandingChanged = errors.New("the installation is no longer this install's") // errFilesOccupied is the files install's refusal when the files directory // holds anything, or .env exists, that this install did not write: // it is not the install's to clear, so the install's refusal is written // instead (a landing's rollback marker, a return's stuck mark). var errFilesOccupied = errors.New("the files paths hold what this install did not write") // capture reads the installation record and requires it at the holder's // incarnation and stamp: the identity every guarded install runs under. func (ir *installRun) capture(holder hostclaim.Holder) (hostguard.Captured, error) { captured, err := hostguard.Capture(ir.cfg().DataDir, ir.rec.Host) if err != nil { return captured, fmt.Errorf("read the installation record: %w", err) } if captured.Incarnation != holder.Incarnation || captured.Stamp != holder.Stamp { 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) } return captured, nil } // guarded runs fn inside the host guard for this install's installation // (final review, finding 2; hostbox ColdRestore.Install's pattern): under // the host's lock, the installation record still active at the captured // stamp, no fence over it, the claim read fresh and still passing the // install's claim check at phase (a landing's op at phase with this box // executing; a return's restoring/claimed with this box executing) with the // holder at the captured stamp, and the record read fresh and still this // install at phase (installSource.RecordPhase: a landing with no rollback // reason; a dirty return). fn re-checks its destination and makes the // install; it must not call anything that takes the guard itself // (park.Local's retirement and inventory), nor wait on the network. func (ir *installRun) guarded(ctx context.Context, captured hostguard.Captured, phase string, fn func() error) error { return ir.guardedWith(ctx, captured, phase, nil, fn) } // guardedWith is guarded with one more check of the fresh claim (nil for // none), run after the install's own. func (ir *installRun) guardedWith(ctx context.Context, captured hostguard.Captured, phase string, extra func(*hostclaim.Claim) error, fn func() error) error { check := func(c *hostclaim.Claim) error { if err := ir.src.ClaimCheck(phase)(c); err != nil { return err } if extra != nil { if err := extra(c); err != nil { return err } } if c.Holder == nil || c.Holder.Stamp != captured.Stamp { return fmt.Errorf("%w: the claim's holder is not at the installation stamp %d", errLandingChanged, captured.Stamp) } rec, err := provision.GetRecord(ctx, ir.s, ir.rec.Account, ir.rec.App, ir.rec.Host) if err != nil { return fmt.Errorf("re-read the record: %w", err) } if at, err := ir.src.RecordPhase(rec); err != nil || at != phase { return fmt.Errorf("%w: the record is no longer the %s of %s at %s", errLandingChanged, ir.src.Name, ir.g.ID, phase) } return nil } return ir.guard.With(ctx, ir.rec.Host, captured, check, fn) } // stagedFiles names the staged extraction by identity (device and inode): // the files directory's and the env file's, Env "" when the archive // carries none. A rename keeps an identity, so the same record, journalled // as the install receipt once the tree is placed, is what tells this // landing's own tree at the live paths from anything else there. type stagedFiles struct { Files string `json:"files"` Env string `json:"env,omitempty"` } // stageDir is the op's own extraction staging, /extract/: the // one place a retry clears (final review, finding 1). func (ir *installRun) stageDir() string { return filepath.Join(ir.journalDir(), "extract") } // journalledStaged reads a stagedFiles journal step. func journalledStaged(j *moves.Journal, step string) (stagedFiles, error) { var st stagedFiles raw, ok := j.Steps[step] if !ok { return st, fmt.Errorf("the journal has no %s step", step) } if err := json.Unmarshal([]byte(raw), &st); err != nil || st.Files == "" { return st, fmt.Errorf("the journal's %s step %q does not name the staged files (%v)", step, raw, err) } return st, nil } // installFiles is local's sub-step (3) (final review, finding 1). The // archive is extracted into the op's staging, never at the live paths, and // its identities journalled as `staged`; a retry before that clears only // the staging and extracts again. Then, inside the host guard, the staged // tree and env are moved to the files directory and .env when those // hold nothing this install did not write (absent, or an empty directory, // or this install's own tree by identity after a crash mid-move). Anything // else there is not the install's to clear: nothing is moved and the // rollback is marked. The install receipt (`files_placed`) is journalled, // the empty staging removed, and only then `extracted`. func (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) { stage := ir.stageDir() stFiles, stEnv := filepath.Join(stage, "files"), filepath.Join(stage, "env") var staged stagedFiles if j.Done(landStepStaged) { var err error if staged, err = journalledStaged(j, landStepStaged); err != nil { return restoreWaiting, err } } else { // Only the op's own staging is cleared: nothing at the live paths // is touched before the guarded move. if err := removeTree(stage); err != nil { return restoreWaiting, fmt.Errorf("clear the extraction staging: %w", err) } if err := os.MkdirAll(stage, 0o700); err != nil { return restoreWaiting, err } if err := ir.extractArchive(ctx, seal, want, stFiles, stEnv); err != nil { return restoreWaiting, fmt.Errorf("extract the archive: %w", err) } var err error if staged.Files, err = pathIdentity(stFiles); err != nil { return restoreWaiting, err } if present, err := regularFile(stEnv); err != nil { return restoreWaiting, err } else if present { if staged.Env, err = pathIdentity(stEnv); err != nil { return restoreWaiting, err } } body, err := json.Marshal(staged) if err != nil { return restoreWaiting, err } if err := mark(landStepStaged, string(body)); err != nil { return restoreWaiting, err } } err := ir.guarded(ctx, captured, ir.src.Phases[3], func() error { return ir.placeStaged(staged, stFiles, stEnv, dir, envPath) }) if errors.Is(err, errFilesOccupied) { log.Printf("moves: %s: %v; nothing was moved or removed", ir.rec.Host, err) ok, err := ir.refuse(ctx, refuseFilesOccupied) if err != nil || !ok { return restoreWaiting, err } return restoreMarked, nil } if err != nil { return restoreWaiting, fmt.Errorf("install the files: %w", err) } if err := mark(landStepFilesPlaced, j.Steps[landStepStaged]); err != nil { return restoreWaiting, err } if err := removeTree(stage); err != nil { return restoreWaiting, fmt.Errorf("remove the extraction staging: %w", err) } if err := mark(landStepExtracted, ""); err != nil { return restoreWaiting, err } return restoreInstalled, nil } // placeStaged runs inside the guard. Every destination is decided before // anything moves, so a refusal changes nothing: the files directory must be // this install's tree already (by identity: a crash after its move) or // absent or an empty directory, and .env this install's env already, // or absent. An empty directory gives its mode to the tree and is replaced, // as Extract has always done. func (ir *installRun) placeStaged(staged stagedFiles, stFiles, stEnv, dir, envPath string) error { filesDone, err := sameIdentity(dir, staged.Files) if err != nil { return err } var emptyDir bool var mode fs.FileMode if !filesDone { if ok, err := sameIdentity(stFiles, staged.Files); err != nil || !ok { return fmt.Errorf("the staged files at %s are not the ones journalled (%v)", stFiles, err) } if emptyDir, mode, err = emptyOrAbsentDir(dir); err != nil { return err } } envDone := false if staged.Env != "" { if envDone, err = sameIdentity(envPath, staged.Env); err != nil { return err } if !envDone { if ok, err := sameIdentity(stEnv, staged.Env); err != nil || !ok { return fmt.Errorf("the staged env at %s is not the one journalled (%v)", stEnv, err) } } } if !envDone { if _, err := os.Lstat(envPath); err == nil { return fmt.Errorf("%w: %s exists", errFilesOccupied, envPath) } else if !errors.Is(err, os.ErrNotExist) { return err } } if !filesDone { if emptyDir { if err := os.Chmod(stFiles, mode); err != nil { return err } if err := syncPath(stFiles); err != nil { return err } // os.Remove refuses a directory that is no longer empty. if err := os.Remove(dir); err != nil { return err } } if err := os.Rename(stFiles, dir); err != nil { return fmt.Errorf("place the files: %w", err) } if err := syncPath(filepath.Dir(dir)); err != nil { return err } if err := ir.fault("after-files-rename"); err != nil { return err } } if staged.Env != "" && !envDone { if err := os.Rename(stEnv, envPath); err != nil { return fmt.Errorf("place the env file: %w", err) } if err := syncPath(filepath.Dir(envPath)); err != nil { return err } } return nil } // emptyOrAbsentDir reports whether path is an empty directory (and its // mode); absent is false with no error; anything else (a file, a symlink, // a directory with anything in it) is errFilesOccupied. func emptyOrAbsentDir(path string) (bool, fs.FileMode, error) { fi, err := os.Lstat(path) if errors.Is(err, os.ErrNotExist) { return false, 0, nil } if err != nil { return false, 0, err } if !fi.IsDir() { return false, 0, fmt.Errorf("%w: %s is not a directory", errFilesOccupied, path) } f, err := os.Open(path) if err != nil { return false, 0, err } names, err := f.Readdirnames(1) f.Close() if len(names) > 0 { return false, 0, fmt.Errorf("%w: %s holds %s", errFilesOccupied, path, names[0]) } if err != nil && err != io.EOF { return false, 0, err } return true, fi.Mode().Perm(), nil } // pathIdentity is the device and inode of path itself (not a symlink's // target), "dev:ino". Off unix there is none, and the landing refuses. func pathIdentity(path string) (string, error) { fi, err := os.Lstat(path) if err != nil { return "", err } dev, ino, ok := fileInode(fi) if !ok { return "", fmt.Errorf("%s: this platform cannot identify files", path) } return fmt.Sprintf("%d:%d", dev, ino), nil } // sameIdentity reports whether path exists with identity id. func sameIdentity(path, id string) (bool, error) { got, err := pathIdentity(path) if errors.Is(err, os.ErrNotExist) { return false, nil } if err != nil { return false, err } return id != "" && got == id, nil } // removeTree removes a tree this install made under its own journal // directory, whose directories may carry the archive's restrictive modes: // each is made writable before its contents are removed. Absent is nothing // to do. func removeTree(path string) error { if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) { return nil } filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error { if err == nil && d.IsDir() { os.Chmod(p, 0o700) } return nil }) return os.RemoveAll(path) } // filesGenMarker is the .files-gen body (§5.1): the op and the incarnation. type filesGenMarker struct { Op string `json:"op"` Incarnation string `json:"incarnation"` } // extractArchive reads the seal's archive and manifest by exact version from // this box's own bucket, verifies the archive against the manifest and the // record's digests (archive.Verify: the sealed digest, every entry), and // extracts it into dir and envPath (archive.Open under the box's secrets // key, then archive.Extract, which admits nothing it did not read whole). func (ir *installRun) extractArchive(ctx context.Context, seal moves.Seal, want provision.MoveFilesDigest, dir, envPath string) error { cfg := ir.cfg() var sealedObj, manifestObj *moves.SealedObject for i := range seal.Files { switch o := &seal.Files[i]; { case strings.HasSuffix(o.Key, ".sealed"): sealedObj = o case strings.HasSuffix(o.Key, ".json"): manifestObj = o } } if sealedObj == nil || manifestObj == nil { return errors.New("the seal does not name the archive and its manifest") } vs, err := ir.versioned(ctx) if err != nil { return err } raw, err := vs.GetVersion(ctx, cfg.OwnBucket, manifestObj.Key, manifestObj.VersionID) if err != nil { return fmt.Errorf("read the archive manifest: %w", err) } var m archive.Manifest if err := json.Unmarshal(raw, &m); err != nil { return fmt.Errorf("the archive manifest: %w", err) } if m.Op != ir.src.OpID || m.Incarnation != ir.rec.Move.Incarnation || m.Plain != want.Plain || m.Sealed != want.Sealed { 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) } if err := os.MkdirAll(ir.journalDir(), 0o700); err != nil { return err } sealedPath := filepath.Join(ir.journalDir(), "archive.sealed") if err := os.Remove(sealedPath); err != nil && !errors.Is(err, os.ErrNotExist) { return err } defer os.Remove(sealedPath) if err := downloadVersion(ctx, vs, cfg.OwnBucket, sealedObj.Key, sealedObj.VersionID, sealedPath); err != nil { return fmt.Errorf("read the archive: %w", err) } f, err := os.Open(sealedPath) if err != nil { return err } defer f.Close() bind := appconfig.Binding{Account: ir.rec.Account, App: ir.rec.App} if err := archive.Verify(f, m, cfg.SecretsPriv, bind); err != nil { return fmt.Errorf("verify the archive: %w", err) } return openAndExtract(f, m, cfg.SecretsPriv, bind, dir, envPath) } // openAndExtract pipes the opened archive into Extract, as carriedEnv does. func openAndExtract(f *os.File, m archive.Manifest, priv *ecdh.PrivateKey, bind appconfig.Binding, dir, envPath string) error { if _, err := f.Seek(0, io.SeekStart); err != nil { return err } pr, pw := io.Pipe() done := make(chan error, 1) go func() { err := archive.Open(f, m.SealedDataKey, priv, bind, pw) pw.CloseWithError(err) done <- err }() xerr := archive.Extract(pr, dir, envPath) pr.CloseWithError(errors.New("extract stopped reading")) if oerr := <-done; oerr != nil && xerr == nil { xerr = oerr } return xerr } // sealedTimers reads the sealed timers snapshot by exact version and checks // its digest against the record's; every row is this host's. func (ir *installRun) sealedTimers(ctx context.Context, seal moves.Seal, wantDigest string) ([]registry.Timer, error) { cfg := ir.cfg() if len(seal.Timers) != 1 { return nil, fmt.Errorf("the seal names %d timers objects, want 1", len(seal.Timers)) } vs, err := ir.versioned(ctx) if err != nil { return nil, err } raw, err := vs.GetVersion(ctx, cfg.OwnBucket, seal.Timers[0].Key, seal.Timers[0].VersionID) if err != nil { return nil, fmt.Errorf("read the timers snapshot: %w", err) } snap, err := moves.DecodeTimers(raw) if err != nil { return nil, err } if snap.Host != ir.rec.Host { return nil, fmt.Errorf("the timers snapshot is %s's, not %s's", snap.Host, ir.rec.Host) } if got := moves.TimersDigest(snap.Rows); got != wantDigest { return nil, fmt.Errorf("the timers snapshot digests to %s, the record says %s", got, wantDigest) } for _, r := range snap.Rows { if r.Host != ir.rec.Host { return nil, fmt.Errorf("timer %q is %s's, not %s's", r.Name, r.Host, ir.rec.Host) } } return snap.Rows, nil } // sameBundle compares two bundle etag maps. func sameBundle(a, b map[string]string) bool { if len(a) != len(b) { return false } for k, v := range a { if bv, ok := b[k]; !ok || bv != v { return false } } return true } // restoreOutcome is what restore (or local's files install) ended with: the // database installed and its markers written (or the files placed); the // record changed instead (the rollback marker, fix round 1, item 2: a // marker written is never read as an install); or the phase waiting for // the next pass (the location's lease held elsewhere). type restoreOutcome int const ( restoreInstalled restoreOutcome = iota restoreMarked restoreWaiting ) // restore is the install of the built database (§7.6 restored, R6-3): // its sha256 must be the manifest's and CheckDB must pass, else the // rollback marker; then, inside the host guard (final review, finding 2; // hostbox ColdRestore.Install's pattern), the destination checked again, // the rename into DBPath and the generation marker. The returned sha // carries the record's "sha256:" prefix, as Source.DBSHA256 does. // // The build, the verification and the rollback marker run outside the // guard (they read the network or write the record); the observation they // start from is only a hint, and the guarded block decides: a database at // DBPath that is not this build, a marker naming another generation, or a // WAL beside an absent database is a successor's or someone else's, and // is never replaced. func (ir *installRun) restore(ctx context.Context, obs hostclaim.Observed) (sha string, outcome restoreOutcome, err error) { cfg := ir.cfg() host, id := ir.rec.Host, ir.src.GenerationOp m, waiting, err := ir.installManifest(ctx, obs) if err != nil || waiting { return "", restoreWaiting, err } want := "sha256:" + m.DBSHA256 dbPath := ir.dbPath() captured, err := ir.capture(*obs.Claim.Holder) if err != nil { return "", restoreWaiting, err } present, err := regularFile(dbPath) if err != nil { return "", restoreWaiting, err } built := filepath.Join(ir.buildDir(), host+".db") if !present { if ok, err := regularFile(built); err != nil { return "", restoreWaiting, err } else if !ok { // The scratch was lost (a rebuilt data directory): build again // from the pinned set, which reproduces the same bytes, under // the location's lease. if _, waiting, err := ir.rebuild(ctx, obs); err != nil || waiting { return "", restoreWaiting, err } } if err := ir.fault("before-db-verify"); err != nil { return "", restoreWaiting, err } got, err := fileSHA256(built) if err != nil { return "", restoreWaiting, err } if got != want { log.Printf("moves: %s: the built database is %s, the manifest says %s", host, got, want) return ir.refuseInstall(ctx) } if err := lease.CheckDB(built); err != nil { log.Printf("moves: %s: the built database does not pass its check: %v", host, err) return ir.refuseInstall(ctx) } for _, side := range []string{"-wal", "-shm"} { os.Remove(built + side) } } err = ir.guarded(ctx, captured, ir.src.Phases[4], func() error { // Every destination is Lstat-checked, never read through: a symlink // or any other non-regular file at .gen, DBPath or the WAL // paths, dangling or not, is not this install's and is never // replaced (final re-review residual). ReadGenerationMarker follows // a symlink and reads a dangling one as absent. if fi, err := os.Lstat(dbPath + ".gen"); err == nil && !fi.Mode().IsRegular() { return fmt.Errorf("%w: %s.gen is not a regular file (%s)", errLandingChanged, dbPath, fi.Mode()) } else if err != nil && !errors.Is(err, os.ErrNotExist) { return err } gen, err := hostbox.ReadGenerationMarker(dbPath + ".gen") if err != nil { return fmt.Errorf("read %s.gen: %w", dbPath, err) } if gen != "" && gen != id { return fmt.Errorf("%w: %s.gen names generation %s", errLandingChanged, dbPath, gen) } now, err := regularFile(dbPath) // Lstat: a non-regular DBPath is refused if err != nil { return fmt.Errorf("%w: %v", errLandingChanged, err) } switch { case now: // This landing's own install (a replay after the rename) is // the build's bytes; anything else is not to be replaced. got, err := fileSHA256(dbPath) if err != nil { return err } if got != want { return fmt.Errorf("%w: %s holds a database that is not this install's build (%s, want %s)", errLandingChanged, dbPath, got, want) } case present: return fmt.Errorf("%w: the database observed at %s is gone", errLandingChanged, dbPath) default: for _, side := range []string{"-wal", "-shm"} { if _, err := os.Lstat(dbPath + side); err == nil { return fmt.Errorf("%w: %s%s exists beside an absent database", errLandingChanged, dbPath, side) } else if !errors.Is(err, os.ErrNotExist) { return err } } // No seed marker may stand beside the database this landing // installs: it would name another seed's leftover for it // (re-review finding 8). if err := os.Remove(dbPath + activator.SeedMarkerSuffix); err != nil && !errors.Is(err, os.ErrNotExist) { return err } if err := ir.notePreInstall(); err != nil { return err } if err := os.Rename(built, dbPath); err != nil { return fmt.Errorf("install the database: %w", err) } if err := syncPath(filepath.Dir(dbPath)); err != nil { return err } } if err := ir.fault("after-db-rename"); err != nil { return err } if err := cfg.WriteGen(dbPath+".gen", id); err != nil { return fmt.Errorf("write the generation marker: %w", err) } return syncPath(filepath.Dir(dbPath)) }) if err != nil { return "", restoreWaiting, err } log.Printf("moves: %s: database installed at %s (%s), generation %s", host, dbPath, want, id) return want, restoreInstalled, nil } // preInstall is the landStepPreInstall record: which of the database's // unchecked siblings (installSiblingPaths) were absent, relative to the // data directory, when the install was about to put the database in place. // Only a sibling recorded absent can later count as this op's (the target's // cleaning, rollbackOwnership; the final re-review's residual): one already // there was not made by anything of this install's. type preInstall struct { Absent []string `json:"absent"` } // installSiblingPaths are the database's siblings the install does not // refuse beside an absent database (it refuses a WAL and shared memory): // the quarantine and the litestream shadow, relative to the data directory. func installSiblingPaths(host string) []string { db := host + ".db" return []string{db + ".corrupt", ".activator-" + db + "-litestream"} } // notePreInstall journals preInstall inside the database install's guard, // right before the rename: each sibling Lstat'ed, never followed. A replay // before the rename (a crash after the step) looks again and journals what // it sees, so the record is always the look that immediately preceded the // rename, under the same guard. A replay that finds the database already // installed never reaches here, so a step lost that way is missing, and its // siblings are never counted as the op's. func (ir *installRun) notePreInstall() error { j, err := ir.journal() if err != nil { return err } raw, replay := j.Steps[landStepPreInstall] rec := preInstall{Absent: []string{}} for _, p := range installSiblingPaths(ir.rec.Host) { _, err := os.Lstat(filepath.Join(ir.cfg().DataDir, p)) switch { case errors.Is(err, os.ErrNotExist): rec.Absent = append(rec.Absent, p) case err != nil: return err default: 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) } } body, err := json.Marshal(rec) if err != nil { return err } if replay && string(body) == raw { return nil } j.Mark(landStepPreInstall, string(body)) if err := j.Commit(ir.cfg().DataDir); err != nil { return err } return ir.fault("after-" + landStepPreInstall) } // refuseInstall writes the install's refusal for a build that does not // verify. A refusal that landed is restoreMarked; one refused (the record // moved on) is restoreWaiting, and nothing is installed either way. func (ir *installRun) refuseInstall(ctx context.Context) (string, restoreOutcome, error) { ok, err := ir.refuse(ctx, refuseInstallMismatch) if err != nil || !ok { return "", restoreWaiting, err } return "", restoreMarked, nil } // installManifest is the journal's, or the build's again when the journal // lost it. waiting is true when the rebuild could not take the lease. func (ir *installRun) installManifest(ctx context.Context, obs hostclaim.Observed) (moves.InstallManifest, bool, error) { j, err := ir.journal() if err != nil { return moves.InstallManifest{}, false, err } var m moves.InstallManifest 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 != "" { return m, false, nil } return ir.rebuild(ctx, obs) } // rebuild runs BuildGeneration again over the seal: a replay that reuses // every object it finds and restores the same bytes into the scratch. It // takes the location's lease first (published released it), runs every // generation request under it, checks it again before recording the result // and releases it after (fix round 1, item 3). waiting is true when another // holder has the lease; nothing is written then. func (ir *installRun) rebuild(ctx context.Context, obs hostclaim.Observed) (moves.InstallManifest, bool, error) { cfg := ir.cfg() seal, err := ir.seal(obs.Claim) if err != nil { return moves.InstallManifest{}, false, err } vs, err := ir.versioned(ctx) if err != nil { return moves.InstallManifest{}, false, err } holder := *obs.Claim.Holder held, waiting, err := ir.lease(ctx, holder) if waiting || err != nil { return moves.InstallManifest{}, waiting, err } defer ir.releaseLease() 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, GenerationPrefix: moves.GenerationPrefix(holder.Location, ir.src.GenerationOp), Op: ir.src.GenerationOp, Host: ir.rec.Host, WorkDir: ir.buildDir(), Now: cfg.Now}) if err != nil { return moves.InstallManifest{}, false, fmt.Errorf("rebuild the generation: %w", err) } if err := held.Check(ctx); err != nil { return moves.InstallManifest{}, false, fmt.Errorf("the location's lease after the rebuild: %w", err) } j, err := ir.journal() if err != nil { return m, false, err } body, err := json.Marshal(m) if err != nil { return m, false, err } j.Mark(landStepInstall, string(body)) return m, false, j.Commit(cfg.DataDir) } // writeFileAtomic writes body to path through a temporary file in the same // directory, synced and renamed, then syncs the directory. func writeFileAtomic(path string, body []byte, mode os.FileMode) error { dir := filepath.Dir(path) f, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*") if err != nil { return err } tmp := f.Name() if _, err := f.Write(body); err != nil { f.Close() os.Remove(tmp) return err } if err := f.Chmod(mode); err != nil { f.Close() os.Remove(tmp) return err } if err := f.Sync(); err != nil { f.Close() os.Remove(tmp) return err } if err := f.Close(); err != nil { os.Remove(tmp) return err } if err := os.Rename(tmp, path); err != nil { os.Remove(tmp) return err } return syncPath(dir) } // syncPath fsyncs a file or directory. func syncPath(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() return f.Sync() }