From 98ad96c8fd883bd48fd0fa3a2218dcaf91e4255e Mon Sep 17 00:00:00 2001 From: Dat Truong Date: Wed, 28 Jul 2021 14:41:06 +0200 Subject: [PATCH 1/3] Add writeToFile function --- template/funcs.go | 72 ++++++++++++++++++++++++++++++++++++++++++++ template/template.go | 3 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/template/funcs.go b/template/funcs.go index cd6ea6d29..66990b1c2 100644 --- a/template/funcs.go +++ b/template/funcs.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "os" "os/exec" + "os/user" "path/filepath" "reflect" "regexp" @@ -1596,6 +1597,77 @@ 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. +// +// For example: +// 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" +// +func writeToFile(path, username, groupName, permissions string, args ...string) (string, error) { + // Parse arguments + flags := "" + if len(args) == 2 { + flags = args[0] + } + content := args[len(args)-1] + + p_u, err := strconv.ParseUint(permissions, 8, 32) + if err != nil { + return "", err + } + perm := os.FileMode(p_u) + + // Write to file + var f *os.File + shouldAppend := strings.Contains(flags, "append") + if shouldAppend { + f, err = os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, perm) + if err != nil { + return "", err + } + } else { + f, err = os.Create(path) + if err != nil { + return "", err + } + } + defer f.Close() + + writingContent := []byte(content) + shouldAddNewLine := strings.Contains(flags, "newline") + if shouldAddNewLine { + writingContent = append(writingContent, []byte("\n")...) + } + if _, err = f.Write(writingContent); err != nil { + return "", err + } + + // 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 + } + + err = os.Chmod(path, perm) + if err != nil { + return "", err + } + + return "", nil +} + func spewSdump(args ...interface{}) (string, error) { return spewLib.Sdump(args...), nil } diff --git a/template/template.go b/template/template.go index a637cc2c1..3dbf10114 100644 --- a/template/template.go +++ b/template/template.go @@ -294,7 +294,8 @@ func funcMap(i *funcMapInput) template.FuncMap { "split": split, "byMeta": byMeta, "sockaddr": sockaddr, - + "writeToFile": writeToFile, + // Math functions "add": add, "subtract": subtract, From 42e20cb68ca27df381ef9fd1d3d5c838ba3ee54d Mon Sep 17 00:00:00 2001 From: Dat Truong Date: Wed, 28 Jul 2021 14:46:18 +0200 Subject: [PATCH 2/3] Add writeToFile documentation --- docs/templating-language.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/templating-language.md b/docs/templating-language.md index 8a233b2e8..dfcf00200 100644 --- a/docs/templating-language.md +++ b/docs/templating-language.md @@ -79,6 +79,7 @@ provides the following functions: - [toUpper](#toupper) - [toYAML](#toyaml) - [sockaddr](#sockaddr) + - [writeToFile](#writeToFile) - [Math Functions](#math-functions) - [add](#add) - [subtract](#subtract) @@ -575,7 +576,7 @@ To iterate and list over every secret in the generic secret backend in Vault: {{ with secret (printf "secret/%s" .) }}{{ range $k, $v := .Data }} {{ $k }}: {{ $v }} {{ end }}{{ end }}{{ end }} -``` +``` `.Data` should be replaced with `.Data.data` for KV-V2 secrets engines. @@ -1573,6 +1574,19 @@ Takes a quote-escaped template string as an argument and passes it on to See [hashicorp/go-sockaddr documentation](https://godoc.org/github.com/hashicorp/go-sockaddr) for more information. +### `writeToFile` + +Writes the content to a file with username, group name, permissions. There are optional flags to +select appending mode or add a newline. + +For example: + +```golang +{{ 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" }} +``` + --- ## Math Functions @@ -1704,7 +1718,7 @@ or an error. renders ```golang -> +> (map[string]interface {}) (len=1) { (string) (len=3) "foo": (map[string]interface {}) (len=3) { (string) (len=3) "bar": (bool) true, @@ -1727,7 +1741,7 @@ Creates a string containing the values with full newlines, indentation, type, an renders ```golang -> +> (map[string]interface {}) (len=1) { (string) (len=3) "foo": (map[string]interface {}) (len=3) { (string) (len=3) "bar": (bool) true, @@ -1760,19 +1774,19 @@ Given this template fragment, {{- $OBJ := parseJSON $JSON -}} ``` -#### using `%v` +#### using `%v` ```golang {{- spew_printf "%v\n" $OBJ }} ``` -outputs +outputs ```golang map[foo:map[bar:true baz:string theAnswer:42]] ``` -#### using `%+v` +#### using `%+v` ```golang From d5c13e46104ce9e2dc1cc75e9d1acd341427c64a Mon Sep 17 00:00:00 2001 From: Dat Truong Date: Thu, 29 Jul 2021 16:39:10 +0200 Subject: [PATCH 3/3] Add writeToFile tests --- template/template_test.go | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/template/template_test.go b/template/template_test.go index 583783f2d..138628c1e 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -5,7 +5,9 @@ import ( "fmt" "io/ioutil" "os" + "os/user" "reflect" + "strconv" "testing" "time" @@ -1876,3 +1878,126 @@ func TestTemplate_Execute(t *testing.T) { }) } } + +func Test_writeToFile(t *testing.T) { + cases := []struct { + name string + content string + permissions string + flags string + expectation string + wantErr bool + }{ + { + "writeToFile_without_flags", + "after", + "0644", + "", + "after", + false, + }, + { + "writeToFile_with_different_file_permissions", + "after", + "0666", + "", + "after", + false, + }, + { + "writeToFile_with_append", + "after", + "0644", + `"append"`, + "beforeafter", + false, + }, + { + "writeToFile_with_newline", + "after", + "0644", + `"newline"`, + "after\n", + false, + }, + { + "writeToFile_with_append_and_newline", + "after", + "0644", + `"append,newline"`, + "beforeafter\n", + false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + outDir, err := ioutil.TempDir("", "") + if err != nil { + 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) + } + 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) + ti := &NewTemplateInput{ + Contents: templateContent, + } + tpl, err := NewTemplate(ti) + if err != nil { + t.Fatal(err) + } + + a, err := tpl.Execute(nil) + if (err != nil) != tc.wantErr { + t.Errorf("writeToFile() error = %v, wantErr %v", err, tc.wantErr) + return + } + + // Compare generated file content with the expectation. + // The function should generate an empty string to the output. + _generatedFileContent, err := ioutil.ReadFile(outputFile.Name()) + generatedFileContent := string(_generatedFileContent) + if err != nil { + t.Fatal(err) + } + if a != nil && !bytes.Equal([]byte(""), a.Output) { + t.Errorf("writeToFile() template = %v, want empty string", a.Output) + } + if generatedFileContent != tc.expectation { + t.Errorf("writeToFile() got = %v, want %v", generatedFileContent, tc.expectation) + } + // Assert output file permissions + sts, err := outputFile.Stat() + if err != nil { + t.Fatal(err) + } + p_u, err := strconv.ParseUint(tc.permissions, 8, 32) + if err != nil { + t.Fatal(err) + } + perm := os.FileMode(p_u) + if sts.Mode() != perm { + t.Errorf("writeToFile() wrong permissions got = %v, want %v", perm, tc.permissions) + } + }) + } +}