Files
mesh-controller/internal/catalogue/handed_test.go
T

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)
}
}