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