A service may omit its state, so unassigning an uplink module never stops the machine's network manager (hq ADR 0117)
This commit is contained in:
+89
-9
@@ -54,6 +54,8 @@ type Outcome struct {
|
||||
into *store.Into
|
||||
// kept is where this apply kept the original of a file it wrote over (novox/hq ADR 0100).
|
||||
kept string
|
||||
// stateless is a service whose unit's lifecycle is the machine's (novox/hq ADR 0117).
|
||||
stateless bool
|
||||
// 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
|
||||
@@ -458,19 +460,21 @@ func ApplyKeeping(
|
||||
Origin: origin,
|
||||
ID: resource.Identity(), Type: string(resource.Kind()),
|
||||
Target: outcome.Target, AppliedAt: time.Now().UTC(),
|
||||
Wrote: outcome.wrote,
|
||||
Into: outcome.into,
|
||||
Kept: kept,
|
||||
Reads: outcome.reads,
|
||||
Holds: holds(resource),
|
||||
Wrote: outcome.wrote,
|
||||
Into: outcome.into,
|
||||
Kept: kept,
|
||||
Reads: outcome.reads,
|
||||
Stateless: outcome.stateless,
|
||||
Holds: holds(resource),
|
||||
})
|
||||
// Its module has been taken, and what was held for it is now the mesh's. A file written
|
||||
// into replaced nothing that was found, so its outcome says what the write did, not that
|
||||
// a cutover happened; its hold from when it was declared whole goes all the same — here,
|
||||
// after the write worked, so a failed one keeps the hold and where its original is.
|
||||
// into, or a service whose lifecycle is the machine's, replaced nothing that was found, so
|
||||
// its outcome says what the apply did, not that a cutover happened; a hold from when it was
|
||||
// declared otherwise goes all the same — here, after the apply worked, so a failed one
|
||||
// keeps the hold and where its original is.
|
||||
if wasHeld {
|
||||
known.Release(held.ID)
|
||||
if f, isFile := resource.(*declaration.File); !isFile || f.Into == "" {
|
||||
if !replacesNothing(resource) {
|
||||
outcome.Detail = takenDetail(held)
|
||||
}
|
||||
}
|
||||
@@ -919,6 +923,9 @@ type unitReloader interface {
|
||||
|
||||
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||
changed map[string]bool) (Outcome, error) {
|
||||
if r.Stateless() {
|
||||
return reflectOnly(ctx, sys, r, run, changed)
|
||||
}
|
||||
out := begin(r)
|
||||
var changes []string
|
||||
|
||||
@@ -1029,6 +1036,73 @@ func applyService(ctx context.Context, sys system.System, r *declaration.Service
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// reflectOnly is a service whose unit's lifecycle is the machine's (novox/hq ADR 0117): nothing is
|
||||
// started, stopped, enabled or disabled, and a changed trigger is acted on only where the unit is
|
||||
// already running. An inactive unit is left so — started, it would be a second network manager on
|
||||
// a machine that uses another — and it reads the change when whatever starts it does.
|
||||
func reflectOnly(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||
changed map[string]bool) (Outcome, error) {
|
||||
out := begin(r)
|
||||
out.stateless = true
|
||||
restart := reflected(r, changed)
|
||||
reload := restartedBy(r.ReloadOn, changed)
|
||||
if len(restart) == 0 && len(reload) == 0 {
|
||||
out.Action = "unchanged"
|
||||
out.Detail = "its lifecycle is the machine's; nothing it reflects changed"
|
||||
return out, nil
|
||||
}
|
||||
state, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if state != "running" {
|
||||
out.Action = "unchanged"
|
||||
out.Detail = "not running; the change applies at its next start"
|
||||
return out, nil
|
||||
}
|
||||
if len(restart) > 0 {
|
||||
// The same as a stated service: the unit's own file may be what changed, and the manager
|
||||
// reads that again only when told to.
|
||||
if u, ok := sys.(unitReloader); ok {
|
||||
if err := u.ReloadUnits(ctx, run); err != nil {
|
||||
return out, fmt.Errorf("reloading the service manager's units for %s: %w", r.Unit, err)
|
||||
}
|
||||
}
|
||||
if err := sys.SetServiceState(ctx, run, r.Unit, "stopped"); err != nil {
|
||||
return out, fmt.Errorf("restarting %s: stopping it: %w", r.Unit, err)
|
||||
}
|
||||
if err := sys.SetServiceState(ctx, run, r.Unit, "running"); err != nil {
|
||||
return out, fmt.Errorf("restarting %s: starting it again: %w", r.Unit, err)
|
||||
}
|
||||
} else {
|
||||
reloader, ok := sys.(serviceReloader)
|
||||
if !ok {
|
||||
return out, fmt.Errorf("%s must be reloaded for %s and this machine's service manager "+
|
||||
"cannot reload a unit", r.Unit, strings.Join(reload, ", "))
|
||||
}
|
||||
if err := reloader.ReloadService(ctx, run, r.Unit); err != nil {
|
||||
return out, fmt.Errorf("reloading %s: %w", r.Unit, err)
|
||||
}
|
||||
}
|
||||
// Read back: it was running, and a restart or reload that left it otherwise is a failure —
|
||||
// the machine's network manager down is not a change to report and move past.
|
||||
after, err := sys.ServiceState(ctx, run, r.Unit)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Action = "updated"
|
||||
if len(restart) > 0 {
|
||||
out.Detail = "restarted for " + strings.Join(restart, ", ")
|
||||
} else {
|
||||
out.Detail = "reloaded for " + strings.Join(reload, ", ")
|
||||
}
|
||||
if after != "running" {
|
||||
return out, fmt.Errorf("%s was %s to pick up a change and is %s", r.Unit,
|
||||
strings.Fields(out.Detail)[0], after)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// remove undoes one resource the host applied and the declaration no longer names, and reports
|
||||
// what it actually did.
|
||||
//
|
||||
@@ -1086,6 +1160,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.
|
||||
//
|
||||
|
||||
+17
-3
@@ -108,7 +108,7 @@ func lookBefore(ctx context.Context, sys system.System, d *declaration.Declarati
|
||||
}
|
||||
}
|
||||
case *declaration.Service:
|
||||
if known.Recorded(string(declaration.TypeService), res.Unit) {
|
||||
if res.Stateless() || known.Recorded(string(declaration.TypeService), res.Unit) {
|
||||
continue
|
||||
}
|
||||
// **Found is a unit somebody put on this machine, or one the machine uses.**
|
||||
@@ -261,6 +261,19 @@ func heldContainer(known store.State, name string) (store.Held, bool) {
|
||||
return store.Held{}, false
|
||||
}
|
||||
|
||||
// replacesNothing is a resource that takes nothing found on the machine from it, so on an adopted
|
||||
// node it is never held and never previewed as replacing what was found: a file written into
|
||||
// (novox/hq ADR 0102), and a service whose unit's lifecycle is the machine's (novox/hq ADR 0117).
|
||||
func replacesNothing(r declaration.Resource) bool {
|
||||
switch res := r.(type) {
|
||||
case *declaration.File:
|
||||
return res.Into != ""
|
||||
case *declaration.Service:
|
||||
return res.Stateless()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// holdOnAdopted decides whether a resource of an adopted node is held rather than applied, and
|
||||
// holds it (novox/hq ADR 0100, ADR 0103). For a module not yet taken, what is present with no
|
||||
// record is kept as it is: a file or a container under its name, a directory, a service's unit,
|
||||
@@ -297,8 +310,9 @@ func holdOnAdopted(ctx context.Context, sys system.System, r declaration.Resourc
|
||||
// A file written into replaces nothing that was found, so it is never held (novox/hq ADR
|
||||
// 0102) — and a hold from when it was declared whole must not keep the mesh's keys out. That
|
||||
// hold is released by the apply once the write has worked, not here: a write that fails keeps
|
||||
// it, and with it where the original was kept.
|
||||
if f, ok := r.(*declaration.File); ok && f.Into != "" {
|
||||
// it, and with it where the original was kept. A service whose lifecycle is the machine's
|
||||
// replaces nothing either (novox/hq ADR 0117).
|
||||
if replacesNothing(r) {
|
||||
return false, false, out, nil
|
||||
}
|
||||
|
||||
|
||||
+12
-5
@@ -80,6 +80,9 @@ 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 {
|
||||
step.Verb, step.Why = "forget", "no longer declared; its unit's state was never the mesh'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"
|
||||
protecting = append(protecting, step)
|
||||
@@ -139,11 +142,9 @@ func planned(r declaration.Resource, d *declaration.Declaration, known store.Sta
|
||||
}
|
||||
}
|
||||
}
|
||||
// A file written into replaces nothing that was found, so it is never held (ADR 0102).
|
||||
into := false
|
||||
if f, ok := r.(*declaration.File); ok && f.Into != "" {
|
||||
into = true
|
||||
}
|
||||
// A file written into, or a service whose lifecycle is the machine's, replaces nothing that
|
||||
// was found, so it is never held (ADR 0102, ADR 0117).
|
||||
into := replacesNothing(r)
|
||||
h, held := known.HeldAt(r.Identity())
|
||||
module, untaken := d.Adoption.UntakenModuleOf(r.Identity())
|
||||
switch {
|
||||
@@ -182,6 +183,12 @@ func planned(r declaration.Resource, d *declaration.Declaration, known store.Sta
|
||||
return step
|
||||
}
|
||||
|
||||
if svc, ok := r.(*declaration.Service); ok && svc.Stateless() {
|
||||
// Nothing is created: the unit and whether it runs are the machine's (novox/hq ADR 0117).
|
||||
step.Verb, step.Why = "check", "its lifecycle is the machine's; reloaded or restarted only if "+
|
||||
"running when what it reflects changes"
|
||||
return step
|
||||
}
|
||||
was, recorded := known.Find(r.Identity())
|
||||
if !recorded {
|
||||
step.Verb, step.Why = "create", "no record of it on this node"
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package apply
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0117: a service that omits its state leaves the unit's lifecycle to the
|
||||
// machine. The mesh reflects its triggers on a unit already running and does nothing else to it —
|
||||
// never starts, stops, enables or disables it, and forgets it when undeclared.
|
||||
|
||||
// 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) {
|
||||
line := name + " " + strings.Join(args, " ")
|
||||
*commands = append(*commands, line)
|
||||
switch {
|
||||
case strings.Contains(line, "is-enabled"):
|
||||
return "enabled", nil
|
||||
case strings.Contains(line, "show") && strings.Contains(line, "ActiveState"):
|
||||
if active {
|
||||
return "LoadState=loaded\nActiveState=active\nSubState=running", nil
|
||||
}
|
||||
return "LoadState=loaded\nActiveState=inactive\nSubState=dead", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func statelessDecl(path, content, triggers string) string {
|
||||
return fmt.Sprintf(`{"declaration":1,"resources":[
|
||||
{"id":"uplink.conf","type":"file","path":%q,"into":"block","content":%q},
|
||||
{"id":"uplink.manager","type":"service","unit":"NetworkManager.service",%s}
|
||||
]}`, path, content, triggers)
|
||||
}
|
||||
|
||||
// touched is whether any command would change the unit's lifecycle.
|
||||
func touched(commands []string) []string {
|
||||
var changing []string
|
||||
for _, c := range commands {
|
||||
for _, verb := range []string{" start ", " stop ", " restart ", " enable ", " disable ", " reload "} {
|
||||
if strings.Contains(c+" ", verb) {
|
||||
changing = append(changing, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
return changing
|
||||
}
|
||||
|
||||
func TestAStatelessServiceRunningIsReloadedForItsTrigger(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "mesh.conf")
|
||||
var commands []string
|
||||
report, _, err := Apply(context.Background(), archHost(t),
|
||||
parse(t, statelessDecl(path, "[main]\ndns=none\n", `"reload-on":["uplink.conf"]`)),
|
||||
store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
joined := strings.Join(commands, "\n")
|
||||
if !strings.Contains(joined, "systemctl reload NetworkManager.service") {
|
||||
t.Errorf("the running manager was not reloaded; commands were %v", commands)
|
||||
}
|
||||
for _, c := range touched(commands) {
|
||||
if !strings.Contains(c, "reload") {
|
||||
t.Errorf("the manager's lifecycle was touched: %s", c)
|
||||
}
|
||||
}
|
||||
if o := outcomeOf(report, "uplink.manager"); o.Action != "updated" || !strings.Contains(o.Detail, "reloaded for uplink.conf") {
|
||||
t.Errorf("reported as %q: %s", o.Action, o.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAStatelessServiceNotRunningIsLeftSo(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "mesh.conf")
|
||||
for _, triggers := range []string{`"reload-on":["uplink.conf"]`, `"restart-on":["uplink.conf"]`} {
|
||||
var commands []string
|
||||
report, _, err := Apply(context.Background(), archHost(t),
|
||||
parse(t, statelessDecl(path, fmt.Sprintf("# %s\n", triggers), triggers)),
|
||||
store.State{}, store.OriginDeclared, unitIn(false, &commands), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changing := touched(commands); len(changing) > 0 {
|
||||
t.Errorf("%s: an inactive unit was acted on: %v", triggers, changing)
|
||||
}
|
||||
if o := outcomeOf(report, "uplink.manager"); o.Action != "unchanged" ||
|
||||
o.Detail != "not running; the change applies at its next start" {
|
||||
t.Errorf("%s: reported as %q: %s", triggers, o.Action, o.Detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAStatelessServiceIsRestartedOnlyForItsRestartTrigger(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "mesh.conf")
|
||||
var commands []string
|
||||
d := parse(t, statelessDecl(path, "x\n", `"restart-on":["uplink.conf"]`))
|
||||
_, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared,
|
||||
unitIn(true, &commands), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(strings.Join(commands, "\n"), "stop NetworkManager.service") {
|
||||
t.Errorf("not restarted for its restart trigger; commands were %v", commands)
|
||||
}
|
||||
// Nothing it reflects changed: nothing is asked of the unit at all.
|
||||
commands = nil
|
||||
report, _, err := Apply(context.Background(), archHost(t), d, state, store.OriginDeclared,
|
||||
unitIn(true, &commands), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changing := touched(commands); len(changing) > 0 {
|
||||
t.Errorf("with nothing changed the unit was acted on: %v", changing)
|
||||
}
|
||||
if got := outcomeOf(report, "uplink.manager").Action; got != "unchanged" {
|
||||
t.Errorf("with nothing changed it was %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAStatelessServiceUndeclaredIsForgottenNotStopped(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "mesh.conf")
|
||||
var commands []string
|
||||
_, state, err := Apply(context.Background(), archHost(t),
|
||||
parse(t, statelessDecl(path, "x\n", `"reload-on":["uplink.conf"]`)),
|
||||
store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec, _ := state.Find("uplink.manager"); !rec.Stateless {
|
||||
t.Fatal("the record does not say the service was stateless")
|
||||
}
|
||||
if steps := Plan(somethingElse(t), state, store.OriginDeclared); !hasStep(steps, "forget", "uplink.manager") {
|
||||
t.Errorf("the preview does not forget it: %v", steps)
|
||||
}
|
||||
commands = nil
|
||||
report, _, err := Apply(context.Background(), archHost(t), somethingElse(t), state, store.OriginDeclared,
|
||||
unitIn(true, &commands), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changing := touched(commands); len(changing) > 0 {
|
||||
t.Errorf("undeclaring acted on the unit: %v", changing)
|
||||
}
|
||||
o := outcomeOf(report, "uplink.manager")
|
||||
if o.Action != "forgotten" || o.Detail != "its state was never the mesh's" {
|
||||
t.Errorf("undeclaring was %q: %s", o.Action, o.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAStatelessServiceIsNeverHeldOnAnAdoptedNode(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "mesh.conf")
|
||||
resources := fmt.Sprintf(`{"id":"uplink.conf","type":"file","path":%q,"into":"block","content":"x\n"},
|
||||
{"id":"uplink.manager","type":"service","unit":"NetworkManager.service","reload-on":["uplink.conf"]}`, path)
|
||||
d := adopted(t, `{"taken":[],"untaken":{"uplink":["uplink.conf","uplink.manager"]}}`, resources)
|
||||
for _, s := range Plan(d, store.State{}, store.OriginDeclared) {
|
||||
if s.ID == "uplink.manager" && (s.Verb == "hold" || s.Verb == "create" || strings.Contains(s.Why, "replaces")) {
|
||||
t.Errorf("the preview holds or replaces a stateless service: %+v", s)
|
||||
}
|
||||
}
|
||||
var commands []string
|
||||
report, state, err := ApplyKeeping(context.Background(), archHost(t), d, store.State{},
|
||||
store.OriginDeclared, unitIn(true, &commands), nil, nil, KeepIn(t.TempDir()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := outcomeOf(report, "uplink.manager").Action; got == "held" {
|
||||
t.Fatal("a stateless service was held, though it replaces nothing that was found")
|
||||
}
|
||||
if len(state.Held) != 0 {
|
||||
t.Errorf("something was held: %+v", state.Held)
|
||||
}
|
||||
for _, c := range touched(commands) {
|
||||
if !strings.Contains(c, "reload") {
|
||||
t.Errorf("the adopted node's manager lifecycle was touched: %s", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hasStep(steps []Step, verb, id string) bool {
|
||||
for _, s := range steps {
|
||||
if s.Verb == verb && s.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -645,10 +645,20 @@ func (d *Process) validate(where string, _ bool) []string {
|
||||
// (it will come back at boot), or disabled and running (started by hand, gone after a reboot).
|
||||
// Folding them into one field would make the second expressible only by accident.
|
||||
type Service struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
State string `json:"state"`
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Unit string `json:"unit"`
|
||||
// State is "running" or "stopped" — or absent, and then **the unit's lifecycle is the
|
||||
// machine's; the mesh only reflects its triggers** (novox/hq ADR 0117). The uplink modules
|
||||
// declare the machine's own network manager this way: the mesh writes into its configuration
|
||||
// and needs it to read that again, and nothing more. Stated, the host would start the manager
|
||||
// on a machine that uses another one — two managers fighting over the same links — and, when
|
||||
// the module was unassigned, stop it: the machine's network, the channel the mesh itself
|
||||
// arrives on, gone at the moment of a routine change. So a service without a state is never
|
||||
// started, stopped, enabled or disabled, is reloaded or restarted only when a trigger changed
|
||||
// and it is already running, and undeclared is simply forgotten. It says nothing unless it
|
||||
// names a trigger, and it may not say boot or takes-over, which are both lifecycle.
|
||||
State string `json:"state,omitempty"`
|
||||
// Boot is "enabled" or "disabled" — whether the unit starts at boot. Optional: absent means
|
||||
// the host asserts nothing about it and leaves whatever is there.
|
||||
//
|
||||
@@ -692,6 +702,10 @@ type TakeOver struct {
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
// Stateless reports whether the unit's lifecycle is the machine's, and the mesh only reflects the
|
||||
// service's triggers (novox/hq ADR 0117).
|
||||
func (s *Service) Stateless() bool { return s.State == "" }
|
||||
|
||||
func (s *Service) Identity() string { return s.ID }
|
||||
func (s *Service) Kind() Type { return TypeService }
|
||||
func (s *Service) Target() string { return s.Unit }
|
||||
@@ -701,9 +715,18 @@ func (s *Service) validate(where string, _ bool) []string {
|
||||
if s.Unit == "" {
|
||||
problems = append(problems, where+": a service needs a unit")
|
||||
}
|
||||
if s.State != "running" && s.State != "stopped" {
|
||||
switch {
|
||||
case s.State == "running" || s.State == "stopped":
|
||||
case s.State != "":
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: state %q; a service is \"running\" or \"stopped\"", where, s.State))
|
||||
"%s: state %q; a service is \"running\" or \"stopped\", or omits state to leave the "+
|
||||
"unit's lifecycle to the machine", where, s.State))
|
||||
case s.Boot != "" || s.TakesOver != nil:
|
||||
problems = append(problems, where+": a service that omits state leaves the unit's lifecycle "+
|
||||
"to the machine, and boot and takes-over are both its lifecycle")
|
||||
case len(s.RestartOn) == 0 && len(s.ReloadOn) == 0:
|
||||
problems = append(problems, where+": a service that omits state leaves the unit's lifecycle "+
|
||||
"to the machine, and names no restart-on or reload-on — it declares nothing")
|
||||
}
|
||||
if s.Boot != "" && s.Boot != "enabled" && s.Boot != "disabled" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
@@ -718,7 +741,7 @@ func (s *Service) validate(where string, _ bool) []string {
|
||||
case t.Unit == s.Unit:
|
||||
problems = append(problems, fmt.Sprintf("%s: takes-over names %s, which is this service's own unit",
|
||||
where, t.Unit))
|
||||
case s.State != "running":
|
||||
case s.State != "running" && s.State != "":
|
||||
problems = append(problems, where+": a service that takes over a tunnel is running — stopping "+
|
||||
"the found one for a service that will not run would leave the peers with nothing")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Defends novox/hq ADR 0117: a service may leave its unit's lifecycle to the machine, and then
|
||||
// says nothing but its triggers.
|
||||
func TestAServiceWithoutAStateSaysOnlyItsTriggers(t *testing.T) {
|
||||
for name, c := range map[string]struct{ resource, refusal string }{
|
||||
"no trigger": {`{"id":"s","type":"service","unit":"NetworkManager.service"}`, "declares nothing"},
|
||||
"with boot": {`{"id":"s","type":"service","unit":"NetworkManager.service","boot":"enabled","reload-on":["f"]}`, "boot and takes-over"},
|
||||
"with takes-over": {`{"id":"s","type":"service","unit":"a.service","reload-on":["f"],"takes-over":{"interface":"wg0","unit":"b.service","config":"/etc/x"}}`, "boot and takes-over"},
|
||||
"an unknown state": {`{"id":"s","type":"service","unit":"a.service","state":"paused"}`, "omits state to leave the unit's lifecycle to the machine"},
|
||||
} {
|
||||
_, err := Parse([]byte(`{"declaration":1,"resources":[{"id":"f","type":"file","path":"/etc/x","content":"x"},` + c.resource + `]}`))
|
||||
if err == nil || !strings.Contains(err.Error(), c.refusal) {
|
||||
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
|
||||
}
|
||||
}
|
||||
d, err := Parse([]byte(`{"declaration":1,"resources":[{"id":"f","type":"file","path":"/etc/x","content":"x"},
|
||||
{"id":"s","type":"service","unit":"NetworkManager.service","restart-on":["f"]}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s := d.Resources[1].(*Service); !s.Stateless() {
|
||||
t.Error("a service without a state was not read as stateless")
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,11 @@ type Applied struct {
|
||||
// said once in a log line is not a path the host can find again.
|
||||
Kept string `json:"kept,omitempty"`
|
||||
|
||||
// Stateless is, for a service, that its unit's lifecycle was never the mesh's (novox/hq ADR
|
||||
// 0117) — kept here because removal happens once the declaration that said so is gone, and a
|
||||
// service removed as if it had a state is stopped: the machine's network manager, for one.
|
||||
Stateless bool `json:"stateless,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.
|
||||
|
||||
Reference in New Issue
Block a user