diff --git a/.gitignore b/.gitignore index 3b4bb84f..fcc74c33 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,4 @@ vkv vendor/ coverage.out /dist -.envrc -format.yaml \ No newline at end of file +.envrc \ No newline at end of file diff --git a/Makefile b/Makefile index 11d22511..148383e7 100644 --- a/Makefile +++ b/Makefile @@ -60,9 +60,4 @@ vault: .PHONY: kill kill: - @kill -9 $(shell pgrep -x vault) 2> /dev/null || true - -.PHONY: gif -gif: - terminalizer record demo -k -c assets/config.yml - terminalizer render demo -o assets/demo.gif \ No newline at end of file + @kill -9 $(shell pgrep -x vault) 2> /dev/null || true \ No newline at end of file diff --git a/README.md b/README.md index 0b7804f0..74303789 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# vkv +
+

vkv

+ drawing -[![Test](https://github.com/FalcoSuessgott/vkv/actions/workflows/test.yml/badge.svg)](https://github.com/FalcoSuessgott/vkv/actions/workflows/test.yml) [![golangci-lint](https://github.com/FalcoSuessgott/vkv/actions/workflows/lint.yml/badge.svg)](https://github.com/FalcoSuessgott/vkv/actions/workflows/lint.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/FalcoSuessgott/vkv)](https://goreportcard.com/report/github.com/FalcoSuessgott/vkv) [![codecov](https://codecov.io/gh/FalcoSuessgott/vkv/branch/master/graph/badge.svg?token=UYVZ8LTA45)](https://codecov.io/gh/FalcoSuessgott/vkv) + [![Test](https://github.com/FalcoSuessgott/vkv/actions/workflows/test.yml/badge.svg)](https://github.com/FalcoSuessgott/vkv/actions/workflows/test.yml) [![golangci-lint](https://github.com/FalcoSuessgott/vkv/actions/workflows/lint.yml/badge.svg)](https://github.com/FalcoSuessgott/vkv/actions/workflows/lint.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/FalcoSuessgott/vkv)](https://goreportcard.com/report/github.com/FalcoSuessgott/vkv) [![codecov](https://codecov.io/gh/FalcoSuessgott/vkv/branch/master/graph/badge.svg?token=UYVZ8LTA45)](https://codecov.io/gh/FalcoSuessgott/vkv) [![Github all releases](https://img.shields.io/github/downloads/FalcoSuessgott/vkv/total.svg)](https://GitHub.com/FalcoSuessgott/vkv/releases/) +
-![img](assets/demo.gif) # Description `vkv` recursively list you all key-value entries from Vaults KV2 secret engine in various formats. `vkv` flags can be devided into input, modifying and output format flags. @@ -16,22 +18,23 @@ So far `vkv` offers: ### Modifying flags * `--only-keys`: show only keys (env: `VKV_ONLY_KEYS`, default: `false`) * `--only-paths`: show only paths (env: `VKV_ONLY_PATHS`, default: `false`) -* `-show-values`: dont mask values (env: `VKV_SHOW_VALUES`, default: `false`) +* `--show-values`: dont mask values (env: `VKV_SHOW_VALUES`, default: `false`) * `--max-value-length`: maximum char length of values (set to `-1` for disabling) (env: `VKV_MAX_VALUE_LENGTH`, default: `12`) +* `--template-file`: path to a file containing Go-template syntax to render the KV entries (env: `VKV_TEMPLATE_FILE`) +* `--template-string`: string containting Go-template syntax to render KV entries (env: `VKV_TEMPLATE_STRING`) -### Output Flags -* `-f | --format`: output format (options: `base`, `yaml`, `json`, `export`, `markdown`) (env: `"VKV_FORMAT"`, default: `"base"`) +### Output Flags (see [Supported Formats](https://github.com/FalcoSuessgott/vkv/tree/template#supported-formats)) +* `-f | --format`: output format (options: `base`, `yaml`, `json`, `export`, `markdown`, `template`) (env: `"VKV_FORMAT"`, default: `"base"`) ⚠️ **A flag always preceed its environment variable** You can combine most of those flags in order to receive the desired output. -For examples see the [Examples](https://github.com/FalcoSuessgott/vkv#examples) # Installation Find the corresponding binaries, `.rpm` and `.deb` packages in the [release](https://github.com/FalcoSuessgott/vkv/releases) section. # Supported OS and Vault Versions -`vkv` is being tested on `Windows`, `MacOS` and `Linux` and also against Vault Version < `v1.8.0` (but it also may work with lower versions). +`vkv` is being tested on `Windows`, `MacOS` and `Linux` and also against Vault Version >= `v1.8.0` (but it also may work with lower versions). # Authentication `vkv` supports token based authentication. It is clear that you can only see the secrets that are allowed by your token policy. @@ -48,257 +51,14 @@ SET VAULT_TOKEN=s.XXX vkv.exe -p ``` -# Examples -Imagine you have the following KV2 structure mounted at path `secret/`: +# Supported Formats +| | | +|:-------------:|:-------------------------------:| +| `base`
![](assets/base.svg)| `markdown`
![](assets/markdown.svg) | +| `json`
![](assets/json.svg)| `yaml`
![](assets/yaml.svg) | -``` -secret/ - demo - foo=bar - - sub - sub=passw0rd - - sub/demo - demo="hello world" - password=s3cre5 - user=admin - - sub/sub2/demo - value=nevermind - password=secret2 - user=database -``` - -## Input -### list secrets (`--path` | `-p` | `VKV_PATHS="kv1:kv2"`) -You can list all secrets recursively by running: - -```bash -$> vkv --path secret -secret/ -├── demo -│ └── foo=*** -├── sub -│ └── sub=******** -├── sub/ -│ └── demo -│ ├── demo=*********** -│ ├── password=****** -│ └── user=***** -└── sub/ - └── sub2/ - └── demo - ├── user=************ - └── value=********* -``` - -You can also specifiy a specific subpaths: - -```bash -$> vkv --path secret/sub/sub2 -secret/sub/sub2/ -└── sub/ - └── sub2/ - └── demo - ├── user=************ - └── value=********* -``` - -and list as much paths as you want: - -```bash -# or as comma separated with no spaces! -$> vkv -p secret -p secret2 -secret/ -├── demo -│ └── foo=*** -├── sub -│ └── sub=******** -├── sub/ -│ └── demo -│ ├── demo=*********** -│ ├── password=****** -│ └── user=***** -└── sub/ - └── sub2/ - └── demo - ├── user=************ - └── value=********* -secret_2/ -├── demo -│ └── foo=*** -├── sub -│ └── sub=******** -├── sub/ -│ └── demo -│ ├── foo=*** -│ ├── password=******** -│ └── user=**** -└── sub/ - └── sub2/ - └── demo - ├── foo=*** - ├── password=******** - └── user=**** -``` - -## Modifying -### list only paths (`--only-paths` | `VKV_ONLY_PATHS=true`) -We can receive only the paths by running - -```bash -$> vkv -p secret --only-paths -secret/ -├── demo -├── sub -├── sub/ -│ └── demo -└── sub/ - └── sub2/ - └── demo -``` - -### list only secret keys (`--only-keys` | `VKV_ONLY_KEYS=true`) -If we want to know just the keys in every directory we can run - -```bash -$> vkv -p secret --only-keys -secret/ -├── demo -│ └── foo -├── sub -│ └── sub -├── sub/ -│ └── demo -│ ├── demo -│ ├── password -│ └── user -└── sub/ - └── sub2/ - └── demo - ├── user - └── value -``` - -### show values (`--show-values` | `VKV_SHOW_VALUES=true`) -Per default values are masked. Using `--show-values` shows the values. **Use with Caution** - -```bash -$> vkv -p secret --show-values -secret/ -├── demo -│ └── foo=bar -├── sub -│ └── sub=password -├── sub/ -│ └── demo -│ ├── demo=hello world -│ ├── password=s3cre5 -│ └── user=admin -└── sub/ - └── sub2/ - └── demo - ├── user=databasepassword=secret2 - └── value=nevermind -``` - -## Output Format -### export format (`--format=export` | `VKV_FORMAT=export`) -You can print out the entries in `export key=value` format for further processing: - -```bash -$> vkv --path secret/sub/sub2 --format=export -export demo="hello world" -export password="s3cre5" -export user="admin" -export user="databasepassword=secret2" -export value="nevermind" -export foo="bar" -export sub="password -``` - -You can then use `eval` to source those env vars: - -```bash -echo $foo # not defined -eval $(vkv -f=export --path secret/sub/sub2) -echo $foo -"bar" # value under the specific key exported -``` - -## markdown (`--format=markdown` | `VKV_FORMAT=markdown`) -```bash -vkv -p secret --format=markdown -``` - -returns: - -| MOUNT | PATHS | KEYS | VALUES | -|--------|----------------------|----------|--------------| -| secret | secret/demo | foo | *** | -| | secret/sub | sub | ******** | -| | secret/sub/demo | demo | *********** | -| | | password | ****** | -| | | user | ***** | -| | secret/sub/sub2/demo | user | ************ | -| | | value | ********* | - - -### json (`--format=json` | `VKV_FORMAT=json`) -You can combine all flags and export the result to json by running: - -```bash -vkv -p secret --show-values --format=json -``` - -```json -{ - "secret": { - "secret/demo": { - "foo": "***" - }, - "secret/sub": { - "sub": "********" - }, - "secret/sub/demo": { - "demo": "***********", - "password": "******", - "user": "*****" - }, - "secret/sub/sub2/demo": { - "user": "************", - "value": "*********" - } - } -}% -``` - -### yaml (`--format=yaml` | `VKV_FORMAT=yaml`) -Same applies for yaml: - -```bash -vkv --path secret --show-values --format=yaml -``` - -```yaml -secret: - secret/demo: - foo: '***' - secret/sub: - sub: '********' - secret/sub/demo: - demo: '***********' - password: '******' - user: '*****' - secret/sub/sub2/demo: - user: '************' - value: '*********' -``` +| | +|:---:| +| `template`
| -# Acknowledgements / Similar tools -`vkv` is inspired by: -* https://github.com/jonasvinther/medusa -Similar tools are: -* https://github.com/kir4h/rvault diff --git a/assets/base.svg b/assets/base.svg new file mode 100644 index 00000000..9c5ce64f --- /dev/null +++ b/assets/base.svg @@ -0,0 +1,17 @@ +
\ No newline at end of file diff --git a/assets/config.yml b/assets/config.yml deleted file mode 100644 index e83de77d..00000000 --- a/assets/config.yml +++ /dev/null @@ -1,54 +0,0 @@ -# https://github.com/faressoft/terminalizer/blob/master/config.yml -command: "/bin/bash -l" -cwd: null -env: - VAULT_TOKEN: "root" - VAULT_ADDR: "http://127.0.0.1:8200" - VAULT_SKIP_VERIFY: "true" - recording: true -cols: 100 -rows: 20 -repeat: 0 -quality: 100 -frameDelay: auto -maxIdleTime: 2000 -frameBox: - type: floating - title: vkv - style: - border: 0px black solid - # boxShadow: none - # margin: 0px -watermark: - imagePath: null - style: - position: absolute - right: 15px - bottom: 15px - width: 100px - opacity: 0.9 -cursorStyle: block -fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" -fontSize: 16 -lineHeight: 1 -letterSpacing: 0 -theme: - background: "transparent" - foreground: "#afafaf" - cursor: "#c7c7c7" - black: "#232628" - red: "#fc4384" - green: "#b3e33b" - yellow: "#ffa727" - blue: "#75dff2" - magenta: "#ae89fe" - cyan: "#708387" - white: "#d5d5d0" - brightBlack: "#626566" - brightRed: "#ff7fac" - brightGreen: "#c8ed71" - brightYellow: "#ebdf86" - brightBlue: "#75dff2" - brightMagenta: "#ae89fe" - brightCyan: "#b1c6ca" - brightWhite: "#f9f9f4" diff --git a/assets/demo.gif b/assets/demo.gif deleted file mode 100644 index 02e957fb..00000000 Binary files a/assets/demo.gif and /dev/null differ diff --git a/assets/demo_cmds.txt b/assets/demo_cmds.txt deleted file mode 100644 index f67eb23f..00000000 --- a/assets/demo_cmds.txt +++ /dev/null @@ -1,8 +0,0 @@ -vault login -vkv -p secret -vkv -p secret --format=export -eval $(vkv -p secret --format=export) -echo $demo -vkv -p secret --format=json | jq . -vkv -p secret --format=json | yq . -vkv -p secret --format=markdown \ No newline at end of file diff --git a/assets/json.svg b/assets/json.svg new file mode 100644 index 00000000..b457625d --- /dev/null +++ b/assets/json.svg @@ -0,0 +1,17 @@ +
\ No newline at end of file diff --git a/assets/markdown.svg b/assets/markdown.svg new file mode 100644 index 00000000..34e459b7 --- /dev/null +++ b/assets/markdown.svg @@ -0,0 +1,17 @@ +
\ No newline at end of file diff --git a/assets/template.svg b/assets/template.svg new file mode 100644 index 00000000..d0ccb8a8 --- /dev/null +++ b/assets/template.svg @@ -0,0 +1,17 @@ +
\ No newline at end of file diff --git a/assets/yaml.svg b/assets/yaml.svg new file mode 100644 index 00000000..c460ac75 --- /dev/null +++ b/assets/yaml.svg @@ -0,0 +1,17 @@ +
\ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index b3f64b97..09497782 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,6 +27,9 @@ type Options struct { ShowValues bool `env:"SHOW_VALUES"` MaxValueLength int `env:"MAX_VALUE_LENGTH" envDefault:"12"` + TemplateFile string `env:"TEMPLATE_FILE"` + TemplateString string `env:"TEMPLATE_STRING"` + FormatString string `env:"FORMAT" envDefault:"base"` outputFormat printer.OutputFormat @@ -76,11 +79,16 @@ func newRootCmd(version string) *cobra.Command { m[p] = (*s) } + if len(m) == 0 { + return nil + } + printer := printer.NewPrinter( printer.OnlyKeys(o.OnlyKeys), printer.OnlyPaths(o.OnlyPaths), printer.CustomValueLength(o.MaxValueLength), printer.ShowValues(o.ShowValues), + printer.WithTemplate(o.TemplateString, o.TemplateFile), printer.ToFormat(o.outputFormat), ) @@ -104,8 +112,12 @@ func newRootCmd(version string) *cobra.Command { cmd.Flags().IntVar(&o.MaxValueLength, "max-value-length", o.MaxValueLength, "maximum char length of values. Set to \"-1\" for disabling "+ "(env var: VKV_MAX_VALUE_LENGTH)") + // Template + cmd.Flags().StringVar(&o.TemplateFile, "template-file", o.TemplateFile, "path to a file containing Go-template syntax to render the KV entries (env var: VKV_TEMPLATE_FILE)") + cmd.Flags().StringVar(&o.TemplateString, "template-string", o.TemplateString, "template string containing Go-template syntax to render KV entries (env var: VKV_TEMPLATE_STRING)") + // Output format - cmd.Flags().StringVarP(&o.FormatString, "format", "f", o.FormatString, "output format: \"base\", \"json\", \"yaml\", \"export\", \"nmarkdown\") "+ + cmd.Flags().StringVarP(&o.FormatString, "format", "f", o.FormatString, "output format: \"base\", \"json\", \"yaml\", \"export\", \"markdown\", \"template\") "+ "(env var: VKV_FORMAT)") // version @@ -141,6 +153,16 @@ func (o *Options) validateFlags() error { o.outputFormat = printer.Markdown case "base": o.outputFormat = printer.Base + case "template", "tmpl": + o.outputFormat = printer.Template + + if o.TemplateFile != "" && o.TemplateString != "" { + return fmt.Errorf("%w: %s", errInvalidFlagCombination, "cannot specify both --template-file and --template-string") + } + + if o.TemplateFile == "" && o.TemplateString == "" { + return fmt.Errorf("%w: %s", errInvalidFlagCombination, "either --template-file or --template-string is required") + } default: return printer.ErrInvalidFormat } diff --git a/cmd/root_test.go b/cmd/root_test.go index 3aecbff0..7850b6e2 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -59,11 +59,24 @@ func TestOutputFormat(t *testing.T) { format: "base", expected: printer.Base, }, + { + name: "template", + err: false, + format: "template", + expected: printer.Template, + }, + { + name: "tmpl", + err: false, + format: "tmpl", + expected: printer.Template, + }, } for _, tc := range testCases { o := &Options{ FormatString: tc.format, + TemplateFile: "o", // needed for testing template format } err := o.validateFlags() @@ -103,6 +116,26 @@ func TestValidateFlags(t *testing.T) { err: false, args: []string{"--path", ""}, }, + { + name: "test: template with file", + err: false, + args: []string{"--format", "template", "--template-file", "OK"}, + }, + { + name: "test: template with string", + err: false, + args: []string{"--format", "template", "--template-string", "OK"}, + }, + { + name: "test: template no file or string", + err: true, + args: []string{"--format", "template"}, + }, + { + name: "test: template file and string", + err: true, + args: []string{"--format", "template", "--template-string", "ok", "--template-file", "OK"}, + }, } for _, tc := range testCases { @@ -205,6 +238,23 @@ func TestEnvVars(t *testing.T) { FormatString: "base", }, }, + { + name: "show values and max value length", + err: false, + envs: map[string]interface{}{ + "VKV_PATHS": "kv1,kv2,kv3", + "VKV_FORMAT": "template", + "VKV_TEMPLATE_STRING": "string", + "VKV_TEMPLATE_FILE": "path", + }, + expected: &Options{ + MaxValueLength: 12, + Paths: []string{"kv1", "kv2", "kv3"}, + FormatString: "template", + TemplateFile: "path", + TemplateString: "string", + }, + }, } for _, tc := range testCases { diff --git a/pkg/printer/printer.go b/pkg/printer/printer.go index 9707858c..d75175f0 100644 --- a/pkg/printer/printer.go +++ b/pkg/printer/printer.go @@ -7,6 +7,7 @@ import ( "os" "strings" + "github.com/FalcoSuessgott/vkv/pkg/render" "github.com/FalcoSuessgott/vkv/pkg/utils" "github.com/disiqueira/gotree/v3" "github.com/olekukonko/tablewriter" @@ -35,6 +36,9 @@ const ( // Markdown prints the secrets in markdowntable format. Markdown + + // Template renders a given template string or file. + Template ) var ( @@ -55,6 +59,7 @@ type Printer struct { onlyPaths bool showValues bool valueLength int + template string } // CustomValueLength option for trimming down the output of secrets. @@ -105,6 +110,28 @@ func ShowValues(b bool) Option { } } +// WithTemplate sets the template file. +func WithTemplate(str, path string) Option { + return func(p *Printer) { + if str != "" { + p.template = str + + return + } + + if path != "" { + str, err := utils.ReadFile(path) + if err != nil { + log.Fatalf("error reading %s: %s", path, err.Error()) + } + + p.template = string(str) + + return + } + } +} + // NewPrinter return a new printer struct. func NewPrinter(opts ...Option) *Printer { p := &Printer{ @@ -144,6 +171,7 @@ func (p *Printer) Out(secrets map[string]interface{}) error { } fmt.Fprintf(p.writer, "%s", string(out)) + case JSON: out, err := utils.ToJSON(secrets) if err != nil { @@ -151,19 +179,23 @@ func (p *Printer) Out(secrets map[string]interface{}) error { } fmt.Fprintf(p.writer, "%s", string(out)) + case Export: - for _, s := range utils.SortMapKeys(secrets) { - for _, v := range utils.ToMapStringInterface(secrets[s]) { - m, ok := v.(map[string]interface{}) + for _, k := range utils.SortMapKeys(secrets) { + m := utils.ToMapStringInterface(secrets[k]) + + for _, i := range utils.SortMapKeys(m) { + subMap, ok := m[i].(map[string]interface{}) if !ok { - log.Fatalf("cannot convert %T to map[string]interface", secrets[s]) + log.Fatalf("cannot convert %T to map[string]interface", m[i]) } - for _, k := range utils.SortMapKeys(m) { - fmt.Fprintf(p.writer, "export %s=\"%v\"\n", k, m[k]) + for _, j := range utils.SortMapKeys(subMap) { + fmt.Fprintf(p.writer, "export %s=\"%v\"\n", j, subMap[j]) } } } + case Markdown: headers, data := p.buildMarkdownTable(secrets) @@ -172,9 +204,39 @@ func (p *Printer) Out(secrets map[string]interface{}) error { table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") table.AppendBulk(data) - // merge mounts and paths colunmn - table.SetAutoMergeCellsByColumnIndex([]int{0, 1}) + table.SetAutoMergeCellsByColumnIndex([]int{0, 1}) // merge mounts and paths colunmn table.Render() + + case Template: + type entry struct { + Path, Key string + Value interface{} + } + + entries := []entry{} + + for _, k := range utils.SortMapKeys(secrets) { + m := utils.ToMapStringInterface(secrets[k]) + + for _, i := range utils.SortMapKeys(m) { + subMap, ok := m[i].(map[string]interface{}) + if !ok { + log.Fatalf("cannot convert %T to map[string]interface", m[i]) + } + + for _, j := range utils.SortMapKeys(subMap) { + entries = append(entries, entry{Path: i, Key: j, Value: subMap[j]}) + } + } + } + + output, err := render.String([]byte(p.template), entries) + if err != nil { + return err + } + + fmt.Fprintln(p.writer, output.String()) + case Base: for _, k := range utils.SortMapKeys(secrets) { tree := gotree.New(k + utils.Delimiter) diff --git a/pkg/printer/printer_test.go b/pkg/printer/printer_test.go index b53e9c51..03539157 100644 --- a/pkg/printer/printer_test.go +++ b/pkg/printer/printer_test.go @@ -62,6 +62,7 @@ func TestPrint(t *testing.T) { s map[string]interface{} opts []Option output string + err bool }{ { name: "test: default opions", @@ -200,14 +201,14 @@ func TestPrint(t *testing.T) { s: map[string]interface{}{ "secrets": map[string]interface{}{ "key_1": map[string]interface{}{"key": "value", "user": "password"}, - "key_2": map[string]interface{}{"key": 12}, + "key_2": map[string]interface{}{"password": 12}, }, }, opts: []Option{ ToFormat(Export), ShowValues(true), }, - output: "export key=\"value\"\nexport user=\"password\"\nexport key=\"12\"\n", + output: "export key=\"value\"\nexport user=\"password\"\nexport password=\"12\"\n", }, { name: "test: empty export", @@ -258,6 +259,69 @@ func TestPrint(t *testing.T) { }, output: "| MOUNT | PATHS |\n|---------|-------|\n| secrets | key_1 |\n| | key_2 |\n", }, + { + name: "test: markdown only paths", + s: map[string]interface{}{ + "secrets": map[string]interface{}{ + "key_1": map[string]interface{}{"key": "value", "user": "password"}, + "key_2": map[string]interface{}{"key": 12}, + }, + }, + opts: []Option{ + ToFormat(Markdown), + OnlyPaths(true), + }, + output: "| MOUNT | PATHS |\n|---------|-------|\n| secrets | key_1 |\n| | key_2 |\n", + }, + { + name: "test: template", + s: map[string]interface{}{ + "secrets": map[string]interface{}{ + "key_1": map[string]interface{}{"key": "value", "user": "password"}, + }, + }, + opts: []Option{ + ToFormat(Template), + WithTemplate(` +{{ range $entry := . }} +{{ printf "%s:\t%s=%v\n" $entry.Path $entry.Key $entry.Value }} +{{ end }} +`, ""), + }, + output: "\n\nkey_1:\tkey=*****\n\n\nkey_1:\tuser=********\n\n\n\n", + }, + { + name: "test: template show values", + s: map[string]interface{}{ + "secrets": map[string]interface{}{ + "key_1": map[string]interface{}{"key": "value", "user": "password"}, + }, + }, + opts: []Option{ + ToFormat(Template), + ShowValues(true), + WithTemplate(` +{{ range $entry := . }} +{{ printf "%s:\t%s=%v\n" $entry.Path $entry.Key $entry.Value }} +{{ end }} +`, ""), + }, + output: "\n\nkey_1:\tkey=value\n\n\nkey_1:\tuser=password\n\n\n\n", + }, + { + name: "test: template file show values", + s: map[string]interface{}{ + "secrets": map[string]interface{}{ + "key_1": map[string]interface{}{"key": "value", "user": "password"}, + }, + }, + opts: []Option{ + ToFormat(Template), + ShowValues(true), + WithTemplate("", "testdata/policies.tmpl"), + }, + output: "\npath \"key_1/*\" {\n capabilities = [ \"create\", \"read\", \"update\", \"delete\", \"list\" ]\n}\n\npath \"key_1/*\" {\n capabilities = [ \"create\", \"read\", \"update\", \"delete\", \"list\" ]\n}\n\n", + }, } for _, tc := range testCases { @@ -266,7 +330,6 @@ func TestPrint(t *testing.T) { p := NewPrinter(tc.opts...) assert.NoError(t, p.Out(tc.s)) - assert.Equal(t, tc.output, b.String(), tc.name) } } diff --git a/pkg/printer/testdata/policies.tmpl b/pkg/printer/testdata/policies.tmpl new file mode 100644 index 00000000..c688183c --- /dev/null +++ b/pkg/printer/testdata/policies.tmpl @@ -0,0 +1,5 @@ +{{ range $entry := . }} +path "{{ $entry.Path }}/*" { + capabilities = [ "create", "read", "update", "delete", "list" ] +} +{{ end }} \ No newline at end of file diff --git a/pkg/render/render.go b/pkg/render/render.go new file mode 100644 index 00000000..0a76f25c --- /dev/null +++ b/pkg/render/render.go @@ -0,0 +1,22 @@ +package render + +import ( + "bytes" + "text/template" +) + +// String renders byte array input with the given data. +func String(tmpl []byte, input interface{}) (bytes.Buffer, error) { + var buf bytes.Buffer + + tpl, err := template.New("template").Option("missingkey=error").Parse(string(tmpl)) + if err != nil { + return buf, err + } + + if err := tpl.Execute(&buf, input); err != nil { + return buf, err + } + + return buf, nil +} diff --git a/pkg/render/render_test.go b/pkg/render/render_test.go new file mode 100644 index 00000000..5d9d96cd --- /dev/null +++ b/pkg/render/render_test.go @@ -0,0 +1,38 @@ +package render + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testTemplate = []byte("This is a {{ .Test }} template which replaces certain {{ .Values }}!") + +func TestRenderTemplate(t *testing.T) { + result, err := String(testTemplate, map[string]interface{}{ + "Test": "test", + "Values": "values", + }) + + require.NoError(t, err) + assert.Equal(t, "This is a test template which replaces certain values!", result.String()) +} + +func TestRenderInvalidString(t *testing.T) { + _, err := String([]byte("{{ invalid }"), map[string]interface{}{ + "Test": "test", + "Values": "values", + }) + + require.Error(t, err) +} + +func TestRenderExpectError(t *testing.T) { + _, err := String(testTemplate, map[string]interface{}{ + "Test": "test", + "WrongValue": "values", + }) + + assert.Error(t, err) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 55ff8b87..725706d2 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -2,6 +2,7 @@ package utils import ( "encoding/json" + "io/ioutil" "log" "sort" "strings" @@ -17,6 +18,16 @@ const ( // Keys type for receiving all keys of a map. type Keys []string +// ReadFile reads from a file. +func ReadFile(path string) ([]byte, error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + return content, nil +} + // SplitPath splits a given path by / and returns the first element and the joined rest paths. func SplitPath(path string) (string, string) { parts := removeEmptyElements(strings.Split(path, Delimiter))