github.com/mponton/terratest@v0.44.0/modules/terraform/format.go (about)

     1  package terraform
     2  
     3  import (
     4  	"fmt"
     5  	"reflect"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/mponton/terratest/modules/collections"
    10  )
    11  
    12  const runAllCmd = "run-all"
    13  
    14  // TerraformCommandsWithLockSupport is a list of all the Terraform commands that
    15  // can obtain locks on Terraform state
    16  var TerraformCommandsWithLockSupport = []string{
    17  	"plan",
    18  	"plan-all",
    19  	"apply",
    20  	"apply-all",
    21  	"destroy",
    22  	"destroy-all",
    23  	"init",
    24  	"refresh",
    25  	"taint",
    26  	"untaint",
    27  	"import",
    28  }
    29  
    30  // TerraformCommandsWithPlanFileSupport is a list of all the Terraform commands that support interacting with plan
    31  // files.
    32  var TerraformCommandsWithPlanFileSupport = []string{
    33  	"plan",
    34  	"apply",
    35  	"show",
    36  	"graph",
    37  }
    38  
    39  // FormatArgs converts the inputs to a format palatable to terraform. This includes converting the given vars to the
    40  // format the Terraform CLI expects (-var key=value).
    41  func FormatArgs(options *Options, args ...string) []string {
    42  	var terraformArgs []string
    43  	commandType := args[0]
    44  	// If the user is trying to run with run-all, then we need to make sure the command based args are based on the
    45  	// actual terraform command. E.g., we want to base the logic on `plan` when `run-all plan` is passed in, not
    46  	// `run-all`.
    47  	if commandType == runAllCmd {
    48  		commandType = args[1]
    49  	}
    50  	lockSupported := collections.ListContains(TerraformCommandsWithLockSupport, commandType)
    51  	planFileSupported := collections.ListContains(TerraformCommandsWithPlanFileSupport, commandType)
    52  
    53  	// Include -var and -var-file flags unless we're running 'apply' with a plan file
    54  	includeVars := !(commandType == "apply" && len(options.PlanFilePath) > 0)
    55  
    56  	terraformArgs = append(terraformArgs, args...)
    57  
    58  	if includeVars {
    59  		if options.SetVarsAfterVarFiles {
    60  			terraformArgs = append(terraformArgs, FormatTerraformArgs("-var-file", options.VarFiles)...)
    61  			terraformArgs = append(terraformArgs, FormatTerraformVarsAsArgs(options.Vars)...)
    62  		} else {
    63  			terraformArgs = append(terraformArgs, FormatTerraformVarsAsArgs(options.Vars)...)
    64  			terraformArgs = append(terraformArgs, FormatTerraformArgs("-var-file", options.VarFiles)...)
    65  		}
    66  	}
    67  
    68  	terraformArgs = append(terraformArgs, FormatTerraformArgs("-target", options.Targets)...)
    69  
    70  	if options.NoColor {
    71  		terraformArgs = append(terraformArgs, "-no-color")
    72  	}
    73  
    74  	if lockSupported {
    75  		// If command supports locking, handle lock arguments
    76  		terraformArgs = append(terraformArgs, FormatTerraformLockAsArgs(options.Lock, options.LockTimeout)...)
    77  	}
    78  
    79  	if planFileSupported {
    80  		// The plan file arg should be last in the terraformArgs slice. Some commands use it as an input (e.g. show, apply)
    81  		terraformArgs = append(terraformArgs, FormatTerraformPlanFileAsArg(commandType, options.PlanFilePath)...)
    82  	}
    83  
    84  	return terraformArgs
    85  }
    86  
    87  // FormatTerraformPlanFileAsArg formats the out variable as a command-line arg for Terraform (e.g. of the format
    88  // -out=/some/path/to/plan.out or /some/path/to/plan.out). Only plan supports passing in the plan file as -out; the
    89  // other commands expect it as the first positional argument. This returns an empty string if outPath is empty string.
    90  func FormatTerraformPlanFileAsArg(commandType string, outPath string) []string {
    91  	if outPath == "" {
    92  		return nil
    93  	}
    94  	if commandType == "plan" {
    95  		return []string{fmt.Sprintf("%s=%s", "-out", outPath)}
    96  	}
    97  	return []string{outPath}
    98  }
    99  
   100  // FormatTerraformVarsAsArgs formats the given variables as command-line args for Terraform (e.g. of the format
   101  // -var key=value).
   102  func FormatTerraformVarsAsArgs(vars map[string]interface{}) []string {
   103  	return formatTerraformArgs(vars, "-var", true)
   104  }
   105  
   106  // FormatTerraformLockAsArgs formats the lock and lock-timeout variables
   107  // -lock, -lock-timeout
   108  func FormatTerraformLockAsArgs(lockCheck bool, lockTimeout string) []string {
   109  	lockArgs := []string{fmt.Sprintf("-lock=%v", lockCheck)}
   110  	if lockTimeout != "" {
   111  		lockTimeoutValue := fmt.Sprintf("%s=%s", "-lock-timeout", lockTimeout)
   112  		lockArgs = append(lockArgs, lockTimeoutValue)
   113  	}
   114  	return lockArgs
   115  }
   116  
   117  // FormatTerraformPluginDirAsArgs formats the plugin-dir variable
   118  // -plugin-dir
   119  func FormatTerraformPluginDirAsArgs(pluginDir string) []string {
   120  	pluginArgs := []string{fmt.Sprintf("-plugin-dir=%v", pluginDir)}
   121  	if pluginDir == "" {
   122  		return nil
   123  	}
   124  	return pluginArgs
   125  }
   126  
   127  // FormatTerraformArgs will format multiple args with the arg name (e.g. "-var-file", []string{"foo.tfvars", "bar.tfvars", "baz.tfvars.json"})
   128  // returns "-var-file foo.tfvars -var-file bar.tfvars -var-file baz.tfvars.json"
   129  func FormatTerraformArgs(argName string, args []string) []string {
   130  	argsList := []string{}
   131  	for _, argValue := range args {
   132  		argsList = append(argsList, argName, argValue)
   133  	}
   134  	return argsList
   135  }
   136  
   137  // FormatTerraformBackendConfigAsArgs formats the given variables as backend config args for Terraform (e.g. of the
   138  // format -backend-config=key=value).
   139  func FormatTerraformBackendConfigAsArgs(vars map[string]interface{}) []string {
   140  	return formatTerraformArgs(vars, "-backend-config", false)
   141  }
   142  
   143  // Format the given vars into 'Terraform' format, with each var being prefixed with the given prefix. If
   144  // useSpaceAsSeparator is true, a space will separate the prefix and each var (e.g., -var foo=bar). If
   145  // useSpaceAsSeparator is false, an equals will separate the prefix and each var (e.g., -backend-config=foo=bar).
   146  func formatTerraformArgs(vars map[string]interface{}, prefix string, useSpaceAsSeparator bool) []string {
   147  	var args []string
   148  
   149  	for key, value := range vars {
   150  		hclString := toHclString(value, false)
   151  		argValue := fmt.Sprintf("%s=%s", key, hclString)
   152  		if useSpaceAsSeparator {
   153  			args = append(args, prefix, argValue)
   154  		} else {
   155  			args = append(args, fmt.Sprintf("%s=%s", prefix, argValue))
   156  		}
   157  	}
   158  
   159  	return args
   160  }
   161  
   162  // Terraform allows you to pass in command-line variables using HCL syntax (e.g. -var foo=[1,2,3]). Unfortunately,
   163  // while their golang hcl library can convert an HCL string to a Go type, they don't seem to offer a library to convert
   164  // arbitrary Go types to an HCL string. Therefore, this method is a simple implementation that correctly handles
   165  // ints, booleans, lists, and maps. Everything else is forced into a string using Sprintf. Hopefully, this approach is
   166  // good enough for the type of variables we deal with in Terratest.
   167  func toHclString(value interface{}, isNested bool) string {
   168  	// Ideally, we'd use a type switch here to identify slices and maps, but we can't do that, because Go doesn't
   169  	// support generics, and the type switch only matches concrete types. So we could match []interface{}, but if
   170  	// a user passes in []string{}, that would NOT match (the same logic applies to maps). Therefore, we have to
   171  	// use reflection and manually convert into []interface{} and map[string]interface{}.
   172  
   173  	if slice, isSlice := tryToConvertToGenericSlice(value); isSlice {
   174  		return sliceToHclString(slice)
   175  	} else if m, isMap := tryToConvertToGenericMap(value); isMap {
   176  		return mapToHclString(m)
   177  	} else {
   178  		return primitiveToHclString(value, isNested)
   179  	}
   180  }
   181  
   182  // Try to convert the given value to a generic slice. Return the slice and true if the underlying value itself was a
   183  // slice and an empty slice and false if it wasn't. This is necessary because Go is a shitty language that doesn't
   184  // have generics, nor useful utility methods built-in. For more info, see: http://stackoverflow.com/a/12754757/483528
   185  func tryToConvertToGenericSlice(value interface{}) ([]interface{}, bool) {
   186  	reflectValue := reflect.ValueOf(value)
   187  	if reflectValue.Kind() != reflect.Slice {
   188  		return []interface{}{}, false
   189  	}
   190  
   191  	genericSlice := make([]interface{}, reflectValue.Len())
   192  
   193  	for i := 0; i < reflectValue.Len(); i++ {
   194  		genericSlice[i] = reflectValue.Index(i).Interface()
   195  	}
   196  
   197  	return genericSlice, true
   198  }
   199  
   200  // Try to convert the given value to a generic map. Return the map and true if the underlying value itself was a
   201  // map and an empty map and false if it wasn't. This is necessary because Go is a shitty language that doesn't
   202  // have generics, nor useful utility methods built-in. For more info, see: http://stackoverflow.com/a/12754757/483528
   203  func tryToConvertToGenericMap(value interface{}) (map[string]interface{}, bool) {
   204  	reflectValue := reflect.ValueOf(value)
   205  	if reflectValue.Kind() != reflect.Map {
   206  		return map[string]interface{}{}, false
   207  	}
   208  
   209  	reflectType := reflect.TypeOf(value)
   210  	if reflectType.Key().Kind() != reflect.String {
   211  		return map[string]interface{}{}, false
   212  	}
   213  
   214  	genericMap := make(map[string]interface{}, reflectValue.Len())
   215  
   216  	mapKeys := reflectValue.MapKeys()
   217  	for _, key := range mapKeys {
   218  		genericMap[key.String()] = reflectValue.MapIndex(key).Interface()
   219  	}
   220  
   221  	return genericMap, true
   222  }
   223  
   224  // Convert a slice to an HCL string. See ToHclString for details.
   225  func sliceToHclString(slice []interface{}) string {
   226  	hclValues := []string{}
   227  
   228  	for _, value := range slice {
   229  		hclValue := toHclString(value, true)
   230  		hclValues = append(hclValues, hclValue)
   231  	}
   232  
   233  	return fmt.Sprintf("[%s]", strings.Join(hclValues, ", "))
   234  }
   235  
   236  // Convert a map to an HCL string. See ToHclString for details.
   237  func mapToHclString(m map[string]interface{}) string {
   238  	keyValuePairs := []string{}
   239  
   240  	for key, value := range m {
   241  		keyValuePair := fmt.Sprintf(`"%s" = %s`, key, toHclString(value, true))
   242  		keyValuePairs = append(keyValuePairs, keyValuePair)
   243  	}
   244  
   245  	return fmt.Sprintf("{%s}", strings.Join(keyValuePairs, ", "))
   246  }
   247  
   248  // Convert a primitive, such as a bool, int, or string, to an HCL string. If this isn't a primitive, force its value
   249  // using Sprintf. See ToHclString for details.
   250  func primitiveToHclString(value interface{}, isNested bool) string {
   251  	if value == nil {
   252  		return "null"
   253  	}
   254  
   255  	switch v := value.(type) {
   256  
   257  	case bool:
   258  		return strconv.FormatBool(v)
   259  
   260  	case string:
   261  		// If string is nested in a larger data structure (e.g. list of string, map of string), ensure value is quoted
   262  		if isNested {
   263  			return fmt.Sprintf("\"%v\"", v)
   264  		}
   265  
   266  		return fmt.Sprintf("%v", v)
   267  
   268  	default:
   269  		return fmt.Sprintf("%v", v)
   270  	}
   271  }