A module depends on the node seats that apply its resources (hq ADR 0207)
Seed node-package-manager and node-container-runtime. Derive each module's dependencies from its declared service, package and container resources; judge them over the node's whole set, exempting the foundation. Refuse at assign (several modules may go on as one act) and at unassign of the last holder; report at composition in status, behind one switch.
This commit is contained in:
@@ -147,6 +147,10 @@ type Resolution struct {
|
||||
// dropped nor fatal to the rest. A module that is *required* by something running here is a
|
||||
// different case — that set is incoherent and is refused (see checkCapabilities).
|
||||
Unhostable []Unhostable
|
||||
// Unheld is every dependency of this node's modules on a seat nothing here holds (novox/hq ADR
|
||||
// 0207) — reported rather than refused while enforceSeatDependencies is off, so a node short of a
|
||||
// holder still converges and `status` says what it is short of.
|
||||
Unheld []Unheld
|
||||
}
|
||||
|
||||
// Unhostable is one directly-assigned module the machine cannot run.
|
||||
@@ -628,6 +632,17 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
||||
problems = append(problems, checkResources(resolution.Modules)...)
|
||||
resolution.Claims = claims
|
||||
|
||||
// Judged over the closure — what this node will actually run — so a holder pulled in by a
|
||||
// requirement counts, and the holders' mutual dependence resolves (novox/hq ADR 0207 §3).
|
||||
// Reported until the switch; refused after it, though never in the first pass, whose refusals
|
||||
// make a machine vanish from the network rather than report anything.
|
||||
resolution.Unheld = UnheldDependencies(catalogue, node.Name, resolution.Modules, nil)
|
||||
if enforceSeatDependencies && !world.Unchecked {
|
||||
for _, u := range resolution.Unheld {
|
||||
problems = append(problems, u.String())
|
||||
}
|
||||
}
|
||||
|
||||
if len(problems) > 0 {
|
||||
sort.Strings(problems)
|
||||
return Resolution{}, &Refusal{Problems: problems}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A module depends on the node seats that apply its resources (novox/hq ADR 0207).
|
||||
//
|
||||
// Some of what a module declares is applied through software on the machine that is itself a
|
||||
// module: a service through the service manager, a package through the package manager, a container
|
||||
// through the container runtime. A *capability* only says that software is installed; it does not
|
||||
// say that a module of the mesh holds the role and answers for it. So the dependency is derived from
|
||||
// the resources — never stated in a manifest, because a module that adds a service and forgets a
|
||||
// field would pass — and is met when some module assigned to the same node holds the seat.
|
||||
|
||||
// The three seats that apply resources (novox/hq ADR 0207 §1). Named once, because the derivation,
|
||||
// the seed and the messages all turn on these strings.
|
||||
const (
|
||||
ServiceManagerSeat = "node-service-manager"
|
||||
PackageManagerSeat = "node-package-manager"
|
||||
ContainerRuntimeSeat = "node-container-runtime"
|
||||
)
|
||||
|
||||
// appliedThrough is which seat applies a resource of each type. **Only these three**: ADR 0207
|
||||
// names them and no more. A process is supervised by the host itself, a file, a directory, an
|
||||
// archive, a user or an action is the host's own act, and a module's other kinds reach the machine
|
||||
// without a role in between — adding one here is a decision, not a refinement.
|
||||
var appliedThrough = map[string]string{
|
||||
"service": ServiceManagerSeat,
|
||||
"package": PackageManagerSeat,
|
||||
"container": ContainerRuntimeSeat,
|
||||
}
|
||||
|
||||
// enforceSeatDependencies is the one-line switch ADR 0207 §4 names. Off, an unmet dependency at
|
||||
// composition is *reported* — in the resolution, in `status`, once in the log — and the node still
|
||||
// resolves; on, it is refused like any unresolved requirement. Off until `status` reports none,
|
||||
// which is when the three holders are assigned to every node: switching it before then would stop
|
||||
// every machine lacking one from being sent anything at all.
|
||||
//
|
||||
// A variable rather than a constant only so a test can hold both behaviours; nothing else sets it.
|
||||
var enforceSeatDependencies = false
|
||||
|
||||
// foundationModules are the pieces genesis lays before any module exists (novox/hq ADR 0207 §5):
|
||||
// the host and the private network. Registered as modules so they can be assigned, but what they
|
||||
// declare is the installation's, not a module's, so it is never judged. The third piece, the
|
||||
// bootstrap container runtime, is not a module at all: its package and service are in the genesis
|
||||
// bundle the host applies itself, and never pass through a resolution here.
|
||||
//
|
||||
// The private network's module is overlay.Name, written out because the overlay package composes
|
||||
// on top of this one; its resources are computed, which exempts it by the rule below as well.
|
||||
var foundationModules = map[string]bool{
|
||||
"mesh-host": true,
|
||||
"mesh-wireguard": true,
|
||||
}
|
||||
|
||||
// isFoundation is whether a module's declarations are the foundation's rather than its own. A
|
||||
// module whose resources are computed is the mesh's by construction — the private network's peer
|
||||
// list and its tools are the controller's, written per node — so it counts whatever its name.
|
||||
func isFoundation(m Manifest) bool {
|
||||
return foundationModules[m.Module] || m.Computed != ""
|
||||
}
|
||||
|
||||
// DependsOn is every seat a module needs held on its node, derived from the resource types it
|
||||
// declares itself (novox/hq ADR 0207 §2), sorted.
|
||||
//
|
||||
// **The module's own `resources` only.** What the controller composes around a module — its
|
||||
// filter, jails, certificates, kept files, bundles, the guard — is the mesh's, put there because the
|
||||
// module is assigned, and depending on it would make the module answer for the mesh's choices.
|
||||
func DependsOn(m Manifest) []string {
|
||||
if isFoundation(m) {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, r := range m.Resources {
|
||||
if seat, applied := appliedThrough[fmt.Sprint(r["type"])]; applied {
|
||||
seen[seat] = true
|
||||
}
|
||||
}
|
||||
out := make([]string, 0, len(seen))
|
||||
for s := range seen {
|
||||
out = append(out, s)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// claimsSeat is whether a module claims a node seat, by its current name or one it used to have
|
||||
// (ADR 0122), so a rename leaves the dependency met.
|
||||
func claimsSeat(m Manifest, seat string) bool {
|
||||
for _, c := range m.Claims {
|
||||
if c.At() != ScopeNode {
|
||||
continue
|
||||
}
|
||||
name := c.Name
|
||||
if s, known := SeatNamed(name); known {
|
||||
name = s.Name
|
||||
}
|
||||
if name == seat {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// PossibleHolders is every module in the catalogue that claims a seat at node scope — what a
|
||||
// refusal names as the remedy.
|
||||
func PossibleHolders(catalogue map[string]Manifest, seat string) []string {
|
||||
var out []string
|
||||
for name, m := range catalogue {
|
||||
if claimsSeat(m, seat) {
|
||||
out = append(out, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Unheld is one dependency of one module on a node that nothing on that node holds.
|
||||
type Unheld struct {
|
||||
Node string `json:"node"`
|
||||
Module string `json:"module"`
|
||||
Seat string `json:"seat"`
|
||||
// Holders are the modules in the catalogue that could hold the seat: assigning one meets it.
|
||||
Holders []string `json:"holders"`
|
||||
}
|
||||
|
||||
// String is the line a refusal and a report both say, so the two never drift.
|
||||
func (u Unheld) String() string {
|
||||
remedy := "and no module in the catalogue claims it yet"
|
||||
if len(u.Holders) > 0 {
|
||||
remedy = "— assign one that holds it: " + strings.Join(u.Holders, ", ")
|
||||
}
|
||||
return fmt.Sprintf("%s on %s depends on %s, which nothing on %s holds (novox/hq ADR 0207) %s",
|
||||
u.Module, u.Node, u.Seat, u.Node, remedy)
|
||||
}
|
||||
|
||||
// UnheldDependencies is every dependency of the modules in `judged` that the node's whole set
|
||||
// leaves unmet (novox/hq ADR 0207 §3).
|
||||
//
|
||||
// **Judged over the whole set, never one module at a time.** The holders depend on each other:
|
||||
// the service manager's own package needs the package manager, and the package manager's timer
|
||||
// needs the service manager. Asked one by one, neither could ever be first; asked of the set, the
|
||||
// two assigned together meet each other. A module holding a seat it depends on meets its own
|
||||
// dependency. `judged` nil judges every module of the set.
|
||||
func UnheldDependencies(catalogue map[string]Manifest, node string, set []Manifest, judged map[string]bool) []Unheld {
|
||||
held := map[string]bool{}
|
||||
for _, m := range set {
|
||||
for seat := range seatsApplying() {
|
||||
if claimsSeat(m, seat) {
|
||||
held[seat] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []Unheld
|
||||
for _, m := range set {
|
||||
if judged != nil && !judged[m.Module] {
|
||||
continue
|
||||
}
|
||||
for _, seat := range DependsOn(m) {
|
||||
if held[seat] {
|
||||
continue
|
||||
}
|
||||
out = append(out, Unheld{Node: node, Module: m.Module, Seat: seat,
|
||||
Holders: PossibleHolders(catalogue, seat)})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Module != out[j].Module {
|
||||
return out[i].Module < out[j].Module
|
||||
}
|
||||
return out[i].Seat < out[j].Seat
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func seatsApplying() map[string]bool {
|
||||
out := map[string]bool{}
|
||||
for _, s := range appliedThrough {
|
||||
out[s] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// manifestsOf is the catalogue's definitions of the named modules; a name the catalogue does not
|
||||
// know contributes nothing, as it does to a resolution.
|
||||
func manifestsOf(catalogue map[string]Manifest, names []string) []Manifest {
|
||||
var out []Manifest
|
||||
seen := map[string]bool{}
|
||||
for _, n := range names {
|
||||
if m, known := catalogue[n]; known && !seen[n] {
|
||||
seen[n] = true
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AssignRefusal is why assigning `adding` to a node already running `assigned` is refused, or
|
||||
// nothing (novox/hq ADR 0207 §4): each new module's dependency the node's assignments, the new ones
|
||||
// included, leave unmet.
|
||||
//
|
||||
// **Only the new modules are judged.** A node already short of a holder is reported by `status`;
|
||||
// refusing an unrelated assignment for it would make the remedy — assigning the holder — refused too.
|
||||
//
|
||||
// **A dependency nothing in the catalogue can meet is said, not refused.** A refusal names the
|
||||
// module that would meet it; with none registered there is no remedy to name, and refusing would
|
||||
// stop every assignment of that kind until a module that does not exist yet is written. The answer
|
||||
// still says it, and `status` reports it, until the switch (enforceSeatDependencies) makes the mesh
|
||||
// refuse what it cannot meet. The first return is those lines.
|
||||
func AssignRefusal(catalogue map[string]Manifest, node string, assigned, adding []string) ([]string, error) {
|
||||
set := manifestsOf(catalogue, append(append([]string(nil), assigned...), adding...))
|
||||
judged := map[string]bool{}
|
||||
for _, a := range adding {
|
||||
judged[a] = true
|
||||
}
|
||||
var refused, said []string
|
||||
for _, u := range UnheldDependencies(catalogue, node, set, judged) {
|
||||
if len(u.Holders) == 0 && !enforceSeatDependencies {
|
||||
said = append(said, u.String())
|
||||
continue
|
||||
}
|
||||
refused = append(refused, u.String())
|
||||
}
|
||||
if len(refused) > 0 {
|
||||
return said, &Refusal{Problems: append(refused,
|
||||
fmt.Sprintf("holders that depend on each other are assigned together: `assign %s <module> <module>…`", node))}
|
||||
}
|
||||
return said, nil
|
||||
}
|
||||
|
||||
// UnassignRefusal is why taking `removing` off a node running `assigned` is refused, or nothing
|
||||
// (novox/hq ADR 0207): a seat the removed modules hold that nothing left on the node holds, while
|
||||
// a module left there depends on it. Names the dependents, because they are what must go first —
|
||||
// or the holder's replacement come.
|
||||
func UnassignRefusal(catalogue map[string]Manifest, node string, assigned, removing []string) error {
|
||||
gone := map[string]bool{}
|
||||
for _, r := range removing {
|
||||
gone[r] = true
|
||||
}
|
||||
var left []string
|
||||
for _, a := range assigned {
|
||||
if !gone[a] {
|
||||
left = append(left, a)
|
||||
}
|
||||
}
|
||||
before := map[string]bool{}
|
||||
for _, u := range UnheldDependencies(catalogue, node, manifestsOf(catalogue, assigned), nil) {
|
||||
before[u.Module+"\x00"+u.Seat] = true
|
||||
}
|
||||
dependents := map[string][]string{}
|
||||
for _, u := range UnheldDependencies(catalogue, node, manifestsOf(catalogue, left), nil) {
|
||||
if before[u.Module+"\x00"+u.Seat] {
|
||||
continue // unmet already; not this removal's doing
|
||||
}
|
||||
dependents[u.Seat] = append(dependents[u.Seat], u.Module)
|
||||
}
|
||||
if len(dependents) == 0 {
|
||||
return nil
|
||||
}
|
||||
seats := make([]string, 0, len(dependents))
|
||||
for s := range dependents {
|
||||
seats = append(seats, s)
|
||||
}
|
||||
sort.Strings(seats)
|
||||
var problems []string
|
||||
for _, s := range seats {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s holds %s on %s, and %s depend on it (novox/hq ADR 0207) — unassign them with it, "+
|
||||
"or assign another holder first", strings.Join(removing, ", "), s, node,
|
||||
strings.Join(dependents[s], ", ")))
|
||||
}
|
||||
return &Refusal{Problems: problems}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -148,8 +148,19 @@ var defaultSeats = []Seat{
|
||||
// system or user scope; the holder answers questions and operator acts about them, each verb
|
||||
// taking the unit and an optional scope. The holder runs nothing of its own: its verbs are
|
||||
// served by the node tools runtime (ADR 0175).
|
||||
{Name: "node-service-manager", Scope: ScopeNode, Decision: "novox/hq ADR 0177",
|
||||
{Name: ServiceManagerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0177",
|
||||
Serves: serviceManagerVerbs()},
|
||||
// The machine's package manager (novox/hq ADR 0207). A module declaring a `package` depends on
|
||||
// it being held on its node, as one declaring a `service` depends on node-service-manager: the
|
||||
// mesh's word for "something on this machine answers for installing", where a capability only
|
||||
// says the software is there. No verbs yet — the seat says who answers, and what may be asked
|
||||
// of it is decided when someone needs to ask.
|
||||
{Name: PackageManagerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0207"},
|
||||
// The machine's container runtime (novox/hq ADR 0166, seeded now by ADR 0207): a module
|
||||
// declaring a `container` depends on it being held on its node. Its verbs, and the host creating
|
||||
// containers through its holder, wait for ADR 0166's acceptance — seeded without them so the
|
||||
// dependency has a seat to name and the runtime's module has one to claim.
|
||||
{Name: ContainerRuntimeSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0166, ADR 0207"},
|
||||
// The operator account's environment (novox/hq ADR 0203): one module per machine writes it, and
|
||||
// every module contributes to it. No verbs — the seat says who places the environment's files,
|
||||
// and their path is its protocol: a shell sources ~/.config/mesh/environment.sh without knowing
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
// vocabulary test follows. If this fails because a seat was added, the fix is a record in novox/hq
|
||||
// and a row in to-be 26, not a new number here.
|
||||
func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
record := regexp.MustCompile(`^novox/hq ADR \d{4}$`)
|
||||
// A seat a later record extends names both, "novox/hq ADR 0166, ADR 0207": the one that defined
|
||||
// it and the one that seeded it.
|
||||
record := regexp.MustCompile(`^novox/hq ADR \d{4}(, ADR \d{4})*$`)
|
||||
seen := map[string]bool{}
|
||||
delivered := map[string]string{}
|
||||
for _, s := range Seats() {
|
||||
@@ -44,11 +46,12 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
|
||||
delivered[s.Delivers] = s.Name
|
||||
}
|
||||
}
|
||||
// Nineteen since node-environment and node-login-shell (novox/hq ADR 0203, ADR 0204), after
|
||||
// node-build-agent made seventeen (ADR 0190) — eighteen once the retired mesh-build-machine row
|
||||
// goes, when no registered manifest claims it any more.
|
||||
if len(Seats()) != 19 {
|
||||
t.Errorf("the mesh defines %d seats rather than 19; the set is closed, so a change here is "+
|
||||
// Twenty-one since node-package-manager and node-container-runtime (novox/hq ADR 0207), after
|
||||
// node-environment and node-login-shell made nineteen (ADR 0203, ADR 0204) and node-build-agent
|
||||
// seventeen (ADR 0190) — twenty once the retired mesh-build-machine row goes, when no registered
|
||||
// manifest claims it any more.
|
||||
if len(Seats()) != 21 {
|
||||
t.Errorf("the mesh defines %d seats rather than 21; the set is closed, so a change here is "+
|
||||
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +102,13 @@ var ControllerVerbs = []Verb{
|
||||
}, nil)},
|
||||
{Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it.",
|
||||
Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})},
|
||||
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there.",
|
||||
Input: schema(map[string]string{"node": "the machine's name", "module": "the module's name"}, []string{"node", "module"})},
|
||||
{Name: "unassign", Description: "Take a module off a machine.",
|
||||
Input: schema(map[string]string{"node": "the machine's name", "module": "the module's name"}, []string{"node", "module"})},
|
||||
{Name: "assign", Description: "Put a module on a machine. Refused with the mesh's own words when it cannot resolve there, " +
|
||||
"or when a seat its resources are applied through is held by nothing on the machine (novox/hq ADR 0207).",
|
||||
Input: schema(map[string]string{"node": "the machine's name",
|
||||
"module": "the module's name; several comma-separated are judged together"}, []string{"node", "module"})},
|
||||
{Name: "unassign", Description: "Take a module off a machine. Refused when it holds a seat a module left there depends on.",
|
||||
Input: schema(map[string]string{"node": "the machine's name",
|
||||
"module": "the module's name; several comma-separated are judged together"}, []string{"node", "module"})},
|
||||
{Name: "pin", Description: "Tell a machine which provider answers a provision for it — the module, and the node " +
|
||||
"it runs on, both. Asked for when more than one could answer; the refusal lists them.",
|
||||
Input: schema(map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user