package main import ( "encoding/json" "strings" "testing" "github.com/novox/mesh-controller/internal/catalogue" ) // Defends novox/hq ADR 0207 at the controller's acts: `assign` refuses a module whose resources a // seat nothing on the node holds applies, `unassign` refuses taking the last holder from under its // dependents, and `status` reports what composition does not yet refuse. func serviceManagerHolder() catalogue.Manifest { return catalogue.Manifest{Module: "systemd", Version: "1", Claims: []catalogue.Claim{{Name: catalogue.ServiceManagerSeat, Scope: catalogue.ScopeNode, Serves: []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal"}}}, Resources: []map[string]any{{"id": "systemd", "type": "package", "package": "systemd"}}} } func packageManagerHolder() catalogue.Manifest { return catalogue.Manifest{Module: "pacman", Version: "1", Claims: []catalogue.Claim{{Name: catalogue.PackageManagerSeat, Scope: catalogue.ScopeNode}}, Resources: []map[string]any{{"id": "refresh", "type": "service", "unit": "pacman-refresh.timer"}}} } func aDaemon() catalogue.Manifest { return catalogue.Manifest{Module: "sshd", Version: "1", Resources: []map[string]any{{"id": "sshd", "type": "service", "unit": "sshd.service"}}} } func TestAnAssignmentWithoutItsHolderIsRefusedAndNotKept(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, serviceManagerHolder()) register(t, open, packageManagerHolder()) register(t, open, aDaemon()) _, err := assign(ctx, open, "laptop", "sshd") if err == nil { t.Fatal("sshd went onto a machine nothing holds the service manager of") } for _, want := range []string{catalogue.ServiceManagerSeat, "systemd", "ADR 0207"} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not say %q:\n%v", want, err) } } assigned, err := open.inventory.Assigned(ctx, "laptop") if err != nil { t.Fatal(err) } if contains(assigned, "sshd") { t.Fatalf("a refused assignment was kept: %v", assigned) } // The holders depend on each other, so neither goes on alone — and both go on in one act. if _, err := assign(ctx, open, "laptop", "systemd"); err == nil { t.Fatal("systemd went on alone though its package needs a package manager") } if said, err := assign(ctx, open, "laptop", "systemd", "pacman"); err != nil { t.Fatalf("the two holders assigned together were refused: %v\n%s", err, said) } if said, err := assign(ctx, open, "laptop", "sshd"); err != nil { t.Fatalf("sshd beside its holder was refused: %v\n%s", err, said) } } func TestTheControllerSeatsAssignTakesSeveralModulesAsOneAct(t *testing.T) { argv, err := argvFor("assign", map[string]any{"node": "laptop", "module": "systemd, pacman"}) if err != nil { t.Fatal(err) } if strings.Join(argv, " ") != "assign laptop systemd pacman" { t.Errorf("the seat's assign became %v", argv) } } func TestUnassigningTheLastHolderUnderItsDependentsIsRefused(t *testing.T) { open := aMesh(t) ctx := t.Context() register(t, open, serviceManagerHolder()) register(t, open, packageManagerHolder()) register(t, open, aDaemon()) if _, err := assign(ctx, open, "laptop", "systemd", "pacman", "sshd"); err != nil { t.Fatal(err) } _, err := unassign(ctx, open, "laptop", "systemd") if err == nil { t.Fatal("the service manager came off a machine still running services") } for _, want := range []string{catalogue.ServiceManagerSeat, "sshd", "pacman"} { if !strings.Contains(err.Error(), want) { t.Errorf("the refusal does not name %q:\n%v", want, err) } } assigned, _ := open.inventory.Assigned(ctx, "laptop") if !contains(assigned, "systemd") { t.Fatalf("a refused unassignment took the module off anyway: %v", assigned) } if _, err := unassign(ctx, open, "laptop", "sshd"); err != nil { t.Fatalf("a dependent could not come off: %v", err) } } func TestStatusReportsAnUnheldDependencyWithoutRefusingTheMachine(t *testing.T) { // The mesh as it ran before the switch (novox/hq ADR 0207 §4). defer catalogue.EnforcingSeatDependencies(false)() open := aMesh(t) ctx := t.Context() register(t, open, serviceManagerHolder()) register(t, open, aDaemon()) // Assigned straight into the store: a machine whose modules predate the rule, which is every // machine on the day it ships. if _, err := open.inventory.Assign(ctx, "laptop", "sshd"); err != nil { t.Fatal(err) } asked, err := theThreeQuestions(ctx, open) if err != nil { t.Fatal(err) } if _, refused := asked.refused["laptop"]; refused { t.Fatalf("an unmet dependency refused the machine before the switch: %s", asked.refused["laptop"]) } if asked.well() { t.Error("a mesh with an unheld dependency reads as all well") } got := printed(t, func() error { return printStatus(asked) }) for _, want := range []string{"unheld", "laptop", "sshd", catalogue.ServiceManagerSeat, "systemd"} { if !strings.Contains(got, want) { t.Errorf("status does not say %q:\n%s", want, got) } } body, err := statusAsJSON(asked) if err != nil { t.Fatal(err) } var doc struct { Unheld []catalogue.Unheld `json:"unheld"` } if err := json.Unmarshal(body, &doc); err != nil { t.Fatal(err) } if len(doc.Unheld) != 1 || doc.Unheld[0].Module != "sshd" || doc.Unheld[0].Seat != catalogue.ServiceManagerSeat { t.Errorf("the document's unheld is %+v", doc.Unheld) } }