github.com/terraform-modules-krish/terratest@v0.29.0/modules/terraform/format.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "reflect" 6 "strconv" 7 "strings" 8 9 "github.com/terraform-modules-krish/terratest/modules/collections" 10 ) 11 12 // TerraformDefaultLockingStatus - The terratest default command lock status (backwards compatibility) 13 var TerraformCommandsWithLockSupport = []string{ 14 "plan", 15 "apply", 16 "destroy", 17 "init", 18 "refresh", 19 "taint", 20 "untaint", 21 "import", 22 } 23 24 // FormatArgs converts the inputs to a format palatable to terraform. This includes converting the given vars to the 25 // format the Terraform CLI expects (-var key=value). 26 func FormatArgs(options *Options, args ...string) []string { 27 var terraformArgs []string 28 commandType := args[0] 29 lockSupported := collections.ListContains(TerraformCommandsWithLockSupport, commandType) 30 31 terraformArgs = append(terraformArgs, args...) 32 terraformArgs = append(terraformArgs, FormatTerraformVarsAsArgs(options.Vars)...) 33 terraformArgs = append(terraformArgs, FormatTerraformArgs("-var-file", options.VarFiles)...) 34 terraformArgs = append(terraformArgs, FormatTerraformArgs("-target", options.Targets)...) 35 36 if lockSupported { 37 // If command supports locking, handle lock arguments 38 terraformArgs = append(terraformArgs, FormatTerraformLockAsArgs(options.Lock, options.LockTimeout)...) 39 } 40 41 return terraformArgs 42 } 43 44 // FormatTerraformVarsAsArgs formats the given variables as command-line args for Terraform (e.g. of the format 45 // -var key=value). 46 func FormatTerraformVarsAsArgs(vars map[string]interface{}) []string { 47 return formatTerraformArgs(vars, "-var", true) 48 } 49 50 // FormatTerraformLockAsArgs formats the lock and lock-timeout variables 51 // -lock, -lock-timeout 52 func FormatTerraformLockAsArgs(lockCheck bool, lockTimeout string) []string { 53 lockArgs := []string{fmt.Sprintf("-lock=%v", lockCheck)} 54 if lockTimeout != "" { 55 lockTimeoutValue := fmt.Sprintf("%s=%s", "-lock-timeout", lockTimeout) 56 lockArgs = append(lockArgs, lockTimeoutValue) 57 } 58 return lockArgs 59 } 60 61 // FormatTerraformArgs will format multiple args with the arg name (e.g. "-var-file", []string{"foo.tfvars", "bar.tfvars"}) 62 // returns "-var-file foo.tfvars -var-file bar.tfvars" 63 func FormatTerraformArgs(argName string, args []string) []string { 64 argsList := []string{} 65 for _, argValue := range args { 66 argsList = append(argsList, argName, argValue) 67 } 68 return argsList 69 } 70 71 // FormatTerraformBackendConfigAsArgs formats the given variables as backend config args for Terraform (e.g. of the 72 // format -backend-config=key=value). 73 func FormatTerraformBackendConfigAsArgs(vars map[string]interface{}) []string { 74 return formatTerraformArgs(vars, "-backend-config", false) 75 } 76 77 // Format the given vars into 'Terraform' format, with each var being prefixed with the given prefix. If 78 // useSpaceAsSeparator is true, a space will separate the prefix and each var (e.g., -var foo=bar). If 79 // useSpaceAsSeparator is false, an equals will separate the prefix and each var (e.g., -backend-config=foo=bar). 80 func formatTerraformArgs(vars map[string]interface{}, prefix string, useSpaceAsSeparator bool) []string { 81 var args []string 82 83 for key, value := range vars { 84 hclString := toHclString(value, false) 85 argValue := fmt.Sprintf("%s=%s", key, hclString) 86 if useSpaceAsSeparator { 87 args = append(args, prefix, argValue) 88 } else { 89 args = append(args, fmt.Sprintf("%s=%s", prefix, argValue)) 90 } 91 } 92 93 return args 94 } 95 96 // Terraform allows you to pass in command-line variables using HCL syntax (e.g. -var foo=[1,2,3]). Unfortunately, 97 // while their golang hcl library can convert an HCL string to a Go type, they don't seem to offer a library to convert 98 // arbitrary Go types to an HCL string. Therefore, this method is a simple implementation that correctly handles 99 // ints, booleans, lists, and maps. Everything else is forced into a string using Sprintf. Hopefully, this approach is 100 // good enough for the type of variables we deal with in Terratest. 101 func toHclString(value interface{}, isNested bool) string { 102 // Ideally, we'd use a type switch here to identify slices and maps, but we can't do that, because Go doesn't 103 // support generics, and the type switch only matches concrete types. So we could match []interface{}, but if 104 // a user passes in []string{}, that would NOT match (the same logic applies to maps). Therefore, we have to 105 // use reflection and manually convert into []interface{} and map[string]interface{}. 106 107 if slice, isSlice := tryToConvertToGenericSlice(value); isSlice { 108 return sliceToHclString(slice) 109 } else if m, isMap := tryToConvertToGenericMap(value); isMap { 110 return mapToHclString(m) 111 } else { 112 return primitiveToHclString(value, isNested) 113 } 114 } 115 116 // Try to convert the given value to a generic slice. Return the slice and true if the underlying value itself was a 117 // slice and an empty slice and false if it wasn't. This is necessary because Go is a shitty language that doesn't 118 // have generics, nor useful utility methods built-in. For more info, see: http://stackoverflow.com/a/12754757/483528 119 func tryToConvertToGenericSlice(value interface{}) ([]interface{}, bool) { 120 reflectValue := reflect.ValueOf(value) 121 if reflectValue.Kind() != reflect.Slice { 122 return []interface{}{}, false 123 } 124 125 genericSlice := make([]interface{}, reflectValue.Len()) 126 127 for i := 0; i < reflectValue.Len(); i++ { 128 genericSlice[i] = reflectValue.Index(i).Interface() 129 } 130 131 return genericSlice, true 132 } 133 134 // Try to convert the given value to a generic map. Return the map and true if the underlying value itself was a 135 // map and an empty map and false if it wasn't. This is necessary because Go is a shitty language that doesn't 136 // have generics, nor useful utility methods built-in. For more info, see: http://stackoverflow.com/a/12754757/483528 137 func tryToConvertToGenericMap(value interface{}) (map[string]interface{}, bool) { 138 reflectValue := reflect.ValueOf(value) 139 if reflectValue.Kind() != reflect.Map { 140 return map[string]interface{}{}, false 141 } 142 143 reflectType := reflect.TypeOf(value) 144 if reflectType.Key().Kind() != reflect.String { 145 return map[string]interface{}{}, false 146 } 147 148 genericMap := make(map[string]interface{}, reflectValue.Len()) 149 150 mapKeys := reflectValue.MapKeys() 151 for _, key := range mapKeys { 152 genericMap[key.String()] = reflectValue.MapIndex(key).Interface() 153 } 154 155 return genericMap, true 156 } 157 158 // Convert a slice to an HCL string. See ToHclString for details. 159 func sliceToHclString(slice []interface{}) string { 160 hclValues := []string{} 161 162 for _, value := range slice { 163 hclValue := toHclString(value, true) 164 hclValues = append(hclValues, hclValue) 165 } 166 167 return fmt.Sprintf("[%s]", strings.Join(hclValues, ", ")) 168 } 169 170 // Convert a map to an HCL string. See ToHclString for details. 171 func mapToHclString(m map[string]interface{}) string { 172 keyValuePairs := []string{} 173 174 for key, value := range m { 175 keyValuePair := fmt.Sprintf(`"%s" = %s`, key, toHclString(value, true)) 176 keyValuePairs = append(keyValuePairs, keyValuePair) 177 } 178 179 return fmt.Sprintf("{%s}", strings.Join(keyValuePairs, ", ")) 180 } 181 182 // Convert a primitive, such as a bool, int, or string, to an HCL string. If this isn't a primitive, force its value 183 // using Sprintf. See ToHclString for details. 184 func primitiveToHclString(value interface{}, isNested bool) string { 185 if value == nil { 186 return "null" 187 } 188 189 switch v := value.(type) { 190 191 case bool: 192 return strconv.FormatBool(v) 193 194 case string: 195 // If string is nested in a larger data structure (e.g. list of string, map of string), ensure value is quoted 196 if isNested { 197 return fmt.Sprintf("\"%v\"", v) 198 } 199 200 return fmt.Sprintf("%v", v) 201 202 default: 203 return fmt.Sprintf("%v", v) 204 } 205 }