A module whose container names groups needs the container-groups capability, so it is held, naming it, on an engine that does not read them (issue 1638, ADR 0480 clause 7)
mesh/merge-gate head b5d8eb03cea3 merged into trunk 863a981e014e — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head b5d8eb03cea3 merged into trunk 863a981e014e — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-sdk@90b20f5a3bda (merged as d4deecdb into main, walk plan-179259365188251…

This commit is contained in:
2026-10-21 15:03:32 +02:00
parent 863a981e01
commit b5d8eb03ce
3 changed files with 51 additions and 2 deletions
+31
View File
@@ -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)
}
}
+18 -1
View File
@@ -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
}
+2 -1
View File
@@ -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",
}