Hold a found directory, a found service's unit, a container that would mount found data, and a step run in a held container on an adopted node (hq ADR 0103)
This commit is contained in:
+270
-11
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/system"
|
||||
)
|
||||
|
||||
// Keep records the original of a file found on an adopted node, before anything else happens to
|
||||
@@ -45,14 +46,230 @@ func KeepIn(dir string) Keep {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 == ""
|
||||
// foundBefore is what an adopted apply finds on the machine before it changes anything: each
|
||||
// directory, service unit and container mount source of an untaken module that is present with no
|
||||
// record (novox/hq ADR 0103). Looked at first, because the apply itself makes such things — a
|
||||
// file's parent directory, a unit file a module writes, a package that brings its unit — and what
|
||||
// the mesh made in this apply was not found.
|
||||
type foundBefore map[string]bool
|
||||
|
||||
func lookBefore(ctx context.Context, sys system.System, d *declaration.Declaration, known store.State,
|
||||
run Runner) foundBefore {
|
||||
seen := foundBefore{}
|
||||
if d.Adoption == nil {
|
||||
return seen
|
||||
}
|
||||
return r.Kind() == declaration.TypeContainer
|
||||
cri, asked := "", false
|
||||
for _, r := range d.Resources {
|
||||
if _, untaken := d.Adoption.UntakenModuleOf(r.Identity()); !untaken {
|
||||
continue
|
||||
}
|
||||
if _, held := known.HeldAt(r.Identity()); held {
|
||||
continue
|
||||
}
|
||||
switch res := r.(type) {
|
||||
case *declaration.Directory:
|
||||
if present(res.Path) && !recordedPath(known, res.Path) {
|
||||
seen["path:"+res.Path] = true
|
||||
}
|
||||
case *declaration.Service:
|
||||
if known.Recorded(string(declaration.TypeService), res.Unit) {
|
||||
continue
|
||||
}
|
||||
// A unit the service manager cannot find is not there; one it can read is, whatever
|
||||
// state it is in.
|
||||
if _, err := sys.ServiceState(ctx, run, res.Unit); err == nil {
|
||||
seen["unit:"+res.Unit] = true
|
||||
}
|
||||
case *declaration.Container:
|
||||
if known.Recorded(string(declaration.TypeContainer), res.Name) {
|
||||
continue
|
||||
}
|
||||
for _, v := range res.Volumes {
|
||||
src := mountSource(v)
|
||||
switch {
|
||||
case src == "":
|
||||
case strings.HasPrefix(src, "/"):
|
||||
if present(src) && !recordedPath(known, src) {
|
||||
seen["path:"+src] = true
|
||||
}
|
||||
default:
|
||||
if !asked {
|
||||
cri, _ = containerRuntime(ctx, run)
|
||||
asked = true
|
||||
}
|
||||
if cri == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := run(ctx, cri, "volume", "inspect", src); err == nil {
|
||||
seen["volume:"+src] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
||||
func present(path string) bool {
|
||||
_, err := os.Lstat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// recordedPath is whether this host has a record of making something at a path.
|
||||
func recordedPath(known store.State, path string) bool {
|
||||
for _, kind := range []declaration.Type{declaration.TypeDirectory, declaration.TypeFile,
|
||||
declaration.TypeArchive, declaration.TypeAccess} {
|
||||
if known.Recorded(string(kind), path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mountSource is what a volume mapping mounts: a path on the machine, or a named volume. Empty for
|
||||
// an anonymous volume, which mounts nothing that could already be there.
|
||||
func mountSource(mapping string) string {
|
||||
src, _, ok := strings.Cut(mapping, ":")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
// runsIn is the container a resource runs inside, if any: an action's `in`, or a run-once step
|
||||
// sharing a container's namespace.
|
||||
func runsIn(r declaration.Resource) string {
|
||||
switch res := r.(type) {
|
||||
case *declaration.Action:
|
||||
return res.In
|
||||
case *declaration.Container:
|
||||
if res.RunOnce {
|
||||
if name, ok := strings.CutPrefix(res.Network, "container:"); ok {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// heldContainer is what is held under a container's name.
|
||||
func heldContainer(known store.State, name string) (store.Held, bool) {
|
||||
for _, h := range known.Held {
|
||||
if h.Kind == string(declaration.TypeContainer) && h.Target == name {
|
||||
return h, true
|
||||
}
|
||||
}
|
||||
return store.Held{}, false
|
||||
}
|
||||
|
||||
// holdOnAdopted decides whether a resource of an adopted node is held rather than applied, and
|
||||
// holds it (novox/hq ADR 0100, ADR 0103). For a module not yet taken, what is present with no
|
||||
// record is kept as it is: a file or a container under its name, a directory, a service's unit,
|
||||
// and a container that would mount a path or a volume found there. An action or a run-once step
|
||||
// run inside a held container is held with it. Once held, a resource stays held — changed or gone
|
||||
// — until its module is taken, and it is never recorded as applied, so never removed as an orphan.
|
||||
//
|
||||
// Held is false for a resource to apply as usual. News is whether the hold is new or changed,
|
||||
// which is what is worth a line in the log.
|
||||
func holdOnAdopted(ctx context.Context, sys system.System, r declaration.Resource, d *declaration.Declaration,
|
||||
known *store.State, before foundBefore, run Runner, keep Keep, changed map[string]bool,
|
||||
now time.Time) (held, news bool, out Outcome, err error) {
|
||||
was, already := known.HeldAt(r.Identity())
|
||||
|
||||
if in := runsIn(r); in != "" {
|
||||
if container, isHeld := heldContainer(*known, in); isHeld {
|
||||
module, untaken := d.Adoption.UntakenModuleOf(r.Identity())
|
||||
if !untaken {
|
||||
module = container.Module
|
||||
}
|
||||
h := was
|
||||
if !already {
|
||||
h = store.Held{ID: r.Identity(), Kind: string(r.Kind()), Target: r.Target(), Since: now}
|
||||
}
|
||||
h.Module, h.Why = module, "runs in "+in
|
||||
known.RecordHeld(h)
|
||||
out = begin(r)
|
||||
out.Action = "held"
|
||||
out.Detail = fmt.Sprintf("runs in %s, which is held as found; not run until %s is taken", in, module)
|
||||
return true, !already, out, nil
|
||||
}
|
||||
}
|
||||
|
||||
module, untaken := d.Adoption.UntakenModuleOf(r.Identity())
|
||||
if !untaken {
|
||||
return false, false, out, nil
|
||||
}
|
||||
why := was.Why
|
||||
isFound := already
|
||||
if !already {
|
||||
switch res := r.(type) {
|
||||
case *declaration.File:
|
||||
if res.Into == "" {
|
||||
if isFound, err = found(ctx, r, run, *known); err != nil {
|
||||
return false, false, begin(r), err
|
||||
}
|
||||
}
|
||||
case *declaration.Container:
|
||||
if known.Recorded(string(declaration.TypeContainer), res.Name) {
|
||||
break
|
||||
}
|
||||
_, exists, err := inspectFound(ctx, res.Name, run)
|
||||
if err != nil {
|
||||
return false, false, begin(r), err
|
||||
}
|
||||
if exists {
|
||||
if isFound, err = found(ctx, r, run, *known); err != nil {
|
||||
return false, false, begin(r), err
|
||||
}
|
||||
break
|
||||
}
|
||||
// Not there under its name, and still it would share what was found: created, it
|
||||
// would mount the predecessor's data beside the predecessor's own container.
|
||||
for _, v := range res.Volumes {
|
||||
src := mountSource(v)
|
||||
key := "volume:" + src
|
||||
if strings.HasPrefix(src, "/") {
|
||||
key = "path:" + src
|
||||
}
|
||||
if src != "" && before[key] {
|
||||
isFound, why = true, "would mount "+src+", found on the machine"
|
||||
break
|
||||
}
|
||||
}
|
||||
case *declaration.Directory:
|
||||
isFound = before["path:"+res.Path]
|
||||
case *declaration.Service:
|
||||
isFound = before["unit:"+res.Unit]
|
||||
}
|
||||
}
|
||||
if !isFound {
|
||||
return false, false, out, nil
|
||||
}
|
||||
|
||||
out, h, err := hold(ctx, sys, r, module, was, already, why, run, keep, now)
|
||||
if err != nil {
|
||||
return true, false, out, err
|
||||
}
|
||||
// A held service is not started, stopped, enabled or restarted — but a reload stops nothing,
|
||||
// so one the module names still happens (novox/hq ADR 0102, ADR 0103).
|
||||
if svc, ok := r.(*declaration.Service); ok && svc.State == "running" {
|
||||
if which := restartedBy(svc.ReloadOn, changed); len(which) > 0 {
|
||||
if state, err := sys.ServiceState(ctx, run, svc.Unit); err == nil && state == "running" {
|
||||
reloader, can := sys.(serviceReloader)
|
||||
if !can {
|
||||
return true, false, out, fmt.Errorf("%s must be reloaded for %s and this machine's "+
|
||||
"service manager cannot reload a unit", svc.Unit, strings.Join(which, ", "))
|
||||
}
|
||||
if err := reloader.ReloadService(ctx, run, svc.Unit); err != nil {
|
||||
return true, false, out, fmt.Errorf("reloading the held %s: %w", svc.Unit, err)
|
||||
}
|
||||
out.Detail += "; reloaded for " + strings.Join(which, ", ") + ", which stops nothing"
|
||||
}
|
||||
}
|
||||
}
|
||||
known.RecordHeld(h)
|
||||
return true, !already || h.Changed != was.Changed, out, nil
|
||||
}
|
||||
|
||||
// found is whether a declared file or container is present on the machine with no record of this
|
||||
@@ -111,15 +328,16 @@ func inspectFound(ctx context.Context, name string, run Runner) (foundContainer,
|
||||
// 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) {
|
||||
func hold(ctx context.Context, sys system.System, r declaration.Resource, module string, was store.Held,
|
||||
already bool, why string, 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}
|
||||
Target: r.Target(), Since: now, Why: why}
|
||||
}
|
||||
h.Module = module
|
||||
detail := "found on the machine; kept until " + module + " is taken"
|
||||
|
||||
var changed string
|
||||
switch res := r.(type) {
|
||||
@@ -159,7 +377,45 @@ func hold(ctx context.Context, r declaration.Resource, module string, was store.
|
||||
changed = "rewritten"
|
||||
}
|
||||
}
|
||||
case *declaration.Directory:
|
||||
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 held", res.Path)
|
||||
}
|
||||
changed = "gone"
|
||||
case err != nil:
|
||||
return out, h, err
|
||||
case !already:
|
||||
// Its mode and owner as found, which the mesh leaves: a database refuses to start
|
||||
// on a data directory whose mode changed.
|
||||
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)
|
||||
}
|
||||
}
|
||||
detail = "found on the machine; its mode, owner and contents kept until " + module + " is taken"
|
||||
case *declaration.Service:
|
||||
state, err := sys.ServiceState(ctx, run, res.Unit)
|
||||
switch {
|
||||
case err != nil && !already:
|
||||
return out, h, fmt.Errorf("the unit %s was found and cannot be read to hold it: %w", res.Unit, err)
|
||||
case err != nil:
|
||||
changed = "gone"
|
||||
case !already:
|
||||
h.Running = state == "running"
|
||||
case h.Running && state != "running":
|
||||
changed = "stopped"
|
||||
}
|
||||
detail = "its unit was found on the machine; its state and whether it starts at boot are " +
|
||||
"kept until " + module + " is taken"
|
||||
case *declaration.Container:
|
||||
if h.Why != "" && h.Container == "" {
|
||||
// Held for what it would mount, never created: there is nothing of it to compare.
|
||||
detail = "not created: it " + h.Why + "; kept until " + module + " is taken"
|
||||
break
|
||||
}
|
||||
seen, exists, err := inspectFound(ctx, res.Name, run)
|
||||
if err != nil {
|
||||
return out, h, err
|
||||
@@ -188,7 +444,7 @@ func hold(ctx context.Context, r declaration.Resource, module string, was store.
|
||||
}
|
||||
}
|
||||
out.Action = "held"
|
||||
out.Detail = "found on the machine; kept until " + module + " is taken"
|
||||
out.Detail = detail
|
||||
if h.Changed != "" {
|
||||
out.Detail += "; " + h.Changed + " by something other than the mesh since it was found, and not reverted"
|
||||
}
|
||||
@@ -197,6 +453,9 @@ func hold(ctx context.Context, r declaration.Resource, module string, was store.
|
||||
|
||||
// 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.Kind == string(declaration.TypeAction) || (h.Why != "" && h.Container == "") {
|
||||
return "taken: no longer held (" + h.Why + ")"
|
||||
}
|
||||
if h.Kept != "" {
|
||||
return "taken: replaced what was found; original kept at " + h.Kept
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user