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