1package tmpl
2
3import (
4	"fmt"
5	"github.com/ghodss/yaml"
6	"github.com/roboll/helmfile/pkg/helmexec"
7	"golang.org/x/sync/errgroup"
8	"io"
9	"os"
10	"os/exec"
11	"path/filepath"
12	"reflect"
13	"strings"
14	"text/template"
15)
16
17type Values = map[string]interface{}
18
19func (c *Context) createFuncMap() template.FuncMap {
20	funcMap := template.FuncMap{
21		"exec":             c.Exec,
22		"readFile":         c.ReadFile,
23		"toYaml":           ToYaml,
24		"fromYaml":         FromYaml,
25		"setValueAtPath":   SetValueAtPath,
26		"requiredEnv":      RequiredEnv,
27		"get":              get,
28		"getOrNil":         getOrNil,
29		"tpl":              c.Tpl,
30		"required":         Required,
31		"fetchSecretValue": fetchSecretValue,
32		"expandSecretRefs": fetchSecretValues,
33	}
34	if c.preRender {
35		// disable potential side-effect template calls
36		funcMap["exec"] = func(string, []interface{}, ...string) (string, error) {
37			return "", nil
38		}
39		funcMap["readFile"] = func(string) (string, error) {
40			return "", nil
41		}
42	}
43
44	return funcMap
45}
46
47func (c *Context) Exec(command string, args []interface{}, inputs ...string) (string, error) {
48	var input string
49	if len(inputs) > 0 {
50		input = inputs[0]
51	}
52
53	strArgs := make([]string, len(args))
54	for i, a := range args {
55		switch a.(type) {
56		case string:
57			strArgs[i] = a.(string)
58		default:
59			return "", fmt.Errorf("unexpected type of arg \"%s\" in args %v at index %d", reflect.TypeOf(a), args, i)
60		}
61	}
62
63	cmd := exec.Command(command, strArgs...)
64	cmd.Dir = c.basePath
65
66	g := errgroup.Group{}
67
68	if len(input) > 0 {
69		stdin, err := cmd.StdinPipe()
70		if err != nil {
71			return "", err
72		}
73
74		g.Go(func() error {
75			defer stdin.Close()
76
77			size := len(input)
78
79			i := 0
80
81			for {
82				n, err := io.WriteString(stdin, input[i:])
83				if err != nil {
84					return fmt.Errorf("failed while writing %d bytes to stdin of \"%s\": %v", len(input), command, err)
85				}
86
87				i += n
88
89				if i == size {
90					return nil
91				}
92			}
93		})
94	}
95
96	var bytes []byte
97
98	g.Go(func() error {
99		// We use CombinedOutput to produce helpful error messages
100		// See https://github.com/roboll/helmfile/issues/1158
101		bs, err := helmexec.Output(cmd)
102		if err != nil {
103			return err
104		}
105
106		bytes = bs
107
108		return nil
109	})
110
111	if err := g.Wait(); err != nil {
112		return "", err
113	}
114
115	return string(bytes), nil
116}
117
118func (c *Context) ReadFile(filename string) (string, error) {
119	var path string
120	if filepath.IsAbs(filename) {
121		path = filename
122	} else {
123		path = filepath.Join(c.basePath, filename)
124	}
125
126	bytes, err := c.readFile(path)
127	if err != nil {
128		return "", err
129	}
130	return string(bytes), nil
131}
132
133func (c *Context) Tpl(text string, data interface{}) (string, error) {
134	buf, err := c.RenderTemplateToBuffer(text, data)
135	if err != nil {
136		return "", err
137	}
138	return buf.String(), nil
139}
140
141func ToYaml(v interface{}) (string, error) {
142	data, err := yaml.Marshal(v)
143	if err != nil {
144		return "", err
145	}
146	return string(data), nil
147}
148
149func FromYaml(str string) (Values, error) {
150	m := Values{}
151
152	if err := yaml.Unmarshal([]byte(str), &m); err != nil {
153		return nil, fmt.Errorf("%s, offending yaml: %s", err, str)
154	}
155	return m, nil
156}
157
158func SetValueAtPath(path string, value interface{}, values Values) (Values, error) {
159	var current interface{}
160	current = values
161	components := strings.Split(path, ".")
162	pathToMap := components[:len(components)-1]
163	key := components[len(components)-1]
164	for _, k := range pathToMap {
165		var elem interface{}
166
167		switch typedCurrent := current.(type) {
168		case map[string]interface{}:
169			v, exists := typedCurrent[k]
170			if !exists {
171				return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" does not exist", path, k)
172			}
173			elem = v
174		case map[interface{}]interface{}:
175			v, exists := typedCurrent[k]
176			if !exists {
177				return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" does not exist", path, k)
178			}
179			elem = v
180		default:
181			return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" was not a map", path, k)
182		}
183
184		switch typedElem := elem.(type) {
185		case map[string]interface{}, map[interface{}]interface{}:
186			current = typedElem
187		default:
188			return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" was not a map", path, k)
189		}
190	}
191
192	switch typedCurrent := current.(type) {
193	case map[string]interface{}:
194		typedCurrent[key] = value
195	case map[interface{}]interface{}:
196		typedCurrent[key] = value
197	default:
198		return nil, fmt.Errorf("failed to set value at path \"%s\": value for key \"%s\" was not a map", path, key)
199	}
200	return values, nil
201}
202
203func RequiredEnv(name string) (string, error) {
204	if val, exists := os.LookupEnv(name); exists && len(val) > 0 {
205		return val, nil
206	}
207
208	return "", fmt.Errorf("required env var `%s` is not set", name)
209}
210
211func Required(warn string, val interface{}) (interface{}, error) {
212	if val == nil {
213		return nil, fmt.Errorf(warn)
214	} else if _, ok := val.(string); ok {
215		if val == "" {
216			return nil, fmt.Errorf(warn)
217		}
218	}
219
220	return val, nil
221}
222