diff --git a/go/catalogue/groups_test.go b/go/catalogue/groups_test.go index fe43f99..b12276d 100644 --- a/go/catalogue/groups_test.go +++ b/go/catalogue/groups_test.go @@ -116,3 +116,34 @@ func TestAComposedMachineGetsTheGroupTheConfigValueNames(t *testing.T) { t.Fatalf("refused without the key: %v", err) } } + +// The node-engine reads a container's groups only when it reports the capability (novox/hq ADR 0480 clause 7, issue 1638): +// a module with a container that names groups is hosted on a machine that reports container-groups, and left out of one +// that does not, the capability named, so the field is never sent to an engine that would refuse it. +func TestAModuleWhoseContainerNamesGroupsIsHeldOnAMachineThatDoesNotReadThem(t *testing.T) { + n8n := Manifest{Module: "n8n", Resources: []map[string]any{aContainerWithGroups("${config:media-gid}")}} + plain := Manifest{Module: "plain", Resources: []map[string]any{ + {"id": "server", "type": "container", "name": "p", "image": "x@sha256:" + strings.Repeat("b", 64)}}} + for _, m := range []Manifest{n8n, plain} { + if got := impliedCapabilities(m); (m.Module == "n8n") != reflect.DeepEqual(got, []string{ContainerGroups}) { + t.Errorf("%s implies %v", m.Module, got) + } + } + shelf := shelf(n8n, plain) + reads := Node{Name: "ace", Capabilities: map[string]bool{"container-runtime": true, ContainerGroups: true}} + got, err := Resolve(shelf, []string{"n8n", "plain"}, reads, World{}) + if err != nil || len(got.Unhostable) != 0 || len(got.Modules) != 2 { + t.Fatalf("a machine that reads groups: %v, held %+v, hosted %v", err, got.Unhostable, names(got)) + } + old := Node{Name: "g14", Capabilities: map[string]bool{"container-runtime": true}} + got, err = Resolve(shelf, []string{"n8n", "plain"}, old, World{}) + if err != nil { + t.Fatal(err) + } + if len(got.Unhostable) != 1 || got.Unhostable[0].Module != "n8n" || !reflect.DeepEqual(got.Unhostable[0].Missing, []string{ContainerGroups}) { + t.Fatalf("n8n was not held, naming the capability: %+v", got.Unhostable) + } + if h := names(got); len(h) != 1 || h[0] != "plain" { + t.Errorf("the module without groups is not the one hosted: %v", h) + } +} diff --git a/go/catalogue/resolve.go b/go/catalogue/resolve.go index 6e2e08f..006646e 100644 --- a/go/catalogue/resolve.go +++ b/go/catalogue/resolve.go @@ -904,7 +904,7 @@ func isModule(catalogue map[string]Manifest, want string) bool { // machine can host the module as far as its capabilities go. func missingCapabilities(m Manifest, node Node) []string { var missing []string - for _, c := range m.Capabilities { + for _, c := range append(append([]string(nil), m.Capabilities...), impliedCapabilities(m)...) { if !node.Capabilities[c] { missing = append(missing, c) } @@ -1457,3 +1457,20 @@ func packageNames(m map[string][]string) []string { sort.Strings(names) return names } + +// ContainerGroups is the capability a node-engine reports when it reads a container's `groups` and passes each as a +// supplementary group (novox/hq ADR 0480 clause 7, issue 1638). An older engine refuses the key (strict decoding). +const ContainerGroups = "container-groups" + +// impliedCapabilities are the capabilities a module needs for what its resources say, beyond those it declares: a +// container that names groups needs an engine that reads them, so the module is left out of a machine whose report +// lacks the capability, naming it, and the field is sent to no engine that would refuse it. None for a module whose +// containers name no groups. +func impliedCapabilities(m Manifest) []string { + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) == "container" && len(groupsOf(r)) > 0 { + return []string{ContainerGroups} + } + } + return nil +} diff --git a/go/catalogue/seat_data.go b/go/catalogue/seat_data.go index 3a4fc8b..90a1fb7 100644 --- a/go/catalogue/seat_data.go +++ b/go/catalogue/seat_data.go @@ -66,7 +66,8 @@ var capabilityName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its // contribution out on every machine, silently, so it is refused. A detector added there is added here. var KnownCapabilities = []string{ - "battery", "container-runtime", "file-references", "firewall", "graphical-session", "overlay", "package-manager", + "battery", "container-groups", "container-runtime", "file-references", "firewall", "graphical-session", "overlay", + "package-manager", "power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager", "uplink-systemd-networkd", "user-namespaces", "virtualisation", }