package config import ( "bytes" "compress/gzip" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io/ioutil" "math" "net" "net/url" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "github.com/apparentlymart/go-cidr/cidr" "github.com/hashicorp/go-uuid" "github.com/hashicorp/hil" "github.com/hashicorp/hil/ast" "github.com/mitchellh/go-homedir" "golang.org/x/crypto/bcrypt" ) // stringSliceToVariableValue converts a string slice into the value // required to be returned from interpolation functions which return // TypeList. func stringSliceToVariableValue(values []string) []ast.Variable { output := make([]ast.Variable, len(values)) for index, value := range values { output[index] = ast.Variable{ Type: ast.TypeString, Value: value, } } return output } func listVariableValueToStringSlice(values []ast.Variable) ([]string, error) { output := make([]string, len(values)) for index, value := range values { if value.Type != ast.TypeString { return []string{}, fmt.Errorf("list has non-string element (%T)", value.Type.String()) } output[index] = value.Value.(string) } return output, nil } // Funcs is the mapping of built-in functions for configuration. func Funcs() map[string]ast.Function { return map[string]ast.Function{ "abs": interpolationFuncAbs(), "basename": interpolationFuncBasename(), "base64decode": interpolationFuncBase64Decode(), "base64encode": interpolationFuncBase64Encode(), "base64gzip": interpolationFuncBase64Gzip(), "base64sha256": interpolationFuncBase64Sha256(), "base64sha512": interpolationFuncBase64Sha512(), "bcrypt": interpolationFuncBcrypt(), "ceil": interpolationFuncCeil(), "chomp": interpolationFuncChomp(), "cidrhost": interpolationFuncCidrHost(), "cidrnetmask": interpolationFuncCidrNetmask(), "cidrsubnet": interpolationFuncCidrSubnet(), "coalesce": interpolationFuncCoalesce(), "coalescelist": interpolationFuncCoalesceList(), "compact": interpolationFuncCompact(), "concat": interpolationFuncConcat(), "contains": interpolationFuncContains(), "dirname": interpolationFuncDirname(), "distinct": interpolationFuncDistinct(), "element": interpolationFuncElement(), "file": interpolationFuncFile(), "matchkeys": interpolationFuncMatchKeys(), "flatten": interpolationFuncFlatten(), "floor": interpolationFuncFloor(), "format": interpolationFuncFormat(), "formatlist": interpolationFuncFormatList(), "indent": interpolationFuncIndent(), "index": interpolationFuncIndex(), "join": interpolationFuncJoin(), "jsonencode": interpolationFuncJSONEncode(), "length": interpolationFuncLength(), "list": interpolationFuncList(), "log": interpolationFuncLog(), "lower": interpolationFuncLower(), "map": interpolationFuncMap(), "max": interpolationFuncMax(), "md5": interpolationFuncMd5(), "merge": interpolationFuncMerge(), "min": interpolationFuncMin(), "pathexpand": interpolationFuncPathExpand(), "pow": interpolationFuncPow(), "uuid": interpolationFuncUUID(), "replace": interpolationFuncReplace(), "sha1": interpolationFuncSha1(), "sha256": interpolationFuncSha256(), "sha512": interpolationFuncSha512(), "signum": interpolationFuncSignum(), "slice": interpolationFuncSlice(), "sort": interpolationFuncSort(), "split": interpolationFuncSplit(), "substr": interpolationFuncSubstr(), "timestamp": interpolationFuncTimestamp(), "title": interpolationFuncTitle(), "transpose": interpolationFuncTranspose(), "trimspace": interpolationFuncTrimSpace(), "upper": interpolationFuncUpper(), "urlencode": interpolationFuncURLEncode(), "zipmap": interpolationFuncZipMap(), } } // interpolationFuncList creates a list from the parameters passed // to it. func interpolationFuncList() ast.Function { return ast.Function{ ArgTypes: []ast.Type{}, ReturnType: ast.TypeList, Variadic: true, VariadicType: ast.TypeAny, Callback: func(args []interface{}) (interface{}, error) { var outputList []ast.Variable for i, val := range args { switch v := val.(type) { case string: outputList = append(outputList, ast.Variable{Type: ast.TypeString, Value: v}) case []ast.Variable: outputList = append(outputList, ast.Variable{Type: ast.TypeList, Value: v}) case map[string]ast.Variable: outputList = append(outputList, ast.Variable{Type: ast.TypeMap, Value: v}) default: return nil, fmt.Errorf("unexpected type %T for argument %d in list", v, i) } } // we don't support heterogeneous types, so make sure all types match the first if len(outputList) > 0 { firstType := outputList[0].Type for i, v := range outputList[1:] { if v.Type != firstType { return nil, fmt.Errorf("unexpected type %s for argument %d in list", v.Type, i+1) } } } return outputList, nil }, } } // interpolationFuncMap creates a map from the parameters passed // to it. func interpolationFuncMap() ast.Function { return ast.Function{ ArgTypes: []ast.Type{}, ReturnType: ast.TypeMap, Variadic: true, VariadicType: ast.TypeAny, Callback: func(args []interface{}) (interface{}, error) { outputMap := make(map[string]ast.Variable) if len(args)%2 != 0 { return nil, fmt.Errorf("requires an even number of arguments, got %d", len(args)) } var firstType *ast.Type for i := 0; i < len(args); i += 2 { key, ok := args[i].(string) if !ok { return nil, fmt.Errorf("argument %d represents a key, so it must be a string", i+1) } val := args[i+1] variable, err := hil.InterfaceToVariable(val) if err != nil { return nil, err } // Enforce map type homogeneity if firstType == nil { firstType = &variable.Type } else if variable.Type != *firstType { return nil, fmt.Errorf("all map values must have the same type, got %s then %s", firstType.Printable(), variable.Type.Printable()) } // Check for duplicate keys if _, ok := outputMap[key]; ok { return nil, fmt.Errorf("argument %d is a duplicate key: %q", i+1, key) } outputMap[key] = variable } return outputMap, nil }, } } // interpolationFuncCompact strips a list of multi-variable values // (e.g. as returned by "split") of any empty strings. func interpolationFuncCompact() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList}, ReturnType: ast.TypeList, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { inputList := args[0].([]ast.Variable) var outputList []string for _, val := range inputList { strVal, ok := val.Value.(string) if !ok { return nil, fmt.Errorf( "compact() may only be used with flat lists, this list contains elements of %s", val.Type.Printable()) } if strVal == "" { continue } outputList = append(outputList, strVal) } return stringSliceToVariableValue(outputList), nil }, } } // interpolationFuncCidrHost implements the "cidrhost" function that // fills in the host part of a CIDR range address to create a single // host address func interpolationFuncCidrHost() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ ast.TypeString, // starting CIDR mask ast.TypeInt, // host number to insert }, ReturnType: ast.TypeString, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { hostNum := args[1].(int) _, network, err := net.ParseCIDR(args[0].(string)) if err != nil { return nil, fmt.Errorf("invalid CIDR expression: %s", err) } ip, err := cidr.Host(network, hostNum) if err != nil { return nil, err } return ip.String(), nil }, } } // interpolationFuncCidrNetmask implements the "cidrnetmask" function // that returns the subnet mask in IP address notation. func interpolationFuncCidrNetmask() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ ast.TypeString, // CIDR mask }, ReturnType: ast.TypeString, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { _, network, err := net.ParseCIDR(args[0].(string)) if err != nil { return nil, fmt.Errorf("invalid CIDR expression: %s", err) } return net.IP(network.Mask).String(), nil }, } } // interpolationFuncCidrSubnet implements the "cidrsubnet" function that // adds an additional subnet of the given length onto an existing // IP block expressed in CIDR notation. func interpolationFuncCidrSubnet() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ ast.TypeString, // starting CIDR mask ast.TypeInt, // number of bits to extend the prefix ast.TypeInt, // network number to append to the prefix }, ReturnType: ast.TypeString, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { extraBits := args[1].(int) subnetNum := args[2].(int) _, network, err := net.ParseCIDR(args[0].(string)) if err != nil { return nil, fmt.Errorf("invalid CIDR expression: %s", err) } // For portability with 32-bit systems where the subnet number // will be a 32-bit int, we only allow extension of 32 bits in // one call even if we're running on a 64-bit machine. // (Of course, this is significant only for IPv6.) if extraBits > 32 { return nil, fmt.Errorf("may not extend prefix by more than 32 bits") } newNetwork, err := cidr.Subnet(network, extraBits, subnetNum) if err != nil { return nil, err } return newNetwork.String(), nil }, } } // interpolationFuncCoalesce implements the "coalesce" function that // returns the first non null / empty string from the provided input func interpolationFuncCoalesce() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("must provide at least two arguments") } for _, arg := range args { argument := arg.(string) if argument != "" { return argument, nil } } return "", nil }, } } // interpolationFuncCoalesceList implements the "coalescelist" function that // returns the first non empty list from the provided input func interpolationFuncCoalesceList() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList}, ReturnType: ast.TypeList, Variadic: true, VariadicType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("must provide at least two arguments") } for _, arg := range args { argument := arg.([]ast.Variable) if len(argument) > 0 { return argument, nil } } return make([]ast.Variable, 0), nil }, } } // interpolationFuncContains returns true if an element is in the list // and return false otherwise func interpolationFuncContains() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList, ast.TypeString}, ReturnType: ast.TypeBool, Callback: func(args []interface{}) (interface{}, error) { _, err := interpolationFuncIndex().Callback(args) if err != nil { return false, nil } return true, nil }, } } // interpolationFuncConcat implements the "concat" function that concatenates // multiple lists. func interpolationFuncConcat() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList}, ReturnType: ast.TypeList, Variadic: true, VariadicType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { var outputList []ast.Variable for _, arg := range args { for _, v := range arg.([]ast.Variable) { switch v.Type { case ast.TypeString: outputList = append(outputList, v) case ast.TypeList: outputList = append(outputList, v) case ast.TypeMap: outputList = append(outputList, v) default: return nil, fmt.Errorf("concat() does not support lists of %s", v.Type.Printable()) } } } // we don't support heterogeneous types, so make sure all types match the first if len(outputList) > 0 { firstType := outputList[0].Type for _, v := range outputList[1:] { if v.Type != firstType { return nil, fmt.Errorf("unexpected %s in list of %s", v.Type.Printable(), firstType.Printable()) } } } return outputList, nil }, } } // interpolationFuncPow returns base x exponential of y. func interpolationFuncPow() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat, ast.TypeFloat}, ReturnType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { return math.Pow(args[0].(float64), args[1].(float64)), nil }, } } // interpolationFuncFile implements the "file" function that allows // loading contents from a file. func interpolationFuncFile() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { path, err := homedir.Expand(args[0].(string)) if err != nil { return "", err } data, err := ioutil.ReadFile(path) if err != nil { return "", err } return string(data), nil }, } } // interpolationFuncFormat implements the "format" function that does // string formatting. func interpolationFuncFormat() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, Variadic: true, VariadicType: ast.TypeAny, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { format := args[0].(string) return fmt.Sprintf(format, args[1:]...), nil }, } } // interpolationFuncMax returns the maximum of the numeric arguments func interpolationFuncMax() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeFloat, Variadic: true, VariadicType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { max := args[0].(float64) for i := 1; i < len(args); i++ { max = math.Max(max, args[i].(float64)) } return max, nil }, } } // interpolationFuncMin returns the minimum of the numeric arguments func interpolationFuncMin() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeFloat, Variadic: true, VariadicType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { min := args[0].(float64) for i := 1; i < len(args); i++ { min = math.Min(min, args[i].(float64)) } return min, nil }, } } // interpolationFuncPathExpand will expand any `~`'s found with the full file path func interpolationFuncPathExpand() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return homedir.Expand(args[0].(string)) }, } } // interpolationFuncCeil returns the the least integer value greater than or equal to the argument func interpolationFuncCeil() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeInt, Callback: func(args []interface{}) (interface{}, error) { return int(math.Ceil(args[0].(float64))), nil }, } } // interpolationFuncLog returns the logarithnm. func interpolationFuncLog() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat, ast.TypeFloat}, ReturnType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { return math.Log(args[0].(float64)) / math.Log(args[1].(float64)), nil }, } } // interpolationFuncChomp removes trailing newlines from the given string func interpolationFuncChomp() ast.Function { newlines := regexp.MustCompile(`(?:\r\n?|\n)*\z`) return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return newlines.ReplaceAllString(args[0].(string), ""), nil }, } } // interpolationFuncFloorreturns returns the greatest integer value less than or equal to the argument func interpolationFuncFloor() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeInt, Callback: func(args []interface{}) (interface{}, error) { return int(math.Floor(args[0].(float64))), nil }, } } func interpolationFuncZipMap() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ ast.TypeList, // Keys ast.TypeList, // Values }, ReturnType: ast.TypeMap, Callback: func(args []interface{}) (interface{}, error) { keys := args[0].([]ast.Variable) values := args[1].([]ast.Variable) if len(keys) != len(values) { return nil, fmt.Errorf("count of keys (%d) does not match count of values (%d)", len(keys), len(values)) } for i, val := range keys { if val.Type != ast.TypeString { return nil, fmt.Errorf("keys must be strings. value at position %d is %s", i, val.Type.Printable()) } } result := map[string]ast.Variable{} for i := 0; i < len(keys); i++ { result[keys[i].Value.(string)] = values[i] } return result, nil }, } } // interpolationFuncFormatList implements the "formatlist" function that does // string formatting on lists. func interpolationFuncFormatList() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeAny}, Variadic: true, VariadicType: ast.TypeAny, ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { // Make a copy of the variadic part of args // to avoid modifying the original. varargs := make([]interface{}, len(args)-1) copy(varargs, args[1:]) // Verify we have some arguments if len(varargs) == 0 { return nil, fmt.Errorf("no arguments to formatlist") } // Convert arguments that are lists into slices. // Confirm along the way that all lists have the same length (n). var n int listSeen := false for i := 1; i < len(args); i++ { s, ok := args[i].([]ast.Variable) if !ok { continue } // Mark that we've seen at least one list listSeen = true // Convert the ast.Variable to a slice of strings parts, err := listVariableValueToStringSlice(s) if err != nil { return nil, err } // otherwise the list is sent down to be indexed varargs[i-1] = parts // Check length if n == 0 { // first list we've seen n = len(parts) continue } if n != len(parts) { return nil, fmt.Errorf("format: mismatched list lengths: %d != %d", n, len(parts)) } } // If we didn't see a list this is an error because we // can't determine the return value length. if !listSeen { return nil, fmt.Errorf( "formatlist requires at least one list argument") } // Do the formatting. format := args[0].(string) // Generate a list of formatted strings. list := make([]string, n) fmtargs := make([]interface{}, len(varargs)) for i := 0; i < n; i++ { for j, arg := range varargs { switch arg := arg.(type) { default: fmtargs[j] = arg case []string: fmtargs[j] = arg[i] } } list[i] = fmt.Sprintf(format, fmtargs...) } return stringSliceToVariableValue(list), nil }, } } // interpolationFuncIndent indents a multi-line string with the // specified number of spaces func interpolationFuncIndent() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt, ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { spaces := args[0].(int) data := args[1].(string) pad := strings.Repeat(" ", spaces) return strings.Replace(data, "\n", "\n"+pad, -1), nil }, } } // interpolationFuncIndex implements the "index" function that allows one to // find the index of a specific element in a list func interpolationFuncIndex() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList, ast.TypeString}, ReturnType: ast.TypeInt, Callback: func(args []interface{}) (interface{}, error) { haystack := args[0].([]ast.Variable) needle := args[1].(string) for index, element := range haystack { if needle == element.Value { return index, nil } } return nil, fmt.Errorf("Could not find '%s' in '%s'", needle, haystack) }, } } // interpolationFuncBasename implements the "dirname" function. func interpolationFuncDirname() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return filepath.Dir(args[0].(string)), nil }, } } // interpolationFuncDistinct implements the "distinct" function that // removes duplicate elements from a list. func interpolationFuncDistinct() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList}, ReturnType: ast.TypeList, Variadic: true, VariadicType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { var list []string if len(args) != 1 { return nil, fmt.Errorf("accepts only one argument.") } if argument, ok := args[0].([]ast.Variable); ok { for _, element := range argument { if element.Type != ast.TypeString { return nil, fmt.Errorf( "only works for flat lists, this list contains elements of %s", element.Type.Printable()) } list = appendIfMissing(list, element.Value.(string)) } } return stringSliceToVariableValue(list), nil }, } } // helper function to add an element to a list, if it does not already exsit func appendIfMissing(slice []string, element string) []string { for _, ele := range slice { if ele == element { return slice } } return append(slice, element) } // for two lists `keys` and `values` of equal length, returns all elements // from `values` where the corresponding element from `keys` is in `searchset`. func interpolationFuncMatchKeys() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList, ast.TypeList, ast.TypeList}, ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { output := make([]ast.Variable, 0) values, _ := args[0].([]ast.Variable) keys, _ := args[1].([]ast.Variable) searchset, _ := args[2].([]ast.Variable) if len(keys) != len(values) { return nil, fmt.Errorf("length of keys and values should be equal") } for i, key := range keys { for _, search := range searchset { if res, err := compareSimpleVariables(key, search); err != nil { return nil, err } else if res == true { output = append(output, values[i]) break } } } // if searchset is empty, then output is an empty list as well. // if we haven't matched any key, then output is an empty list. return output, nil }, } } // compare two variables of the same type, i.e. non complex one, such as TypeList or TypeMap func compareSimpleVariables(a, b ast.Variable) (bool, error) { if a.Type != b.Type { return false, fmt.Errorf( "won't compare items of different types %s and %s", a.Type.Printable(), b.Type.Printable()) } switch a.Type { case ast.TypeString: return a.Value.(string) == b.Value.(string), nil default: return false, fmt.Errorf( "can't compare items of type %s", a.Type.Printable()) } } // interpolationFuncJoin implements the "join" function that allows // multi-variable values to be joined by some character. func interpolationFuncJoin() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, Variadic: true, VariadicType: ast.TypeList, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { var list []string if len(args) < 2 { return nil, fmt.Errorf("not enough arguments to join()") } for _, arg := range args[1:] { for _, part := range arg.([]ast.Variable) { if part.Type != ast.TypeString { return nil, fmt.Errorf( "only works on flat lists, this list contains elements of %s", part.Type.Printable()) } list = append(list, part.Value.(string)) } } return strings.Join(list, args[0].(string)), nil }, } } // interpolationFuncJSONEncode implements the "jsonencode" function that encodes // a string, list, or map as its JSON representation. func interpolationFuncJSONEncode() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeAny}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { var toEncode interface{} switch typedArg := args[0].(type) { case string: toEncode = typedArg case []ast.Variable: strings := make([]string, len(typedArg)) for i, v := range typedArg { if v.Type != ast.TypeString { variable, _ := hil.InterfaceToVariable(typedArg) toEncode, _ = hil.VariableToInterface(variable) jEnc, err := json.Marshal(toEncode) if err != nil { return "", fmt.Errorf("failed to encode JSON data '%s'", toEncode) } return string(jEnc), nil } strings[i] = v.Value.(string) } toEncode = strings case map[string]ast.Variable: stringMap := make(map[string]string) for k, v := range typedArg { if v.Type != ast.TypeString { variable, _ := hil.InterfaceToVariable(typedArg) toEncode, _ = hil.VariableToInterface(variable) jEnc, err := json.Marshal(toEncode) if err != nil { return "", fmt.Errorf("failed to encode JSON data '%s'", toEncode) } return string(jEnc), nil } stringMap[k] = v.Value.(string) } toEncode = stringMap default: return "", fmt.Errorf("unknown type for JSON encoding: %T", args[0]) } jEnc, err := json.Marshal(toEncode) if err != nil { return "", fmt.Errorf("failed to encode JSON data '%s'", toEncode) } return string(jEnc), nil }, } } // interpolationFuncReplace implements the "replace" function that does // string replacement. func interpolationFuncReplace() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString, ast.TypeString, ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) search := args[1].(string) replace := args[2].(string) // We search/replace using a regexp if the string is surrounded // in forward slashes. if len(search) > 1 && search[0] == '/' && search[len(search)-1] == '/' { re, err := regexp.Compile(search[1 : len(search)-1]) if err != nil { return nil, err } return re.ReplaceAllString(s, replace), nil } return strings.Replace(s, search, replace, -1), nil }, } } func interpolationFuncLength() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeAny}, ReturnType: ast.TypeInt, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { subject := args[0] switch typedSubject := subject.(type) { case string: return len(typedSubject), nil case []ast.Variable: return len(typedSubject), nil case map[string]ast.Variable: return len(typedSubject), nil } return 0, fmt.Errorf("arguments to length() must be a string, list, or map") }, } } func interpolationFuncSignum() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeInt}, ReturnType: ast.TypeInt, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { num := args[0].(int) switch { case num < 0: return -1, nil case num > 0: return +1, nil default: return 0, nil } }, } } // interpolationFuncSlice returns a portion of the input list between from, inclusive and to, exclusive. func interpolationFuncSlice() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ ast.TypeList, // inputList ast.TypeInt, // from ast.TypeInt, // to }, ReturnType: ast.TypeList, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { inputList := args[0].([]ast.Variable) from := args[1].(int) to := args[2].(int) if from < 0 { return nil, fmt.Errorf("from index must be >= 0") } if to > len(inputList) { return nil, fmt.Errorf("to index must be <= length of the input list") } if from > to { return nil, fmt.Errorf("from index must be <= to index") } var outputList []ast.Variable for i, val := range inputList { if i >= from && i < to { outputList = append(outputList, val) } } return outputList, nil }, } } // interpolationFuncSort sorts a list of a strings lexographically func interpolationFuncSort() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList}, ReturnType: ast.TypeList, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { inputList := args[0].([]ast.Variable) // Ensure that all the list members are strings and // create a string slice from them members := make([]string, len(inputList)) for i, val := range inputList { if val.Type != ast.TypeString { return nil, fmt.Errorf( "sort() may only be used with lists of strings - %s at index %d", val.Type.String(), i) } members[i] = val.Value.(string) } sort.Strings(members) return stringSliceToVariableValue(members), nil }, } } // interpolationFuncSplit implements the "split" function that allows // strings to split into multi-variable values func interpolationFuncSplit() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString, ast.TypeString}, ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { sep := args[0].(string) s := args[1].(string) elements := strings.Split(s, sep) return stringSliceToVariableValue(elements), nil }, } } // interpolationFuncLookup implements the "lookup" function that allows // dynamic lookups of map types within a Terraform configuration. func interpolationFuncLookup(vs map[string]ast.Variable) ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeMap, ast.TypeString}, ReturnType: ast.TypeString, Variadic: true, VariadicType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { defaultValue := "" defaultValueSet := false if len(args) > 2 { defaultValue = args[2].(string) defaultValueSet = true } if len(args) > 3 { return "", fmt.Errorf("lookup() takes no more than three arguments") } index := args[1].(string) mapVar := args[0].(map[string]ast.Variable) v, ok := mapVar[index] if !ok { if defaultValueSet { return defaultValue, nil } else { return "", fmt.Errorf( "lookup failed to find '%s'", args[1].(string)) } } if v.Type != ast.TypeString { return nil, fmt.Errorf( "lookup() may only be used with flat maps, this map contains elements of %s", v.Type.Printable()) } return v.Value.(string), nil }, } } // interpolationFuncElement implements the "element" function that allows // a specific index to be looked up in a multi-variable value. Note that this will // wrap if the index is larger than the number of elements in the multi-variable value. func interpolationFuncElement() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList, ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { list := args[0].([]ast.Variable) if len(list) == 0 { return nil, fmt.Errorf("element() may not be used with an empty list") } index, err := strconv.Atoi(args[1].(string)) if err != nil || index < 0 { return "", fmt.Errorf( "invalid number for index, got %s", args[1]) } resolvedIndex := index % len(list) v := list[resolvedIndex] if v.Type != ast.TypeString { return nil, fmt.Errorf( "element() may only be used with flat lists, this list contains elements of %s", v.Type.Printable()) } return v.Value, nil }, } } // interpolationFuncKeys implements the "keys" function that yields a list of // keys of map types within a Terraform configuration. func interpolationFuncKeys(vs map[string]ast.Variable) ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeMap}, ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { mapVar := args[0].(map[string]ast.Variable) keys := make([]string, 0) for k, _ := range mapVar { keys = append(keys, k) } sort.Strings(keys) // Keys are guaranteed to be strings return stringSliceToVariableValue(keys), nil }, } } // interpolationFuncValues implements the "values" function that yields a list of // keys of map types within a Terraform configuration. func interpolationFuncValues(vs map[string]ast.Variable) ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeMap}, ReturnType: ast.TypeList, Callback: func(args []interface{}) (interface{}, error) { mapVar := args[0].(map[string]ast.Variable) keys := make([]string, 0) for k, _ := range mapVar { keys = append(keys, k) } sort.Strings(keys) values := make([]string, len(keys)) for index, key := range keys { if value, ok := mapVar[key].Value.(string); ok { values[index] = value } else { return "", fmt.Errorf("values(): %q has element with bad type %s", key, mapVar[key].Type) } } variable, err := hil.InterfaceToVariable(values) if err != nil { return nil, err } return variable.Value, nil }, } } // interpolationFuncBasename implements the "basename" function. func interpolationFuncBasename() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return filepath.Base(args[0].(string)), nil }, } } // interpolationFuncBase64Encode implements the "base64encode" function that // allows Base64 encoding. func interpolationFuncBase64Encode() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) return base64.StdEncoding.EncodeToString([]byte(s)), nil }, } } // interpolationFuncBase64Decode implements the "base64decode" function that // allows Base64 decoding. func interpolationFuncBase64Decode() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) sDec, err := base64.StdEncoding.DecodeString(s) if err != nil { return "", fmt.Errorf("failed to decode base64 data '%s'", s) } return string(sDec), nil }, } } // interpolationFuncBase64Gzip implements the "gzip" function that allows gzip // compression encoding the result using base64 func interpolationFuncBase64Gzip() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) var b bytes.Buffer gz := gzip.NewWriter(&b) if _, err := gz.Write([]byte(s)); err != nil { return "", fmt.Errorf("failed to write gzip raw data: '%s'", s) } if err := gz.Flush(); err != nil { return "", fmt.Errorf("failed to flush gzip writer: '%s'", s) } if err := gz.Close(); err != nil { return "", fmt.Errorf("failed to close gzip writer: '%s'", s) } return base64.StdEncoding.EncodeToString(b.Bytes()), nil }, } } // interpolationFuncLower implements the "lower" function that does // string lower casing. func interpolationFuncLower() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { toLower := args[0].(string) return strings.ToLower(toLower), nil }, } } func interpolationFuncMd5() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) h := md5.New() h.Write([]byte(s)) hash := hex.EncodeToString(h.Sum(nil)) return hash, nil }, } } func interpolationFuncMerge() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeMap}, ReturnType: ast.TypeMap, Variadic: true, VariadicType: ast.TypeMap, Callback: func(args []interface{}) (interface{}, error) { outputMap := make(map[string]ast.Variable) for _, arg := range args { for k, v := range arg.(map[string]ast.Variable) { outputMap[k] = v } } return outputMap, nil }, } } // interpolationFuncUpper implements the "upper" function that does // string upper casing. func interpolationFuncUpper() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { toUpper := args[0].(string) return strings.ToUpper(toUpper), nil }, } } func interpolationFuncSha1() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) h := sha1.New() h.Write([]byte(s)) hash := hex.EncodeToString(h.Sum(nil)) return hash, nil }, } } // hexadecimal representation of sha256 sum func interpolationFuncSha256() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) h := sha256.New() h.Write([]byte(s)) hash := hex.EncodeToString(h.Sum(nil)) return hash, nil }, } } func interpolationFuncSha512() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) h := sha512.New() h.Write([]byte(s)) hash := hex.EncodeToString(h.Sum(nil)) return hash, nil }, } } func interpolationFuncTrimSpace() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { trimSpace := args[0].(string) return strings.TrimSpace(trimSpace), nil }, } } func interpolationFuncBase64Sha256() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) h := sha256.New() h.Write([]byte(s)) shaSum := h.Sum(nil) encoded := base64.StdEncoding.EncodeToString(shaSum[:]) return encoded, nil }, } } func interpolationFuncBase64Sha512() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) h := sha512.New() h.Write([]byte(s)) shaSum := h.Sum(nil) encoded := base64.StdEncoding.EncodeToString(shaSum[:]) return encoded, nil }, } } func interpolationFuncBcrypt() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, Variadic: true, VariadicType: ast.TypeString, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { defaultCost := 10 if len(args) > 1 { costStr := args[1].(string) cost, err := strconv.Atoi(costStr) if err != nil { return "", err } defaultCost = cost } if len(args) > 2 { return "", fmt.Errorf("bcrypt() takes no more than two arguments") } input := args[0].(string) out, err := bcrypt.GenerateFromPassword([]byte(input), defaultCost) if err != nil { return "", fmt.Errorf("error occured generating password %s", err.Error()) } return string(out), nil }, } } func interpolationFuncUUID() ast.Function { return ast.Function{ ArgTypes: []ast.Type{}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return uuid.GenerateUUID() }, } } // interpolationFuncTimestamp func interpolationFuncTimestamp() ast.Function { return ast.Function{ ArgTypes: []ast.Type{}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { return time.Now().UTC().Format(time.RFC3339), nil }, } } // interpolationFuncTitle implements the "title" function that returns a copy of the // string in which first characters of all the words are capitalized. func interpolationFuncTitle() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { toTitle := args[0].(string) return strings.Title(toTitle), nil }, } } // interpolationFuncSubstr implements the "substr" function that allows strings // to be truncated. func interpolationFuncSubstr() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ ast.TypeString, // input string ast.TypeInt, // offset ast.TypeInt, // length }, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { str := args[0].(string) offset := args[1].(int) length := args[2].(int) // Interpret a negative offset as being equivalent to a positive // offset taken from the end of the string. if offset < 0 { offset += len(str) } // Interpret a length of `-1` as indicating that the substring // should start at `offset` and continue until the end of the // string. Any other negative length (other than `-1`) is invalid. if length == -1 { length = len(str) } else if length >= 0 { length += offset } else { return nil, fmt.Errorf("length should be a non-negative integer") } if offset > len(str) { return nil, fmt.Errorf("offset cannot be larger than the length of the string") } if length > len(str) { return nil, fmt.Errorf("'offset + length' cannot be larger than the length of the string") } return str[offset:length], nil }, } } // Flatten until it's not ast.TypeList func flattener(finalList []ast.Variable, flattenList []ast.Variable) []ast.Variable { for _, val := range flattenList { if val.Type == ast.TypeList { finalList = flattener(finalList, val.Value.([]ast.Variable)) } else { finalList = append(finalList, val) } } return finalList } // Flatten to single list func interpolationFuncFlatten() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeList}, ReturnType: ast.TypeList, Variadic: false, Callback: func(args []interface{}) (interface{}, error) { inputList := args[0].([]ast.Variable) var outputList []ast.Variable return flattener(outputList, inputList), nil }, } } func interpolationFuncURLEncode() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Callback: func(args []interface{}) (interface{}, error) { s := args[0].(string) return url.QueryEscape(s), nil }, } } // interpolationFuncTranspose implements the "transpose" function // that converts a map (string,list) to a map (string,list) where // the unique values of the original lists become the keys of the // new map and the keys of the original map become values for the // corresponding new keys. func interpolationFuncTranspose() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeMap}, ReturnType: ast.TypeMap, Callback: func(args []interface{}) (interface{}, error) { inputMap := args[0].(map[string]ast.Variable) outputMap := make(map[string]ast.Variable) tmpMap := make(map[string][]string) for inKey, inVal := range inputMap { if inVal.Type != ast.TypeList { return nil, fmt.Errorf("transpose requires a map of lists of strings") } values := inVal.Value.([]ast.Variable) for _, listVal := range values { if listVal.Type != ast.TypeString { return nil, fmt.Errorf("transpose requires the given map values to be lists of strings") } outKey := listVal.Value.(string) if _, ok := tmpMap[outKey]; !ok { tmpMap[outKey] = make([]string, 0) } outVal := tmpMap[outKey] outVal = append(outVal, inKey) sort.Strings(outVal) tmpMap[outKey] = outVal } } for outKey, outVal := range tmpMap { values := make([]ast.Variable, 0) for _, v := range outVal { values = append(values, ast.Variable{Type: ast.TypeString, Value: v}) } outputMap[outKey] = ast.Variable{Type: ast.TypeList, Value: values} } return outputMap, nil }, } } // interpolationFuncAbs returns the absolute value of a given float. func interpolationFuncAbs() ast.Function { return ast.Function{ ArgTypes: []ast.Type{ast.TypeFloat}, ReturnType: ast.TypeFloat, Callback: func(args []interface{}) (interface{}, error) { return math.Abs(args[0].(float64)), nil }, } }