package catalogue
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
// A module may name the directory its templates are kept in (`"templates": "
"`, 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)
}
}