diff --git a/libs/template/execute.go b/libs/template/execute.go index 9d2471321..6f63a7acf 100644 --- a/libs/template/execute.go +++ b/libs/template/execute.go @@ -2,6 +2,7 @@ package template import ( "errors" + "io/fs" "os" "path/filepath" "strings" @@ -10,9 +11,10 @@ import ( // Executes the template by applying config on it. Returns the materialized config // as a string -func executeTemplate(config map[string]any, templateDefination string) (string, error) { +// TODO: test this function +func executeTemplate(config map[string]any, templateDefinition string) (string, error) { // configure template with helper functions - tmpl, err := template.New("").Funcs(HelperFuncs).Parse(templateDefination) + tmpl, err := template.New("").Funcs(HelperFuncs).Parse(templateDefinition) if err != nil { return "", err } @@ -26,7 +28,8 @@ func executeTemplate(config map[string]any, templateDefination string) (string, return result.String(), nil } -func generateFile(config map[string]any, pathTemplate, contentTemplate string) error { +// TODO: test this function +func generateFile(config map[string]any, pathTemplate, contentTemplate string, perm fs.FileMode) error { // compute file content fileContent, err := executeTemplate(config, contentTemplate) if errors.Is(err, errSkipThisFile) { @@ -50,41 +53,41 @@ func generateFile(config map[string]any, pathTemplate, contentTemplate string) e } // write content to file - return os.WriteFile(path, []byte(fileContent), 0644) + return os.WriteFile(path, []byte(fileContent), perm) } -func walkFileTree(config map[string]any, templatePath, instancePath string) error { - entries, err := os.ReadDir(templatePath) - if err != nil { - return err - } - for _, entry := range entries { - if entry.IsDir() { - // compute directory name - dirName, err := executeTemplate(config, entry.Name()) - if err != nil { - return err - } - - // recusively generate files and directories inside inside our newly generated - // directory from the template defination - err = walkFileTree(config, filepath.Join(templatePath, entry.Name()), filepath.Join(instancePath, dirName)) - if err != nil { - return err - } - } else { - // case: materialize a template file with it's contents - b, err := os.ReadFile(filepath.Join(templatePath, entry.Name())) - if err != nil { - return err - } - contentTemplate := string(b) - fileNameTemplate := entry.Name() - err = generateFile(config, filepath.Join(instancePath, fileNameTemplate), contentTemplate) - if err != nil { - return err - } +func walkFileTree(config map[string]any, templateRoot, instanceRoot string) error { + return filepath.WalkDir(templateRoot, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err } - } - return nil + + // skip if current entry is a directory + if d.IsDir() { + return nil + } + + // read template file to get the templatized content for the file + b, err := os.ReadFile(path) + if err != nil { + return err + } + contentTemplate := string(b) + + // get relative path to the template file, This forms the template for the + // path to the file + relPathTemplate, err := filepath.Rel(templateRoot, path) + if err != nil { + return err + } + + // Get info about the template file. Used to ensure instance path also + // has the same permission bits + info, err := d.Info() + if err != nil { + return err + } + + return generateFile(config, filepath.Join(instanceRoot, relPathTemplate), contentTemplate, info.Mode().Perm()) + }) } diff --git a/libs/template/materialize.go b/libs/template/materialize.go index 15bb1a0e1..b50b7a4f2 100644 --- a/libs/template/materialize.go +++ b/libs/template/materialize.go @@ -8,9 +8,9 @@ const ConfigFileName = "config.json" const schemaFileName = "schema.json" const templateDirName = "template" -func Materialize(templatePath, instancePath, configPath string) error { +func Materialize(templateRoot, instanceRoot, configPath string) error { // read the file containing schema for template input parameters - schema, err := ReadSchema(filepath.Join(templatePath, schemaFileName)) + schema, err := ReadSchema(filepath.Join(templateRoot, schemaFileName)) if err != nil { return err } @@ -22,5 +22,5 @@ func Materialize(templatePath, instancePath, configPath string) error { } // materialize the template - return walkFileTree(config, filepath.Join(templatePath, templateDirName), instancePath) + return walkFileTree(config, filepath.Join(templateRoot, templateDirName), instanceRoot) } diff --git a/libs/template/materialize_test.go b/libs/template/materialize_test.go index d5149eede..07fac5184 100644 --- a/libs/template/materialize_test.go +++ b/libs/template/materialize_test.go @@ -1,6 +1,7 @@ package template import ( + "io/fs" "os" "path/filepath" "testing" @@ -20,6 +21,12 @@ func setupConfig(t *testing.T, config string) string { return tmp } +func assertFilePerm(t *testing.T, path string, perm fs.FileMode) { + stat, err := os.Stat(path) + require.NoError(t, err) + assert.Equal(t, stat.Mode().Perm(), perm) +} + func TestMaterializeEmptyDirsAreNotGenerated(t *testing.T) { tmp := setupConfig(t, ` { @@ -51,3 +58,67 @@ func TestMaterializeEmptyDirsAreNotGenerated(t *testing.T) { assert.DirExists(t, filepath.Join(tmp2, "dir-not-skipped-this-time")) assert.FileExists(t, filepath.Join(tmp2, "dir-not-skipped-this-time/foo")) } + +func TestMaterializedTemplatesHaveIdenticalFilePermissionsAsTemplate(t *testing.T) { + // create template + tmp := t.TempDir() + err := os.Mkdir(filepath.Join(tmp, "my_tmpl"), 0777) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "schema.json"), []byte(` + { + "version": 0, + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "string" + } + } + }`), 0644) + require.NoError(t, err) + + // A normal file with the executable bit not flipped + err = os.Mkdir(filepath.Join(tmp, "my_tmpl", "template"), 0777) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "template", "{{.a}}"), []byte("abc"), 0600) + require.NoError(t, err) + + // A read only file + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "template", "{{.b}}"), []byte("def"), 0400) + require.NoError(t, err) + + // A read only executable file + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "template", "foo"), []byte("ghi"), 0500) + require.NoError(t, err) + + // An executable script, accessable by non user access classes + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "template", "bar"), []byte("ghi"), 0755) + require.NoError(t, err) + + // create config.json file + err = os.Mkdir(filepath.Join(tmp, "config"), 0777) + require.NoError(t, err) + configPath := filepath.Join(tmp, "config", "config.json") + err = os.WriteFile(configPath, []byte(` + { + "a": "Amsterdam", + "b": "Hague" + }`), 0644) + require.NoError(t, err) + + // create directory to initialize the template in + instanceRoot := filepath.Join(tmp, "instance") + err = os.Mkdir(instanceRoot, 0777) + require.NoError(t, err) + + // materialize the template + err = Materialize(filepath.Join(tmp, "my_tmpl"), instanceRoot, configPath) + require.NoError(t, err) + + // assert template files have the correct permission bits set + assertFilePerm(t, filepath.Join(instanceRoot, "Amsterdam"), 0600) + assertFilePerm(t, filepath.Join(instanceRoot, "Hague"), 0400) + assertFilePerm(t, filepath.Join(instanceRoot, "foo"), 0500) + assertFilePerm(t, filepath.Join(instanceRoot, "bar"), 0755) +}