131 lines
5.1 KiB
Go
131 lines
5.1 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0207: a module depends on the node seats that apply its resources.
|
|
|
|
func res(kind, id string) map[string]any {
|
|
r := map[string]any{"id": id, "type": kind}
|
|
switch kind {
|
|
case "service":
|
|
r["unit"] = id + ".service"
|
|
case "package":
|
|
r["package"] = id
|
|
case "container":
|
|
r["image"] = id
|
|
case "file":
|
|
r["path"] = "/etc/" + id
|
|
}
|
|
return r
|
|
}
|
|
|
|
func withResources(m Manifest, rs ...map[string]any) Manifest {
|
|
m.Resources = rs
|
|
return m
|
|
}
|
|
|
|
// The three holders as to-be 42 names them, each declaring what it really does: systemd's own
|
|
// package needs the package manager, pacman's timer needs the service manager, docker's package and
|
|
// service need both.
|
|
func coreThree() []Manifest {
|
|
return []Manifest{
|
|
withResources(mod("systemd", nil, nil, nil, Claim{Name: ServiceManagerSeat}), res("package", "systemd")),
|
|
withResources(mod("pacman", nil, nil, nil, Claim{Name: PackageManagerSeat}), res("service", "pacman-refresh")),
|
|
withResources(mod("docker", nil, nil, nil, Claim{Name: ContainerRuntimeSeat}),
|
|
res("package", "docker"), res("service", "docker")),
|
|
}
|
|
}
|
|
|
|
func TestADependencyIsDerivedFromEachResourceTypeThatAppliesThroughASeat(t *testing.T) {
|
|
cases := map[string][]string{
|
|
"service": {ServiceManagerSeat},
|
|
"package": {PackageManagerSeat},
|
|
"container": {ContainerRuntimeSeat},
|
|
// The host's own acts, or the mesh's: nothing in between holds a role for them.
|
|
"file": nil, "directory": nil, "process": nil, "archive": nil, "user": nil,
|
|
"action": nil, "network": nil, "access": nil,
|
|
}
|
|
for kind, want := range cases {
|
|
got := DependsOn(nil, withResources(mod("m", nil, nil, nil), res(kind, "x")))
|
|
if len(got) == 0 {
|
|
got = nil
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("a %s resource depends on %v, want %v", kind, got, want)
|
|
}
|
|
}
|
|
all := DependsOn(nil, withResources(mod("m", nil, nil, nil),
|
|
res("container", "a"), res("service", "b"), res("package", "c"), res("package", "d")))
|
|
if want := []string{ContainerRuntimeSeat, PackageManagerSeat, ServiceManagerSeat}; !reflect.DeepEqual(all, want) {
|
|
t.Errorf("a module of every kind depends on %v, want each seat once: %v", all, want)
|
|
}
|
|
}
|
|
|
|
func TestTheSeatsThatApplyResourcesAreTheMeshsOwnAtNodeScope(t *testing.T) {
|
|
for _, name := range []string{ServiceManagerSeat, PackageManagerSeat, ContainerRuntimeSeat} {
|
|
s, ok := SeatNamed(name)
|
|
if !ok {
|
|
t.Fatalf("%s is not in the mesh's set", name)
|
|
}
|
|
if s.Scope != ScopeNode {
|
|
t.Errorf("%s is held per %s, want per node", name, s.Scope)
|
|
}
|
|
}
|
|
// No verbs yet for either new seat: ADR 0207 seeds the package manager without a protocol, and
|
|
// the runtime's verbs wait for ADR 0166's acceptance.
|
|
for _, name := range []string{PackageManagerSeat, ContainerRuntimeSeat} {
|
|
if s, _ := SeatNamed(name); len(s.Serves)+len(s.Accepts)+len(s.Emits) > 0 {
|
|
t.Errorf("%s carries a protocol; ADR 0207 seeds it with none", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatusIsToldOfAnUnmetDependencyAndTheNodeStillResolves(t *testing.T) {
|
|
// Before the switch (the state the mesh ran in until every node held the three seats).
|
|
enforceSeatDependencies = false
|
|
defer func() { enforceSeatDependencies = true }()
|
|
web := withResources(mod("web", nil, nil, nil), res("container", "web"))
|
|
cat := shelf(append(coreThree(), web)...)
|
|
got, err := Resolve(cat, []string{"web"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatalf("an unmet dependency refused the node before the switch: %v", err)
|
|
}
|
|
want := []Unheld{{Node: "workstation", Module: "web", Seat: ContainerRuntimeSeat, Holders: []string{"docker"}}}
|
|
if !reflect.DeepEqual(got.Unheld, want) {
|
|
t.Errorf("reported %+v, want %+v", got.Unheld, want)
|
|
}
|
|
}
|
|
|
|
func TestWithTheSwitchFlippedAnUnmetDependencyRefusesTheNode(t *testing.T) {
|
|
enforceSeatDependencies = true
|
|
defer func() { enforceSeatDependencies = true }()
|
|
web := withResources(mod("web", nil, nil, nil), res("container", "web"))
|
|
cat := shelf(append(coreThree(), web)...)
|
|
_, err := Resolve(cat, []string{"web"}, workstation(), World{})
|
|
if err == nil || !strings.Contains(err.Error(), ContainerRuntimeSeat) || !strings.Contains(err.Error(), "docker") {
|
|
t.Fatalf("with the switch on, an unmet dependency gave %v", err)
|
|
}
|
|
// Never in the first pass, whose refusals take a machine off the network instead.
|
|
if _, err := Resolve(cat, []string{"web"}, workstation(), World{Unchecked: true}); err != nil {
|
|
t.Errorf("the first pass refused an unmet dependency: %v", err)
|
|
}
|
|
}
|
|
|
|
// A module-declared seat's needs are judged exactly as a compiled seat's once were (novox/hq ADR
|
|
// 0220, ADR 0378 §8): "mail" declares the node seat "mail-notifier" needing node-notifier, the
|
|
// mesh's own (desktop notifications); "notifier" claims mail-notifier; "dunst" claims node-notifier,
|
|
// as it does in the real catalogue.
|
|
func mailNotifierAndDunst() map[string]Manifest {
|
|
return shelf(
|
|
Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
|
|
Name: "mail-notifier", Scope: ScopeNode, Needs: []string{NotifierSeat},
|
|
}}},
|
|
mod("notifier", nil, nil, nil, Claim{Name: "mail-notifier"}),
|
|
mod("dunst", nil, nil, nil, Claim{Name: NotifierSeat}),
|
|
)
|
|
}
|