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:
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user