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  }