package catalogue import ( "errors" "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(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(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 TestANodeWhoseAssignmentsHoldTheSeatsResolvesWithNothingUnheld(t *testing.T) { web := withResources(mod("web", nil, nil, nil), res("container", "web"), res("service", "web-timer")) cat := shelf(append(coreThree(), web)...) got, err := Resolve(cat, []string{"systemd", "pacman", "docker", "web"}, workstation(), World{}) if err != nil { t.Fatal(err) } if len(got.Unheld) != 0 { t.Errorf("a node holding all three seats reports %v", got.Unheld) } if _, err := AssignRefusal(cat, "workstation", []string{"systemd", "pacman", "docker"}, []string{"web"}); err != nil { t.Errorf("assigning beside the three holders was refused: %v", err) } } func TestAnAssignmentMissingAHolderIsRefusedNamingTheSeatAndItsPossibleHolders(t *testing.T) { web := withResources(mod("web", nil, nil, nil), res("container", "web"), res("service", "web-timer")) cat := shelf(append(coreThree(), web)...) _, err := AssignRefusal(cat, "workstation", []string{"systemd", "pacman"}, []string{"web"}) var refusal *Refusal if !errors.As(err, &refusal) { t.Fatalf("web assigned to a node without a container runtime was not refused: %v", err) } msg := err.Error() for _, want := range []string{"web on workstation depends on " + ContainerRuntimeSeat, "docker", "ADR 0207"} { if !strings.Contains(msg, want) { t.Errorf("the refusal does not say %q:\n%s", want, msg) } } // What the node does hold is not named as missing. if strings.Contains(msg, "depends on "+ServiceManagerSeat) { t.Errorf("the refusal names a seat systemd already holds:\n%s", msg) } } func TestADependencyNoCatalogueModuleCanMeetIsSaidNotRefusedUntilTheSwitch(t *testing.T) { // No runtime module in the catalogue: refusing would stop every container's assignment until one // is written, with no remedy to name. web := withResources(mod("web", nil, nil, nil), res("container", "web")) cat := shelf(web) said, err := AssignRefusal(cat, "workstation", nil, []string{"web"}) if err != nil { t.Fatalf("a dependency nothing could meet was refused: %v", err) } if len(said) != 1 || !strings.Contains(said[0], "no module in the catalogue claims it yet") { t.Errorf("the assignment does not say what it depends on: %v", said) } enforceSeatDependencies = true defer func() { enforceSeatDependencies = false }() if _, err := AssignRefusal(cat, "workstation", nil, []string{"web"}); err == nil { t.Error("with the switch on, a dependency nothing could meet was not refused") } } func TestTheHoldersMutualDependenceResolvesWhenAssignedTogether(t *testing.T) { cat := shelf(coreThree()...) // Alone, each needs the other. if _, err := AssignRefusal(cat, "workstation", nil, []string{"systemd"}); err == nil || !strings.Contains(err.Error(), "pacman") { t.Errorf("systemd alone was not refused naming pacman: %v", err) } if _, err := AssignRefusal(cat, "workstation", nil, []string{"pacman"}); err == nil || !strings.Contains(err.Error(), "systemd") { t.Errorf("pacman alone was not refused naming systemd: %v", err) } // Together, in one act, they meet each other — and docker meets its own seat. if _, err := AssignRefusal(cat, "workstation", nil, []string{"systemd", "pacman", "docker"}); err != nil { t.Errorf("the three holders assigned together were refused: %v", err) } got, err := Resolve(cat, []string{"systemd", "pacman"}, workstation(), World{}) if err != nil { t.Fatal(err) } if len(got.Unheld) != 0 { t.Errorf("systemd and pacman together report %v", got.Unheld) } } func TestStatusIsToldOfAnUnmetDependencyAndTheNodeStillResolves(t *testing.T) { 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 = false }() 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) } } func TestTheFoundationsDeclarationsAreNotJudged(t *testing.T) { // The private network, as the controller computes it: its tools' package and its service are // the mesh's, written per node, and so are never a module's dependency. network := withResources(mod("mesh-wireguard", []string{"private-network"}, nil, nil), res("package", "wireguard-tools"), res("service", "overlay-up")) network.Computed = "mesh-wireguard" // The host, whatever it declares. host := withResources(mod("mesh-host", nil, nil, nil), res("file", "launcher"), res("service", "nox-mesh-host")) cat := shelf(append(coreThree(), network, host)...) for _, m := range []Manifest{network, host} { if d := DependsOn(m); len(d) != 0 { t.Errorf("%s, the foundation's, depends on %v", m.Module, d) } } got, err := Resolve(cat, []string{"mesh-wireguard", "mesh-host"}, workstation(), World{}) if err != nil { t.Fatal(err) } if len(got.Unheld) != 0 { t.Errorf("the foundation on a node with no holders reports %v", got.Unheld) } if _, err := AssignRefusal(cat, "workstation", nil, []string{"mesh-wireguard", "mesh-host"}); err != nil { t.Errorf("assigning the foundation was refused: %v", err) } } func TestUnassigningTheLastHolderWhileDependentsRemainIsRefused(t *testing.T) { sshd := withResources(mod("sshd", nil, nil, nil), res("service", "sshd")) cat := shelf(append(coreThree(), sshd)...) on := []string{"systemd", "pacman", "docker", "sshd"} err := UnassignRefusal(cat, "workstation", on, []string{"systemd"}) if err == nil { t.Fatal("the last service manager came off a node still running services") } for _, want := range []string{ServiceManagerSeat, "sshd", "pacman", "docker"} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not name %q:\n%v", want, err) } } // A dependent comes off freely, and the holders with everything depending on them in one act. if err := UnassignRefusal(cat, "workstation", on, []string{"sshd"}); err != nil { t.Errorf("a dependent's unassignment was refused: %v", err) } if err := UnassignRefusal(cat, "workstation", on, on); err != nil { t.Errorf("unassigning everything together was refused: %v", err) } }