Files
mesh-controller/internal/catalogue/templates_dir_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

152 lines
6.8 KiB
Go

package catalogue
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// A module may name the directory its templates are kept in (`"templates": "<dir>"`, default files/; issue 731): a
// module whose files/ is already an archive unpacked onto the machine keeps its templates in a directory of their
// own, packed into an archive no resource names, so its unpacked files/ is exactly what it was.
// The two forms of one module whose files/ is unpacked onto the machine: its text inline, and in templates/.
const (
unpackingInline = `{"module":"tidy","version":"1",
"build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}]},
"resources":[
{"id":"scripts","type":"archive","path":"/usr/local/lib/tidy","artifact":"scripts"},
{"id":"conf","type":"directory","path":"${dir:conf}"},
{"id":"main","type":"file","path":"/etc/tidy.conf","mode":"0644","content":"` + mainText + `"},
{"id":"extra","type":"file","path":"/etc/extra.conf","content":"` + extraText + `"}]}`
unpackingTemplated = `{"module":"tidy","version":"1",
"templates":"templates",
"build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"},
{"name":"templates","kind":"archive","from":"templates"}]},
"resources":[
{"id":"scripts","type":"archive","path":"/usr/local/lib/tidy","artifact":"scripts"},
{"id":"conf","type":"directory","path":"${dir:conf}"},
{"id":"main","type":"file","path":"/etc/tidy.conf","mode":"0644","template":"templates/tidy.conf"},
{"id":"extra","type":"file","path":"/etc/extra.conf","template":"templates/deeper/extra.conf"}]}`
)
func builtUnpacking(templates map[string]string) []Built {
built := []Built{{Name: "scripts", Kind: ArtifactArchive,
Reference: "https://store.invalid/scripts.tar.gz",
Digest: "sha256:" + strings.Repeat("a", 64)}}
if templates != nil {
built = append(built, Built{Name: "templates", Kind: ArtifactArchive,
Reference: ArtifactStoreScheme + "tidy/templates/blobs/sha256:" + strings.Repeat("b", 64),
Digest: "sha256:" + strings.Repeat("b", 64), Texts: templates})
}
return built
}
func TestAModuleWithItsOwnTemplatesDirectoryComposesByteIdenticallyToItsInlineTwin(t *testing.T) {
inline := composed(t, held(t, unpackingInline, builtUnpacking(nil)))
templated := composed(t, held(t, unpackingTemplated, builtUnpacking(templateTexts(t))))
if inline != templated {
t.Fatalf("the two forms compose differently:\ninline: %s\ntemplated: %s", inline, templated)
}
// The unpacked archive is the files/ one, by its digest; the templates archive reaches no machine.
if !strings.Contains(templated, strings.Repeat("a", 64)) || strings.Contains(templated, strings.Repeat("b", 64)) {
t.Fatalf("the composition does not unpack files/ alone: %s", templated)
}
}
func TestTheManifestTheMeshHoldsNamesNoTemplatesDirectory(t *testing.T) {
m := held(t, unpackingTemplated, builtUnpacking(templateTexts(t)))
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), `"templates"`) {
t.Fatalf("the held manifest still names its templates directory: %s", raw)
}
}
func TestAModuleWithItsOwnTemplatesDirectoryIsReadFromItsTree(t *testing.T) {
dir := t.TempDir()
for name, text := range templateTexts(t) {
full := filepath.Join(dir, "templates", filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte(text), 0o644); err != nil {
t.Fatal(err)
}
}
// files/ holds what the machine unpacks, a symlink among it judged by nobody here: it is not the templates.
if err := os.MkdirAll(filepath.Join(dir, "files", "bin"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "files", "bin", "tidy"), []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
m, err := ParseManifest([]byte(unpackingTemplated))
if err != nil {
t.Fatal(err)
}
filled, err := WithTemplatesFrom(m, dir)
if err != nil {
t.Fatal(err)
}
resolved, err := filled.Resolve(builtUnpacking(map[string]string{}))
if err != nil {
t.Fatal(err)
}
if got, want := composed(t, resolved), composed(t, held(t, unpackingInline, builtUnpacking(nil))); got != want {
t.Fatalf("read from the tree, the module composes differently:\ninline: %s\ntemplated: %s", want, got)
}
// A symlink under the templates directory is refused by its path, as under files/.
if err := os.Symlink("/etc/hostname", filepath.Join(dir, "templates", "elsewhere")); err != nil {
t.Fatal(err)
}
if _, err := WithTemplatesFrom(m, dir); err == nil || !strings.Contains(err.Error(), "templates/elsewhere is a symlink") {
t.Fatalf("a symlink under templates/ was not refused by its path: %v", err)
}
}
func TestATemplateOutsideTheNamedTemplatesDirectoryIsRefused(t *testing.T) {
raw := strings.Replace(unpackingTemplated, `"template":"templates/tidy.conf"`, `"template":"files/tidy.conf"`, 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "files/tidy.conf") ||
!strings.Contains(err.Error(), "templates/") {
t.Fatalf("a template outside the named templates directory was not refused by name: %v", err)
}
}
func TestATemplatesDirectoryNoArchiveIsBuiltFromIsRefused(t *testing.T) {
raw := strings.Replace(unpackingTemplated, `,
{"name":"templates","kind":"archive","from":"templates"}`, "", 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "no archive from templates/") {
t.Fatalf("a module whose build packs no archive from its templates directory was not refused: %v", err)
}
}
func TestATemplatesDirectoryOnAModuleNamingNoTemplateIsRefused(t *testing.T) {
raw := strings.Replace(unpackingInline, `"version":"1",`, `"version":"1","templates":"templates",`, 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "names no template") {
t.Fatalf("a templates directory read by nothing was not refused: %v", err)
}
}
func TestATemplatesDirectoryThatIsNotAPlainRelativeDirectoryIsRefused(t *testing.T) {
for _, dir := range []string{"/etc", "../other", "..", "templates/../files", "./templates", ".", "a\\b", "templates/"} {
raw := strings.Replace(unpackingTemplated, `"templates":"templates"`, `"templates":`+jsonString(dir), 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "templates directory") {
t.Errorf("the templates directory %q was not refused: %v", dir, err)
}
}
}
func TestATemplatesDirectoryNamedFilesIsTheDefault(t *testing.T) {
raw := strings.Replace(templatedTwin, `"version":"1",`, `"version":"1","templates":"files",`, 1)
if got, want := composed(t, held(t, raw, builtFiles(templateTexts(t)))),
composed(t, held(t, inlineTwin, builtFiles(nil))); got != want {
t.Fatalf("naming files/ as the templates directory composes differently:\n%s\n%s", got, want)
}
}