mesh/merge-gate head e0e1a91496de merged into trunk 863a981e014e — fail: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus step; 9 wait(…
mesh/repo-check head e0e1a91496de merged into trunk 863a981e014e — fail: head e0e1a91496de does not merge cleanly into trunk 863a981e014e: in conflict go/c…
mesh/delivery superseded: a newer head of the same pull request
150 lines
6.8 KiB
Go
150 lines
6.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A container's supplementary groups may be an operator's value (novox/hq ADR 0480 clause 7, ADR 0348 §11,
|
|
// issue 1636): `groups: ["${config:media-gid}"]`, each element a whole placeholder or a whole number, filled at
|
|
// composition with the number the config value holds.
|
|
|
|
func aContainerWithGroups(groups ...any) map[string]any {
|
|
return map[string]any{"id": "server", "type": "container", "name": "n8n", "image": "x@sha256:" + strings.Repeat("a", 64),
|
|
"groups": groups}
|
|
}
|
|
|
|
// 1636/AC3: a manifest container with groups: ["${setting:media-gid}"] resolves to the number the setting says.
|
|
func TestAContainersGroupIsFilledFromTheSetting(t *testing.T) {
|
|
r := aContainerWithGroups("${setting:media-gid}", float64(2001))
|
|
layers := []Layer{{From: "ace", Values: map[string]any{"media-gid": float64(2000)}}}
|
|
if err := settingInto(r, layers, "n8n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := []any{2000, 2001}; !reflect.DeepEqual(r["groups"], want) {
|
|
t.Fatalf("composed as %#v, want %#v", r["groups"], want)
|
|
}
|
|
// A value the operator wrote as text is read as the number it spells.
|
|
r = aContainerWithGroups("${setting:media-gid}")
|
|
if err := settingInto(r, []Layer{{From: "ace", Values: map[string]any{"media-gid": "2000"}}}, "n8n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := []any{2000}; !reflect.DeepEqual(r["groups"], want) {
|
|
t.Fatalf("composed as %#v, want %#v", r["groups"], want)
|
|
}
|
|
}
|
|
|
|
// 1636/AC3: an unset value refuses the module by name, and the key and the remedy are named; nothing is dropped.
|
|
func TestAnUnsetGroupRefusesTheModuleByName(t *testing.T) {
|
|
r := aContainerWithGroups("${setting:media-gid}")
|
|
err := settingInto(r, nil, "n8n")
|
|
if err == nil || !strings.Contains(err.Error(), "n8n") || !strings.Contains(err.Error(), "media-gid") {
|
|
t.Fatalf("want a refusal naming n8n and media-gid, got %v", err)
|
|
}
|
|
if got, _ := r["groups"].([]any); len(got) != 1 || got[0] != "${setting:media-gid}" {
|
|
t.Fatalf("the resource was changed: %#v", r["groups"])
|
|
}
|
|
}
|
|
|
|
// 1636/AC2: a value that is not a whole number above zero cannot be a group.
|
|
func TestAGroupThatIsNotAWholeNumberAboveZeroIsRefused(t *testing.T) {
|
|
for _, bad := range []any{"media", "-1", "0", float64(0), float64(2000.5), "2000:2000", "", true} {
|
|
r := aContainerWithGroups("${setting:media-gid}")
|
|
err := settingInto(r, []Layer{{From: "ace", Values: map[string]any{"media-gid": bad}}}, "n8n")
|
|
if err == nil || !strings.Contains(err.Error(), "media-gid") {
|
|
t.Errorf("value %#v: want a refusal naming the key, got %v", bad, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A manifest in the config form registers with the group a config value fills, and the value is placed (a container's
|
|
// groups count as a place for it); a placeholder in any other shape is refused where the manifest is read.
|
|
func TestAManifestWhoseContainerTakesItsGroupFromConfigRegisters(t *testing.T) {
|
|
m := map[string]any{
|
|
"module": "n8n", "version": "1",
|
|
"config": map[string]any{"media-gid": map[string]any{"type": "number", "minimum": 1, "required": true,
|
|
"description": "the group that owns the media library on this machine"}},
|
|
"resources": []any{aContainerWithGroups("${config:media-gid}")},
|
|
}
|
|
if _, err := parseRaw(t, m); err != nil {
|
|
t.Fatalf("refused: %v", err)
|
|
}
|
|
for _, shape := range []string{"x${config:media-gid}", "${config:media-gid}:1", "${config:other}"} {
|
|
m["resources"] = []any{aContainerWithGroups(shape)}
|
|
if _, err := parseRaw(t, m); err == nil {
|
|
t.Errorf("groups %q registered", shape)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1636/AC3 composed for a machine, the way a push writes it: n8n's group is the number the machine's config value
|
|
// says, written as a number, and the module is left out by name when the machine says none.
|
|
func TestAComposedMachineGetsTheGroupTheConfigValueNames(t *testing.T) {
|
|
manifest, err := parseRaw(t, map[string]any{
|
|
"module": "n8n", "version": "1",
|
|
"config": map[string]any{"media-gid": map[string]any{"type": "number", "minimum": 1, "required": true,
|
|
"description": "the group that owns the media library on this machine"}},
|
|
"resources": []any{aContainerWithGroups("${config:media-gid}")},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := anAdoptedAnchor()
|
|
r.Modules = append(r.Modules, manifest)
|
|
with := anchorRendering(false)
|
|
with.Settings["n8n"] = []Layer{{From: "anchor", Values: map[string]any{"media-gid": float64(2000)}}}
|
|
composed, err := r.Compose(with)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if why, left := composed.LeftOut["n8n"]; left {
|
|
t.Fatalf("left out: %s", why)
|
|
}
|
|
if got := byID(composed.Resources)["n8n.server"]["groups"]; !reflect.DeepEqual(got, []any{2000}) {
|
|
t.Fatalf("composed groups %#v", got)
|
|
}
|
|
|
|
without := anchorRendering(false)
|
|
composed, err = r.Compose(without)
|
|
if err == nil {
|
|
why, left := composed.LeftOut["n8n"]
|
|
if !left || !strings.Contains(why, "media-gid") {
|
|
t.Fatalf("a machine that says no group kept n8n or left it out without the key: %q (%v)", why, left)
|
|
}
|
|
} else if !strings.Contains(err.Error(), "media-gid") {
|
|
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)
|
|
}
|
|
}
|