Skip to content

Commit

Permalink
Merge pull request #1495 from anhdat/1077/write-to-file-function
Browse files Browse the repository at this point in the history
Add writeToFile function
  • Loading branch information
eikenb authored Jul 29, 2021
2 parents 518b957 + 86b97d8 commit 0b1d208
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 7 deletions.
26 changes: 20 additions & 6 deletions docs/templating-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ provides the following functions:
- [toUpper](#toupper)
- [toYAML](#toyaml)
- [sockaddr](#sockaddr)
- [writeToFile](#writeToFile)
- [Math Functions](#math-functions)
- [add](#add)
- [subtract](#subtract)
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions template/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"os/user"
"path/filepath"
"reflect"
"regexp"
Expand Down Expand Up @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,8 @@ func funcMap(i *funcMapInput) template.FuncMap {
"split": split,
"byMeta": byMeta,
"sockaddr": sockaddr,

"writeToFile": writeToFile,

// Math functions
"add": add,
"subtract": subtract,
Expand Down
125 changes: 125 additions & 0 deletions template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"io/ioutil"
"os"
"os/user"
"reflect"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -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)
}
})
}
}

0 comments on commit 0b1d208

Please sign in to comment.