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