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  }