mesh/merge-gate head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: [release feat/57-config-and-provisioning, judg…
mesh/delivery stopped: its walk plan-1792114347281764422 was stopped by a person (mesh-controller): mesh-host#114 merged into the release branch feat/57-…
mesh/delivery-group group 57-config-and-provisioning stopped: a member was stopped
The node-engine renders the feature's templates, collections and moved providers' files, on the SDK the feature's modules build on (feature 57, config and provisioning).
231 lines
9.5 KiB
Go
231 lines
9.5 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// WriteFilled writes what the provisioner's client filled into one file the declaration names as filled (novox/hq
|
|
// ADR 0349 §4), and makes what reads the file take the change, exactly as an apply that changed it would.
|
|
//
|
|
// **The declared file says everything but the content.** Its path, mode, owner and who owns it at runtime are the
|
|
// declaration's; the client's request carries the content alone. So the file is applied as the declared one with
|
|
// that content in place of the declaration's: an owned file whole through applyFile, a seed only when absent, a
|
|
// managed file's keys through applyInto or its block through applyBlock — the same guards on where nothing is
|
|
// placed, the same Keep of an original found there, the same record. What the declaration seals or fills with
|
|
// `${secret:}` the client has already filled, so the content is written as it came; a file that carried a secret
|
|
// keeps the mode a secret is written with when the declaration gives none.
|
|
//
|
|
// **Then what reads it, in the apply's order** (afterWhatTheyRead): a service naming it under restart-on is
|
|
// restarted and under reload-on reloaded, a process or container naming it under restart-on is restarted, and a
|
|
// running container that reads it at creation — an env-file, a file mounted into it — is recreated, because what
|
|
// it read is recorded by digest and this write changed the digest. Each of those is applied and recorded as an
|
|
// apply does, and what moves goes on moving what names it. Nothing else of the declaration is touched.
|
|
//
|
|
// The caller holds the apply's lock: a write and an apply never write one file at once. log is said the path, the
|
|
// action and the digest, never the content.
|
|
func WriteFilled(
|
|
ctx context.Context,
|
|
sys system.System,
|
|
d *declaration.Declaration,
|
|
known store.State,
|
|
path string,
|
|
content []byte,
|
|
run Runner,
|
|
log func(string),
|
|
keep Keep,
|
|
windows *Windows,
|
|
) (report Report, _ store.State, _ error) {
|
|
if log == nil {
|
|
log = func(string) {}
|
|
}
|
|
if !d.Fills(path) {
|
|
return report, known, &Error{Resource: path, Err: fmt.Errorf("this node's declaration does not name %s "+
|
|
"as a file the provisioner fills, so nothing was written", path)}
|
|
}
|
|
var declared []*declaration.File
|
|
for _, r := range d.Resources {
|
|
if f, ok := r.(*declaration.File); ok && f.Path == path {
|
|
declared = append(declared, f)
|
|
}
|
|
}
|
|
if len(declared) != 1 {
|
|
// The parser refuses a filled path no file declares; two at one path would need the request to say whose
|
|
// keys or block it carries, and it carries the content only.
|
|
return report, known, &Error{Resource: path, Err: fmt.Errorf("%d resources declare %s, and a filled file "+
|
|
"is written for exactly one, so nothing was written", len(declared), path)}
|
|
}
|
|
file := filledAs(declared[0], content)
|
|
if held, isHeld := known.HeldAt(file.ID); isHeld {
|
|
return report, known, &Error{Resource: file.ID, Err: fmt.Errorf("%s is held as found for a module not "+
|
|
"yet taken (%s), so nothing was written", path, held.Why)}
|
|
}
|
|
|
|
in := inputs{declares: map[string]string{}, known: &known, windows: windows, away: newStoreAway(windows, log),
|
|
groups: groupsWanted(d.Resources), handed: Handed{}}
|
|
refreshRuntimeRoots(ctx)
|
|
in.refused = refusedPlaces(d.Resources)
|
|
in.agentHomes = rootNeverHomes(d)
|
|
in.dirsBefore = map[string]bool{}
|
|
// A container that reads the file and mounts a library whose disk or mount is not there is held, as a full apply
|
|
// holds it: not created over the missing disk (novox/hq ADR 0328 §9).
|
|
in.held = heldLibraries(ctx, d.Libraries)
|
|
for _, r := range d.Resources {
|
|
in.declares[r.Identity()] = declaredDigest(r)
|
|
}
|
|
changed := map[string]bool{}
|
|
|
|
// The file, as the apply loop applies it.
|
|
was, _ := known.Find(file.ID)
|
|
var keepFound Keep
|
|
if (was.ID == "" || was.Target != file.Path) && !known.Recorded(string(declaration.TypeFile), file.Path) {
|
|
keepFound = keep
|
|
}
|
|
outcome, err := applyOne(ctx, sys, file, run, changed, in, was, nil, keepFound)
|
|
if err != nil {
|
|
log(fmt.Sprintf(" failed %s (%s): %v", file.ID, file.Path, err))
|
|
return report, known, &Error{Resource: file.ID, Err: err, Done: report}
|
|
}
|
|
recordKeeping(&known, file, outcome, was)
|
|
report.Outcomes = append(report.Outcomes, outcome)
|
|
if !moved(outcome) {
|
|
return report, known, nil
|
|
}
|
|
changed[file.ID] = true
|
|
line := fmt.Sprintf(" %s %s (%s), filled: %s", outcome.Action, file.ID, file.Path, outcome.wrote)
|
|
if outcome.Detail != "" {
|
|
line += ": " + outcome.Detail
|
|
}
|
|
log(line)
|
|
|
|
// What reads it, in the apply's order, each moving what names it in turn.
|
|
var failures []*Error
|
|
for _, r := range afterWhatTheyRead(d.Resources) {
|
|
if r.Identity() == file.ID || !readsChange(r, path, changed) {
|
|
continue
|
|
}
|
|
if _, isHeld := known.HeldAt(r.Identity()); isHeld {
|
|
log(fmt.Sprintf(" held %s (%s): held as found for a module not yet taken, so it was not restarted "+
|
|
"for %s", r.Identity(), r.Target(), file.ID))
|
|
continue
|
|
}
|
|
was, _ := known.Find(r.Identity())
|
|
o, err := applyOne(ctx, sys, r, run, changed, in, was, nil, nil)
|
|
if err != nil {
|
|
if o.found != nil {
|
|
known.KeepFound(r.Identity(), store.OriginDeclared, *o.found)
|
|
}
|
|
failures = append(failures, &Error{Resource: r.Identity(), Err: err})
|
|
log(fmt.Sprintf(" failed %s (%s): %v", r.Identity(), o.Target, err))
|
|
continue
|
|
}
|
|
if o.Action == "waiting" {
|
|
if o.found != nil {
|
|
known.KeepFound(r.Identity(), store.OriginDeclared, *o.found)
|
|
}
|
|
report.Outcomes = append(report.Outcomes, o)
|
|
log(fmt.Sprintf(" waiting %s (%s): %s", o.ID, o.Target, o.Detail))
|
|
continue
|
|
}
|
|
recordKeeping(&known, r, o, was)
|
|
report.Outcomes = append(report.Outcomes, o)
|
|
if o.busy != nil {
|
|
report.Busy = append(report.Busy, *o.busy)
|
|
}
|
|
if moved(o) {
|
|
changed[r.Identity()] = true
|
|
line := fmt.Sprintf(" %s %s (%s)", o.Action, o.ID, o.Target)
|
|
if o.Detail != "" {
|
|
line += ": " + o.Detail
|
|
}
|
|
log(line)
|
|
} else if o.Action != "unchanged" {
|
|
log(fmt.Sprintf(" %s %s (%s): %s", o.Action, o.ID, o.Target, o.Detail))
|
|
}
|
|
}
|
|
if len(failures) > 0 {
|
|
first := failures[0]
|
|
first.Done = report
|
|
first.Others = len(failures) - 1
|
|
return report, known, first
|
|
}
|
|
return report, known, nil
|
|
}
|
|
|
|
// filledAs is the declared file with the filled content in place of the declaration's.
|
|
func filledAs(declared *declaration.File, content []byte) *declaration.File {
|
|
f := *declared
|
|
f.Content, f.Sealed, f.VaultSealed, f.Bytes, f.Secrets = string(content), "", nil, "", nil
|
|
if f.Mode == "" && (declared.Secret() || len(declared.Secrets) > 0) {
|
|
// Written as the declaration would have written it: a file carrying a credential is not world-readable.
|
|
f.Mode = "0600"
|
|
}
|
|
return &f
|
|
}
|
|
|
|
// readsChange reports whether applying r is what makes it take a change of what changed: a service's restart-on or
|
|
// reload-on, a process's or container's restart-on, or a running container that reads path at creation.
|
|
func readsChange(r declaration.Resource, path string, changed map[string]bool) bool {
|
|
switch res := r.(type) {
|
|
case *declaration.Service:
|
|
return len(restartedBy(res.RestartOn, changed)) > 0 || len(restartedBy(res.ReloadOn, changed)) > 0
|
|
case *declaration.Process:
|
|
return len(restartedBy(res.RestartOn, changed)) > 0
|
|
case *declaration.Container:
|
|
if len(restartedBy(res.RestartOn, changed)) > 0 {
|
|
return true
|
|
}
|
|
if res.RunOnce || res.Schedule != "" {
|
|
return false
|
|
}
|
|
if slices.Contains(res.EnvFile, path) {
|
|
return true
|
|
}
|
|
for _, v := range res.Volumes {
|
|
if mountSource(v) == path {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// recordKeeping records r as applied, carrying where the original it replaced was kept from its record when this
|
|
// write kept none — for as long as the resource is recorded at the same target, as the apply loop carries it.
|
|
func recordKeeping(known *store.State, r declaration.Resource, o Outcome, was store.Applied) {
|
|
kept, keptMode, keptOwner := o.kept, o.keptMode, o.keptOwner
|
|
if kept == "" && was.Target == o.Target {
|
|
kept, keptMode, keptOwner = was.Kept, was.KeptMode, was.KeptOwner
|
|
}
|
|
recordApplied(known, store.OriginDeclared, r, o, kept, keptMode, keptOwner)
|
|
}
|
|
|
|
// leftToTheClient is a filled file as an apply leaves it (novox/hq ADR 0349 §5, design 57 P7): untouched, and said
|
|
// so. A file the mesh wrote at the same path under another id, going in this apply, is handed to it as an apply hands
|
|
// a whole file (novox/hq ADR 0223): its record — what the mesh wrote there, and the original it kept — is the filled
|
|
// file's now, so the verb judges what is there against it, and the file is neither removed nor written.
|
|
func leftToTheClient(f *declaration.File, known *store.State, handedFiles map[string]store.Applied, report *Report,
|
|
log func(string)) Outcome {
|
|
if from, handed := handedFiles[f.ID]; handed {
|
|
if _, recorded := known.Find(f.ID); !recorded {
|
|
rec := from
|
|
rec.ID = f.ID
|
|
known.Record(rec)
|
|
}
|
|
delete(handedFiles, f.ID)
|
|
known.Forget(from.ID)
|
|
detail := "no longer declared; " + f.ID + " declares the file now, filled by the provisioner's client, so " +
|
|
"it was handed to it, not removed"
|
|
report.Outcomes = append(report.Outcomes, Outcome{ID: from.ID, Type: from.Type, Target: from.Target,
|
|
Action: "forgotten", Detail: detail})
|
|
log(fmt.Sprintf(" forgotten %s (%s): %s", from.ID, from.Target, detail))
|
|
}
|
|
return Outcome{ID: f.ID, Type: string(declaration.TypeFile), Target: f.Path, Action: "unchanged",
|
|
Detail: "filled by the provisioner's client, which alone has it written"}
|
|
}
|