mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/233-a-failed-probe-fails-only-what-needs-the-tool-2 delivered: every member is delivered
155 lines
6.6 KiB
Go
155 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os/exec"
|
|
"sort"
|
|
|
|
"github.com/novox/mesh-host/internal/apply"
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// packageManagers are the tools a system's probe asks, for a system whose probe is its package manager.
|
|
var packageManagers = map[string]string{"arch": "pacman", "alpine": "apk"}
|
|
|
|
// unanswered is a probe of the package manager that failed before an apply: which tool, and its words.
|
|
type unanswered struct {
|
|
tool string
|
|
words string
|
|
}
|
|
|
|
// failedKey is how a report names the probe among what failed: one key, the same on every report, so the
|
|
// mesh counts a package manager that keeps not answering as one failure that goes on.
|
|
func (u *unanswered) failedKey() string { return "the package manager (" + u.tool + ")" }
|
|
|
|
// said is the line the apply says before it begins.
|
|
func (u *unanswered) said() string {
|
|
return u.tool + " does not answer here, so every package in this declaration fails and the rest of it is " +
|
|
"applied (novox/hq issue 233): " + u.words
|
|
}
|
|
|
|
// partial is the failure of an apply that applied everything else, when no package was declared to fail.
|
|
func (u *unanswered) partial() error {
|
|
return errors.New("applied only in part: " + u.words + "\n\nevery resource that does not need " + u.tool +
|
|
" was attempted; none here needed it, and " + u.tool + " must answer before this machine applies whole " +
|
|
"(novox/hq issue 233)")
|
|
}
|
|
|
|
// probePackageManager proves the machine is the system the host was built for, before anything is applied.
|
|
//
|
|
// **A package manager that does not answer fails only what needs it** (novox/hq issue 233). The probe was a
|
|
// refusal of the whole declaration — right for a host installed on a machine it was not built for, and a
|
|
// wedge when what the probe reads is the mesh's own: a declaration that left the package manager's module
|
|
// out removed its configuration file, and from then on every declaration was refused, the one that would
|
|
// write the file back and the node-engine's own upgrade included. A person on the machine was the only way
|
|
// back. Now the rest applies — files, units, accounts, the node-engine itself — and each package fails with
|
|
// the probe's words (the system's PackageInstalled asks the same probe first), so a later declaration can
|
|
// undo what an earlier one did. Before the rest is applied, a declaration composed from an older view of the
|
|
// assignments than this machine applied has already been refused (issue 234's generation).
|
|
//
|
|
// **A package manager that is not there at all still refuses whole.** That is the host installed on a
|
|
// machine it was not built for — the arch host on Alpine — which is what the probe exists to catch, and no
|
|
// declaration the mesh sends removes a package manager's program. Only one that is there and errors (a
|
|
// configuration file gone, a database locked) is the mesh's own fault and applies in part.
|
|
//
|
|
// A system with no package manager (android) still refuses whole: what its probe asks is the system itself,
|
|
// which no resource the mesh declares can remove, and nothing else stands between it and the wrong machine.
|
|
func probePackageManager(ctx context.Context, sys system.System) (*unanswered, error) {
|
|
err := sys.Confirm(ctx, apply.ExecRunner)
|
|
if err == nil {
|
|
return nil, nil
|
|
}
|
|
tool, ok := packageManagers[sys.Name()]
|
|
if !ok || errors.Is(err, exec.ErrNotFound) {
|
|
return nil, err
|
|
}
|
|
return &unanswered{tool: tool, words: err.Error()}, nil
|
|
}
|
|
|
|
// applyPass is one apply of the declaration over what the node's state says.
|
|
type applyPass func(known store.State) (apply.Report, store.State, error)
|
|
|
|
// applyMindingProbe applies, and **once more when the package manager did not answer and this apply wrote
|
|
// something** (novox/hq issue 233). What it wrote may be what the package manager needed — the declaration
|
|
// that names its module again writes its configuration file back — and then the packages go in now rather
|
|
// than at the next reconcile. Asked again only then: an apply that wrote nothing changed nothing the package
|
|
// manager reads. The second pass is the same declaration over the state the first left, so what the first
|
|
// applied reads unchanged and only what needs the package manager moves.
|
|
//
|
|
// It returns the last pass's account, the probe as it stands after it (nil once the package manager
|
|
// answers), and the first pass's outcomes when a second ran, for whoever counts what an apply moved.
|
|
func applyMindingProbe(ctx context.Context, sys system.System, probe *unanswered, known store.State,
|
|
pass applyPass, say func(string)) (apply.Report, store.State, error, *unanswered, []apply.Outcome) {
|
|
report, updated, err := pass(known)
|
|
if probe == nil || !wrote(report, err) {
|
|
return report, updated, err, probe, nil
|
|
}
|
|
again, refused := probePackageManager(ctx, sys)
|
|
if refused != nil || again != nil {
|
|
return report, updated, err, probe, nil
|
|
}
|
|
say(probe.tool + " answers now that this apply wrote what it reads; applying again, so its packages are " +
|
|
"installed in this apply (novox/hq issue 233)")
|
|
first := append([]apply.Outcome(nil), report.Outcomes...)
|
|
var partial *apply.Error
|
|
if errors.As(err, &partial) {
|
|
first = append(first, partial.Done.Outcomes...)
|
|
}
|
|
report, updated, err = pass(updated)
|
|
return report, updated, err, nil, first
|
|
}
|
|
|
|
// wrote is whether an apply moved anything on the machine, failed or not.
|
|
func wrote(report apply.Report, err error) bool {
|
|
if report.Changed() {
|
|
return true
|
|
}
|
|
var partial *apply.Error
|
|
return errors.As(err, &partial) && partial.Done.Changed()
|
|
}
|
|
|
|
// unapplied is which declared resources an apply did not apply, failed or not attempted, sorted; nil for a
|
|
// clean one.
|
|
func unapplied(d *declaration.Declaration, report apply.Report, err error) []string {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
done := map[string]bool{}
|
|
for _, o := range report.Outcomes {
|
|
done[o.ID] = o.Action != "skipped"
|
|
}
|
|
var partial *apply.Error
|
|
if errors.As(err, &partial) {
|
|
for _, o := range partial.Done.Outcomes {
|
|
done[o.ID] = done[o.ID] || o.Action != "skipped"
|
|
}
|
|
}
|
|
var out []string
|
|
for _, r := range d.Resources {
|
|
if !done[r.Identity()] {
|
|
out = append(out, r.Identity())
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
if len(out) == 0 {
|
|
// Failed, and every resource has an account: what failed is no resource (the firewall found, a removal).
|
|
out = []string{"(apply)"}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// withUnanswered adds the probe to what a report says failed.
|
|
func withUnanswered(failed map[string]string, u *unanswered) map[string]string {
|
|
if u == nil {
|
|
return failed
|
|
}
|
|
if failed == nil {
|
|
failed = map[string]string{}
|
|
}
|
|
failed[u.failedKey()] = u.said()
|
|
return failed
|
|
}
|