Third review of mesh-catalog #147. A unit's name can now come from a setting (a template's instance), so every systemctl call that names a unit passes `--` before it, and a name can never be read as an option. On removal, a unit is started or enabled again only when it is exactly the stopped and disabled the mesh held it to. A masked unit, or one somebody started or enabled since, is left as it is, and the report says why. A restore that fails (the start refused, or the unit not running after it) puts back what this attempt changed and is reported as waiting, so no apply is made fatal by it and the next one tries again. The tests' fake service managers read systemctl's arguments in the shape they were written for (oldShape); internal/system's own test holds the `--`.
219 lines
7.5 KiB
Go
219 lines
7.5 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// A taken container keeps a found network by a per-machine setting (novox/hq ADR 0163, rule 4):
|
|
// joined once it runs, part of its spec, and refused when it cannot be joined.
|
|
func TestAContainerJoinsTheNetworksItKeeps(t *testing.T) {
|
|
var ran []string
|
|
connectFails := false
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
args = oldShape(name, args)
|
|
if name != "docker" {
|
|
return "", errors.New("not installed")
|
|
}
|
|
ran = append(ran, strings.Join(args, " "))
|
|
switch args[0] {
|
|
case "info":
|
|
return "29.0.0\n", nil
|
|
case "container":
|
|
if len(ran) > 2 {
|
|
return "true\t" + specOfLast, nil
|
|
}
|
|
return "false\t\n", errors.New("no such container")
|
|
case "run":
|
|
return "deadbeef\n", nil
|
|
case "network":
|
|
if connectFails {
|
|
return "", errors.New("network predecessor_default not found")
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"app","type":"container","name":"app","image":"`+pinned+`",
|
|
"networks":["predecessor_default"]}
|
|
]}`)
|
|
specOfLast = containerSpec(d.Resources[0].(*declaration.Container), inputs{})
|
|
alone := *d.Resources[0].(*declaration.Container)
|
|
alone.Networks = nil
|
|
if specOfLast == containerSpec(&alone, inputs{}) {
|
|
t.Fatal("the kept network is not part of the container's spec: kept or let go, the container would be left alone")
|
|
}
|
|
report, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
joined := false
|
|
for i, line := range ran {
|
|
if line == "network connect predecessor_default app" {
|
|
joined = true
|
|
if ran[i-1] != "container inspect --format {{.State.Running}}\t{{index .Config.Labels \""+specLabel+"\"}} app" &&
|
|
!strings.HasPrefix(ran[i-1], "container inspect") {
|
|
t.Errorf("joined before the container was read back as running: %v", ran)
|
|
}
|
|
}
|
|
}
|
|
if !joined || report.Outcomes[0].Action != "created" {
|
|
t.Fatalf("the container did not join the kept network: %v\n%+v", ran, report.Outcomes)
|
|
}
|
|
|
|
connectFails, ran = true, nil
|
|
if _, _, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil); err == nil ||
|
|
!strings.Contains(err.Error(), "could not join the kept network predecessor_default") {
|
|
t.Fatalf("a network that cannot be joined was passed over: %v", err)
|
|
}
|
|
}
|
|
|
|
var specOfLast string
|
|
|
|
// A module the mesh left out of a declaration is not a module the mesh removed (novox/hq ADR 0163,
|
|
// rule 6): what the host wrote for it stays, recorded and said; what it holds for it stays held.
|
|
// A module simply absent is removed as it always was.
|
|
func TestALeftOutModuleIsNeitherRemovedNorForgotten(t *testing.T) {
|
|
var removed []string
|
|
gone := map[string]bool{}
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
args = oldShape(name, args)
|
|
if name != "docker" {
|
|
return "", nil
|
|
}
|
|
switch args[0] {
|
|
case "info":
|
|
return "29.0.0\n", nil
|
|
case "rm":
|
|
removed = append(removed, args[len(args)-1])
|
|
gone[args[len(args)-1]] = true
|
|
case "container":
|
|
if gone[args[len(args)-1]] {
|
|
return "", errors.New("no such container")
|
|
}
|
|
return "true\tspec", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
known := store.State{
|
|
Resources: []store.Applied{
|
|
{ID: "web.server", Type: "container", Target: "web", Origin: store.OriginDeclared},
|
|
{ID: "old.server", Type: "container", Target: "old", Origin: store.OriginDeclared},
|
|
},
|
|
Held: []store.Held{{ID: "web.page", Module: "web", Kind: "file", Target: "/srv/web/index.html"}},
|
|
}
|
|
d := parse(t, `{"declaration":1,"left_out":["web"],"resources":[
|
|
{"id":"notes.conf","type":"file","path":"`+t.TempDir()+`/notes.conf","content":"x"}
|
|
]}`)
|
|
report, state, err := Apply(context.Background(), archHost(t), d, known, store.OriginDeclared, run, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(removed) != 1 || removed[0] != "old" {
|
|
t.Fatalf("removed %v; only the module that is absent goes", removed)
|
|
}
|
|
if _, kept := state.At("container", "web"); !kept {
|
|
t.Fatal("the left-out module's record was forgotten")
|
|
}
|
|
if _, held := state.HeldAt("web.page"); !held {
|
|
t.Fatal("the left-out module's hold was released")
|
|
}
|
|
said := false
|
|
for _, o := range report.Outcomes {
|
|
if o.ID == "web.server" && o.Action == "unchanged" && strings.Contains(o.Detail, "web was left out of this declaration by the mesh") {
|
|
said = true
|
|
}
|
|
if o.ID == "web.server" && o.Action != "unchanged" {
|
|
t.Errorf("the left-out module's container was %s", o.Action)
|
|
}
|
|
}
|
|
if !said {
|
|
t.Fatalf("keeping the left-out module's container was not said: %+v", report.Outcomes)
|
|
}
|
|
}
|
|
|
|
// A container's capabilities reach the runtime and are part of its spec (novox/hq ADR 0170).
|
|
func TestACapabilityReachesTheRuntimeAndTheSpec(t *testing.T) {
|
|
var ran []string
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
args = oldShape(name, args)
|
|
if name != "docker" {
|
|
return "", errors.New("not installed")
|
|
}
|
|
switch args[0] {
|
|
case "info":
|
|
return "29.0.0\n", nil
|
|
case "container":
|
|
return "false\t\n", errors.New("no such container")
|
|
case "run":
|
|
ran = args
|
|
return "deadbeef\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
d := parseTrusted(t, `{"declaration":1,"resources":[
|
|
{"id":"fw","type":"container","name":"fw","image":"`+pinned+`","network":"host","capabilities":["NET_ADMIN"]}
|
|
]}`)
|
|
_, _, _ = Apply(context.Background(), archHost(t), d, store.State{}, store.OriginCarried, run, nil, nil)
|
|
granted := false
|
|
for i, a := range ran {
|
|
if a == "--cap-add" && i+1 < len(ran) && ran[i+1] == "NET_ADMIN" {
|
|
granted = true
|
|
}
|
|
}
|
|
if !granted {
|
|
t.Fatalf("the capability was not granted: %v", ran)
|
|
}
|
|
with := d.Resources[0].(*declaration.Container)
|
|
without := *with
|
|
without.Capabilities = nil
|
|
if containerSpec(with, inputs{}) == containerSpec(&without, inputs{}) {
|
|
t.Fatal("a capability is not part of the container's spec")
|
|
}
|
|
}
|
|
|
|
// A package may be declared absent (novox/hq ADR 0180): 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) {
|
|
args = oldShape(name, args)
|
|
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)
|
|
}
|
|
}
|