Skip to content

Commit edb76d8

Browse files
Overhaul tgen: add support for Helm-like values file.
1 parent 660098e commit edb76d8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+13921
-950
lines changed

README.md

+34-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# `tgen`
1+
# `tgen`: a tiny template tool
22

33
[![Tests passing](https://img.shields.io/github/workflow/status/patrickdappollonio/tgen/Continuous%20Integration/master?logo=github&style=flat-square)](https://github.com/patrickdappollonio/tgen/actions)
44
[![Downloads](https://img.shields.io/github/downloads/patrickdappollonio/tgen/total?color=blue&logo=github&style=flat-square)](https://github.com/patrickdappollonio/tgen/releases)
@@ -15,22 +15,24 @@ Usage:
1515
tgen [flags]
1616
1717
Flags:
18-
-d, --delimiter string delimiter (default "{{}}")
1918
-e, --environment string an optional environment file to use (key=value formatted) to perform replacements
19+
-f, --file string the template file to process
20+
-d, --delimiter string template delimiter (default "{{}}")
2021
-x, --execute string a raw template to execute directly, without providing --file
21-
-f, --file string the template file to process (required)
22+
-v, --values string a file containing values to use for the template, a la Helm
23+
--with-values automatically include a values.yaml file from the current working directory
24+
-s, --strict strict mode: if an environment variable or value is used in the template but not set, it fails rendering
2225
-h, --help help for tgen
23-
-s, --strict enables strict mode: if an environment variable in the file is defined but not set, it'll fail
2426
--version version for tgen
2527
```
2628

2729
### Environment file
2830

29-
`tgen` supports an optional environment variable collection in a file but it's a pretty basic implementation of a simple key/value pair. The environment file works by finding lines that aren't empty or preceded by a pound `#` -- since they're treated as comments -- and then tries to find at least one equal (`=`) sign. If it can find at least one, all values on the left side of the equal sign become the key -- which is also uppercased so it's compatible with the `env` function defined above -- and the contents on the right side become the value. If the same line has more than one equal, only the first one is honored and all remaining ones become part of the value.
31+
`tgen` supports an optional environment variable collection in a file but it's a pretty basic implementation of a simple key/value pair. The environment file works by finding lines that aren't empty or preceded by a pound `#` -- since they're treated as comments -- and then tries to find at least one equal (`=`) sign. If it can find at least one, all values on the left side of the equal sign become the key and the contents on the right side become the value. If the same line has more than one equal, only the first one is honored and all remaining ones become part of the value.
3032

31-
As an important note, environment variables found in the environment have preference over the environment file. That way, the environment file can define `A=1` but then the application can be run with `A=2 tgen [flags]` so it overrides `A` to the value of `2`.
33+
There's no support for Bash interpolation or multiline values. If this is needed, consider using a YAML values file instead.
3234

33-
### Example
35+
#### Example
3436

3537
Consider the following template, named `template.txt`:
3638

@@ -44,14 +46,14 @@ And the following environment file, named `contents.env`:
4446
element=Oil
4547
```
4648

47-
After being passed to `tgen` by executing `tgen -e contents.env -f template.txt`, the output becomes:
49+
After being passed to `tgen`, the output becomes:
4850

4951
```bash
5052
$ tgen -e contents.env -f template.txt
5153
The dog licked the Oil and everyone laughed.
5254
```
5355

54-
Using the inline mode to execute a template, you can also call `tgen -x '{{ env "element" }}' -e contents.env` (note the use of single-quotes since in Go, strings are always double-quoted) which will yield the same result:
56+
Using the inline mode to execute a template, you can also call the program as such (note the use of single-quotes since in Go, strings are always double-quoted) which will yield the same result:
5557

5658
```bash
5759
$ tgen -x '{{ env "element" }}' -e contents.env
@@ -60,7 +62,7 @@ The dog licked the Oil and everyone laughed.
6062

6163
Do note as well that using single quotes for the template allows you to prevent any bash special parsing logic that your terminal might have.
6264

63-
### Template Generation _a la Helm_
65+
### Helm-style values
6466

6567
`tgen` can be used to generate templates, in a very similar way as `helm` can be used. However, do note that `tgen`'s intention is not to replace `helm` since it can't handle application lifecycle the way `helm` does, however, it can do a great job generating resources with very similar code.
6668

@@ -118,7 +120,7 @@ metadata:
118120
type: kubernetes.io/tls
119121
data:
120122
tls.crt: |
121-
TmV2ZXIgZ29ubmEgZ2l2ZSB5b3UgdXAsIG5ldmVyIGdvbm5hIGxldCB5b3UgZG93bgpOZXZlciBnb25uYSBydW4gYXJvdW5kIGFuZCBkZXNlcnQgeW91Ck5ldmVyIGdvbm5hIG1ha2UgeW91IGNyeSwgbmV2ZXIgZ29ubmEgc2F5IGdvb2RieWUKTmV2ZXIgZ29ubmEgdGVsbCBhIGxpZSBhbmQgaHVydCB5b3UK
123+
Rk9PQkFSQkFaCg==
122124
```
123125

124126
This output can be then passed to Kubernetes as follows:
@@ -129,6 +131,27 @@ tgen -f secret.yaml | kubectl apply -f -
129131

130132
Do keep in mind though your DevOps requirements in terms of keeping a copy of your YAML files, rendered. Additionally, the `readfile` function is akin to `helm`'s `.Files`, with the exception that **you can read any file the `tgen` binary has access**, including potentially sensitive files such as `/etc/passwd`. If this is a concern, please run `tgen` in a CI/CD environment or where access to these resources is limited.
131133

134+
You can also use a `values.yaml` file like Helm. `tgen` will allow you to read values from the values file as `.variable` or `.Values.variable`. The latter is the same as Helm's `.Values.variable` and the former is a shortcut to `.Values.variable` for convenience. Consider the following YAML values file:
135+
136+
```yaml
137+
name: Patrick
138+
```
139+
140+
And the following template:
141+
142+
```yaml
143+
Hello, my name is {{ .name }}.
144+
```
145+
146+
Running `tgen` with the values file will yield the following output:
147+
148+
```bash
149+
$ tgen -f template.yaml -v values.yaml
150+
Hello, my name is Patrick.
151+
```
152+
153+
If your values file is called `values.yaml`, you also have the handy shortcut of simply specifying `--with-values` and `tgen` will automatically include the values file from the current working directory.
154+
132155
### Template functions
133156

134157
See [template functions](docs/functions.md) for a list of all the functions available.

command.go

+30-52
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,59 @@
11
package main
22

33
import (
4-
"bytes"
5-
"errors"
64
"io"
75
"os"
8-
"text/template"
96
)
107

118
func command(w io.Writer, c conf) error {
12-
var b *bytes.Buffer
13-
14-
if c.templateFile != "" {
15-
if c.rawTemplate != "" {
16-
return &conflictingArgsError{"file", "raw"}
17-
}
18-
19-
if c.stdin {
20-
return &conflictingArgsError{"file", "stdin"}
21-
}
22-
23-
bt, err := loadFile(c.templateFile)
24-
if err != nil {
25-
return err
26-
}
27-
28-
b = bt
9+
// You can't pass "--file" and "--execute" together
10+
if c.templateFilePath != "" && c.stdinTemplateFile != "" {
11+
return &conflictingArgsError{"file", "execute"}
2912
}
3013

31-
if c.rawTemplate != "" {
32-
if c.templateFile != "" {
33-
return &conflictingArgsError{"raw", "file"}
34-
}
35-
36-
if c.stdin {
37-
return &conflictingArgsError{"raw", "stdin"}
38-
}
14+
tg := &tgen{Strict: c.strictMode}
3915

40-
b = bytes.NewBufferString(c.rawTemplate)
16+
// Read template from "-x" or "--execute" flag
17+
if c.stdinTemplateFile != "" {
18+
tg.setTemplate("-", c.stdinTemplateFile)
4119
}
4220

43-
if c.stdin {
44-
if c.templateFile != "" {
45-
return &conflictingArgsError{"stdin", "file"}
21+
// Read template file (either from "--file" or stdin)
22+
if pathToOpen := c.templateFilePath; pathToOpen != "" {
23+
var err error
24+
switch pathToOpen {
25+
case "-":
26+
err = tg.loadTemplateFile("", os.Stdin)
27+
default:
28+
err = tg.loadTemplatePath(pathToOpen)
4629
}
4730

48-
if c.rawTemplate != "" {
49-
return &conflictingArgsError{"stdin", "raw"}
50-
}
51-
52-
bt, err := loadFile(os.Stdin.Name())
5331
if err != nil {
5432
return err
5533
}
56-
57-
b = bt
5834
}
5935

60-
if b == nil {
61-
return errors.New("needs to specify either a template file (using --file) or a raw template (using --raw or --stdin)")
36+
// Set delimiters
37+
if c.customDelimiters != "" {
38+
if err := tg.setDelimiters(c.customDelimiters); err != nil {
39+
return err
40+
}
6241
}
6342

64-
envVars, err := loadVirtualEnv(c.environmentFile)
65-
if err != nil {
66-
return err
43+
// Load environment variable file
44+
if c.environmentFile != "" {
45+
if err := tg.loadEnvValues(c.environmentFile); err != nil {
46+
return err
47+
}
6748
}
6849

69-
c.t = template.New(appName).Funcs(getTemplateFunctions(envVars, c.strictMode))
70-
71-
if c.customDelimiters != "" {
72-
l, r, err := getDelimiter(c.customDelimiters)
73-
if err != nil {
50+
// Load yaml values file
51+
if c.valuesFile != "" {
52+
if err := tg.loadYAMLValues(c.valuesFile); err != nil {
7453
return err
7554
}
76-
77-
c.t = c.t.Delims(l, r)
7855
}
7956

80-
return executeTemplate(c.t, c.templateFile, w, envVars, b)
57+
// Render code
58+
return tg.render(w)
8159
}

go.mod

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
module github.com/patrickdappollonio/tgen
22

33
require (
4-
github.com/spf13/cobra v1.5.0
5-
golang.org/x/text v0.3.7
4+
github.com/spf13/cobra v1.6.1
5+
golang.org/x/text v0.4.0
6+
gopkg.in/yaml.v3 v3.0.1
67
)
78

89
require (
9-
github.com/inconshreveable/mousetrap v1.0.0 // indirect
10+
github.com/inconshreveable/mousetrap v1.0.1 // indirect
1011
github.com/spf13/pflag v1.0.5 // indirect
1112
)
1213

go.sum

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2-
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
3-
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
2+
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
3+
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
44
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5-
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
6-
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
5+
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
6+
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
77
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
88
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9-
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
10-
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
9+
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
10+
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
11+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1112
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
12-
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
13+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import (
88

99
const appName = "tgen"
1010

11-
var (
12-
version = "development"
13-
)
11+
var version = "development"
1412

1513
func main() {
1614
if err := run(); err != nil {
@@ -20,23 +18,31 @@ func main() {
2018

2119
func run() error {
2220
var configs conf
21+
var withValues bool
2322

2423
var root = &cobra.Command{
2524
Use: appName,
2625
Short: appName + " is a template generator with the power of Go Templates",
2726
Version: version,
2827
SilenceUsage: true,
2928
RunE: func(cmd *cobra.Command, args []string) error {
29+
if withValues {
30+
configs.valuesFile = "values.yaml"
31+
}
32+
3033
return command(os.Stdout, configs)
3134
},
3235
}
3336

3437
root.Flags().StringVarP(&configs.environmentFile, "environment", "e", "", "an optional environment file to use (key=value formatted) to perform replacements")
35-
root.Flags().StringVarP(&configs.templateFile, "file", "f", "", "the template file to process (required)")
38+
root.Flags().StringVarP(&configs.templateFilePath, "file", "f", "", "the template file to process")
3639
root.Flags().StringVarP(&configs.customDelimiters, "delimiter", "d", "", `template delimiter (default "{{}}")`)
37-
root.Flags().StringVarP(&configs.rawTemplate, "execute", "x", "", "a raw template to execute directly, without providing --file")
38-
root.Flags().BoolVarP(&configs.stdin, "stdin", "i", false, "a stdin input to execute directly, without providing --file or --execute")
39-
root.Flags().BoolVarP(&configs.strictMode, "strict", "s", false, "enables strict mode: if an environment variable in the file is defined but not set, it'll fail")
40+
root.Flags().StringVarP(&configs.stdinTemplateFile, "execute", "x", "", "a raw template to execute directly, without providing --file")
41+
root.Flags().StringVarP(&configs.valuesFile, "values", "v", "", "a file containing values to use for the template, a la Helm")
42+
root.Flags().BoolVar(&withValues, "with-values", false, "automatically include a values.yaml file from the current working directory")
43+
root.Flags().BoolVarP(&configs.strictMode, "strict", "s", false, "strict mode: if an environment variable or value is used in the template but not set, it fails rendering")
44+
45+
root.Flags().SortFlags = false
4046

4147
return root.Execute()
4248
}

structs.go

+12-11
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@ package main
22

33
import (
44
"fmt"
5-
"text/template"
65
)
76

87
type conf struct {
9-
environmentFile string
10-
templateFile string
11-
rawTemplate string
12-
stdin bool
13-
customDelimiters string
14-
strictMode bool
15-
16-
t *template.Template
8+
environmentFile string
9+
templateFilePath string
10+
stdinTemplateFile string
11+
valuesFile string
12+
strictMode bool
13+
customDelimiters string
1714
}
1815

1916
type enotfounderr struct{ name string }
@@ -22,10 +19,14 @@ func (e *enotfounderr) Error() string {
2219
return "strict mode on: environment variable not found: $" + e.name
2320
}
2421

25-
type conflictingArgsError struct {
26-
F1, F2 string
22+
type emissingkeyerr struct{ name string }
23+
24+
func (e *emissingkeyerr) Error() string {
25+
return "strict mode on: missing value in values file: " + e.name
2726
}
2827

28+
type conflictingArgsError struct{ F1, F2 string }
29+
2930
func (e *conflictingArgsError) Error() string {
3031
return fmt.Sprintf("defined both --%s and --%s, only one must be used", e.F1, e.F2)
3132
}

template_functions.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import (
2424

2525
func getTemplateFunctions(virtualKV map[string]string, strict bool) template.FuncMap {
2626
return template.FuncMap{
27-
"raw": func(s string) string {
28-
return s
29-
},
27+
"raw": raw,
3028

3129
// Go built-ins
3230
"lowercase": strings.ToLower,
@@ -83,6 +81,10 @@ func getTemplateFunctions(virtualKV map[string]string, strict bool) template.Fun
8381
}
8482
}
8583

84+
func raw(s string) string {
85+
return s
86+
}
87+
8688
func readfile(path string) (string, error) {
8789
contents, err := os.ReadFile(path)
8890
if err != nil {

0 commit comments

Comments
 (0)