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