package apply import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "os" "path/filepath" "strings" "syscall" "time" "github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/store" ) // Keep records the original of a file found on an adopted node, before anything else happens to // it, and says where (novox/hq ADR 0100). It never overwrites an original it already kept: the // first copy is the one that was there before the mesh. type Keep func(path string, content []byte, mode os.FileMode) (string, error) // KeepIn keeps originals under dir/kept, each named for the path it came from, readable by root // alone — a predecessor's configuration may carry its credentials. func KeepIn(dir string) Keep { return func(path string, content []byte, _ os.FileMode) (string, error) { sum := sha256.Sum256([]byte(path)) kept := filepath.Join(dir, "kept", hex.EncodeToString(sum[:])[:16]+"-"+filepath.Base(path)) if _, err := os.Lstat(kept); err == nil { return kept, nil } if err := os.MkdirAll(filepath.Dir(kept), 0o700); err != nil { return "", err } if err := writeAtomically(kept, content, 0o600); err != nil { return "", err } back, err := os.ReadFile(kept) if err != nil || string(back) != string(content) { return "", fmt.Errorf("kept the original of %s at %s and cannot read it back", path, kept) } return kept, nil } } // holdable is whether a resource is one a predecessor can already have on the machine: a file at // a path, or a container under a name. A file written into is not: it replaces nothing that was // found, only adds the mesh's keys beside it (novox/hq ADR 0102). func holdable(r declaration.Resource) bool { if f, ok := r.(*declaration.File); ok { return f.Into == "" } return r.Kind() == declaration.TypeContainer } // found is whether a declared file or container is present on the machine with no record of this // host making it (novox/hq ADR 0100). A container carrying the host's own spec label was made by // a host, whatever this store says, so it is never found. func found(ctx context.Context, r declaration.Resource, run Runner, known store.State) (bool, error) { if known.Recorded(string(r.Kind()), r.Target()) { return false, nil } switch res := r.(type) { case *declaration.File: _, err := os.Lstat(res.Path) if errors.Is(err, os.ErrNotExist) { return false, nil } return err == nil, err case *declaration.Container: seen, exists, err := inspectFound(ctx, res.Name, run) if err != nil || !exists { return false, err } return seen.spec == "", nil } return false, nil } type foundContainer struct { id string running bool spec string } // inspectFound reads a container by name the way a hold needs it: its id, whether it runs, and // whether a host made it. func inspectFound(ctx context.Context, name string, run Runner) (foundContainer, bool, error) { cri, err := containerRuntime(ctx, run) if err != nil { return foundContainer{}, false, fmt.Errorf("%w, so nothing can be said about %q", err, name) } out, err := run(ctx, cri, "inspect", "--format", "{{.Id}}\t{{.State.Running}}\t{{index .Config.Labels \""+specLabel+"\"}}", name) if err != nil { return foundContainer{}, false, nil } parts := strings.Split(strings.TrimSpace(out), "\t") for len(parts) < 3 { parts = append(parts, "") } spec := strings.TrimSpace(parts[2]) if spec == "" { spec = "" } return foundContainer{id: strings.TrimSpace(parts[0]), running: parts[1] == "true", spec: spec}, true, nil } // hold keeps a found file or container as it is, and reports it — the first time by recording // what was found, every time after by comparing against that. Nothing is reverted, restarted or // created: a held target that disappears stays held and gone until its module is taken. func hold(ctx context.Context, r declaration.Resource, module string, was store.Held, already bool, run Runner, keep Keep, now time.Time) (Outcome, store.Held, error) { out := begin(r) h := was if !already { h = store.Held{ID: r.Identity(), Module: module, Kind: string(r.Kind()), Target: r.Target(), Since: now} } h.Module = module var changed string switch res := r.(type) { case *declaration.File: info, err := os.Lstat(res.Path) switch { case errors.Is(err, os.ErrNotExist): if !already { return out, h, fmt.Errorf("%s was found and is gone before it could be kept", res.Path) } changed = "gone" case err != nil: return out, h, err default: content, err := os.ReadFile(res.Path) if err != nil { return out, h, fmt.Errorf("%s was found and cannot be read to keep it: %w", res.Path, err) } if !already { // The original first, before anything is recorded: a hold with no kept copy // would be a promise the host cannot keep. if keep == nil { return out, h, fmt.Errorf( "%s was found on this adopted node and this host has nowhere to keep its original", res.Path) } kept, err := keep(res.Path, content, info.Mode().Perm()) if err != nil { return out, h, fmt.Errorf("keeping the original of %s: %w", res.Path, err) } h.Kept = kept h.Digest = digestOf(string(content)) h.Mode = fmt.Sprintf("%04o", info.Mode().Perm()) if st, ok := info.Sys().(*syscall.Stat_t); ok { h.Owner = fmt.Sprintf("%d:%d", st.Uid, st.Gid) } } else if digestOf(string(content)) != h.Digest { changed = "rewritten" } } case *declaration.Container: seen, exists, err := inspectFound(ctx, res.Name, run) if err != nil { return out, h, err } switch { case !exists && !already: return out, h, fmt.Errorf("container %s was found and is gone before it could be held", res.Name) case !already: h.Container, h.Running = seen.id, seen.running case !exists: changed = "gone" case seen.id != h.Container: changed = "replaced" case h.Running && !seen.running: changed = "stopped" } default: return out, h, fmt.Errorf("a %s cannot be held", r.Kind()) } if changed != h.Changed { h.Changed = changed h.ChangedAt = now if changed == "" { h.ChangedAt = time.Time{} } } out.Action = "held" out.Detail = "found on the machine; kept until " + module + " is taken" if h.Changed != "" { out.Detail += "; " + h.Changed + " by something other than the mesh since it was found, and not reverted" } return out, h, nil } // takenDetail is what an outcome says when a module's cutover replaced what was held for it. func takenDetail(h store.Held) string { if h.Kept != "" { return "taken: replaced what was found; original kept at " + h.Kept } return "taken: replaced what was found" }