Undeclaring gives a unit back the state it was found in, and removes a process the mesh made (hq ADR 0118, issue 130)
A service undeclared used to be stopped: unassigning the private network stopped the container runtime, unassigning sshd would stop ssh, an uplink module would take the machine offline. The host now records the unit's state when it first applies it and restores that on undeclare — found running stays running; started by the mesh (the converge filter) is stopped again; nothing is started on the way out; a pre-existing record leaves the unit alone. An undeclared process had no removal at all and failed every apply on its node; its unit, timer and bundle are now removed.
This commit is contained in:
+85
-26
@@ -56,6 +56,8 @@ type Outcome struct {
|
||||
kept string
|
||||
// stateless is a service whose unit's lifecycle is the machine's (novox/hq ADR 0117).
|
||||
stateless bool
|
||||
// found is, for a service, its unit as the host first found it (novox/hq ADR 0118).
|
||||
found *store.FoundUnit
|
||||
// reads is, for a container, the digest of each file it was created reading, by path — so
|
||||
// the next apply can say which one changed (novox/hq 04-ISSUES/103).
|
||||
reads map[string]string
|
||||
@@ -465,6 +467,7 @@ func ApplyKeeping(
|
||||
Kept: kept,
|
||||
Reads: outcome.reads,
|
||||
Stateless: outcome.stateless,
|
||||
Found: outcome.found,
|
||||
Holds: holds(resource),
|
||||
})
|
||||
// Its module has been taken, and what was held for it is now the mesh's. A file written
|
||||
@@ -545,7 +548,7 @@ func applyOne(ctx context.Context, sys system.System, r declaration.Resource, ru
|
||||
case *declaration.File:
|
||||
return applyFile(res, previous, unseal, keepFound)
|
||||
case *declaration.Service:
|
||||
return applyService(ctx, sys, res, run, changed)
|
||||
return applyService(ctx, sys, res, run, changed, previous)
|
||||
case *declaration.Package:
|
||||
return applyPackage(ctx, sys, res, run)
|
||||
case *declaration.Container:
|
||||
@@ -922,11 +925,34 @@ type unitReloader interface {
|
||||
}
|
||||
|
||||
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||
changed map[string]bool) (Outcome, error) {
|
||||
changed map[string]bool, previous store.Applied) (Outcome, error) {
|
||||
if r.Stateless() {
|
||||
return reflectOnly(ctx, sys, r, run, changed)
|
||||
}
|
||||
out := begin(r)
|
||||
|
||||
// **What the unit was before the mesh touched it**, read once — the first time this host
|
||||
// applies it — and carried in the record from then on (novox/hq ADR 0118). Undeclared, the
|
||||
// unit is given back to exactly this: it is the one fact that separates the container runtime,
|
||||
// running before the mesh arrived and to be left running, from the mesh's packet filter,
|
||||
// stopped until the mesh started it and to be stopped again. A record that predates this
|
||||
// field is not read now: the unit's state by then is the mesh's doing, not what was found.
|
||||
switch {
|
||||
case previous.Found != nil:
|
||||
out.found = previous.Found
|
||||
case previous.ID == "":
|
||||
state, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
found := &store.FoundUnit{State: state}
|
||||
if r.Boot != "" {
|
||||
if found.Boot, err = sys.ServiceBoot(ctx, run, r.Unit); err != nil {
|
||||
return out, err
|
||||
}
|
||||
}
|
||||
out.found = found
|
||||
}
|
||||
var changes []string
|
||||
|
||||
// A file the service reflects changed, and it may be the unit's own file or a drop-in: the
|
||||
@@ -1160,30 +1186,12 @@ func remove(ctx context.Context, sys system.System, a store.Applied, run Runner)
|
||||
return "removed", "no longer declared", nil
|
||||
|
||||
case declaration.TypeService:
|
||||
// A unit whose lifecycle was the machine's is left exactly as it is (novox/hq ADR 0117):
|
||||
// stopping it here is how unassigning an uplink module would take down the machine's
|
||||
// network manager, and with it the channel the mesh reaches the machine on.
|
||||
if a.Stateless {
|
||||
return "forgotten", "its state was never the mesh's", nil
|
||||
}
|
||||
// A unit that is no longer declared is stopped, not deleted. The host did not install
|
||||
// it and does not own the unit file — only the state it put the unit into.
|
||||
//
|
||||
// A unit that no longer EXISTS is already in the state removal is trying to reach, and
|
||||
// saying so matters: stopping it fails, and a failure here fails the whole apply. A
|
||||
// host holding a record of an uninstalled unit would then be unable to apply anything,
|
||||
// ever, with no way out but editing its state by hand. Removal is idempotent for the
|
||||
// same reason `os.RemoveAll` is.
|
||||
if _, err := sys.ServiceState(ctx, run, a.Target); err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist on this machine") {
|
||||
return "forgotten", "the unit no longer exists", nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
if err := sys.SetServiceState(ctx, run, a.Target, "stopped"); err != nil {
|
||||
return "", "", fmt.Errorf("stopping %s: %w", a.Target, err)
|
||||
}
|
||||
return "removed", "stopped; the unit file is not the host's to delete", nil
|
||||
return removeService(ctx, sys, a, run)
|
||||
|
||||
case declaration.TypeProcess:
|
||||
// The other side of the same line: a process's unit is the host's own — it wrote the unit
|
||||
// file and unpacked the bundle — so it goes with its declaration (novox/hq ADR 0118).
|
||||
return removeProcess(ctx, a, run)
|
||||
|
||||
case declaration.TypeContainer:
|
||||
// The host CREATED this one, so the host removes it. That is the line: it removes what
|
||||
@@ -2019,3 +2027,54 @@ func declaredDigest(r declaration.Resource) string {
|
||||
}
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(material)))
|
||||
}
|
||||
|
||||
// removeService gives a unit back the state the host first found it in, and nothing more.
|
||||
//
|
||||
// **Removes what it made, gives back what it changed, leaves what was the machine's**
|
||||
// (novox/hq ADR 0118). A service resource never installs a unit; it puts one that already existed
|
||||
// into a state. So undeclaring it cannot mean stopping it — that is how unassigning the private
|
||||
// network stopped the container runtime and every container with it, how unassigning sshd would
|
||||
// have stopped ssh, and how an uplink module would have taken a machine off its only link
|
||||
// (novox/hq issue 130). It means undoing what the mesh did to it: a unit found running is left
|
||||
// running; a unit the mesh started — the packet filter a converge loaded, which returning to
|
||||
// adopted must unload — is stopped again, and disabled again if the mesh enabled 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.
|
||||
func removeService(ctx context.Context, sys system.System, a store.Applied, run Runner) (string, string, error) {
|
||||
if a.Stateless {
|
||||
return "forgotten", "its state was never the mesh's", nil
|
||||
}
|
||||
if a.Found == nil {
|
||||
// Recorded before the host kept what it found. Not knowing, it leaves the unit as it is:
|
||||
// a unit left running can be stopped by the operator, and one stopped by mistake may be
|
||||
// the link the operator would use to do it.
|
||||
return "forgotten", "the unit is the machine's; left as it is", nil
|
||||
}
|
||||
if a.Found.State != "stopped" && (a.Found.Boot == "" || a.Found.Boot == "enabled") {
|
||||
return "forgotten", "it was running before the mesh; left as it is", nil
|
||||
}
|
||||
// Something is to be given back, so the unit must still be there to give it to. One since
|
||||
// uninstalled is already as far from the mesh as it can be, and saying so keeps a record of it
|
||||
// from failing every apply.
|
||||
if _, err := sys.ServiceState(ctx, run, a.Target); err != nil {
|
||||
if strings.Contains(err.Error(), "does not exist on this machine") {
|
||||
return "forgotten", "the unit no longer exists", nil
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
var gave []string
|
||||
if a.Found.State == "stopped" {
|
||||
if err := sys.SetServiceState(ctx, run, a.Target, "stopped"); err != nil {
|
||||
return "", "", fmt.Errorf("stopping %s, which the mesh started: %w", a.Target, err)
|
||||
}
|
||||
gave = append(gave, "stopped again")
|
||||
}
|
||||
if a.Found.Boot == "disabled" {
|
||||
if err := sys.SetServiceBoot(ctx, run, a.Target, "disabled"); err != nil {
|
||||
return "", "", fmt.Errorf("disabling %s, which the mesh enabled: %w", a.Target, err)
|
||||
}
|
||||
gave = append(gave, "disabled at boot again")
|
||||
}
|
||||
return "restored", strings.Join(gave, ", ") + ", as the host found it", nil
|
||||
}
|
||||
|
||||
@@ -348,33 +348,111 @@ func TestAnUnknownServiceStateIsRefusedNotGuessed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestADroppedServiceIsStoppedNotDeleted(t *testing.T) {
|
||||
// The host did not install the unit and does not own the unit file — only the state it put
|
||||
// the unit into.
|
||||
var commands []string
|
||||
func TestADroppedServiceIsGivenBackTheStateItWasFoundIn(t *testing.T) {
|
||||
// novox/hq ADR 0118: undeclaring removes what the mesh made, gives back what it changed, and
|
||||
// leaves what was the machine's. A service resource never installs a unit — so undeclaring it
|
||||
// undoes what the mesh did to the unit, and nothing more. Stopping every undeclared unit is
|
||||
// how unassigning the private network stopped the container runtime (novox/hq issue 130).
|
||||
cases := []struct {
|
||||
name string
|
||||
found *store.FoundUnit
|
||||
action string
|
||||
stop bool
|
||||
disable bool
|
||||
}{
|
||||
{"recorded before the host kept what it found", nil, "forgotten", false, false},
|
||||
{"running before the mesh", &store.FoundUnit{State: "running"}, "forgotten", false, false},
|
||||
{"running and enabled before the mesh", &store.FoundUnit{State: "running", Boot: "enabled"}, "forgotten", false, false},
|
||||
{"started by the mesh", &store.FoundUnit{State: "stopped"}, "restored", true, false},
|
||||
{"started and enabled by the mesh", &store.FoundUnit{State: "stopped", Boot: "disabled"}, "restored", true, true},
|
||||
{"enabled by the mesh, running before it", &store.FoundUnit{State: "running", Boot: "disabled"}, "restored", false, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var commands []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
commands = append(commands, strings.Join(args, " "))
|
||||
if args[0] == "show" {
|
||||
return "LoadState=loaded\nActiveState=active\n", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
state := store.State{Resources: []store.Applied{
|
||||
{ID: "s", Type: "service", Target: "unit.service", Found: c.found},
|
||||
}}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
{"id":"other","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
|
||||
]}`)
|
||||
report, after, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(commands, "; ")
|
||||
if stopped := strings.Contains(joined, "stop unit.service"); stopped != c.stop {
|
||||
t.Errorf("stopped %v, want %v: %s", stopped, c.stop, joined)
|
||||
}
|
||||
if disabled := strings.Contains(joined, "disable unit.service"); disabled != c.disable {
|
||||
t.Errorf("disabled %v, want %v: %s", disabled, c.disable, joined)
|
||||
}
|
||||
if strings.Contains(joined, "start unit.service") || strings.Contains(joined, "mask") {
|
||||
t.Errorf("the host started or masked a unit on its way out: %s", joined)
|
||||
}
|
||||
if o := outcomeOf(report, "s"); o.Action != c.action {
|
||||
t.Errorf("outcome %+v, want %s", o, c.action)
|
||||
}
|
||||
if _, still := after.Find("s"); still {
|
||||
t.Error("the host still believes it owns the undeclared service")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAServiceRecordsTheStateItWasFoundInOnceAndCarriesIt(t *testing.T) {
|
||||
// Read the first time the host applies the unit — before it starts or enables anything —
|
||||
// and never again: by the next apply, the unit's state is the mesh's doing.
|
||||
active := "inactive"
|
||||
enabled := "disabled"
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
commands = append(commands, strings.Join(args, " "))
|
||||
if args[0] == "show" {
|
||||
return "LoadState=loaded\nActiveState=active\n", nil
|
||||
switch args[0] {
|
||||
case "show":
|
||||
return "LoadState=loaded\nActiveState=" + active + "\n", nil
|
||||
case "is-enabled":
|
||||
return enabled, nil
|
||||
case "start":
|
||||
active = "active"
|
||||
case "enable":
|
||||
enabled = "enabled"
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
state := store.State{Resources: []store.Applied{
|
||||
{ID: "s", Type: "service", Target: "gone.service"},
|
||||
}}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
{"id":"other","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
|
||||
{"id":"s","type":"service","unit":"filter.service","state":"running","boot":"enabled"}
|
||||
]}`)
|
||||
|
||||
if _, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginCarried, run, nil, nil); err != nil {
|
||||
_, first, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(commands, "; ")
|
||||
if !strings.Contains(joined, "stop gone.service") {
|
||||
t.Errorf("the dropped service was not stopped: %s", joined)
|
||||
rec, _ := first.Find("s")
|
||||
if rec.Found == nil || rec.Found.State != "stopped" || rec.Found.Boot != "disabled" {
|
||||
t.Fatalf("first apply recorded %+v, want stopped and disabled", rec.Found)
|
||||
}
|
||||
if strings.Contains(joined, "disable") || strings.Contains(joined, "mask") {
|
||||
t.Errorf("the host did more than stop a unit it does not own: %s", joined)
|
||||
_, second, err := Apply(context.Background(), archHost(t), d, first, store.OriginCarried, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec, _ = second.Find("s")
|
||||
if rec.Found == nil || rec.Found.State != "stopped" {
|
||||
t.Errorf("a later apply replaced what was found with what the mesh made: %+v", rec.Found)
|
||||
}
|
||||
|
||||
// A record from before the host kept what it found is not given one later.
|
||||
old := store.State{Resources: []store.Applied{{ID: "s", Type: "service", Target: "filter.service"}}}
|
||||
_, third, err := Apply(context.Background(), archHost(t), d, old, store.OriginCarried, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec, _ = third.Find("s"); rec.Found != nil {
|
||||
t.Errorf("a record that predates the field was given one after the mesh had acted: %+v", rec.Found)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -359,7 +359,9 @@ func TestReturningToAdoptedLoadsTheGuardBeforeRemovingTheFilter(t *testing.T) {
|
||||
return "", nil
|
||||
}
|
||||
converged := store.State{Resources: []store.Applied{
|
||||
{ID: "nftables.load", Type: "service", Target: "mesh-filter.service", Origin: store.OriginDeclared}}}
|
||||
{ID: "nftables.load", Type: "service", Target: "mesh-filter.service", Origin: store.OriginDeclared,
|
||||
// The mesh loaded this filter at converge: it was not running before (novox/hq ADR 0118).
|
||||
Found: &store.FoundUnit{State: "stopped"}}}}
|
||||
_, state, err := applyWith(t, adopted(t, `{"taken":[]}`, withConf(dir)+","+guardFile), converged, run)
|
||||
if !guardUpAtStop {
|
||||
t.Errorf("stop fails %v: the derived filter was stopped before the guard was written", stopFails)
|
||||
|
||||
+10
-1
@@ -80,8 +80,17 @@ func Plan(d *declaration.Declaration, known store.State, origin string) []Step {
|
||||
for _, orphan := range known.Orphans(declared, origin) {
|
||||
step := Step{Verb: "remove", Type: orphan.Type, ID: orphan.ID, Target: orphan.Target,
|
||||
Why: "recorded here and no longer declared"}
|
||||
if orphan.Stateless {
|
||||
switch {
|
||||
case orphan.Stateless:
|
||||
step.Verb, step.Why = "forget", "no longer declared; its unit's state was never the mesh's and is left as it is"
|
||||
case orphan.Type == string(declaration.TypeService):
|
||||
// What removal will do, said before it does it (novox/hq ADR 0118): a unit is given
|
||||
// back what the host found, so the preview names which units that stops.
|
||||
if f := orphan.Found; f != nil && (f.State == "stopped" || f.Boot == "disabled") {
|
||||
step.Verb, step.Why = "restore", "no longer declared; the mesh started or enabled it, and it goes back as it was found"
|
||||
} else {
|
||||
step.Verb, step.Why = "forget", "no longer declared; the unit is the machine's and is left as it is"
|
||||
}
|
||||
}
|
||||
if d.Adoption == nil && strings.HasPrefix(orphan.ID, declaration.AdoptionPrefix) {
|
||||
step.Why = "what protected this node while adopted; removed last, once everything else applied"
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
// Under the mesh's own directory rather than somewhere a distribution owns: these are files the
|
||||
// mesh puts there and replaces, and putting them where a package manager also writes is how two
|
||||
// owners end up disagreeing about one path.
|
||||
const daemonRoot = "/var/lib/mesh/daemons"
|
||||
var daemonRoot = "/var/lib/mesh/daemons"
|
||||
|
||||
// unitDir is where the mesh writes the units it owns. A variable only so a test can point it at a
|
||||
// directory of its own.
|
||||
@@ -290,3 +290,53 @@ func unitValue(key, value string) string {
|
||||
).Replace(value)
|
||||
return `"` + key + "=" + escaped + `"`
|
||||
}
|
||||
|
||||
// removeProcess takes away a process the mesh no longer declares: its timer and unit stopped and
|
||||
// disabled, their files removed, the supervisor told, and the unpacked bundle deleted.
|
||||
//
|
||||
// **All of it is the host's**, which is why all of it goes (novox/hq ADR 0118). Before this there
|
||||
// was no way to remove a process at all, and one left undeclared failed every apply on its node
|
||||
// until someone edited the host's state by hand — unassigning any module that ran its own code
|
||||
// stranded the machine.
|
||||
//
|
||||
// Idempotent, like every removal: a unit already gone is not an error, and a record whose files
|
||||
// have all vanished is forgotten rather than reported as removed.
|
||||
func removeProcess(ctx context.Context, a store.Applied, run Runner) (string, string, error) {
|
||||
name := a.Target
|
||||
if name == "" || strings.ContainsAny(name, "/ \t") {
|
||||
// The name is a unit name and a directory under the mesh's own. One that could climb out
|
||||
// of either is refused rather than acted on, whatever wrote it into the record.
|
||||
return "", "", fmt.Errorf("a process recorded under %q cannot be removed by name", name)
|
||||
}
|
||||
found := false
|
||||
for _, unit := range []string{name + ".timer", name + ".service"} {
|
||||
path := filepath.Join(unitDir, unit)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// The timer first, so a scheduled run cannot start the service between the two.
|
||||
if _, err := run(ctx, "systemctl", "disable", "--now", unit); err != nil {
|
||||
return "", "", fmt.Errorf("stopping %s: %w", unit, err)
|
||||
}
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
if found {
|
||||
if _, err := run(ctx, "systemctl", "daemon-reload"); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
bundle := filepath.Join(daemonRoot, name)
|
||||
if _, err := os.Stat(bundle); err == nil {
|
||||
found = true
|
||||
if err := os.RemoveAll(bundle); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "forgotten", "no longer there", nil
|
||||
}
|
||||
return "removed", "stopped; its unit and its bundle removed — the mesh's own code", nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/declaration"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
func aProcess() *declaration.Process {
|
||||
@@ -163,3 +167,60 @@ func TestAnEnvironmentValueMeansWhatTheDeclarationSaid(t *testing.T) {
|
||||
t.Fatalf("a quote was not escaped, so the value ends early: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUndeclaredProcessIsRemovedWithItsUnitAndBundle(t *testing.T) {
|
||||
// novox/hq ADR 0118: a process's unit and bundle are the host's own, so they go with the
|
||||
// declaration. Before, there was no way to remove a process at all, and one left undeclared
|
||||
// failed every apply on its node.
|
||||
units, bundles := t.TempDir(), t.TempDir()
|
||||
wasUnits, wasBundles := unitDir, daemonRoot
|
||||
unitDir, daemonRoot = units, bundles
|
||||
t.Cleanup(func() { unitDir, daemonRoot = wasUnits, wasBundles })
|
||||
|
||||
for _, f := range []string{"mesh-job.service", "mesh-job.timer"} {
|
||||
if err := os.WriteFile(filepath.Join(units, f), []byte("[Unit]\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(bundles, "mesh-job", "bin"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var commands []string
|
||||
run := func(ctx context.Context, name string, args ...string) (string, error) {
|
||||
commands = append(commands, strings.Join(args, " "))
|
||||
return "", nil
|
||||
}
|
||||
known := store.State{Resources: []store.Applied{{ID: "p", Type: "process", Target: "mesh-job"}}}
|
||||
d := parse(t, `{"declaration":1,"resources":[
|
||||
{"id":"f","type":"file","path":"`+filepath.Join(t.TempDir(), "a")+`","content":"a\n"}
|
||||
]}`)
|
||||
report, after, err := Apply(context.Background(), archHost(t), d, known, store.OriginCarried, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("an undeclared process failed the apply: %v", err)
|
||||
}
|
||||
joined := strings.Join(commands, "; ")
|
||||
timer, service := strings.Index(joined, "disable --now mesh-job.timer"), strings.Index(joined, "disable --now mesh-job.service")
|
||||
if timer < 0 || service < 0 || timer > service {
|
||||
t.Errorf("the timer and the unit were not stopped, timer first: %s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "daemon-reload") {
|
||||
t.Errorf("the service manager was not told its units changed: %s", joined)
|
||||
}
|
||||
for _, gone := range []string{filepath.Join(units, "mesh-job.service"), filepath.Join(units, "mesh-job.timer"), filepath.Join(bundles, "mesh-job")} {
|
||||
if _, err := os.Stat(gone); !os.IsNotExist(err) {
|
||||
t.Errorf("%s is still there", gone)
|
||||
}
|
||||
}
|
||||
if o := outcomeOf(report, "p"); o.Action != "removed" {
|
||||
t.Errorf("outcome %+v, want removed", o)
|
||||
}
|
||||
if _, still := after.Find("p"); still {
|
||||
t.Error("the host still believes it owns the process")
|
||||
}
|
||||
|
||||
// Again, with everything already gone: forgotten, not an error.
|
||||
_, _, err = Apply(context.Background(), archHost(t), d, known, store.OriginCarried, run, nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("removing a process that is already gone failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,13 @@ type Applied struct {
|
||||
// service removed as if it had a state is stopped: the machine's network manager, for one.
|
||||
Stateless bool `json:"stateless,omitempty"`
|
||||
|
||||
// Found is, for a service, the state its unit was in when this host first applied it — before
|
||||
// the mesh started, stopped, enabled or disabled anything. Removal gives that back and nothing
|
||||
// more (novox/hq ADR 0118): a unit that was running before the mesh arrived keeps running
|
||||
// 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"`
|
||||
|
||||
// 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
|
||||
// file itself was — so undeclaring it gives the machine back exactly what it had.
|
||||
@@ -447,3 +454,12 @@ func originOf(r Applied) string {
|
||||
}
|
||||
return r.Origin
|
||||
}
|
||||
|
||||
// FoundUnit is a service's unit as the host first found it.
|
||||
type FoundUnit struct {
|
||||
// State is "running" or "stopped".
|
||||
State string `json:"state"`
|
||||
// Boot is "enabled" or "disabled" — or empty when the declaration never set it, and the host
|
||||
// never touched it.
|
||||
Boot string `json:"boot,omitempty"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user