A package may be declared absent, and an uninstalled front end is retired for good (hq ADR 0175) #71
@@ -1407,6 +1407,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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -870,6 +870,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 }
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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