Merge pull request 'Start again on removal what the mesh's own declaration stopped (hq ADR 0269)' (#66) from feat/give-back-a-unit-the-mesh-stopped into main
This commit was merged in pull request #66.
This commit is contained in:
+94
-4
@@ -64,6 +64,8 @@ type Outcome struct {
|
||||
scope, user string
|
||||
// found is, for a service, its unit as the host first found it (novox/hq ADR 0118).
|
||||
found *store.FoundUnit
|
||||
// declared is, for a service with a state, what this apply held its unit to (novox/hq ADR 0269).
|
||||
declared *store.FoundUnit
|
||||
// shell is, for a user, the login shell it was found with and the one the mesh set (novox/hq
|
||||
// ADR 0176 §2, issue 228).
|
||||
shell *store.LoginShell
|
||||
@@ -736,6 +738,7 @@ func ApplyMindingWindows(
|
||||
Scope: outcome.scope,
|
||||
User: outcome.user,
|
||||
Found: outcome.found,
|
||||
Declared: outcome.declared,
|
||||
Shell: outcome.shell,
|
||||
Linger: outcome.linger,
|
||||
Groups: outcome.groups,
|
||||
@@ -1533,6 +1536,10 @@ func applyServiceIn(ctx context.Context, sys system.System, r *declaration.Servi
|
||||
}
|
||||
}
|
||||
|
||||
// What the mesh holds the unit to, recorded with what was found, so its removal can tell the mesh's
|
||||
// own stopping from anybody else's (novox/hq ADR 0269).
|
||||
out.declared = &store.FoundUnit{Unit: r.Unit, State: r.State, Boot: r.Boot}
|
||||
|
||||
before, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
if err != nil {
|
||||
return out, err
|
||||
@@ -2771,9 +2778,11 @@ func declaredDigest(r declaration.Resource) string {
|
||||
// disabled at boot, as a process's unit is, before its file goes: orphans are removed newest
|
||||
// first, and a unit's file is declared before the service that starts it.
|
||||
//
|
||||
// **Never started on the way out.** A unit the mesh stopped is not started again when its
|
||||
// declaration goes: starting something is a decision, and the operator makes it. The report says
|
||||
// what was actually done — a unit already as it was found is forgotten, not "restored".
|
||||
// **Never started on the way out — but for what the mesh's own declaration stopped.** A unit the mesh
|
||||
// stopped is not started again when its declaration goes: starting something is a decision, and the
|
||||
// operator makes it. The exception is a unit found running or enabled that the declaration itself held
|
||||
// stopped or disabled: that is the mesh's doing, and it is undone (novox/hq ADR 0269, startUnitBack). The
|
||||
// report says what was actually done — a unit already as it was found is forgotten, not "restored".
|
||||
func removeService(ctx context.Context, sys system.System, a store.Applied, run Runner,
|
||||
made bool) (string, string, error) {
|
||||
if a.Stateless {
|
||||
@@ -2832,6 +2841,20 @@ func giveUnitBack(ctx context.Context, sys system.System, a store.Applied, run R
|
||||
stop := made || a.Found.State == "stopped"
|
||||
disable := made || a.Found.Boot == "disabled"
|
||||
|
||||
// **What the mesh's own declaration stopped is started again** (novox/hq ADR 0269, narrowing ADR
|
||||
// 0118's "never started on the way out"): a unit found running that the mesh held stopped, and one
|
||||
// found enabled that it held disabled. Only the mesh's declaration did that, and leaving it so is the
|
||||
// mesh's doing outliving the mesh — a pool whose distribution scrub timer a module stopped would have
|
||||
// no schedule at all once the module went. A unit stopped for any other reason is not known to have
|
||||
// been stopped by the mesh, and is left as before.
|
||||
if !made && a.Declared != nil && (a.Declared.Unit == "" || a.Declared.Unit == a.Target) {
|
||||
start := a.Found.State == "running" && a.Declared.State == "stopped"
|
||||
enable := a.Found.Boot == "enabled" && a.Declared.Boot == "disabled"
|
||||
if start || enable {
|
||||
return startUnitBack(ctx, sys, a, run, start, enable)
|
||||
}
|
||||
}
|
||||
|
||||
if !stop && !disable {
|
||||
// Found running, and not disabled by anyone but the mesh: nothing to give back. What the
|
||||
// mesh did since is said, so a unit left stopped is not reported as the machine's doing —
|
||||
@@ -2907,6 +2930,73 @@ func giveUnitBack(ctx context.Context, sys system.System, a store.Applied, run R
|
||||
return "restored", detail, nil
|
||||
}
|
||||
|
||||
// startUnitBack gives back a unit the mesh's declaration stopped or disabled, found running or enabled
|
||||
// (novox/hq ADR 0269).
|
||||
//
|
||||
// **Only a unit exactly as the mesh held it.** A masked unit is somebody's word that it must not run, and
|
||||
// one started or enabled since the mesh held it is somebody's doing: both are left as they are, and the
|
||||
// report says why. **A restore that fails waits**: the start refused, or the unit not running after it,
|
||||
// puts back what this attempt changed — so the next apply finds the unit as the mesh held it and tries
|
||||
// again — and is reported as waiting, never as an error that would make an apply fatal.
|
||||
func startUnitBack(ctx context.Context, sys system.System, a store.Applied, run Runner, start, enable bool) (string, string, error) {
|
||||
state, serr := sys.ServiceState(ctx, run, a.Target)
|
||||
boot, berr := sys.ServiceBoot(ctx, run, a.Target)
|
||||
for _, err := range []error{serr, berr} {
|
||||
switch {
|
||||
case err == nil:
|
||||
case strings.Contains(err.Error(), "does not exist on this machine"):
|
||||
return "forgotten", "the unit no longer exists", nil
|
||||
case strings.Contains(err.Error(), "masked"):
|
||||
return "forgotten", a.Target + " is masked, which is somebody's word that it must not run; left as it is", nil
|
||||
default:
|
||||
return "", "", fmt.Errorf("%w: %s could not be read to give it back (%v); tried again by the next apply",
|
||||
errRemovalWaits, a.Target, err)
|
||||
}
|
||||
}
|
||||
if state != a.Declared.State || (a.Declared.Boot != "" && boot != a.Declared.Boot) {
|
||||
return "forgotten", fmt.Sprintf("it is %s and %s at boot, not the %s and %s the mesh held it to: somebody "+
|
||||
"changed it since, so it is left as it is", state, boot, a.Declared.State, orUnset(a.Declared.Boot)), nil
|
||||
}
|
||||
var did []string
|
||||
if enable {
|
||||
if err := sys.SetServiceBoot(ctx, run, a.Target, "enabled"); err != nil {
|
||||
return "", "", fmt.Errorf("%w: enabling %s again failed (%v); tried again by the next apply", errRemovalWaits, a.Target, err)
|
||||
}
|
||||
did = append(did, "enabled at boot")
|
||||
}
|
||||
if start {
|
||||
why := ""
|
||||
if err := sys.SetServiceState(ctx, run, a.Target, "running"); err != nil {
|
||||
why = "its start failed: " + err.Error()
|
||||
} else if after, err := sys.ServiceState(ctx, run, a.Target); err != nil {
|
||||
why = "it could not be read after its start: " + err.Error()
|
||||
} else if after != "running" {
|
||||
why = "it is " + after + " after its start, not running"
|
||||
}
|
||||
if why != "" {
|
||||
undo := ""
|
||||
if enable {
|
||||
if err := sys.SetServiceBoot(ctx, run, a.Target, "disabled"); err != nil {
|
||||
undo = "; it stays enabled at boot, its disabling again failed: " + err.Error()
|
||||
} else {
|
||||
undo = "; disabled at boot again, as the mesh held it, until then"
|
||||
}
|
||||
}
|
||||
return "", "", fmt.Errorf("%w: giving %s back running, as the host found it: %s%s; tried again by the next apply",
|
||||
errRemovalWaits, a.Target, why, undo)
|
||||
}
|
||||
did = append(did, "started")
|
||||
}
|
||||
return "restored", strings.Join(did, " and ") + " again, as the host found it before the mesh's declaration stopped it", nil
|
||||
}
|
||||
|
||||
func orUnset(s string) string {
|
||||
if s == "" {
|
||||
return "as it was at boot"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// foundAs is what a service's unit was before the mesh touched it, as far as this host can know:
|
||||
// what an earlier apply recorded, or — the first time the mesh is about to act on the unit — what
|
||||
// is there now (novox/hq ADR 0118). Nil when it cannot be known: a record written before the host
|
||||
@@ -2937,7 +3027,7 @@ func foundAs(ctx context.Context, sys system.System, r *declaration.Service, run
|
||||
// Given back as if undeclared, never as the mesh's own: whether the mesh wrote the old
|
||||
// unit's file is known to the removal of orphans, and that file's own record goes with it.
|
||||
action, detail, err := removeService(ctx, sys,
|
||||
store.Applied{Type: string(declaration.TypeService), Target: unit, Found: f,
|
||||
store.Applied{Type: string(declaration.TypeService), Target: unit, Found: f, Declared: previous.Declared,
|
||||
Scope: previous.Scope, User: previous.User}, run, false)
|
||||
switch {
|
||||
case errors.Is(err, errRemovalWaits):
|
||||
|
||||
@@ -313,6 +313,7 @@ func TestAServiceIsReadBackNotAssumed(t *testing.T) {
|
||||
// running. A unit that starts and immediately dies satisfies the command.
|
||||
started := false
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "show" {
|
||||
if started {
|
||||
return "LoadState=loaded\nActiveState=failed\n", nil // started, then died
|
||||
@@ -337,6 +338,7 @@ func TestAServiceIsReadBackNotAssumed(t *testing.T) {
|
||||
|
||||
func TestAnUnknownServiceStateIsRefusedNotGuessed(t *testing.T) {
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
return "LoadState=loaded\nActiveState=reticent\n", nil
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
@@ -371,6 +373,7 @@ func TestADroppedServiceIsGivenBackTheStateItWasFoundIn(t *testing.T) {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var commands []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
commands = append(commands, strings.Join(args, " "))
|
||||
if args[0] == "show" {
|
||||
return "LoadState=loaded\nActiveState=active\n", nil
|
||||
@@ -413,6 +416,7 @@ func TestAServiceRecordsTheStateItWasFoundInOnceAndCarriesIt(t *testing.T) {
|
||||
active := "inactive"
|
||||
enabled := "disabled"
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "show":
|
||||
return "LoadState=loaded\nActiveState=" + active + "\n", nil
|
||||
@@ -464,6 +468,7 @@ func TestAUnitThatDoesNotExistIsNotStopped(t *testing.T) {
|
||||
// Absence read as satisfaction — 04-ISSUES/007 wearing a different hat, and the mirror of
|
||||
// the degraded-init bug the capability detector had.
|
||||
absent := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
return "LoadState=not-found\nActiveState=inactive\n", nil
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
@@ -486,6 +491,7 @@ func TestAMaskedUnitIsRefused(t *testing.T) {
|
||||
// Masked means someone deliberately made it unstartable. Applying over that would undo a
|
||||
// decision the host did not make and cannot see the reason for.
|
||||
masked := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
return "LoadState=masked\nActiveState=inactive\n", nil
|
||||
}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
@@ -506,6 +512,7 @@ func TestForgettingAUnitThatIsGoneDoesNotStrandTheNode(t *testing.T) {
|
||||
// already true.
|
||||
var stopped bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "show" {
|
||||
return "LoadState=not-found\nActiveState=inactive\n", nil
|
||||
}
|
||||
@@ -557,6 +564,7 @@ func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) {
|
||||
// answer would silently reinstall on a machine whose package manager is broken, or report
|
||||
// "installed nothing" as success. The apply must fail instead.
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
return "", errors.New("pacman: error: could not lock database")
|
||||
}
|
||||
d := parseTrusted(t, `{"declaration":1,"resources":[
|
||||
@@ -575,6 +583,7 @@ func TestABrokenPackageDatabaseIsNotReadAsNotInstalled(t *testing.T) {
|
||||
func TestAnInstalledPackageIsNotReinstalled(t *testing.T) {
|
||||
var installed bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "-S" {
|
||||
installed = true
|
||||
}
|
||||
@@ -602,6 +611,7 @@ func TestAPackageIsNeverUninstalled(t *testing.T) {
|
||||
// machine may have had it before the mesh ever saw it. Undeclaring is not "remove it".
|
||||
var uninstalled bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if len(args) > 0 && (args[0] == "-R" || args[0] == "-Rs") {
|
||||
uninstalled = true
|
||||
}
|
||||
@@ -635,6 +645,7 @@ func TestAnActionThatIsAlreadyTrueDoesNotRun(t *testing.T) {
|
||||
// database is, so "is it already there" is a question only the declaration can ask.
|
||||
var ran bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "create-db" {
|
||||
ran = true
|
||||
}
|
||||
@@ -661,6 +672,7 @@ func TestAnActionThatSucceedsAndDoesNothingFails(t *testing.T) {
|
||||
// this repository's most expensive failure shape. Here the command "succeeds" every time
|
||||
// and verify never passes.
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "has-db" {
|
||||
return "", errors.New("no such database")
|
||||
}
|
||||
@@ -687,6 +699,7 @@ func TestAnActionRunsInsideTheContainerItNames(t *testing.T) {
|
||||
// there is any mesh to ask.
|
||||
var sawExec bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "docker" && args[0] == "exec" && args[1] == "store" {
|
||||
sawExec = true
|
||||
return "", nil
|
||||
@@ -710,6 +723,7 @@ func TestAContainerThatExitsImmediatelyFailsTheApply(t *testing.T) {
|
||||
// still running. A container whose entrypoint dies satisfies the command exactly as one
|
||||
// that came up does — which is the read-back rule, in the place it matters most.
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch {
|
||||
case args[0] == "info":
|
||||
return "27.0\n", nil
|
||||
@@ -748,6 +762,7 @@ func TestAContainerWhoseDeclarationChangedIsReplaced(t *testing.T) {
|
||||
|
||||
var removed, created bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -786,6 +801,7 @@ func TestAContainerThatMatchesIsLeftAlone(t *testing.T) {
|
||||
|
||||
var touched bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -831,6 +847,7 @@ func TestAContainerIsRecreatedWhenARestartOnResourceChanged(t *testing.T) {
|
||||
|
||||
var removed, created bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -886,6 +903,7 @@ func TestRestartOnFiresOnlyForResourcesThatChanged(t *testing.T) {
|
||||
func systemctlStub(t *testing.T, load, active, enabled string, verbs *[]string) Runner {
|
||||
t.Helper()
|
||||
return func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "show":
|
||||
return "LoadState=" + load + "\nActiveState=" + active + "\n", nil
|
||||
@@ -1043,6 +1061,7 @@ func TestTheRuntimeProbeIsPerRuntime(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var probedWith string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != tc.present {
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
@@ -1071,6 +1090,7 @@ func TestAContainerUsesTheRuntimeTheMachineHas(t *testing.T) {
|
||||
// machine already has (novox/hq research 012), so hardcoding one contradicts it.
|
||||
var calledWith []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "docker" {
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
@@ -1107,6 +1127,7 @@ func TestNoRuntimeIsSaidPlainly(t *testing.T) {
|
||||
// Naming what was tried, because "docker: command not found" on a machine that deliberately
|
||||
// runs podman sends the reader looking for the wrong thing.
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
d := parseTrusted(t, `{"declaration":1,"resources":[
|
||||
@@ -1250,6 +1271,7 @@ func TestAServiceIsNotRestartedByAChangeItDoesNotName(t *testing.T) {
|
||||
// before and after one.
|
||||
func recordingServices(commands *[]string) Runner {
|
||||
return func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
line := name + " " + strings.Join(args, " ")
|
||||
*commands = append(*commands, line)
|
||||
switch {
|
||||
@@ -1398,6 +1420,7 @@ func TestAFailedActionStopsWhatFollows(t *testing.T) {
|
||||
func TestAContainerIsGivenTheMeshsNames(t *testing.T) {
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "docker" {
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
@@ -1433,6 +1456,7 @@ func TestAContainerIsGivenTheMeshsNames(t *testing.T) {
|
||||
func TestAContainerGivenNoNamesIsRunAsBefore(t *testing.T) {
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "docker" {
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
@@ -1477,6 +1501,7 @@ func TestResourcesAreAppliedInTheOrderTheyWereDeclared(t *testing.T) {
|
||||
var order []string
|
||||
done := map[string]bool{}
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
order = append(order, name+" "+strings.Join(args, " "))
|
||||
// `verify` is the idempotency check, so it has to fail before the step and pass after —
|
||||
// a stub that always succeeds means every action is already done and nothing ever runs,
|
||||
@@ -1627,6 +1652,7 @@ func TestAContainerStaleFromAnEarlierApplyIsReplaced(t *testing.T) {
|
||||
|
||||
var removed, created bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
|
||||
@@ -43,6 +43,7 @@ func watching(t *testing.T, path string) Runner {
|
||||
var commands []string
|
||||
services := recordingServices(&commands)
|
||||
return func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Errorf("the file was not there while %s %s ran: %v", name, strings.Join(args, " "), err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
// starts (ADR 0141).
|
||||
func TestTheHostsOwnFormerArchiveIsLeftInPlaceNotFatal(t *testing.T) {
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "docker" && args[0] == "info" {
|
||||
return "29.0.0\n", nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func TestAUnitWhoseFileTheMeshWroteIsStoppedWhateverItsRecordSays(t *testing.T)
|
||||
m := unitsMachine(map[string]*fakeUnit{"mesh-guard.service": {active: "active", enabled: "enabled"}})
|
||||
fileThereAtStop := false
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "systemctl" && args[0] == "stop" {
|
||||
_, err := os.Stat(unitFile)
|
||||
fileThereAtStop = err == nil
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// A unit the mesh found running and enabled, and held stopped and disabled by its own declaration, is
|
||||
// given back running and enabled when that declaration goes (the zfs module holds the distribution's
|
||||
// scrub timer stopped while its own timer scrubs the pool; unassigning it must not leave the pool with no
|
||||
// schedule at all). The report says so.
|
||||
func TestAUnitTheMeshStoppedAndDisabledIsGivenBackRunningAndEnabled(t *testing.T) {
|
||||
m := unitsMachine(map[string]*fakeUnit{
|
||||
"zfs-scrub-weekly@storage.timer": {active: "active", enabled: "enabled"},
|
||||
})
|
||||
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
||||
{"id":"zfs.distribution-weekly","type":"service","unit":"zfs-scrub-weekly@storage.timer","state":"stopped","boot":"disabled"}]}`,
|
||||
store.State{}, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u := m.units["zfs-scrub-weekly@storage.timer"]; u.active != "inactive" || u.enabled != "disabled" {
|
||||
t.Fatalf("the fixture did not stop and disable it: %s and %s", u.active, u.enabled)
|
||||
}
|
||||
report, _, err := applyOn(t, nothingButA(t), copyOf(t, state), m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u := m.units["zfs-scrub-weekly@storage.timer"]; u.active != "active" || u.enabled != "enabled" {
|
||||
t.Errorf("given back as %s and %s; it was found running and enabled", u.active, u.enabled)
|
||||
}
|
||||
said := ""
|
||||
for _, r := range report.Outcomes {
|
||||
if r.ID == "zfs.distribution-weekly" {
|
||||
said = r.Action + ": " + r.Detail
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(said, "restored: ") || !strings.Contains(said, "started") || !strings.Contains(said, "enabled at boot") {
|
||||
t.Errorf("the report says %q", said)
|
||||
}
|
||||
}
|
||||
|
||||
// Only what the mesh's own declaration did is undone: a unit the mesh held stopped that was found stopped
|
||||
// stays stopped, and one found running that the mesh never stopped is left running as before.
|
||||
func TestOnlyWhatTheMeshsDeclarationStoppedIsStartedAgain(t *testing.T) {
|
||||
m := unitsMachine(map[string]*fakeUnit{
|
||||
"found-stopped.timer": {active: "inactive", enabled: "disabled"},
|
||||
"left-running.timer": {active: "active", enabled: "enabled"},
|
||||
})
|
||||
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
||||
{"id":"a","type":"service","unit":"found-stopped.timer","state":"stopped","boot":"disabled"},
|
||||
{"id":"b","type":"service","unit":"left-running.timer","state":"running","boot":"enabled"}]}`,
|
||||
store.State{}, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := applyOn(t, nothingButA(t), state, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u := m.units["found-stopped.timer"]; u.active != "inactive" || u.enabled != "disabled" {
|
||||
t.Errorf("found stopped and disabled, given back as %s and %s", u.active, u.enabled)
|
||||
}
|
||||
if u := m.units["left-running.timer"]; u.active != "active" || u.enabled != "enabled" {
|
||||
t.Errorf("found running, given back as %s and %s", u.active, u.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// A record from before the host kept what it declared says nothing about who stopped the unit: it is
|
||||
// left as it is, never started.
|
||||
func TestARecordWithoutWhatWasDeclaredStartsNothing(t *testing.T) {
|
||||
m := unitsMachine(map[string]*fakeUnit{"old.timer": {active: "inactive", enabled: "disabled"}})
|
||||
state := store.State{}
|
||||
state.Record(store.Applied{ID: "old", Type: "service", Target: "old.timer", Origin: store.OriginDeclared,
|
||||
Found: &store.FoundUnit{Unit: "old.timer", State: "running", Boot: "enabled"}})
|
||||
if _, _, err := applyOn(t, nothingButA(t), state, m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u := m.units["old.timer"]; u.active != "inactive" || u.enabled != "disabled" {
|
||||
t.Errorf("an old record started %s and %s", u.active, u.enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// The plan says it before the apply does it.
|
||||
func TestThePlanSaysAStoppedUnitIsStartedAgain(t *testing.T) {
|
||||
m := unitsMachine(map[string]*fakeUnit{"zfs-scrub-weekly@storage.timer": {active: "active", enabled: "enabled"}})
|
||||
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
||||
{"id":"w","type":"service","unit":"zfs-scrub-weekly@storage.timer","state":"stopped","boot":"disabled"}]}`,
|
||||
store.State{}, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
said := ""
|
||||
for _, s := range Plan(parse(t, nothingButA(t)), state, store.OriginDeclared) {
|
||||
if s.ID == "w" {
|
||||
said = s.Verb + ": " + s.Why
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(said, "restore: ") || !strings.Contains(said, "restore if still as the mesh held it") ||
|
||||
strings.Contains(said, "so it is started") {
|
||||
t.Errorf("the plan says %q", said)
|
||||
}
|
||||
}
|
||||
|
||||
// Third review: on removal a unit is started again only when it is exactly as the mesh held it, stopped and
|
||||
// disabled. A masked unit, or one somebody started or enabled since, is left alone, and the report says why.
|
||||
func TestAMaskedOrChangedUnitIsLeftAloneOnRemoval(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name, active, enabled, says string
|
||||
}{
|
||||
{"masked", "inactive", "masked", "masked"},
|
||||
{"enabled since", "inactive", "enabled", "not the stopped and disabled"},
|
||||
{"started since", "active", "disabled", "not the stopped and disabled"},
|
||||
} {
|
||||
m := unitsMachine(map[string]*fakeUnit{"w.timer": {active: "active", enabled: "enabled"}})
|
||||
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
||||
{"id":"w","type":"service","unit":"w.timer","state":"stopped","boot":"disabled"}]}`, store.State{}, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.units["w.timer"].active, m.units["w.timer"].enabled = c.active, c.enabled
|
||||
report, _, err := applyOn(t, nothingButA(t), state, m)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.name, err)
|
||||
}
|
||||
if u := m.units["w.timer"]; u.active != c.active || u.enabled != c.enabled {
|
||||
t.Errorf("%s: changed to %s and %s", c.name, u.active, u.enabled)
|
||||
}
|
||||
said := ""
|
||||
for _, o := range report.Outcomes {
|
||||
if o.ID == "w" {
|
||||
said = o.Action + ": " + o.Detail
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(said, "forgotten: ") || !strings.Contains(said, c.says) {
|
||||
t.Errorf("%s: the report says %q", c.name, said)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A restore that fails — the start refused, or the unit not running after it — waits: the record stays,
|
||||
// the report says so, and no apply is made fatal by it.
|
||||
func TestAFailedRestoreWaitsAndIsNeverFatal(t *testing.T) {
|
||||
m := unitsMachine(map[string]*fakeUnit{"w.timer": {active: "active", enabled: "enabled"}})
|
||||
_, state, err := applyOn(t, `{"declaration":1,"resources":[
|
||||
{"id":"w","type":"service","unit":"w.timer","state":"stopped","boot":"disabled"}]}`, store.State{}, m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.units["w.timer"].wontStart = true
|
||||
for i := 0; i < 2; i++ {
|
||||
report, next, err := applyOn(t, nothingButA(t), state, m)
|
||||
if err != nil {
|
||||
t.Fatalf("apply %d failed: %v", i+1, err)
|
||||
}
|
||||
said := ""
|
||||
for _, o := range report.Outcomes {
|
||||
if o.ID == "w" {
|
||||
said = o.Action + ": " + o.Detail
|
||||
}
|
||||
}
|
||||
if !strings.Contains(said, "wait") || !strings.Contains(said, "not running") {
|
||||
t.Errorf("apply %d: the report says %q", i+1, said)
|
||||
}
|
||||
if _, kept := next.Find("w"); !kept {
|
||||
t.Errorf("apply %d: the record went, so nothing would try again", i+1)
|
||||
}
|
||||
state = next
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,8 @@ func newGroupDB(in []string, exists ...string) *groupDB {
|
||||
}
|
||||
|
||||
func (g *groupDB) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
g.asked = append(g.asked, name+" "+strings.Join(args, " "))
|
||||
switch {
|
||||
case name == "getent" && args[0] == "passwd":
|
||||
|
||||
@@ -30,6 +30,8 @@ type aMachine struct {
|
||||
}
|
||||
|
||||
func (m *aMachine) run(ctx context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
line := name + " " + strings.Join(args, " ")
|
||||
m.commands = append(m.commands, line)
|
||||
switch {
|
||||
@@ -245,6 +247,7 @@ func TestAStepThatFailsGatesItsModule(t *testing.T) {
|
||||
body, digest := anArchive(t, map[string]string{"mesh-controller": "#!/bin/sh\n"})
|
||||
src := serving(t, body)
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "systemctl" && len(args) > 1 && args[0] == "start" {
|
||||
return "", errors.New("Job for mesh-controller-prepare.service failed")
|
||||
}
|
||||
|
||||
@@ -107,7 +107,28 @@ type fakeContainer struct {
|
||||
networks, mounts, ports []string
|
||||
}
|
||||
|
||||
// oldShape is a systemctl call as these fakes were written to read it: the verb, the unit, then the
|
||||
// options. The host passes `--` before every unit (third review of mesh-catalog #147), which
|
||||
// internal/system's own test holds; the fakes read the unit where they always did.
|
||||
func oldShape(name string, args []string) []string {
|
||||
if name != "systemctl" || len(args) < 2 || args[len(args)-2] != "--" {
|
||||
return args
|
||||
}
|
||||
v := 0
|
||||
for v < len(args) && strings.HasPrefix(args[v], "-") {
|
||||
v++
|
||||
}
|
||||
if v >= len(args)-2 {
|
||||
return args
|
||||
}
|
||||
out := append([]string{}, args[:v+1]...)
|
||||
out = append(out, args[len(args)-1])
|
||||
return append(out, args[v+1:len(args)-2]...)
|
||||
}
|
||||
|
||||
func (m *machine) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
m.asked = append(m.asked, name+" "+strings.Join(args, " "))
|
||||
if name == "systemctl" {
|
||||
return m.systemctl(args)
|
||||
@@ -907,6 +928,8 @@ func TestMountingTheMachinesOwnPlumbingIsNotFoundData(t *testing.T) {
|
||||
type unreachable struct{ asked []string }
|
||||
|
||||
func (u *unreachable) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
u.asked = append(u.asked, name+" "+strings.Join(args, " "))
|
||||
if name == "docker" && args[0] == "info" {
|
||||
return "27.0\n", nil
|
||||
@@ -958,6 +981,7 @@ func TestAHeldContainerStaysHeldWhenTheRuntimeCannotAnswer(t *testing.T) {
|
||||
func TestAVolumeTheRuntimeCannotBeAskedAboutStopsTheContainer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch {
|
||||
case name == "docker" && args[0] == "info":
|
||||
return "27.0\n", nil
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
// not catch a regression back to it — this one refuses to, on purpose.
|
||||
func dockerLikeByKind(containers map[string]bool) func(context.Context, string, ...string) (string, error) {
|
||||
return func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "docker" {
|
||||
return "", errors.New("unexpected program: " + name)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ func TestAContainerJoinsTheNetworksItKeeps(t *testing.T) {
|
||||
var ran []string
|
||||
connectFails := false
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "docker" {
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
@@ -81,6 +82,7 @@ func TestALeftOutModuleIsNeitherRemovedNorForgotten(t *testing.T) {
|
||||
var removed []string
|
||||
gone := map[string]bool{}
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "docker" {
|
||||
return "", nil
|
||||
}
|
||||
@@ -139,6 +141,7 @@ func TestALeftOutModuleIsNeitherRemovedNorForgotten(t *testing.T) {
|
||||
func TestACapabilityReachesTheRuntimeAndTheSpec(t *testing.T) {
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "docker" {
|
||||
return "", errors.New("not installed")
|
||||
}
|
||||
@@ -180,6 +183,7 @@ func TestAPackageDeclaredAbsentIsRemovedWhenPresentAndLeftWhenNot(t *testing.T)
|
||||
installed := true
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||
if name != "pacman" {
|
||||
return "", nil
|
||||
|
||||
@@ -42,6 +42,8 @@ func (u *ufwMachine) iptables(args []string) (string, error) {
|
||||
}
|
||||
|
||||
func (u *ufwMachine) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
u.asked = append(u.asked, name+" "+strings.Join(args, " "))
|
||||
switch name {
|
||||
case "nft":
|
||||
@@ -374,6 +376,7 @@ func TestReturningToAdoptedLoadsTheGuardBeforeRemovingTheFilter(t *testing.T) {
|
||||
}
|
||||
guardUpAtStop := false
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "systemctl" {
|
||||
return "", &exec.Error{Name: name, Err: exec.ErrNotFound}
|
||||
}
|
||||
@@ -471,6 +474,7 @@ func TestAGuardThatFailsLeavesTheDerivedFilterInForce(t *testing.T) {
|
||||
`","content":"table inet mesh_guard {}\n"}`
|
||||
stopped := false
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name != "systemctl" {
|
||||
return "", &exec.Error{Name: name, Err: exec.ErrNotFound}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ func TestNoAccessIsAtTheMachinesOwn(t *testing.T) {
|
||||
func TestNoContainerMountsTheMachinesOwn(t *testing.T) {
|
||||
ran := false
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch {
|
||||
case args[0] == "info":
|
||||
return "27.0\n", nil
|
||||
|
||||
@@ -46,6 +46,7 @@ func TestBelowAHomeOnlyThatAccountsDirectoryIsPlaced(t *testing.T) {
|
||||
// A container mounting a refused directory is refused with it, and not run.
|
||||
ran := false
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
|
||||
@@ -125,6 +125,20 @@ func Plan(d *declaration.Declaration, known store.State, origin string) []Step {
|
||||
case f == nil:
|
||||
step.Verb, step.Why = "forget", "no longer declared; recorded before the host kept what it "+
|
||||
"found, so it is left as it is"
|
||||
case orphan.Declared != nil && ((f.State == "running" && orphan.Declared.State == "stopped") ||
|
||||
(f.Boot == "enabled" && orphan.Declared.Boot == "disabled")):
|
||||
// startUnitBack's words (novox/hq ADR 0269): what the mesh's declaration stopped goes back.
|
||||
var back []string
|
||||
if f.State == "running" && orphan.Declared.State == "stopped" {
|
||||
back = append(back, "started")
|
||||
}
|
||||
if f.Boot == "enabled" && orphan.Declared.Boot == "disabled" {
|
||||
back = append(back, "enabled at boot")
|
||||
}
|
||||
// A preview: whether it is still as the mesh held it is read when the removal runs, and a
|
||||
// masked unit or one changed since is left as it is.
|
||||
step.Verb, step.Why = "restore", "no longer declared; restore if still as the mesh held it: "+
|
||||
strings.Join(back, " and ")+" again, as the host found it, unless it is masked or was changed since"
|
||||
case f.State == "stopped" || f.Boot == "disabled":
|
||||
var back []string
|
||||
if f.State == "stopped" {
|
||||
|
||||
@@ -103,6 +103,8 @@ type both struct {
|
||||
}
|
||||
|
||||
func (b *both) run(ctx context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
switch name {
|
||||
case "nft", "ufw", "iptables", "ip6tables", "firewall-cmd":
|
||||
return b.u.run(ctx, name, args...)
|
||||
|
||||
@@ -29,6 +29,7 @@ func TestARunOnceProcessRunsAsItsOneshotUnit(t *testing.T) {
|
||||
var commands []string
|
||||
fail := false
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
commands = append(commands, name+" "+strings.Join(args, " "))
|
||||
if fail && name == "systemctl" && len(args) > 0 && args[0] == "start" {
|
||||
return "", errors.New("exit status 1")
|
||||
|
||||
@@ -187,6 +187,7 @@ func TestAnUndeclaredProcessIsRemovedWithItsUnitAndBundle(t *testing.T) {
|
||||
}
|
||||
var commands []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
commands = append(commands, strings.Join(args, " "))
|
||||
return "", nil
|
||||
}
|
||||
@@ -269,6 +270,7 @@ func TestAProcessAppliedAgainIsUnchangedAndNotRestarted(t *testing.T) {
|
||||
body, digest := anArchive(t, map[string]string{"main.js": "console.log(1)\n"})
|
||||
var commands []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
commands = append(commands, name+" "+strings.Join(args, " "))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestAnUnassignedModulesProcessIsStoppedAfterTheConfigurationItReads(t *test
|
||||
|
||||
var configAtStop []bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "systemctl" && strings.Join(args, " ") == "disable --now nfs-server-exports.service" {
|
||||
_, err := os.Stat(config)
|
||||
configAtStop = append(configAtStop, err == nil)
|
||||
@@ -96,6 +97,7 @@ func TestAProcessRecordedAfterItsConfigurationIsStoppedFirst(t *testing.T) {
|
||||
|
||||
var configAtStop []bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "systemctl" && strings.Join(args, " ") == "disable --now m-exports.service" {
|
||||
_, err := os.Stat(config)
|
||||
configAtStop = append(configAtStop, err == nil)
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestAServiceIsRestartedOnlyOnceEveryFileItNamesIsWritten(t *testing.T) {
|
||||
var commands []string
|
||||
services := recordingServices(&commands)
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if strings.Contains(strings.Join(args, " "), "start resolver.service") {
|
||||
if _, err := os.Stat(zones); err != nil {
|
||||
commands = append(commands, name+" "+strings.Join(args, " "))
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestARunOnceStepIsRunToCompletionNotLeftRunning(t *testing.T) {
|
||||
// container exits) nor --restart (a step that is restarted is not a step).
|
||||
var runArgs []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -78,6 +79,7 @@ func TestARunOnceStepThatExitsNonZeroFailsTheApply(t *testing.T) {
|
||||
// Run in the foreground, a non-zero exit is an error the runtime hands back. That must fail
|
||||
// the apply, not be swallowed — a seed that did not happen leaves the machine unready.
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -110,6 +112,7 @@ func TestAFailedRunOnceStepGatesWhatFollows(t *testing.T) {
|
||||
// mirror of a failed action stopping what follows.
|
||||
var startedNames []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -159,6 +162,7 @@ func TestARunOnceStepAlreadyCompletedIsNotReRun(t *testing.T) {
|
||||
|
||||
var ran bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -194,6 +198,7 @@ func TestARunOnceStepReRunsWhenItsDeclarationChanged(t *testing.T) {
|
||||
|
||||
var ran bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -236,6 +241,7 @@ func TestARunOnceStepRunsAgainWhenWhatItReadsChanged(t *testing.T) {
|
||||
|
||||
var ran bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -284,6 +290,7 @@ func TestAContainerNamingARunOnceStepIsRecreatedWhenItRan(t *testing.T) {
|
||||
|
||||
var removed, created bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -326,6 +333,7 @@ func TestAFailedStepGatesItsModuleAndNotTheMachine(t *testing.T) {
|
||||
// unreachable must not stop every module declared after it.
|
||||
var startedNames []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
|
||||
@@ -44,6 +44,8 @@ type recordingRun struct {
|
||||
}
|
||||
|
||||
func (r *recordingRun) run(ctx context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.all = append(r.all, append([]string{name}, args...))
|
||||
@@ -91,6 +93,7 @@ func TestInstallingAScheduleDoesNotRunItAndReportsCurrent(t *testing.T) {
|
||||
var fired bool // a `docker run` — the container was started
|
||||
var pulled bool // an image reported present was pulled anyway
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil // a container runtime answers the apply's probe
|
||||
@@ -154,6 +157,7 @@ func TestInstallingAScheduleEnsuresItsImageIsPresentWithoutStartingIt(t *testing
|
||||
var pulledImage string
|
||||
var fired bool
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -205,6 +209,7 @@ func TestAScheduledStepDoesNotGateWhatFollows(t *testing.T) {
|
||||
var startedWeb bool
|
||||
specs := map[string]string{} // name -> spec label, so read-back sees a running container
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -295,6 +300,8 @@ func TestAFailedRunIsRecordedAndDoesNotFailAnything(t *testing.T) {
|
||||
}
|
||||
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
switch args[0] {
|
||||
case "info":
|
||||
return "27.0\n", nil
|
||||
@@ -343,6 +350,7 @@ func TestASlowRunSkipsTheNextDueRunRatherThanStacking(t *testing.T) {
|
||||
var enters int
|
||||
var mu sync.Mutex
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "info" {
|
||||
return "27.0\n", nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
// unitIn is a service manager whose one unit is active or not, recording what it is asked.
|
||||
func unitIn(active bool, commands *[]string) Runner {
|
||||
return func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
line := name + " " + strings.Join(args, " ")
|
||||
*commands = append(*commands, line)
|
||||
switch {
|
||||
|
||||
@@ -20,6 +20,7 @@ func TestAServiceThatDiesJustAfterItsRestartIsNotReportedRestarted(t *testing.T)
|
||||
// refuses what it was just given.
|
||||
started, looks := false, 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "show" {
|
||||
state := "active"
|
||||
if started {
|
||||
@@ -56,6 +57,7 @@ func TestAServiceThatDiesJustAfterItsRestartIsNotReportedRestarted(t *testing.T)
|
||||
func TestAUnitStillStartingIsNotAFailure(t *testing.T) {
|
||||
serviceSettle = 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "show" {
|
||||
return "LoadState=loaded\nActiveState=activating\n", nil
|
||||
}
|
||||
@@ -74,6 +76,7 @@ func TestAServiceAskedToStopIsNotWaitedOn(t *testing.T) {
|
||||
serviceSettle = 0
|
||||
shows := 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if args[0] == "show" {
|
||||
shows++
|
||||
if shows == 1 {
|
||||
|
||||
@@ -22,6 +22,8 @@ type logins struct {
|
||||
}
|
||||
|
||||
func (l *logins) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
l.asked = append(l.asked, name+" "+strings.Join(args, " "))
|
||||
who := args[len(args)-1]
|
||||
switch name {
|
||||
|
||||
@@ -59,6 +59,8 @@ func (a *account) did(prefix string) bool {
|
||||
}
|
||||
|
||||
func (a *account) run(_ context.Context, name string, args ...string) (string, error) {
|
||||
|
||||
args = oldShape(name, args)
|
||||
line := name + " " + strings.Join(args, " ")
|
||||
a.asked = append(a.asked, line)
|
||||
switch name {
|
||||
@@ -256,6 +258,7 @@ func TestAManagerThatStopsDuringTheApplyIsWaitedFor(t *testing.T) {
|
||||
a.busDown = true
|
||||
calls := 0
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "systemctl" && len(args) == 2 && args[0] == "is-active" {
|
||||
calls++
|
||||
if calls > 1 {
|
||||
|
||||
@@ -259,6 +259,7 @@ func TestANetworkIsCreatedAndThenRemovedWhenNoLongerDeclared(t *testing.T) {
|
||||
var calls []string
|
||||
there := map[string]bool{}
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
calls = append(calls, name+" "+strings.Join(args, " "))
|
||||
if name != "docker" || len(args) < 2 || args[0] != "network" {
|
||||
return "", nil // the runtime probe
|
||||
@@ -302,6 +303,7 @@ func TestANetworkIsCreatedAndThenRemovedWhenNoLongerDeclared(t *testing.T) {
|
||||
func TestANetworkAlreadyThereIsNotRebuilt(t *testing.T) {
|
||||
var created int
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
if name == "docker" && len(args) > 1 && args[0] == "network" && args[1] == "create" {
|
||||
created++
|
||||
}
|
||||
@@ -375,6 +377,7 @@ func TestASealedValueIsPutIntoTheFileThatNamesIt(t *testing.T) {
|
||||
func TestAContainerIsGivenItsEnvironmentFiles(t *testing.T) {
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||
if len(args) > 0 && args[0] == "container" {
|
||||
return "", fmt.Errorf("no such container")
|
||||
@@ -410,6 +413,7 @@ func TestAContainerIsGivenItsEnvironmentFiles(t *testing.T) {
|
||||
func TestAContainerIsGivenItsResolverAndItsAddress(t *testing.T) {
|
||||
var ran []string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
args = oldShape(name, args)
|
||||
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||
if len(args) > 0 && args[0] == "container" {
|
||||
return "", fmt.Errorf("no such container")
|
||||
|
||||
@@ -99,6 +99,12 @@ type Applied struct {
|
||||
// when its declaration goes; one the mesh started is stopped again. Absent on a record written
|
||||
// before the host kept it, and then the unit is left exactly as it is.
|
||||
Found *FoundUnit `json:"found,omitempty"`
|
||||
// Declared is, for a service with a state, what the mesh last held its unit to: running or stopped,
|
||||
// and enabled or disabled where the declaration said. With Found, it tells a unit the mesh's own
|
||||
// declaration stopped and disabled from one somebody stopped since, so removal gives back running
|
||||
// and enabled only the first (novox/hq ADR 0269). Absent on a record from before the host kept it,
|
||||
// and then nothing is started on the way out.
|
||||
Declared *FoundUnit `json:"declared,omitempty"`
|
||||
|
||||
// Into is set for a file written into rather than over (novox/hq ADR 0102): the format, what
|
||||
// each of the mesh's keys held before it set them, which of them were absent, and whether the
|
||||
|
||||
@@ -187,9 +187,9 @@ func describeAge(when, now time.Time) string {
|
||||
// so LoadState is what is read — and it is the thing an interface spanning systemd and OpenRC
|
||||
// would have had to drop.
|
||||
func (arch) ServiceState(ctx context.Context, run Runner, unit string) (string, error) {
|
||||
out, asked := run(ctx, "systemctl", "show", unit,
|
||||
out, asked := run(ctx, "systemctl", "show",
|
||||
"--property=LoadState", "--property=ActiveState", "--property=Type",
|
||||
"--property=RemainAfterExit", "--property=ExecMainStatus")
|
||||
"--property=RemainAfterExit", "--property=ExecMainStatus", "--", unit)
|
||||
|
||||
var load, active, kind, remains, exited string
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
@@ -266,7 +266,7 @@ func (arch) SetServiceState(ctx context.Context, run Runner, unit, state string)
|
||||
if state == "stopped" {
|
||||
verb = "stop"
|
||||
}
|
||||
_, err := run(ctx, "systemctl", verb, unit)
|
||||
_, err := run(ctx, "systemctl", verb, "--", unit)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ func (arch) SetServiceState(ctx context.Context, run Runner, unit, state string)
|
||||
// install section and CANNOT be enabled. Reading it as "disabled" would have the host try, fail,
|
||||
// and blame the wrong thing — the same shape as reading a missing unit as "stopped".
|
||||
func (arch) ServiceBoot(ctx context.Context, run Runner, unit string) (string, error) {
|
||||
out, _ := run(ctx, "systemctl", "is-enabled", unit)
|
||||
out, _ := run(ctx, "systemctl", "is-enabled", "--", unit)
|
||||
switch state := strings.TrimSpace(out); state {
|
||||
case "enabled", "enabled-runtime", "alias":
|
||||
return "enabled", nil
|
||||
@@ -302,7 +302,7 @@ func (arch) SetServiceBoot(ctx context.Context, run Runner, unit, boot string) e
|
||||
if boot == "disabled" {
|
||||
verb = "disable"
|
||||
}
|
||||
_, err := run(ctx, "systemctl", verb, unit)
|
||||
_, err := run(ctx, "systemctl", verb, "--", unit)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ func (arch) RemoveUserFromGroup(ctx context.Context, run Runner, name, group str
|
||||
// is how the host tells a unit an administrator installed, under /etc or /run, from one a package
|
||||
// ships under /usr (novox/hq ADR 0103). Empty, with no error, for a unit that loads from nowhere.
|
||||
func (arch) ServiceUnitFile(ctx context.Context, run Runner, unit string) (string, error) {
|
||||
out, err := run(ctx, "systemctl", "show", unit, "--property=FragmentPath")
|
||||
out, err := run(ctx, "systemctl", "show", "--property=FragmentPath", "--", unit)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the service manager did not say where %s comes from: %w", unit, err)
|
||||
}
|
||||
@@ -376,7 +376,7 @@ func (arch) ReloadUnits(ctx context.Context, run Runner) error {
|
||||
|
||||
// ReloadService tells a running unit to read its configuration again, without stopping it.
|
||||
func (arch) ReloadService(ctx context.Context, run Runner, unit string) error {
|
||||
_, err := run(ctx, "systemctl", "reload", unit)
|
||||
_, err := run(ctx, "systemctl", "reload", "--", unit)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Third review of mesh-catalog #147: a unit's name reaches systemctl as an argument, and since a setting
|
||||
// may now name a template's instance, a name beginning with '-' must never be read as an option. Every
|
||||
// call that names a unit passes `--` before it.
|
||||
func TestEverySystemctlCallPassesDashDashBeforeTheUnit(t *testing.T) {
|
||||
var asked [][]string
|
||||
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||
if name == "systemctl" {
|
||||
asked = append(asked, args)
|
||||
}
|
||||
return "LoadState=loaded\nActiveState=active\nType=simple\nFragmentPath=/x\n", nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
a := arch{}
|
||||
unit := "-x@y.timer"
|
||||
a.ServiceState(ctx, run, unit)
|
||||
a.SetServiceState(ctx, run, unit, "running")
|
||||
a.SetServiceState(ctx, run, unit, "stopped")
|
||||
a.ServiceBoot(ctx, run, unit)
|
||||
a.SetServiceBoot(ctx, run, unit, "enabled")
|
||||
a.SetServiceBoot(ctx, run, unit, "disabled")
|
||||
if len(asked) != 6 {
|
||||
t.Fatalf("asked %d times: %v", len(asked), asked)
|
||||
}
|
||||
for _, args := range asked {
|
||||
if len(args) < 2 || args[len(args)-1] != unit || args[len(args)-2] != "--" {
|
||||
t.Errorf("systemctl %s", strings.Join(args, " "))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user