feat: Ability to call arbitrary command from a template (#282)

Resolves #244
This commit is contained in:
KUOKA Yusuke 2018-09-03 16:48:03 +09:00 committed by GitHub
parent 6be53b1bcc
commit 8a90e5320c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 99 additions and 0 deletions

View File

@ -533,6 +533,27 @@ Just run `helmfile sync` inside `myteam/`, and you are done.
All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too. All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too.
## Importing values from any source
The `exec` template function that is available in `values.yaml.gotmpl` is useful for importing values from any source
that is accessible by running a command:
An usual usage of `exec` would look like this:
```
mysetting: |
{{ exec "./mycmd" (list "arg1" "arg2" "--flag1") | indent 2 }}
```
Or even with a pipeline:
```
mysetting: |
{{ yourinput | exec "./mycmd-consume-stdin" (list "arg1" "arg2") | indent 2 }}
```
The possibility is endless. Try importing values from your golang app, bash script, jsonnet, or anything!
## Using env files ## Using env files
helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal: helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:

View File

@ -3,8 +3,11 @@ package tmpl
import ( import (
"fmt" "fmt"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
"io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"text/template" "text/template"
) )
@ -13,6 +16,7 @@ type Values = map[string]interface{}
func (c *Context) createFuncMap() template.FuncMap { func (c *Context) createFuncMap() template.FuncMap {
return template.FuncMap{ return template.FuncMap{
"exec": c.Exec,
"readFile": c.ReadFile, "readFile": c.ReadFile,
"toYaml": ToYaml, "toYaml": ToYaml,
"fromYaml": FromYaml, "fromYaml": FromYaml,
@ -21,6 +25,80 @@ func (c *Context) createFuncMap() template.FuncMap {
} }
} }
func (c *Context) Exec(command string, args []interface{}, inputs ...string) (string, error) {
var input string
if len(inputs) > 0 {
input = inputs[0]
}
strArgs := make([]string, len(args))
for i, a := range args {
switch a.(type) {
case string:
strArgs[i] = a.(string)
default:
return "", fmt.Errorf("unexpected type of arg \"%s\" in args %v at index %d", reflect.TypeOf(a), args, i)
}
}
cmd := exec.Command(command, strArgs...)
writeErrs := make(chan error)
cmdErrs := make(chan error)
cmdOuts := make(chan []byte)
if len(input) > 0 {
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
go func(input string, stdin io.WriteCloser) {
defer stdin.Close()
defer close(writeErrs)
size := len(input)
var n int
var err error
i := 0
for {
n, err = io.WriteString(stdin, input[i:])
if err != nil {
writeErrs <- fmt.Errorf("failed while writing %d bytes to stdin of \"%s\": %v", len(input), command, err)
break
}
i += n
if n == size {
break
}
}
}(input, stdin)
}
go func() {
defer close(cmdOuts)
defer close(cmdErrs)
bytes, err := cmd.Output()
if err != nil {
cmdErrs <- fmt.Errorf("exec cmd=%s args=[%s] failed: %v", command, strings.Join(strArgs, ", "), err)
} else {
cmdOuts <- bytes
}
}()
for {
select {
case bytes := <-cmdOuts:
return string(bytes), nil
case err := <-cmdErrs:
return "", err
case err := <-writeErrs:
return "", err
}
}
}
func (c *Context) ReadFile(filename string) (string, error) { func (c *Context) ReadFile(filename string) (string, error) {
path := filepath.Join(c.basePath, filename) path := filepath.Join(c.basePath, filename)