github.com/mponton/terratest@v0.44.0/modules/terraform/var-file.go (about) 1 package terraform 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "reflect" 8 "strings" 9 10 "github.com/mponton/terratest/modules/testing" 11 12 "github.com/hashicorp/hcl/v2" 13 "github.com/hashicorp/hcl/v2/hclparse" 14 "github.com/stretchr/testify/require" 15 "github.com/zclconf/go-cty/cty" 16 "github.com/zclconf/go-cty/cty/gocty" 17 ctyjson "github.com/zclconf/go-cty/cty/json" 18 ) 19 20 // GetVariableAsStringFromVarFile Gets the string representation of a variable from a provided input file found in VarFile 21 // For list or map, use GetVariableAsListFromVarFile or GetVariableAsMapFromVarFile, respectively. 22 func GetVariableAsStringFromVarFile(t testing.TestingT, fileName string, key string) string { 23 result, err := GetVariableAsStringFromVarFileE(t, fileName, key) 24 require.NoError(t, err) 25 26 return result 27 } 28 29 // GetVariableAsStringFromVarFileE Gets the string representation of a variable from a provided input file found in VarFile 30 // Will return an error if GetAllVariablesFromVarFileE returns an error or the key provided does not exist in the file. 31 // For list or map, use GetVariableAsListFromVarFile or GetVariableAsMapFromVarFile, respectively. 32 func GetVariableAsStringFromVarFileE(t testing.TestingT, fileName string, key string) (string, error) { 33 var variables map[string]interface{} 34 err := GetAllVariablesFromVarFileE(t, fileName, &variables) 35 if err != nil { 36 return "", err 37 } 38 39 variable, exists := variables[key] 40 41 if !exists { 42 return "", InputFileKeyNotFound{FilePath: fileName, Key: key} 43 } 44 45 return fmt.Sprintf("%v", variable), nil 46 } 47 48 // GetVariableAsMapFromVarFile Gets the map representation of a variable from a provided input file found in VarFile 49 // Note that this returns a map of strings. For maps containing complex types, use GetAllVariablesFromVarFile. 50 func GetVariableAsMapFromVarFile(t testing.TestingT, fileName string, key string) map[string]string { 51 result, err := GetVariableAsMapFromVarFileE(t, fileName, key) 52 require.NoError(t, err) 53 return result 54 } 55 56 // GetVariableAsMapFromVarFileE Gets the map representation of a variable from a provided input file found in VarFile. 57 // Note that this returns a map of strings. For maps containing complex types, use GetAllVariablesFromVarFile 58 // Returns an error if GetAllVariablesFromVarFileE returns an error, the key provided does not exist, or the value associated with the key is not a map 59 func GetVariableAsMapFromVarFileE(t testing.TestingT, fileName string, key string) (map[string]string, error) { 60 var variables map[string]interface{} 61 err := GetAllVariablesFromVarFileE(t, fileName, &variables) 62 if err != nil { 63 return nil, err 64 } 65 66 variable, exists := variables[key] 67 68 if !exists { 69 return nil, InputFileKeyNotFound{FilePath: fileName, Key: key} 70 } 71 72 if reflect.TypeOf(variable).String() != "map[string]interface {}" { 73 return nil, UnexpectedOutputType{Key: key, ExpectedType: "map[string]interface {}", ActualType: reflect.TypeOf(variable).String()} 74 } 75 76 resultMap := make(map[string]string) 77 for mapKey, mapVal := range variable.(map[string]interface{}) { 78 resultMap[mapKey] = fmt.Sprintf("%v", mapVal) 79 } 80 return resultMap, nil 81 } 82 83 // GetVariableAsListFromVarFile Gets the string list representation of a variable from a provided input file found in VarFile 84 // Note that this returns a list of strings. For lists containing complex types, use GetAllVariablesFromVarFile. 85 func GetVariableAsListFromVarFile(t testing.TestingT, fileName string, key string) []string { 86 result, err := GetVariableAsListFromVarFileE(t, fileName, key) 87 require.NoError(t, err) 88 89 return result 90 } 91 92 // GetVariableAsListFromVarFileE Gets the string list representation of a variable from a provided input file found in VarFile 93 // Note that this returns a list of strings. For lists containing complex types, use GetAllVariablesFromVarFile. 94 // Will return error if GetAllVariablesFromVarFileE returns an error, the key provided does not exist, or the value associated with the key is not a list 95 func GetVariableAsListFromVarFileE(t testing.TestingT, fileName string, key string) ([]string, error) { 96 var variables map[string]interface{} 97 err := GetAllVariablesFromVarFileE(t, fileName, &variables) 98 if err != nil { 99 return nil, err 100 } 101 102 variable, exists := variables[key] 103 if !exists { 104 return nil, InputFileKeyNotFound{FilePath: fileName, Key: key} 105 } 106 107 if reflect.TypeOf(variable).String() != "[]interface {}" { 108 return nil, UnexpectedOutputType{Key: key, ExpectedType: "[]interface {}", ActualType: reflect.TypeOf(variable).String()} 109 } 110 111 resultArray := []string{} 112 for _, item := range variable.([]interface{}) { 113 resultArray = append(resultArray, fmt.Sprintf("%v", item)) 114 } 115 116 return resultArray, nil 117 } 118 119 // GetAllVariablesFromVarFile Parses all data from a provided input file found ind in VarFile and stores the result in 120 // the value pointed to by out. 121 func GetAllVariablesFromVarFile(t testing.TestingT, fileName string, out interface{}) { 122 err := GetAllVariablesFromVarFileE(t, fileName, out) 123 require.NoError(t, err) 124 } 125 126 // GetAllVariablesFromVarFileE Parses all data from a provided input file found ind in VarFile and stores the result in 127 // the value pointed to by out. Returns an error if the specified file does not exist, the specified file is not 128 // readable, or the specified file cannot be decoded from HCL. 129 func GetAllVariablesFromVarFileE(t testing.TestingT, fileName string, out interface{}) error { 130 fileContents, err := ioutil.ReadFile(fileName) 131 if err != nil { 132 return err 133 } 134 135 return parseAndDecodeVarFile(string(fileContents), fileName, out) 136 } 137 138 // parseAndDecodeVarFile uses the HCL2 parser to parse the given varfile string into an HCL or HCL JSON file body, and then decode it 139 // into a map that maps var names to values. 140 func parseAndDecodeVarFile(fileContents string, filename string, out interface{}) (err error) { 141 // The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from 142 // those panics here and convert them to normal errors 143 defer func() { 144 if recovered := recover(); recovered != nil { 145 err = PanicWhileParsingVarFile{RecoveredValue: recovered, ConfigFile: filename} 146 } 147 }() 148 149 parser := hclparse.NewParser() 150 151 var file *hcl.File 152 var parseDiagnostics hcl.Diagnostics 153 154 // determine if a JSON variables file is submitted and parse accordingly 155 if strings.HasSuffix(filename, ".json") { 156 file, parseDiagnostics = parser.ParseJSON([]byte(fileContents), filename) 157 } else { 158 file, parseDiagnostics = parser.ParseHCL([]byte(fileContents), filename) 159 } 160 161 if parseDiagnostics != nil && parseDiagnostics.HasErrors() { 162 return parseDiagnostics 163 } 164 165 // VarFiles should only have attributes, so extract the attributes and decode the expressions into the return map. 166 attrs, hclDiags := file.Body.JustAttributes() 167 if hclDiags != nil && hclDiags.HasErrors() { 168 return hclDiags 169 } 170 171 valMap := map[string]cty.Value{} 172 for name, attr := range attrs { 173 val, hclDiags := attr.Expr.Value(nil) // nil because no function calls or variable references are allowed here 174 if hclDiags != nil && hclDiags.HasErrors() { 175 return hclDiags 176 } 177 valMap[name] = val 178 } 179 180 ctyVal, err := convertValuesMapToCtyVal(valMap) 181 if err != nil { 182 return err 183 } 184 185 typedOut, hasType := out.(*map[string]interface{}) 186 if hasType { 187 genericMap, err := parseCtyValueToMap(ctyVal) 188 if err != nil { 189 return err 190 } 191 *typedOut = genericMap 192 return nil 193 } 194 return gocty.FromCtyValue(ctyVal, out) 195 } 196 197 // This is a hacky workaround to convert a cty Value to a Go map[string]interface{}. cty does not support this directly 198 // (https://github.com/hashicorp/hcl2/issues/108) and doing it with gocty.FromCtyValue is nearly impossible, as cty 199 // requires you to specify all the output types and will error out when it hits interface{}. So, as an ugly workaround, 200 // we convert the given value to JSON using cty's JSON library and then convert the JSON back to a 201 // map[string]interface{} using the Go json library. 202 func parseCtyValueToMap(value cty.Value) (map[string]interface{}, error) { 203 jsonBytes, err := ctyjson.Marshal(value, cty.DynamicPseudoType) 204 if err != nil { 205 return nil, err 206 } 207 208 var ctyJsonOutput CtyJsonOutput 209 if err := json.Unmarshal(jsonBytes, &ctyJsonOutput); err != nil { 210 return nil, err 211 } 212 213 return ctyJsonOutput.Value, nil 214 } 215 216 // When you convert a cty value to JSON, if any of that types are not yet known (i.e., are labeled as 217 // DynamicPseudoType), cty's Marshall method will write the type information to a type field and the actual value to 218 // a value field. This struct is used to capture that information so when we parse the JSON back into a Go struct, we 219 // can pull out just the Value field we need. 220 type CtyJsonOutput struct { 221 Value map[string]interface{} 222 Type interface{} 223 } 224 225 // convertValuesMapToCtyVal takes a map of name - cty.Value pairs and converts to a single cty.Value object that can 226 // then be converted to other go types. 227 func convertValuesMapToCtyVal(valMap map[string]cty.Value) (cty.Value, error) { 228 valMapAsCty := cty.NilVal 229 if valMap != nil && len(valMap) > 0 { 230 var err error 231 valMapAsCty, err = gocty.ToCtyValue(valMap, generateTypeFromValuesMap(valMap)) 232 if err != nil { 233 return valMapAsCty, err 234 } 235 } 236 return valMapAsCty, nil 237 } 238 239 // generateTypeFromValuesMap takes a values map and returns an object type that has the same number of fields, but 240 // bound to each type of the underlying evaluated expression. This is the only way the HCL decoder will be happy, as 241 // object type is the only map type that allows different types for each attribute (cty.Map requires all attributes to 242 // have the same type. 243 func generateTypeFromValuesMap(valMap map[string]cty.Value) cty.Type { 244 outType := map[string]cty.Type{} 245 for k, v := range valMap { 246 outType[k] = v.Type() 247 } 248 return cty.Object(outType) 249 }