diff --git a/template/funcs.go b/template/funcs.go index 2cb24ec1f..c5d1e3bcf 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -1611,10 +1611,12 @@ func md5sum(item string) (string, error) { return fmt.Sprintf("%x", md5.Sum([]byte(item))), nil } -// writeToFile writes the content to a file with username, group name, permissions and optional -// flags to select appending mode or add a newline. +// writeToFile writes the content to a file with permissions and optional username/UID, +// group name/GID, and flags to select appending mode or add a newline. // // For example: +// key "my/key/path" | writeToFile "/my/file/path.txt" "" "" "0644" +// key "my/key/path" | writeToFile "/my/file/path.txt" "100" "1000" "0644" // key "my/key/path" | writeToFile "/my/file/path.txt" "my-user" "my-group" "0644" // key "my/key/path" | writeToFile "/my/file/path.txt" "my-user" "my-group" "0644" "append" // key "my/key/path" | writeToFile "/my/file/path.txt" "my-user" "my-group" "0644" "append,newline" @@ -1642,6 +1644,15 @@ func writeToFile(path, username, groupName, permissions string, args ...string) return "", err } } else { + dirPath := filepath.Dir(path) + + if _, err := os.Stat(dirPath); err != nil { + err := os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + return "", err + } + } + f, err = os.Create(path) if err != nil { return "", err @@ -1659,19 +1670,35 @@ func writeToFile(path, username, groupName, permissions string, args ...string) } // Change ownership and permissions - u, err := user.Lookup(username) - if err != nil { - return "", err - } - g, err := user.LookupGroup(groupName) - if err != nil { - return "", err - } - uid, _ := strconv.Atoi(u.Uid) - gid, _ := strconv.Atoi(g.Gid) - err = os.Chown(path, uid, gid) - if err != nil { - return "", err + if username != "" || groupName != "" { + uid := 0 + gid := 0 + var convErr error + u, err := user.Lookup(username) + if err != nil { + // Check if username string is already a UID + uid, convErr = strconv.Atoi(username) + if convErr != nil { + return "", err + } + } else { + uid, _ = strconv.Atoi(u.Uid) + } + + g, err := user.LookupGroup(groupName) + if err != nil { + gid, convErr = strconv.Atoi(groupName) + if convErr != nil { + return "", err + } + } else { + gid, _ = strconv.Atoi(g.Gid) + } + + err = os.Chown(path, uid, gid) + if err != nil { + return "", err + } } err = os.Chmod(path, perm) diff --git a/template/template_test.go b/template/template_test.go index 1823aa582..d327278b4 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -8,6 +8,7 @@ import ( "os/user" "reflect" "strconv" + "syscall" "testing" "time" @@ -1978,9 +1979,24 @@ func TestTemplate_Execute(t *testing.T) { } func Test_writeToFile(t *testing.T) { + // Use current user and its primary group for input + currentUser, err := user.Current() + if err != nil { + t.Fatal(err) + } + currentUsername := currentUser.Username + currentGroup, err := user.LookupGroupId(currentUser.Gid) + if err != nil { + t.Fatal(err) + } + currentGroupName := currentGroup.Name + cases := []struct { name string + filePath string content string + username string + groupName string permissions string flags string expectation string @@ -1988,7 +2004,10 @@ func Test_writeToFile(t *testing.T) { }{ { "writeToFile_without_flags", + "", "after", + currentUsername, + currentGroupName, "0644", "", "after", @@ -1996,7 +2015,10 @@ func Test_writeToFile(t *testing.T) { }, { "writeToFile_with_different_file_permissions", + "", "after", + currentUsername, + currentGroupName, "0666", "", "after", @@ -2004,7 +2026,10 @@ func Test_writeToFile(t *testing.T) { }, { "writeToFile_with_append", + "", "after", + currentUsername, + currentGroupName, "0644", `"append"`, "beforeafter", @@ -2012,7 +2037,10 @@ func Test_writeToFile(t *testing.T) { }, { "writeToFile_with_newline", + "", "after", + currentUsername, + currentGroupName, "0644", `"newline"`, "after\n", @@ -2020,12 +2048,48 @@ func Test_writeToFile(t *testing.T) { }, { "writeToFile_with_append_and_newline", + "", "after", + currentUsername, + currentGroupName, "0644", `"append,newline"`, "beforeafter\n", false, }, + { + "writeToFile_default_owner", + "", + "after", + "", + "", + "0644", + "", + "after", + false, + }, + { + "writeToFile_provide_uid_gid", + "", + "after", + currentUser.Uid, + currentUser.Gid, + "0644", + "", + "after", + false, + }, + { + "writeToFile_create_directory", + "demo/testing.tmp", + "after", + currentUsername, + currentGroupName, + "0644", + "", + "after", + false, + }, } for _, tc := range cases { @@ -2035,27 +2099,25 @@ func Test_writeToFile(t *testing.T) { t.Fatal(err) } defer os.RemoveAll(outDir) - outputFile, err := ioutil.TempFile(outDir, "") - if err != nil { - t.Fatal(err) - } - outputFile.WriteString("before") - // Use current user and its primary group for input - currentUser, err := user.Current() - if err != nil { - t.Fatal(err) + var outputFilePath string + if tc.filePath == "" { + outputFile, err := ioutil.TempFile(outDir, "") + if err != nil { + t.Fatal(err) + } + _, err = outputFile.WriteString("before") + if err != nil { + t.Fatal(err) + } + outputFilePath = outputFile.Name() + } else { + outputFilePath = outDir + "/" + tc.filePath } - currentUsername := currentUser.Username - currentGroup, err := user.LookupGroupId(currentUser.Gid) - if err != nil { - t.Fatal(err) - } - currentGroupName := currentGroup.Name templateContent := fmt.Sprintf( "{{ \"%s\" | writeToFile \"%s\" \"%s\" \"%s\" \"%s\" %s}}", - tc.content, outputFile.Name(), currentUsername, currentGroupName, tc.permissions, tc.flags) + tc.content, outputFilePath, tc.username, tc.groupName, tc.permissions, tc.flags) ti := &NewTemplateInput{ Contents: templateContent, } @@ -2072,7 +2134,7 @@ func Test_writeToFile(t *testing.T) { // Compare generated file content with the expectation. // The function should generate an empty string to the output. - _generatedFileContent, err := ioutil.ReadFile(outputFile.Name()) + _generatedFileContent, err := ioutil.ReadFile(outputFilePath) generatedFileContent := string(_generatedFileContent) if err != nil { t.Fatal(err) @@ -2084,7 +2146,7 @@ func Test_writeToFile(t *testing.T) { t.Errorf("writeToFile() got = %v, want %v", generatedFileContent, tc.expectation) } // Assert output file permissions - sts, err := outputFile.Stat() + sts, err := os.Stat(outputFilePath) if err != nil { t.Fatal(err) } @@ -2096,6 +2158,13 @@ func Test_writeToFile(t *testing.T) { if sts.Mode() != perm { t.Errorf("writeToFile() wrong permissions got = %v, want %v", perm, tc.permissions) } + + stat := sts.Sys().(*syscall.Stat_t) + u := strconv.FormatUint(uint64(stat.Uid), 10) + g := strconv.FormatUint(uint64(stat.Gid), 10) + if u != currentUser.Uid || g != currentUser.Gid { + t.Errorf("writeToFile() owner = %v:%v, wanted %v:%v", u, g, currentUser.Uid, currentUser.Gid) + } }) } }