From 3e90b130670bc8dc0fa0b8e62e0639cf630ff0b2 Mon Sep 17 00:00:00 2001 From: Yusuke KUOKA Date: Mon, 3 Sep 2018 16:30:45 +0900 Subject: [PATCH] feat: Ability to call arbitrary command from a template Resolves #244 --- README.md | 21 +++++++++++++++++++ tmpl/funcs.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/README.md b/README.md index 835bd12c9..731c4bec6 100644 --- a/README.md +++ b/README.md @@ -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. +## 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 helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal: diff --git a/tmpl/funcs.go b/tmpl/funcs.go index 9458d7415..aea696d1e 100644 --- a/tmpl/funcs.go +++ b/tmpl/funcs.go @@ -3,8 +3,11 @@ package tmpl import ( "fmt" "gopkg.in/yaml.v2" + "io" "os" + "os/exec" "path/filepath" + "reflect" "strings" "text/template" ) @@ -13,6 +16,7 @@ type Values = map[string]interface{} func (c *Context) createFuncMap() template.FuncMap { return template.FuncMap{ + "exec": c.Exec, "readFile": c.ReadFile, "toYaml": ToYaml, "fromYaml": FromYaml, @@ -21,6 +25,58 @@ 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...) + + if len(input) > 0 { + stdin, err := cmd.StdinPipe() + if err != nil { + return "", err + } + go func(input string, stdin io.WriteCloser) { + defer stdin.Close() + + size := len(input) + + var n int + var err error + i := 0 + for { + n, err = io.WriteString(stdin, input[i:]) + if err != nil { + panic(err) + break + } + i += n + if n == size { + break + } + } + }(input, stdin) + } + + bytes, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("exec cmd=%s args=[%s] failed: %v", command, strings.Join(strArgs, ", "), err) + } + return string(bytes), nil +} + func (c *Context) ReadFile(filename string) (string, error) { path := filepath.Join(c.basePath, filename)