Skip to content

Commit

Permalink
Merge pull request #21 from patrickdappollonio/overhaul
Browse files Browse the repository at this point in the history
Overhaul tgen: add support for Helm-like values file.
  • Loading branch information
patrickdappollonio authored Oct 27, 2022
2 parents 660098e + 1abdacf commit 7b180ee
Show file tree
Hide file tree
Showing 66 changed files with 13,922 additions and 951 deletions.
47 changes: 35 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# `tgen`
# `tgen`: a tiny template tool

[![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)
[![Downloads](https://img.shields.io/github/downloads/patrickdappollonio/tgen/total?color=blue&logo=github&style=flat-square)](https://github.com/patrickdappollonio/tgen/releases)
Expand All @@ -15,26 +15,28 @@ Usage:
tgen [flags]
Flags:
-d, --delimiter string delimiter (default "{{}}")
-e, --environment string an optional environment file to use (key=value formatted) to perform replacements
-f, --file string the template file to process
-d, --delimiter string template delimiter (default "{{}}")
-x, --execute string a raw template to execute directly, without providing --file
-f, --file string the template file to process (required)
-v, --values string a file containing values to use for the template, a la Helm
--with-values automatically include a values.yaml file from the current working directory
-s, --strict strict mode: if an environment variable or value is used in the template but not set, it fails rendering
-h, --help help for tgen
-s, --strict enables strict mode: if an environment variable in the file is defined but not set, it'll fail
--version version for tgen
```

### Environment file

`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.
`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.

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`.
There's no support for Bash interpolation or multiline values. If this is needed, consider using a YAML values file instead.

### Example
#### Example

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

```go
```handlebars
The dog licked the {{ env "element" }} and everyone laughed.
```

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

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

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

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:
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:

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

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.

### Template Generation _a la Helm_
### Helm-style values

`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.

Expand Down Expand Up @@ -118,7 +120,7 @@ metadata:
type: kubernetes.io/tls
data:
tls.crt: |
TmV2ZXIgZ29ubmEgZ2l2ZSB5b3UgdXAsIG5ldmVyIGdvbm5hIGxldCB5b3UgZG93bgpOZXZlciBnb25uYSBydW4gYXJvdW5kIGFuZCBkZXNlcnQgeW91Ck5ldmVyIGdvbm5hIG1ha2UgeW91IGNyeSwgbmV2ZXIgZ29ubmEgc2F5IGdvb2RieWUKTmV2ZXIgZ29ubmEgdGVsbCBhIGxpZSBhbmQgaHVydCB5b3UK
Rk9PQkFSQkFaCg==
```

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

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.

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:

```yaml
name: Patrick
```

And the following template:

```handlebars
Hello, my name is {{ .name }}.
```

Running `tgen` with the values file will yield the following output:

```bash
$ tgen -f template.yaml -v values.yaml
Hello, my name is Patrick.
```

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.

### Template functions

See [template functions](docs/functions.md) for a list of all the functions available.
82 changes: 30 additions & 52 deletions command.go
Original file line number Diff line number Diff line change
@@ -1,81 +1,59 @@
package main

import (
"bytes"
"errors"
"io"
"os"
"text/template"
)

func command(w io.Writer, c conf) error {
var b *bytes.Buffer

if c.templateFile != "" {
if c.rawTemplate != "" {
return &conflictingArgsError{"file", "raw"}
}

if c.stdin {
return &conflictingArgsError{"file", "stdin"}
}

bt, err := loadFile(c.templateFile)
if err != nil {
return err
}

b = bt
// You can't pass "--file" and "--execute" together
if c.templateFilePath != "" && c.stdinTemplateFile != "" {
return &conflictingArgsError{"file", "execute"}
}

if c.rawTemplate != "" {
if c.templateFile != "" {
return &conflictingArgsError{"raw", "file"}
}

if c.stdin {
return &conflictingArgsError{"raw", "stdin"}
}
tg := &tgen{Strict: c.strictMode}

b = bytes.NewBufferString(c.rawTemplate)
// Read template from "-x" or "--execute" flag
if c.stdinTemplateFile != "" {
tg.setTemplate("-", c.stdinTemplateFile)
}

if c.stdin {
if c.templateFile != "" {
return &conflictingArgsError{"stdin", "file"}
// Read template file (either from "--file" or stdin)
if pathToOpen := c.templateFilePath; pathToOpen != "" {
var err error
switch pathToOpen {
case "-":
err = tg.loadTemplateFile("", os.Stdin)
default:
err = tg.loadTemplatePath(pathToOpen)
}

if c.rawTemplate != "" {
return &conflictingArgsError{"stdin", "raw"}
}

bt, err := loadFile(os.Stdin.Name())
if err != nil {
return err
}

b = bt
}

if b == nil {
return errors.New("needs to specify either a template file (using --file) or a raw template (using --raw or --stdin)")
// Set delimiters
if c.customDelimiters != "" {
if err := tg.setDelimiters(c.customDelimiters); err != nil {
return err
}
}

envVars, err := loadVirtualEnv(c.environmentFile)
if err != nil {
return err
// Load environment variable file
if c.environmentFile != "" {
if err := tg.loadEnvValues(c.environmentFile); err != nil {
return err
}
}

c.t = template.New(appName).Funcs(getTemplateFunctions(envVars, c.strictMode))

if c.customDelimiters != "" {
l, r, err := getDelimiter(c.customDelimiters)
if err != nil {
// Load yaml values file
if c.valuesFile != "" {
if err := tg.loadYAMLValues(c.valuesFile); err != nil {
return err
}

c.t = c.t.Delims(l, r)
}

return executeTemplate(c.t, c.templateFile, w, envVars, b)
// Render code
return tg.render(w)
}
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
module github.com/patrickdappollonio/tgen

require (
github.com/spf13/cobra v1.5.0
golang.org/x/text v0.3.7
github.com/spf13/cobra v1.6.1
golang.org/x/text v0.4.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

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

const appName = "tgen"

var (
version = "development"
)
var version = "development"

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

func run() error {
var configs conf
var withValues bool

var root = &cobra.Command{
Use: appName,
Short: appName + " is a template generator with the power of Go Templates",
Version: version,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
if withValues {
configs.valuesFile = "values.yaml"
}

return command(os.Stdout, configs)
},
}

root.Flags().StringVarP(&configs.environmentFile, "environment", "e", "", "an optional environment file to use (key=value formatted) to perform replacements")
root.Flags().StringVarP(&configs.templateFile, "file", "f", "", "the template file to process (required)")
root.Flags().StringVarP(&configs.templateFilePath, "file", "f", "", "the template file to process")
root.Flags().StringVarP(&configs.customDelimiters, "delimiter", "d", "", `template delimiter (default "{{}}")`)
root.Flags().StringVarP(&configs.rawTemplate, "execute", "x", "", "a raw template to execute directly, without providing --file")
root.Flags().BoolVarP(&configs.stdin, "stdin", "i", false, "a stdin input to execute directly, without providing --file or --execute")
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")
root.Flags().StringVarP(&configs.stdinTemplateFile, "execute", "x", "", "a raw template to execute directly, without providing --file")
root.Flags().StringVarP(&configs.valuesFile, "values", "v", "", "a file containing values to use for the template, a la Helm")
root.Flags().BoolVar(&withValues, "with-values", false, "automatically include a values.yaml file from the current working directory")
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")

root.Flags().SortFlags = false

return root.Execute()
}
23 changes: 12 additions & 11 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@ package main

import (
"fmt"
"text/template"
)

type conf struct {
environmentFile string
templateFile string
rawTemplate string
stdin bool
customDelimiters string
strictMode bool

t *template.Template
environmentFile string
templateFilePath string
stdinTemplateFile string
valuesFile string
strictMode bool
customDelimiters string
}

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

type conflictingArgsError struct {
F1, F2 string
type emissingkeyerr struct{ name string }

func (e *emissingkeyerr) Error() string {
return "strict mode on: missing value in values file: " + e.name
}

type conflictingArgsError struct{ F1, F2 string }

func (e *conflictingArgsError) Error() string {
return fmt.Sprintf("defined both --%s and --%s, only one must be used", e.F1, e.F2)
}
8 changes: 5 additions & 3 deletions template_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ import (

func getTemplateFunctions(virtualKV map[string]string, strict bool) template.FuncMap {
return template.FuncMap{
"raw": func(s string) string {
return s
},
"raw": raw,

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

func raw(s string) string {
return s
}

func readfile(path string) (string, error) {
contents, err := os.ReadFile(path)
if err != nil {
Expand Down
Loading

0 comments on commit 7b180ee

Please sign in to comment.