The node-engine reads a resource's module and a declaration's builds, says it does in every report (reads_modules), groups the apply's outcome per module with every failing resource named, and writes have.<node>.<module> and have.<node>.node to the bucket only when a value differs, never ahead of or in place of the report (novox/hq ADR 0429 section 4).
372 lines
11 KiB
Go
372 lines
11 KiB
Go
package apply
|
|
|
|
// A directory or archive that says unknown "move-out" keeps only what it knows (issue 1024, novox/hq ADR 0422).
|
|
//
|
|
// Everything inside it that nothing declared, no archive unpacked and `knows` does not name is moved, whole, into
|
|
// `<home>/.local/state/mesh-moved/<module>/<UTC time>/`, with a list beside it that names each move before it is
|
|
// made (ADR 0417 §7). Nothing is deleted: the owner of the path reacts to a file it did not write by keeping it
|
|
// where its writer can find it. The pass runs after the resources of an apply, so a reconcile (every five minutes)
|
|
// runs it too.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// movedOutWait is how old a path is before it is moved: a younger one may be mid-write by whatever made it, and
|
|
// waits for the next look (ADR 0417 §4).
|
|
const movedOutWait = time.Minute
|
|
|
|
// MovedOut is one path a move-out directory moved.
|
|
type MovedOut struct {
|
|
Resource string `json:"resource"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// LeftInPlace is a socket, pipe or device a move-out directory did not move (ADR 0422 §4).
|
|
type LeftInPlace struct {
|
|
Resource string `json:"resource"`
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
// renamePath and deviceOf are the machine's calls, variables so a test can fail a move or name another file system.
|
|
// renamePath renames from to to without following a link in either parent below home (move_out_linux.go).
|
|
var renamePath = renameUnder
|
|
|
|
type movedEntry struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
}
|
|
|
|
// moveOutOwner is the user and group the kept folders and the list are given: the declared owner, or the home's
|
|
// account.
|
|
func moveOutOwner(home, owner string) (uid, gid int, err error) {
|
|
if owner != "" {
|
|
return idsOf(owner)
|
|
}
|
|
acc, ok := accountsOfHomes()[home]
|
|
if !ok {
|
|
return 0, 0, fmt.Errorf("%s is not the home of an account", home)
|
|
}
|
|
if u, g, err := idsOf(acc.Name); err == nil {
|
|
return u, g, nil
|
|
}
|
|
return acc.UID, acc.UID, nil
|
|
}
|
|
|
|
// knownUnder is what a directory knows of its own contents: the declared targets, the files an archive unpacked,
|
|
// `knows` entries, and every folder above one of them.
|
|
type knownUnder struct {
|
|
exact map[string]bool
|
|
prefixes []string
|
|
// own are nested directories and archives that say what they do with unknown paths themselves.
|
|
own map[string]bool
|
|
}
|
|
|
|
func (k *knownUnder) add(root, path string) {
|
|
path = filepath.Clean(path)
|
|
for path != root && strings.HasPrefix(path, root+string(os.PathSeparator)) {
|
|
k.exact[path] = true
|
|
path = filepath.Dir(path)
|
|
}
|
|
}
|
|
|
|
func (k *knownUnder) knows(path string) bool {
|
|
if k.exact[path] {
|
|
return true
|
|
}
|
|
for _, p := range k.prefixes {
|
|
if path == p || strings.HasPrefix(path, p+string(os.PathSeparator)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func knownOf(d *declaration.Declaration, known store.State, id, root string, knows []string) *knownUnder {
|
|
k := &knownUnder{exact: map[string]bool{}, own: map[string]bool{}}
|
|
for _, r := range d.Resources {
|
|
target := filepath.Clean(r.Target())
|
|
if target == "" || target == "." {
|
|
continue
|
|
}
|
|
if r.Identity() != id && strings.HasPrefix(target, root+string(os.PathSeparator)) {
|
|
k.add(root, target)
|
|
switch o := r.(type) {
|
|
case *declaration.Directory:
|
|
if o.Unknown != "" {
|
|
k.own[target] = true
|
|
}
|
|
case *declaration.Archive:
|
|
if o.Unknown != "" {
|
|
k.own[target] = true
|
|
}
|
|
}
|
|
}
|
|
if a, ok := r.(*declaration.Archive); ok {
|
|
if rec, found := known.Find(a.ID); found && rec.Unpacked != nil {
|
|
base := filepath.Clean(a.Path)
|
|
for _, f := range append(append([]string{}, rec.Unpacked.Files...), rec.Unpacked.Dirs...) {
|
|
p := filepath.Join(base, filepath.FromSlash(f))
|
|
if p == root || strings.HasPrefix(p, root+string(os.PathSeparator)) {
|
|
k.add(root, p)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, e := range knows {
|
|
p := filepath.Join(root, filepath.FromSlash(strings.TrimSuffix(e, "/")))
|
|
if strings.HasSuffix(e, "/") {
|
|
k.prefixes = append(k.prefixes, p)
|
|
} else {
|
|
k.exact[p] = true
|
|
}
|
|
k.add(root, p)
|
|
}
|
|
return k
|
|
}
|
|
|
|
// moveOut moves what the directory at dir does not know, for the resource id. It answers the paths moved.
|
|
func moveOut(d *declaration.Declaration, known store.State, id, module, dir, owner string, knows []string,
|
|
log func(string)) ([]MovedOut, []LeftInPlace, error) {
|
|
dir = filepath.Clean(dir)
|
|
home := homeAbove(dir)
|
|
if home == "" {
|
|
return nil, nil, fmt.Errorf("%s: move-out is only for a directory under an account's home, and %s is under none "+
|
|
"(ADR 0422 §5); nothing was moved", id, dir)
|
|
}
|
|
k := knownOf(d, known, id, dir, knows)
|
|
skip := filepath.Join(keptRoot(home), module)
|
|
var candidates []string
|
|
var left []string
|
|
err := filepath.WalkDir(dir, func(path string, e fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
if path == dir {
|
|
return nil
|
|
}
|
|
if path == skip || path == keptRoot(home) {
|
|
return fs.SkipDir
|
|
}
|
|
if k.own[path] {
|
|
if e.IsDir() {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if k.knows(path) {
|
|
if e.IsDir() && !k.exact[path] {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
// Unknown. Moved whole, so never descended into.
|
|
info, err := e.Info()
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
switch {
|
|
case info.Mode()&(os.ModeSocket|os.ModeNamedPipe|os.ModeDevice|os.ModeCharDevice) != 0:
|
|
left = append(left, path)
|
|
case time.Since(info.ModTime()) < movedOutWait:
|
|
default:
|
|
candidates = append(candidates, path)
|
|
}
|
|
if e.IsDir() {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s: cannot look through %s: %w", id, dir, err)
|
|
}
|
|
for _, p := range left {
|
|
log(fmt.Sprintf(" left %s in %s: a socket, pipe or device is not moved (ADR 0422)", p, dir))
|
|
}
|
|
var stayed []LeftInPlace
|
|
for _, p := range left {
|
|
stayed = append(stayed, LeftInPlace{Resource: id, Path: p})
|
|
}
|
|
if len(candidates) == 0 {
|
|
return nil, stayed, nil
|
|
}
|
|
sort.Strings(candidates)
|
|
|
|
uid, gid, err := moveOutOwner(home, owner)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%s: %w", id, err)
|
|
}
|
|
|
|
// The same file system first, so a move is a rename and nothing is copied (ADR 0417 §5).
|
|
ancestor := skip
|
|
for {
|
|
if _, err := os.Lstat(ancestor); err == nil {
|
|
break
|
|
}
|
|
ancestor = filepath.Dir(ancestor)
|
|
}
|
|
devDir, err1 := deviceOf(dir)
|
|
devKept, err2 := deviceOf(ancestor)
|
|
if err1 != nil || err2 != nil {
|
|
return nil, nil, fmt.Errorf("%s: cannot read the file system of %s or %s: %v %v", id, dir, ancestor, err1, err2)
|
|
}
|
|
if devDir != devKept {
|
|
return nil, nil, fmt.Errorf("%s: %s and %s are on different file systems (devices %d and %d), so a move would "+
|
|
"copy; nothing was moved (ADR 0417 §5)", id, dir, ancestor, devDir, devKept)
|
|
}
|
|
|
|
// The kept folder and the list, named by the second; a clash in the same second gets -2, -3.
|
|
stamp := time.Now().UTC().Format("20060102T150405Z")
|
|
kept, listPath := "", ""
|
|
for n := 1; ; n++ {
|
|
name := stamp
|
|
if n > 1 {
|
|
name = fmt.Sprintf("%s-%d", stamp, n)
|
|
}
|
|
kept, listPath = filepath.Join(skip, name), filepath.Join(skip, name+".json")
|
|
if !pathExists(kept) && !pathExists(listPath) {
|
|
break
|
|
}
|
|
}
|
|
var made []string
|
|
cleanup := func() {
|
|
_ = os.Remove(listPath)
|
|
// In the reverse of the order they were made, so a folder goes after what is in it.
|
|
for i := len(made) - 1; i >= 0; i-- {
|
|
_ = os.Remove(made[i])
|
|
}
|
|
}
|
|
chown := func(paths []string) error {
|
|
for _, p := range paths {
|
|
if err := chownIfNeeded(home, p, uid, gid); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
madeNow, err := mkdirAllUnder(home, kept, 0o700)
|
|
made = append(made, reversed(madeNow)...)
|
|
if err == nil {
|
|
err = chown(madeNow)
|
|
}
|
|
if err != nil {
|
|
cleanup()
|
|
return nil, nil, fmt.Errorf("%s: cannot make the kept folder %s: %w", id, kept, err)
|
|
}
|
|
|
|
var list []movedEntry
|
|
var moved []MovedOut
|
|
var failures []string
|
|
for _, from := range candidates {
|
|
rel, _ := filepath.Rel(dir, from)
|
|
to := filepath.Join(kept, rel)
|
|
// Named before it is moved: a move that stops half way leaves its entry (ADR 0417 §7).
|
|
list = append(list, movedEntry{From: from, To: to})
|
|
raw, _ := json.MarshalIndent(list, "", " ")
|
|
if err := writeUnder(home, listPath, append(raw, '\n'), 0o600); err != nil {
|
|
failures = append(failures, fmt.Sprintf("cannot write the list %s: %v", listPath, err))
|
|
break
|
|
}
|
|
if err := chownIfNeeded(home, listPath, uid, gid); err != nil {
|
|
failures = append(failures, err.Error())
|
|
break
|
|
}
|
|
parents, err := mkdirAllUnder(home, filepath.Dir(to), 0o700)
|
|
made = append(made, reversed(parents)...)
|
|
if err == nil {
|
|
err = chown(parents)
|
|
}
|
|
if err == nil {
|
|
err = renamePath(home, from, to)
|
|
}
|
|
if err != nil {
|
|
failures = append(failures, fmt.Sprintf("cannot move %s to %s: %v", from, to, err))
|
|
continue
|
|
}
|
|
log(fmt.Sprintf(" moved %s to %s (ADR 0422)", from, to))
|
|
moved = append(moved, MovedOut{Resource: id, From: from, To: to})
|
|
}
|
|
if len(moved) == 0 {
|
|
cleanup()
|
|
}
|
|
if len(failures) > 0 {
|
|
return moved, stayed, fmt.Errorf("%s: %s", id, strings.Join(failures, "; "))
|
|
}
|
|
return moved, stayed, nil
|
|
}
|
|
|
|
// reversed is paths shallowest first: mkdirAllUnder answers deepest first.
|
|
func reversed(in []string) []string {
|
|
out := make([]string, len(in))
|
|
for i, p := range in {
|
|
out[len(in)-1-i] = p
|
|
}
|
|
return out
|
|
}
|
|
|
|
func keptRoot(home string) string { return filepath.Join(home, ".local", "state", "mesh-moved") }
|
|
|
|
// chownIfNeeded gives a path to uid and gid through the home-safe call, unless it is theirs already.
|
|
func chownIfNeeded(home, path string, uid, gid int) error {
|
|
if u, g, ok := ownerIDs(path); ok && u == uid && g == gid {
|
|
return nil
|
|
}
|
|
return chownUnder(home, path, uid, gid)
|
|
}
|
|
|
|
// moveOutPass runs move-out for every directory and archive of d that says so and applied in this run.
|
|
func moveOutPass(d *declaration.Declaration, known store.State, applied map[string]bool, log func(string)) (
|
|
moved []MovedOut, left []LeftInPlace, failures []*Error) {
|
|
for _, r := range d.Resources {
|
|
var unknown, owner string
|
|
var knows []string
|
|
switch o := r.(type) {
|
|
case *declaration.Directory:
|
|
unknown, owner, knows = o.Unknown, o.Owner, o.Knows
|
|
case *declaration.Archive:
|
|
unknown, owner, knows = o.Unknown, o.Owner, o.Knows
|
|
default:
|
|
continue
|
|
}
|
|
if unknown != declaration.UnknownMoveOut || !applied[r.Identity()] {
|
|
continue
|
|
}
|
|
module, _ := d.ModuleOfID(r.Identity())
|
|
if module == "" {
|
|
module = r.Identity()
|
|
}
|
|
m, l, err := moveOut(d, known, r.Identity(), module, r.Target(), owner, knows, log)
|
|
moved = append(moved, m...)
|
|
left = append(left, l...)
|
|
if err != nil {
|
|
log(fmt.Sprintf(" failed %s (%s): %v", r.Identity(), r.Target(), err))
|
|
failures = append(failures, &Error{Resource: r.Identity(), Err: err})
|
|
}
|
|
}
|
|
return moved, left, failures
|
|
}
|
|
|
|
func pathExists(path string) bool {
|
|
_, err := os.Lstat(path)
|
|
return err == nil
|
|
}
|