Files
mesh-controller/internal/catalogue/handed_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

189 lines
7.7 KiB
Go

package catalogue
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
// A module marked moved hands its templated files to the provisioner (novox/hq ADR 0349 §4, §6, §17, design 57 P6,
// issue 672): on a machine whose node-engine takes a filled file on the client's word and that runs a client, each
// is declared placed and without its text, named among the filled files, and planned for the client; anywhere
// else, and for every module not marked, the composition is byte for byte what it was.
const (
handedNotifier = `{"module":"notify","version":"1",
"config":{"width":{"type":"number","description":"How wide a notification is, in pixels.","default":250}},
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[
{"id":"configuration-dir","type":"directory","path":"${machine:account-home}/.config/notify","owner":"${machine:account}","mode":"0755"},
{"id":"configuration","type":"file","path":"${machine:account-home}/.config/notify/rc","owner":"${machine:account}","mode":"0644","template":"files/rc"},
{"id":"legacy","type":"file","path":"/etc/notify.conf","content":"plain = yes\n"},
{"id":"notifier","type":"service","unit":"notify.service","scope":"user","user":"${machine:account}","reload-on":["configuration"]}]}`
notifierText = "width = ${config:width}\nhome = ${machine:account-home}\n"
)
func notifierMoved() string {
return strings.Replace(handedNotifier, `"version":"1",`, `"version":"1","moved":true,`, 1)
}
func builtNotifier() []Built {
return []Built{{Name: "files", Kind: ArtifactArchive,
Reference: ArtifactStoreScheme + "notify/files/blobs/sha256:" + strings.Repeat("d", 64),
Digest: "sha256:" + strings.Repeat("d", 64), Texts: map[string]string{"rc": notifierText}}}
}
func notifierOn(t *testing.T, m Manifest) Resolution {
t.Helper()
r, err := Resolve(shelf(m), []string{m.Module}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
r.Account = "op"
return r
}
func composeWith(t *testing.T, r Resolution, with Rendering) (Composed, string) {
t.Helper()
c, err := r.Compose(with)
if err != nil {
t.Fatal(err)
}
raw, err := json.Marshal(c.Resources)
if err != nil {
t.Fatal(err)
}
return c, string(raw)
}
func handingTo(store string) Rendering {
return Rendering{Hands: true, Provisioner: "mesh-provisioner-client", ArtifactStore: store}
}
func TestAMovedModulesBuildKeepsWhatTheProvisionerNeeds(t *testing.T) {
m := held(t, notifierMoved(), builtNotifier())
want := &Handed{Source: ArtifactStoreScheme + "notify/files/blobs/sha256:" + strings.Repeat("d", 64),
Digest: "sha256:" + strings.Repeat("d", 64), Templates: map[string]string{"configuration": "rc"}}
if !reflect.DeepEqual(m.Handed, want) {
t.Fatalf("the build kept %+v, want %+v", m.Handed, want)
}
if notMoved := held(t, handedNotifier, builtNotifier()); notMoved.Handed != nil {
t.Fatalf("a module not marked moved kept %+v", notMoved.Handed)
}
}
// Nothing changes for a module that has not moved, nor for a moved one where it cannot be handed.
func TestAMovedModuleComposesAsBeforeWhereItIsNotHanded(t *testing.T) {
_, before := composeWith(t, notifierOn(t, held(t, handedNotifier, builtNotifier())), handingTo("store:5000"))
for name, with := range map[string]Rendering{
"an engine that does not take a filled file alone": {Provisioner: "mesh-provisioner-client", ArtifactStore: "store:5000"},
"no provisioner client on the machine": {Hands: true, ArtifactStore: "store:5000"},
} {
c, got := composeWith(t, notifierOn(t, held(t, notifierMoved(), builtNotifier())), with)
if got != before {
t.Errorf("%s: the moved module composes differently from its unmarked twin:\nbefore: %s\nnow: %s",
name, before, got)
}
if len(c.Filled) > 0 || c.Plans != nil {
t.Errorf("%s: something was handed: %v %v", name, c.Filled, c.Plans)
}
}
if !strings.Contains(before, `width = 250`) {
t.Fatalf("the unmarked module is not filled the old way: %s", before)
}
}
func TestAMovedModulesTemplatedFileIsHandedToTheProvisioner(t *testing.T) {
c, got := composeWith(t, notifierOn(t, held(t, notifierMoved(), builtNotifier())), handingTo("store:5000"))
var file, legacy, dir map[string]any
for _, r := range c.Resources {
switch r["id"] {
case "notify.configuration":
file = r
case "notify.legacy":
legacy = r
case "notify.configuration-dir":
dir = r
}
}
if file == nil || legacy == nil || dir == nil {
t.Fatalf("not composed: %s", got)
}
wantFile := map[string]any{"id": "notify.configuration", "type": "file", "path": "/home/op/.config/notify/rc",
"owner": "op", "mode": "0644", "owner-at-runtime": "owned"}
if !reflect.DeepEqual(file, wantFile) {
t.Fatalf("the handed file is declared %v, want %v (placed, no text, owned at runtime)", file, wantFile)
}
if legacy["content"] != "plain = yes\n" || legacy["owner-at-runtime"] != nil {
t.Fatalf("a file with its text inline is not composed the old way: %v", legacy)
}
if dir["path"] != "/home/op/.config/notify" {
t.Fatalf("the module's other resources are not placed as before: %v", dir)
}
if strings.Contains(got, "width = ") {
t.Fatalf("the controller filled the handed file: %s", got)
}
if !reflect.DeepEqual(c.Filled, []string{"/home/op/.config/notify/rc"}) {
t.Fatalf("filled files %v", c.Filled)
}
want := map[string]Plan{"notify": {
Templates: PlanTemplates{Source: "http://store:5000/v2/notify/files/blobs/sha256:" + strings.Repeat("d", 64),
Digest: "sha256:" + strings.Repeat("d", 64)},
Files: []PlanFile{{ID: "configuration", Path: "/home/op/.config/notify/rc", Template: "rc",
OwnerAtRuntime: "owned", Takes: "reload-on"}},
Placement: map[string]string{"machine:account-home": "/home/op"},
}}
if !reflect.DeepEqual(c.Plans, want) {
t.Fatalf("the plans are %+v, want %+v", c.Plans, want)
}
}
func TestAMovedModulesMarkIsJudged(t *testing.T) {
for name, raw := range map[string]string{
"marked moved and naming no template": `{"module":"bare","version":"1","moved":true,
"resources":[{"id":"conf","type":"file","path":"/etc/bare","content":"x"}]}`,
"handed written in a repository": `{"module":"bare","version":"1","moved":true,
"handed":{"source":"s","digest":"d","templates":{}},
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[{"id":"conf","type":"file","path":"/etc/bare","template":"files/bare"}]}`,
"handed and not moved": `{"module":"bare","version":"1",
"handed":{"source":"s","digest":"d","templates":{}},
"resources":[{"id":"conf","type":"file","path":"/etc/bare","content":"x"}]}`,
} {
if _, err := ParseManifest([]byte(raw)); err == nil {
t.Errorf("%s: accepted", name)
}
}
}
// What `module check` and the merge gate judge: a moved module read from its tree, its text inline. It is still
// marked moved, and composes as before, since nothing of its build's archive is there to hand.
func TestAMovedModuleReadFromItsTreeIsJudgedAsItComposesUnhanded(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "files", "rc"), []byte(notifierText), 0o644); err != nil {
t.Fatal(err)
}
m, err := ParseManifest([]byte(notifierMoved()))
if err != nil {
t.Fatal(err)
}
filled, err := WithTemplatesFrom(m, dir)
if err != nil {
t.Fatalf("a moved module read from its tree is refused: %v", err)
}
if !filled.Moved || filled.Handed != nil {
t.Fatalf("read from its tree: moved %v, handed %+v", filled.Moved, filled.Handed)
}
_, got := composeWith(t, notifierOn(t, filled), handingTo("store:5000"))
_, want := composeWith(t, notifierOn(t, held(t, handedNotifier, builtNotifier())), handingTo("store:5000"))
if got != want {
t.Fatalf("read from its tree it composes differently:\nwant: %s\ngot: %s", want, got)
}
}