Keep what an adopted node was found holding until its module is taken, and report it held (hq ADR 0100)
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
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.
|
||||
func holdable(r declaration.Resource) bool {
|
||||
return r.Kind() == declaration.TypeFile || 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 == "<no value>" {
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user