From 86ac7596c92e6d905a62cb060f541cc724560f6a Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 11:17:45 +0200 Subject: [PATCH] Test that a node without the go or nodejs module refuses the modules needing a toolchain, and with it composes (issue 1573) --- go/catalogue/toolchain_manifests_test.go | 88 ++++++++++ go/testdata/beside/CAPTURED | 1 + .../modules/dev-tools/module.json.captured | 19 +- .../modules/go/module.json.captured | 20 +++ .../modules/lab/module.json.captured | 14 +- .../modules/mesh-build-go/files/forge | 7 + .../modules/mesh-build-go/files/gitconfig | 2 + .../modules/mesh-build-go/files/known_hosts | 1 + .../mesh-build-go/module.json.captured | 145 +++++++++++++++ .../modules/mesh-build-node/files/forge | 7 + .../modules/mesh-build-node/files/gitconfig | 2 + .../modules/mesh-build-node/files/known_hosts | 1 + .../mesh-build-node/module.json.captured | 166 ++++++++++++++++++ .../modules/nodejs/module.json.captured | 25 +++ 14 files changed, 472 insertions(+), 26 deletions(-) create mode 100644 go/catalogue/toolchain_manifests_test.go create mode 100644 go/testdata/beside/mesh-catalog/modules/go/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts create mode 100644 go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured create mode 100644 go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured diff --git a/go/catalogue/toolchain_manifests_test.go b/go/catalogue/toolchain_manifests_test.go new file mode 100644 index 0000000..a56fd2b --- /dev/null +++ b/go/catalogue/toolchain_manifests_test.go @@ -0,0 +1,88 @@ +package catalogue + +import ( + "strings" + "testing" +) + +// Defends novox/hq ADR 0477 ยง4 (issue 1573): the go and nodejs modules own the toolchain packages and provide +// them with the machine's reach, so a node without one refuses the module that needs it at resolution, naming +// the provision, and does not find out at its first build. The manifests are the catalogue's, as they are. + +// builderNode is a machine that can run a build module: the package manager and user namespaces it asks of +// the machine, and the forge account (mesh-scope, provided by the forge) stood in for. +func builderNode() Node { + return Node{Name: "builder", Site: "house", + Capabilities: map[string]bool{"package-manager": true, "user-namespaces": true}} +} + +func toolchainShelf(t *testing.T) map[string]Manifest { + t.Helper() + m := map[string]Manifest{ + "forge": {Module: "forge", Version: "1", Provides: []Offer{{Name: "forge-account"}, {Name: "npm-package-registry"}}}, + } + for _, name := range []string{"go", "nodejs", "mesh-build-go", "mesh-build-node", "dev-tools"} { + m[name] = catalogueManifest(t, name) + } + return m +} + +func TestAModuleNeedingAToolchainIsRefusedOnANodeWithoutIt(t *testing.T) { + for _, c := range []struct{ module, provision string }{ + {"mesh-build-go", "go-toolchain"}, + {"mesh-build-node", "node-toolchain"}, + {"dev-tools", "go-toolchain"}, + } { + _, err := Resolve(toolchainShelf(t), []string{"forge", c.module}, builderNode(), World{}) + if err == nil { + t.Fatalf("%s composed on a node with no toolchain module", c.module) + } + for _, want := range []string{`"` + c.provision + `" is wanted by ` + c.module, + "usable only on the machine that provides it"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("%s: the refusal does not say %q:\n%v", c.module, want, err) + } + } + } +} + +func TestAModuleNeedingAToolchainComposesOnANodeWithIt(t *testing.T) { + for _, c := range []struct{ module, owner string }{ + {"mesh-build-go", "go"}, + {"mesh-build-node", "nodejs"}, + {"dev-tools", "go"}, + } { + assigned := []string{"forge", c.owner, c.module} + if c.module == "dev-tools" { + assigned = append(assigned, "nodejs") + } + got, err := Resolve(toolchainShelf(t), assigned, builderNode(), World{}) + if err != nil { + t.Fatalf("%s with %s did not compose: %v", c.module, c.owner, err) + } + in := map[string]bool{} + for _, n := range names(got) { + in[n] = true + } + if !in[c.module] || !in[c.owner] { + t.Errorf("%s with %s composed %v", c.module, c.owner, names(got)) + } + } +} + +// Only the owners declare the packages: a build module that declared go again would compose without the +// owner's provision being met by anything, which is the gap the provision closes. +func TestOnlyTheToolchainModulesDeclareTheToolchainPackages(t *testing.T) { + owner := map[string]string{"go": "go", "nodejs": "nodejs", "npm": "nodejs"} + for name, m := range toolchainShelf(t) { + for _, r := range m.Resources { + if r["type"] != "package" { + continue + } + pkg, _ := r["package"].(string) + if o, ok := owner[pkg]; ok && o != name { + t.Errorf("%s declares the package %s, which only %s may", name, pkg, o) + } + } + } +} diff --git a/go/testdata/beside/CAPTURED b/go/testdata/beside/CAPTURED index dc56ca5..915bf1a 100644 --- a/go/testdata/beside/CAPTURED +++ b/go/testdata/beside/CAPTURED @@ -11,6 +11,7 @@ Only what go/catalogue's tests read is captured (R4b, issue 1379). Refreshed 202 repositories; before that it was mesh-controller's copy, which still held two modules that had left the catalogue. mesh-catalog bb0fe93bbdddd869ad808210352461efc506c3d3 modules/*/module.json, modules/*/files/**, modules/*/templates/**, predefined.json +mesh-catalog 91c2007c modules/{go,nodejs,dev-tools,lab,mesh-build-go,mesh-build-node}/module.json, and files/** of the two build modules, only (branch fix/1573-toolchain-modules, issue 1573: the toolchain modules); the rest stays as captured above. Refresh all of it from the catalogue's main once that is merged. mesh-host cdcba9ca73755854d21e4e657d7e46c32b9b5265 internal/apply/placement_guard.go To move them, from this repository's go/ directory, with the two repositories checked out beside mesh-sdk: diff --git a/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured b/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured index 8ee6fd1..094f833 100644 --- a/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/dev-tools/module.json.captured @@ -4,12 +4,11 @@ "capabilities": [ "package-manager" ], + "requires": [ + "go-toolchain", + "node-toolchain" + ], "resources": [ - { - "id": "go", - "type": "package", - "package": "go" - }, { "id": "c-compiler", "type": "package", @@ -20,16 +19,6 @@ "type": "package", "package": "make" }, - { - "id": "nodejs", - "type": "package", - "package": "nodejs" - }, - { - "id": "npm", - "type": "package", - "package": "npm" - }, { "id": "python", "type": "package", diff --git a/go/testdata/beside/mesh-catalog/modules/go/module.json.captured b/go/testdata/beside/mesh-catalog/modules/go/module.json.captured new file mode 100644 index 0000000..48a6bf5 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/go/module.json.captured @@ -0,0 +1,20 @@ +{ + "module": "go", + "version": "1", + "capabilities": [ + "package-manager" + ], + "provides": [ + { + "name": "go-toolchain", + "reach": "machine" + } + ], + "resources": [ + { + "id": "go", + "type": "package", + "package": "go" + } + ] +} diff --git a/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured b/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured index e23963d..ef82b87 100644 --- a/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured +++ b/go/testdata/beside/mesh-catalog/modules/lab/module.json.captured @@ -6,7 +6,9 @@ "virtualisation" ], "requires": [ - "npm-package-registry" + "npm-package-registry", + "go-toolchain", + "node-toolchain" ], "contributes": { "npm-package-registry": { @@ -84,16 +86,6 @@ "type": "package", "package": "iproute2" }, - { - "id": "npm", - "type": "package", - "package": "npm" - }, - { - "id": "go", - "type": "package", - "package": "go" - }, { "id": "incus", "type": "package", diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge new file mode 100644 index 0000000..6bbf4e2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/forge @@ -0,0 +1,7 @@ +Host ${bound:forge-account:ssh_alias} + HostName ${bound:forge-account:ssh_host} + Port ${bound:forge-account:ssh_port} + HostKeyAlias ${bound:forge-account:ssh_alias} + User git + IdentityFile ~/.ssh/mesh_ed25519 + IdentitiesOnly yes diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig new file mode 100644 index 0000000..0525230 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/gitconfig @@ -0,0 +1,2 @@ +[url "ssh://${bound:forge-account:ssh_alias}/"] + insteadOf = ${bound:forge-account:https_base} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts new file mode 100644 index 0000000..e0df6d8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/files/known_hosts @@ -0,0 +1 @@ +${bound:forge-account:ssh_alias} ${bound:forge-account:host_key} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured new file mode 100644 index 0000000..74f4d97 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-go/module.json.captured @@ -0,0 +1,145 @@ +{ + "module": "mesh-build-go", + "version": "1", + "moved": true, + "runs-as": "mesh-build-go", + "capabilities": [ + "package-manager", + "user-namespaces" + ], + "state": [ + "toolchains", + "keys", + "steps" + ], + "tools": [ + "build_go_fetch", + "build_go_test", + "build_go_digest", + "build_go_build", + "build_go_status", + "build_go_health" + ], + "streams": [ + { + "name": "toolchains-go", + "events": ["toolchain.**"] + } + ], + "requires": [ + "forge-account", + "go-toolchain" + ], + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-build-go", + "runtime-health": { + "kind": "tool", + "tool": "build_go_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-build-go", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-build-go" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-build-go", + "place": ".", + "unknown": "program" + }, + { + "id": "work", + "type": "directory", + "path": "/var/lib/mesh-build-go-work", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "cache", + "type": "directory", + "path": "/var/lib/mesh-build-go-cache", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "ssh-dir", + "type": "directory", + "path": "${dir:state}/.ssh", + "mode": "0700", + "owner": "mesh-build-go", + "unknown": "program" + }, + { + "id": "ssh-config", + "type": "file", + "path": "${dir:ssh-dir}/config", + "owner": "mesh-build-go", + "mode": "0600", + "template": "files/forge" + }, + { + "id": "known-hosts", + "type": "file", + "path": "${dir:ssh-dir}/known_hosts", + "owner": "mesh-build-go", + "mode": "0644", + "template": "files/known_hosts" + }, + { + "id": "git-config", + "type": "file", + "path": "${dir:state}/.config/git/config", + "owner": "mesh-build-go", + "mode": "0644", + "template": "files/gitconfig" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "openssh", + "type": "package", + "package": "openssh" + }, + { + "id": "postgresql", + "type": "package", + "package": "postgresql" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-build-go", + "binary": "mesh-build-go", + "loads": [ + "mesh-build-go" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge new file mode 100644 index 0000000..6bbf4e2 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/forge @@ -0,0 +1,7 @@ +Host ${bound:forge-account:ssh_alias} + HostName ${bound:forge-account:ssh_host} + Port ${bound:forge-account:ssh_port} + HostKeyAlias ${bound:forge-account:ssh_alias} + User git + IdentityFile ~/.ssh/mesh_ed25519 + IdentitiesOnly yes diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig new file mode 100644 index 0000000..0525230 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/gitconfig @@ -0,0 +1,2 @@ +[url "ssh://${bound:forge-account:ssh_alias}/"] + insteadOf = ${bound:forge-account:https_base} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts new file mode 100644 index 0000000..e0df6d8 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/files/known_hosts @@ -0,0 +1 @@ +${bound:forge-account:ssh_alias} ${bound:forge-account:host_key} diff --git a/go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured new file mode 100644 index 0000000..a11cb96 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/mesh-build-node/module.json.captured @@ -0,0 +1,166 @@ +{ + "module": "mesh-build-node", + "version": "1", + "moved": true, + "runs-as": "mesh-build-node", + "capabilities": [ + "package-manager", + "user-namespaces" + ], + "state": [ + "toolchains", + "keys", + "steps" + ], + "tools": [ + "build_node_fetch", + "build_node_test", + "build_node_build", + "build_node_status", + "build_node_health" + ], + "streams": [ + { + "name": "toolchains-node", + "events": [ + "toolchain.**" + ] + } + ], + "requires": [ + "forge-account", + "npm-package-registry", + "node-toolchain" + ], + "contributes": { + "npm-package-registry": { + "access": "read" + } + }, + "binds": { + "npm-package-registry": "${dir:state}/package-registry.json" + }, + "secrets": { + "npm-package-registry": "${dir:state}/package-registry.secret" + }, + "own-secrets": { + "broker": "${dir:state}/broker" + }, + "secrets-owner": "mesh-build-node", + "runtime-health": { + "kind": "tool", + "tool": "build_node_health", + "interval": "60s", + "timeout": "10s", + "looks": 2, + "grace": "60s" + }, + "resources": [ + { + "id": "account", + "type": "user", + "name": "mesh-build-node", + "shell": "/usr/bin/nologin", + "home": "/var/lib/mesh-build-node" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "mesh-build-node", + "place": ".", + "unknown": "program" + }, + { + "id": "work", + "type": "directory", + "path": "/var/lib/mesh-build-node-work", + "mode": "0700", + "owner": "mesh-build-node", + "unknown": "program" + }, + { + "id": "cache", + "type": "directory", + "path": "/var/lib/mesh-build-node-cache", + "mode": "0700", + "owner": "mesh-build-node", + "unknown": "program" + }, + { + "id": "ssh-dir", + "type": "directory", + "path": "${dir:state}/.ssh", + "mode": "0700", + "owner": "mesh-build-node", + "unknown": "program" + }, + { + "id": "ssh-config", + "type": "file", + "path": "${dir:ssh-dir}/config", + "owner": "mesh-build-node", + "mode": "0600", + "template": "files/forge" + }, + { + "id": "known-hosts", + "type": "file", + "path": "${dir:ssh-dir}/known_hosts", + "owner": "mesh-build-node", + "mode": "0644", + "template": "files/known_hosts" + }, + { + "id": "git-config", + "type": "file", + "path": "${dir:state}/.config/git/config", + "owner": "mesh-build-node", + "mode": "0644", + "template": "files/gitconfig" + }, + { + "id": "toolchain-dir", + "type": "archive", + "path": "${dir:state}/toolchain", + "owner": "mesh-build-node", + "artifact": "toolchain", + "unknown": "program" + }, + { + "id": "git", + "type": "package", + "package": "git" + }, + { + "id": "openssh", + "type": "package", + "package": "openssh" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/mesh-build-node", + "binary": "mesh-build-node", + "loads": [ + "mesh-build-node" + ] + }, + { + "name": "files", + "kind": "archive", + "from": "files" + }, + { + "name": "toolchain", + "kind": "archive", + "from": "toolchain" + } + ] + } +} diff --git a/go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured b/go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured new file mode 100644 index 0000000..c357344 --- /dev/null +++ b/go/testdata/beside/mesh-catalog/modules/nodejs/module.json.captured @@ -0,0 +1,25 @@ +{ + "module": "nodejs", + "version": "1", + "capabilities": [ + "package-manager" + ], + "provides": [ + { + "name": "node-toolchain", + "reach": "machine" + } + ], + "resources": [ + { + "id": "nodejs", + "type": "package", + "package": "nodejs" + }, + { + "id": "npm", + "type": "package", + "package": "npm" + } + ] +}