Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a95c2b6ea9 | ||
|
|
5b73e04192 | ||
|
|
f57386cdea | ||
|
|
f92dd3e286 |
@@ -58,6 +58,8 @@ type Outcome struct {
|
|||||||
kept string
|
kept string
|
||||||
// stateless is a service whose unit's lifecycle is the machine's (novox/hq ADR 0117).
|
// stateless is a service whose unit's lifecycle is the machine's (novox/hq ADR 0117).
|
||||||
stateless bool
|
stateless bool
|
||||||
|
// scope and user are, for a service, whose manager it was applied through (novox/hq ADR 0177).
|
||||||
|
scope, user string
|
||||||
// found is, for a service, its unit as the host first found it (novox/hq ADR 0118).
|
// found is, for a service, its unit as the host first found it (novox/hq ADR 0118).
|
||||||
found *store.FoundUnit
|
found *store.FoundUnit
|
||||||
// reads is, for a container, the digest of each file it was created reading, by path — so
|
// reads is, for a container, the digest of each file it was created reading, by path — so
|
||||||
@@ -563,6 +565,8 @@ func ApplyKeeping(
|
|||||||
Kept: kept,
|
Kept: kept,
|
||||||
Reads: outcome.reads,
|
Reads: outcome.reads,
|
||||||
Stateless: outcome.stateless,
|
Stateless: outcome.stateless,
|
||||||
|
Scope: outcome.scope,
|
||||||
|
User: outcome.user,
|
||||||
Found: outcome.found,
|
Found: outcome.found,
|
||||||
Holds: holds(resource),
|
Holds: holds(resource),
|
||||||
})
|
})
|
||||||
@@ -1043,10 +1047,12 @@ type unitReloader interface {
|
|||||||
|
|
||||||
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||||
changed map[string]bool, previous store.Applied) (Outcome, error) {
|
changed map[string]bool, previous store.Applied) (Outcome, error) {
|
||||||
|
run = managerFor(r.Scope, r.User, run)
|
||||||
if r.Stateless() {
|
if r.Stateless() {
|
||||||
return reflectOnly(ctx, sys, r, run, changed)
|
return reflectOnly(ctx, sys, r, run, changed)
|
||||||
}
|
}
|
||||||
out := begin(r)
|
out := begin(r)
|
||||||
|
out.scope, out.user = r.Scope, r.User
|
||||||
var changes []string
|
var changes []string
|
||||||
|
|
||||||
// A file the service reflects changed, and it may be the unit's own file or a drop-in: the
|
// A file the service reflects changed, and it may be the unit's own file or a drop-in: the
|
||||||
@@ -1186,7 +1192,9 @@ func applyService(ctx context.Context, sys system.System, r *declaration.Service
|
|||||||
// a machine that uses another — and it reads the change when whatever starts it does.
|
// 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,
|
func reflectOnly(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
|
||||||
changed map[string]bool) (Outcome, error) {
|
changed map[string]bool) (Outcome, error) {
|
||||||
|
run = managerFor(r.Scope, r.User, run)
|
||||||
out := begin(r)
|
out := begin(r)
|
||||||
|
out.scope, out.user = r.Scope, r.User
|
||||||
out.stateless = true
|
out.stateless = true
|
||||||
restart := reflected(r, changed)
|
restart := reflected(r, changed)
|
||||||
reload := restartedBy(r.ReloadOn, changed)
|
reload := restartedBy(r.ReloadOn, changed)
|
||||||
@@ -1407,6 +1415,25 @@ func applyPackage(ctx context.Context, sys system.System, r *declaration.Package
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
|
if r.Absent {
|
||||||
|
// Declared absent (novox/hq ADR 0175): removed when it is here, left alone when it is not.
|
||||||
|
if !installed {
|
||||||
|
out.Action = "unchanged"
|
||||||
|
out.Detail = "not installed, as declared"
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
if err := sys.RemovePackage(ctx, run, r.Package); err != nil {
|
||||||
|
return out, fmt.Errorf("removing %s: %w", r.Package, err)
|
||||||
|
}
|
||||||
|
if still, err := sys.PackageInstalled(ctx, run, r.Package); err != nil {
|
||||||
|
return out, err
|
||||||
|
} else if still {
|
||||||
|
return out, fmt.Errorf("%s was removed without error and the package database still has it", r.Package)
|
||||||
|
}
|
||||||
|
out.Action = "removed"
|
||||||
|
out.Detail = "declared absent; its configuration is left where the package manager leaves it"
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
if installed {
|
if installed {
|
||||||
out.Action = "unchanged"
|
out.Action = "unchanged"
|
||||||
out.Detail = "already installed"
|
out.Detail = "already installed"
|
||||||
@@ -2216,6 +2243,7 @@ func declaredDigest(r declaration.Resource) string {
|
|||||||
// what was actually done — a unit already as it was found is forgotten, not "restored".
|
// 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,
|
func removeService(ctx context.Context, sys system.System, a store.Applied, run Runner,
|
||||||
made bool) (string, string, error) {
|
made bool) (string, string, error) {
|
||||||
|
run = managerFor(a.Scope, a.User, run)
|
||||||
if a.Stateless {
|
if a.Stateless {
|
||||||
// Declared with no state (novox/hq ADR 0117): its lifecycle was never the mesh's, and the
|
// Declared with no state (novox/hq ADR 0117): its lifecycle was never the mesh's, and the
|
||||||
// declaration that said so is the operator's word to hold to, a file of the mesh's or not.
|
// declaration that said so is the operator's word to hold to, a file of the mesh's or not.
|
||||||
@@ -2390,3 +2418,23 @@ func moduleOf(identity string) (string, bool) {
|
|||||||
}
|
}
|
||||||
return identity[:at], true
|
return identity[:at], true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// managerFor routes a unit's commands to the manager it belongs to (novox/hq ADR 0177). A system
|
||||||
|
// unit's go to the machine's manager as they always did. A user-scoped unit's go to the account's
|
||||||
|
// own: `systemctl --user --machine=<account>@`, which reaches that manager from the host's own
|
||||||
|
// process without an environment to forge or a user to switch to — and which only answers while
|
||||||
|
// the account's manager runs (a login, or lingering enabled for the account). Done on the runner
|
||||||
|
// rather than in each system: every system's reading of a unit already goes through `systemctl`,
|
||||||
|
// so this is one place instead of one per system and one per method.
|
||||||
|
func managerFor(scope, user string, run Runner) Runner {
|
||||||
|
if scope != declaration.ScopeUser || user == "" {
|
||||||
|
return run
|
||||||
|
}
|
||||||
|
return func(ctx context.Context, name string, args ...string) (string, error) {
|
||||||
|
if name != "systemctl" {
|
||||||
|
return run(ctx, name, args...)
|
||||||
|
}
|
||||||
|
scoped := append([]string{"--user", "--machine=" + user + "@"}, args...)
|
||||||
|
return run(ctx, name, scoped...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,3 +173,42 @@ func TestACapabilityReachesTheRuntimeAndTheSpec(t *testing.T) {
|
|||||||
t.Fatal("a capability is not part of the container's spec")
|
t.Fatal("a capability is not part of the container's spec")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A package may be declared absent (novox/hq ADR 0175): removed when it is installed, read back,
|
||||||
|
// left alone when it is not.
|
||||||
|
func TestAPackageDeclaredAbsentIsRemovedWhenPresentAndLeftWhenNot(t *testing.T) {
|
||||||
|
installed := true
|
||||||
|
var ran []string
|
||||||
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
||||||
|
ran = append(ran, name+" "+strings.Join(args, " "))
|
||||||
|
if name != "pacman" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
switch args[0] {
|
||||||
|
case "-Q":
|
||||||
|
if args[1] == "pacman" || installed {
|
||||||
|
return args[1] + " 1.0\n", nil
|
||||||
|
}
|
||||||
|
return "", errors.New("package not found")
|
||||||
|
case "-R":
|
||||||
|
installed = false
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
d := parseTrusted(t, `{"declaration":1,"resources":[{"id":"front-end","type":"package","package":"ufw","absent":true}]}`)
|
||||||
|
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if report.Outcomes[0].Action != "removed" || !strings.Contains(strings.Join(ran, "\n"), "pacman -R --noconfirm ufw") {
|
||||||
|
t.Fatalf("an installed package declared absent was not removed: %+v\n%v", report.Outcomes[0], ran)
|
||||||
|
}
|
||||||
|
ran = nil
|
||||||
|
report, _, err = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if report.Outcomes[0].Action != "unchanged" || strings.Contains(strings.Join(ran, "\n"), "-R") {
|
||||||
|
t.Fatalf("a package already absent was touched: %+v\n%v", report.Outcomes[0], ran)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ func retireFirewall(ctx context.Context, d *declaration.Declaration, origin stri
|
|||||||
if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive {
|
if origin != store.OriginDeclared || d.Adoption != nil || rec == nil || rec.Kind != string(firewall.UFW) || !rec.WasActive {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
if !firewall.Installed(ctx, run) {
|
||||||
|
// Uninstalled (novox/hq ADR 0175): retired for good, by the module that replaced it. Said
|
||||||
|
// once, and nothing is asked of a command that is not there.
|
||||||
|
if rec.RetiredBy != firewall.RetiredRemoved {
|
||||||
|
rec.RetiredBy = firewall.RetiredRemoved
|
||||||
|
log(" the found firewall (ufw) is no longer installed; the mesh's filter is what filters this machine")
|
||||||
|
return "removed: ufw is no longer installed; the mesh's filter is what filters this machine", nil
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
active := firewall.Active(ctx, run)
|
active := firewall.Active(ctx, run)
|
||||||
if !active && !(rec.Forward != nil && !rec.DisabledByMesh) {
|
if !active && !(rec.Forward != nil && !rec.DisabledByMesh) {
|
||||||
// Inactive, and either the mesh's doing already or nobody's recorded here: said as found,
|
// Inactive, and either the mesh's doing already or nobody's recorded here: said as found,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/novox/mesh-host/internal/declaration"
|
"github.com/novox/mesh-host/internal/declaration"
|
||||||
"github.com/novox/mesh-host/internal/store"
|
"github.com/novox/mesh-host/internal/store"
|
||||||
@@ -522,3 +523,33 @@ func TestUfwIsNotRetiredUntilTheMeshsOwnFilterIsLoaded(t *testing.T) {
|
|||||||
t.Errorf("ufw was not retired once the mesh's filter was loaded: active %v, %+v", u.active, state.Firewall)
|
t.Errorf("ufw was not retired once the mesh's filter was loaded: active %v, %+v", u.active, state.Firewall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A front end that is no longer installed is recorded as removed, said once, and asked nothing of
|
||||||
|
// (novox/hq ADR 0175).
|
||||||
|
func TestAnUninstalledFrontEndIsRetiredForGood(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
u := &ufwMachine{installed: false, ruleset: "table inet mesh\n"}
|
||||||
|
known := store.State{Firewall: &store.FoundFirewall{Kind: "ufw", WasActive: true, DisabledByMesh: true,
|
||||||
|
RetiredBy: "mesh", FoundAt: time.Now()}}
|
||||||
|
converged := parse(t, `{"declaration":1,"resources":[`+withConf(dir)+`]}`)
|
||||||
|
report, state, err := applyWith(t, converged, known, u.run)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if state.Firewall.RetiredBy != "removed" || !strings.Contains(report.Firewall, "no longer installed") {
|
||||||
|
t.Fatalf("record %+v, said %q", state.Firewall, report.Firewall)
|
||||||
|
}
|
||||||
|
u.asked = nil
|
||||||
|
report, _, err = applyWith(t, converged, state, u.run)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if report.Firewall != "" {
|
||||||
|
t.Errorf("said again: %q", report.Firewall)
|
||||||
|
}
|
||||||
|
for _, a := range u.asked {
|
||||||
|
if strings.HasPrefix(a, "ufw") && a != "ufw status" {
|
||||||
|
t.Errorf("asked something of a front end that is not there: %v", u.asked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package apply
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-host/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Defends novox/hq ADR 0177: a unit in the operator account's own service manager is applied
|
||||||
|
// through that manager — `systemctl --user --machine=<account>@` — and never as a system unit of
|
||||||
|
// the same name; its record remembers the scope so removal goes the same way.
|
||||||
|
|
||||||
|
// accountManager is a user's service manager whose one unit starts when asked, recording the
|
||||||
|
// commands and refusing any that reach it outside the account's scope.
|
||||||
|
func accountManager(account string, commands *[]string) Runner {
|
||||||
|
active, enabled := false, false
|
||||||
|
return func(_ context.Context, name string, args ...string) (string, error) {
|
||||||
|
line := name + " " + strings.Join(args, " ")
|
||||||
|
*commands = append(*commands, line)
|
||||||
|
if name == "systemctl" && !strings.HasPrefix(line, "systemctl --user --machine="+account+"@ ") {
|
||||||
|
return "", fmt.Errorf("a system-scope command reached the account's manager: %s", line)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.Contains(line, " start "):
|
||||||
|
active = true
|
||||||
|
return "", nil
|
||||||
|
case strings.Contains(line, " stop "):
|
||||||
|
active = false
|
||||||
|
return "", nil
|
||||||
|
case strings.Contains(line, " enable "):
|
||||||
|
enabled = true
|
||||||
|
return "", nil
|
||||||
|
case strings.Contains(line, " disable "):
|
||||||
|
enabled = false
|
||||||
|
return "", nil
|
||||||
|
case strings.Contains(line, "is-enabled"):
|
||||||
|
if enabled {
|
||||||
|
return "enabled", nil
|
||||||
|
}
|
||||||
|
return "disabled", 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 TestAUserScopedUnitIsAppliedThroughTheAccountsManager(t *testing.T) {
|
||||||
|
var commands []string
|
||||||
|
decl := `{"declaration":1,"resources":[
|
||||||
|
{"id":"i3.watcher","type":"service","unit":"i3-reload-watcher.service","state":"running","boot":"enabled","scope":"user","user":"ops"}
|
||||||
|
]}`
|
||||||
|
report, known, err := Apply(context.Background(), archHost(t), parse(t, decl),
|
||||||
|
store.State{}, store.OriginDeclared, accountManager("ops", &commands), nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("apply: %v\n%s", err, strings.Join(commands, "\n"))
|
||||||
|
}
|
||||||
|
if !report.Changed() {
|
||||||
|
t.Fatal("a unit that was stopped and is now running changed nothing")
|
||||||
|
}
|
||||||
|
var started, enabled bool
|
||||||
|
for _, c := range commands {
|
||||||
|
if c == "systemctl --user --machine=ops@ start i3-reload-watcher.service" {
|
||||||
|
started = true
|
||||||
|
}
|
||||||
|
if c == "systemctl --user --machine=ops@ enable i3-reload-watcher.service" {
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !started || !enabled {
|
||||||
|
t.Fatalf("the unit was not started and enabled in the account's manager:\n%s", strings.Join(commands, "\n"))
|
||||||
|
}
|
||||||
|
recorded, ok := known.At("service", "i3-reload-watcher.service")
|
||||||
|
if !ok || recorded.Scope != "user" || recorded.User != "ops" {
|
||||||
|
t.Fatalf("the record does not say whose manager the unit is in: %+v", recorded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestASystemUnitIsUntouchedByTheScope(t *testing.T) {
|
||||||
|
var commands []string
|
||||||
|
decl := `{"declaration":1,"resources":[
|
||||||
|
{"id":"x.daemon","type":"service","unit":"sshd.service","state":"running","boot":"enabled"}
|
||||||
|
]}`
|
||||||
|
if _, _, err := Apply(context.Background(), archHost(t), parse(t, decl),
|
||||||
|
store.State{}, store.OriginDeclared, unitIn(true, &commands), nil, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, c := range commands {
|
||||||
|
if strings.Contains(c, "--user") || strings.Contains(c, "--machine") {
|
||||||
|
t.Fatalf("a system unit was addressed to an account's manager: %s", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -684,6 +684,16 @@ type Service struct {
|
|||||||
// declaration that reports success and stops being true at the next power cut.
|
// declaration that reports success and stops being true at the next power cut.
|
||||||
Boot string `json:"boot,omitempty"`
|
Boot string `json:"boot,omitempty"`
|
||||||
|
|
||||||
|
// Scope is whose service manager the unit belongs to: "system" (absent means system), or
|
||||||
|
// "user" — the operator account's own manager (novox/hq ADR 0177). A workstation's per-user
|
||||||
|
// daemons — a window manager's reload watcher, an audio mask, a memory guard — are units in
|
||||||
|
// that manager, and until this they had no form the mesh could send. A user-scoped unit names
|
||||||
|
// its User; the host talks to that account's manager and never starts a system unit by the
|
||||||
|
// same name.
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
// User is the account whose manager a user-scoped unit lives in. Required with scope "user",
|
||||||
|
// refused otherwise; a module names it ${machine:account} and never the person.
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
// RestartOn names resources whose change means this service must be restarted.
|
// RestartOn names resources whose change means this service must be restarted.
|
||||||
//
|
//
|
||||||
// Because a running service does not re-read its configuration. Replace the file, find the
|
// Because a running service does not re-read its configuration. Replace the file, find the
|
||||||
@@ -729,11 +739,33 @@ func (s *Service) Identity() string { return s.ID }
|
|||||||
func (s *Service) Kind() Type { return TypeService }
|
func (s *Service) Kind() Type { return TypeService }
|
||||||
func (s *Service) Target() string { return s.Unit }
|
func (s *Service) Target() string { return s.Unit }
|
||||||
|
|
||||||
|
// ScopeSystem and ScopeUser are the two managers a unit may belong to (novox/hq ADR 0177).
|
||||||
|
const (
|
||||||
|
ScopeSystem = "system"
|
||||||
|
ScopeUser = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserScoped is whether this unit lives in an account's own service manager.
|
||||||
|
func (s *Service) UserScoped() bool { return s.Scope == ScopeUser }
|
||||||
|
|
||||||
func (s *Service) validate(where string, _ bool) []string {
|
func (s *Service) validate(where string, _ bool) []string {
|
||||||
var problems []string
|
var problems []string
|
||||||
if s.Unit == "" {
|
if s.Unit == "" {
|
||||||
problems = append(problems, where+": a service needs a unit")
|
problems = append(problems, where+": a service needs a unit")
|
||||||
}
|
}
|
||||||
|
switch s.Scope {
|
||||||
|
case "", ScopeSystem:
|
||||||
|
if s.User != "" {
|
||||||
|
problems = append(problems, where+": a system unit names no user; only a user-scoped unit does")
|
||||||
|
}
|
||||||
|
case ScopeUser:
|
||||||
|
if s.User == "" {
|
||||||
|
problems = append(problems, where+": a user-scoped unit names the account whose manager it lives in")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s: scope %q; a unit is in the \"system\" manager or the operator account's \"user\" one", where, s.Scope))
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case s.State == "running" || s.State == "stopped":
|
case s.State == "running" || s.State == "stopped":
|
||||||
case s.State != "":
|
case s.State != "":
|
||||||
@@ -870,6 +902,12 @@ type Package struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type Type `json:"type"`
|
Type Type `json:"type"`
|
||||||
Package string `json:"package"`
|
Package string `json:"package"`
|
||||||
|
// Absent declares that the package is NOT installed (novox/hq ADR 0175): the host removes it
|
||||||
|
// when it is, and leaves a machine that never had it alone. For the one case a module replaces
|
||||||
|
// software the machine was found with and the operator has decided it does not come back — the
|
||||||
|
// firewall front end a converged machine's filter module retired. Nothing to undo when the
|
||||||
|
// declaration drops it: the host does not install what a declaration stopped saying is absent.
|
||||||
|
Absent bool `json:"absent,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Package) Identity() string { return p.ID }
|
func (p *Package) Identity() string { return p.ID }
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package declaration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// novox/hq ADR 0177: a unit is in the system manager or an account's own; a user-scoped one names
|
||||||
|
// the account, a system one may not, and any other word is refused.
|
||||||
|
func TestAUserScopedUnitNamesItsAccountAndASystemOneMayNot(t *testing.T) {
|
||||||
|
cases := []struct{ scope, user, wants string }{
|
||||||
|
{"user", "ops", ""},
|
||||||
|
{"", "", ""},
|
||||||
|
{"system", "", ""},
|
||||||
|
{"user", "", "names the account"},
|
||||||
|
{"", "ops", "names no user"},
|
||||||
|
{"session", "ops", "scope \"session\""},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
s := &Service{ID: "m.u", Type: TypeService, Unit: "u.service", State: "running", Scope: c.scope, User: c.user}
|
||||||
|
problems := s.validate("m.u", false)
|
||||||
|
got := strings.Join(problems, "; ")
|
||||||
|
if c.wants == "" && len(problems) != 0 {
|
||||||
|
t.Fatalf("scope %q user %q refused: %s", c.scope, c.user, got)
|
||||||
|
}
|
||||||
|
if c.wants != "" && !strings.Contains(got, c.wants) {
|
||||||
|
t.Fatalf("scope %q user %q: wanted a refusal saying %q, got %q", c.scope, c.user, c.wants, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -319,8 +319,17 @@ func Active(ctx context.Context, run Runner) bool {
|
|||||||
return err == nil && statusActive(out)
|
return err == nil && statusActive(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Installed says whether ufw is on this machine at all: a command that is not there is a front end
|
||||||
|
// that was uninstalled (novox/hq ADR 0175), not one that is silent.
|
||||||
|
func Installed(ctx context.Context, run Runner) bool {
|
||||||
|
_, err := run(ctx, "ufw", "status")
|
||||||
|
return !missing(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Retirements of a found firewall, as the host records them.
|
// Retirements of a found firewall, as the host records them.
|
||||||
const (
|
const (
|
||||||
RetiredByMesh = "mesh"
|
RetiredByMesh = "mesh"
|
||||||
RetiredFoundSo = "found-inactive"
|
RetiredFoundSo = "found-inactive"
|
||||||
|
// RetiredRemoved is a front end uninstalled by the module that replaced it (ADR 0175).
|
||||||
|
RetiredRemoved = "removed"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ type Applied struct {
|
|||||||
// 0117) — kept here because removal happens once the declaration that said so is gone, and a
|
// 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.
|
// service removed as if it had a state is stopped: the machine's network manager, for one.
|
||||||
Stateless bool `json:"stateless,omitempty"`
|
Stateless bool `json:"stateless,omitempty"`
|
||||||
|
// Scope and User are, for a service in an account's own manager (novox/hq ADR 0177), which
|
||||||
|
// manager — so removal gives the unit back through the same one it was applied through.
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
|
||||||
// Found is, for a service, the state its unit was in when this host first applied it — before
|
// 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
|
// the mesh started, stopped, enabled or disabled anything. Removal gives that back and nothing
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ func (a alpine) PackageInstalled(ctx context.Context, run Runner, name string) (
|
|||||||
return strings.TrimSpace(out) != "", nil
|
return strings.TrimSpace(out) != "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (alpine) RemovePackage(ctx context.Context, run Runner, name string) error {
|
||||||
|
_, err := run(ctx, "apk", "del", name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (alpine) InstallPackage(ctx context.Context, run Runner, name string) error {
|
func (alpine) InstallPackage(ctx context.Context, run Runner, name string) error {
|
||||||
_, err := run(ctx, "apk", "add", "--no-cache", name)
|
_, err := run(ctx, "apk", "add", "--no-cache", name)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ func (a android) InstallPackage(context.Context, Runner, string) error {
|
|||||||
return fmt.Errorf("%w: package", ErrUnsupported)
|
return fmt.Errorf("%w: package", ErrUnsupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a android) RemovePackage(context.Context, Runner, string) error {
|
||||||
|
return fmt.Errorf("%w: package", ErrUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
func (a android) ServiceState(context.Context, Runner, string) (string, error) {
|
func (a android) ServiceState(context.Context, Runner, string) (string, error) {
|
||||||
return "", fmt.Errorf("%w: service (init is not reachable without root)", ErrUnsupported)
|
return "", fmt.Errorf("%w: service (init is not reachable without root)", ErrUnsupported)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ func (a arch) PackageInstalled(ctx context.Context, run Runner, name string) (bo
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemovePackage removes one package and nothing it depends on: `-R`, not `-Rs`, because what else
|
||||||
|
// relied on a dependency is not this declaration's to know. pacman keeps a configuration file the
|
||||||
|
// operator changed as `.pacsave`, which is what "never flushed" comes to once the front end is gone.
|
||||||
|
func (arch) RemovePackage(ctx context.Context, run Runner, name string) error {
|
||||||
|
_, err := run(ctx, "pacman", "-R", "--noconfirm", name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (arch) InstallPackage(ctx context.Context, run Runner, name string) error {
|
func (arch) InstallPackage(ctx context.Context, run Runner, name string) error {
|
||||||
out, err := run(ctx, "pacman", "-S", "--noconfirm", "--needed", name)
|
out, err := run(ctx, "pacman", "-S", "--noconfirm", "--needed", name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ type System interface {
|
|||||||
|
|
||||||
PackageInstalled(ctx context.Context, run Runner, name string) (bool, error)
|
PackageInstalled(ctx context.Context, run Runner, name string) (bool, error)
|
||||||
InstallPackage(ctx context.Context, run Runner, name string) error
|
InstallPackage(ctx context.Context, run Runner, name string) error
|
||||||
|
// RemovePackage uninstalls one package, leaving its dependencies and anything the operator
|
||||||
|
// changed in its configuration where the package manager leaves them (novox/hq ADR 0175).
|
||||||
|
RemovePackage(ctx context.Context, run Runner, name string) error
|
||||||
|
|
||||||
// ServiceState is "running" or "stopped". A unit that does not exist is an error, never
|
// ServiceState is "running" or "stopped". A unit that does not exist is an error, never
|
||||||
// "stopped" — reporting absence as satisfaction is the fault this host exists to prevent.
|
// "stopped" — reporting absence as satisfaction is the fault this host exists to prevent.
|
||||||
|
|||||||
Reference in New Issue
Block a user