diff --git a/terraform/context.go b/terraform/context.go index f60d5b570..5940dd3f4 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -3,7 +3,6 @@ package terraform import ( "fmt" "log" - "os" "sort" "strings" "sync" @@ -129,109 +128,10 @@ func NewContext(opts *ContextOpts) (*Context, error) { variables := make(map[string]interface{}) if opts.Module != nil { - for _, v := range opts.Module.Config().Variables { - if v.Default != nil { - if v.Type() == config.VariableTypeString { - // v.Default has already been parsed as HCL so there may be - // some stray ints in there - switch typedDefault := v.Default.(type) { - case string: - if typedDefault == "" { - continue - } - variables[v.Name] = typedDefault - case int, int64: - variables[v.Name] = fmt.Sprintf("%d", typedDefault) - case float32, float64: - variables[v.Name] = fmt.Sprintf("%f", typedDefault) - case bool: - variables[v.Name] = fmt.Sprintf("%t", typedDefault) - } - } else { - variables[v.Name] = v.Default - } - } - } - - for _, v := range os.Environ() { - if !strings.HasPrefix(v, VarEnvPrefix) { - continue - } - - // Strip off the prefix and get the value after the first "=" - idx := strings.Index(v, "=") - k := v[len(VarEnvPrefix):idx] - v = v[idx+1:] - - // Override the configuration-default values. Note that *not* finding the variable - // in configuration is OK, as we don't want to preclude people from having multiple - // sets of TF_VAR_whatever in their environment even if it is a little weird. - for _, schema := range opts.Module.Config().Variables { - if schema.Name == k { - varType := schema.Type() - varVal, err := parseVariableAsHCL(k, v, varType) - if err != nil { - return nil, err - } - switch varType { - case config.VariableTypeMap: - if existing, hasMap := variables[k]; !hasMap { - variables[k] = varVal - } else { - if existingMap, ok := existing.(map[string]interface{}); !ok { - panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k)) - } else { - switch typedV := varVal.(type) { - case []map[string]interface{}: - for newKey, newVal := range typedV[0] { - existingMap[newKey] = newVal - } - case map[string]interface{}: - for newKey, newVal := range typedV { - existingMap[newKey] = newVal - } - default: - panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k)) - } - } - } - default: - variables[k] = varVal - } - } - } - } - - for k, v := range opts.Variables { - for _, schema := range opts.Module.Config().Variables { - if schema.Name == k { - switch schema.Type() { - case config.VariableTypeMap: - if existing, hasMap := variables[k]; !hasMap { - variables[k] = v - } else { - if existingMap, ok := existing.(map[string]interface{}); !ok { - panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k)) - } else { - switch typedV := v.(type) { - case []map[string]interface{}: - for newKey, newVal := range typedV[0] { - existingMap[newKey] = newVal - } - case map[string]interface{}: - for newKey, newVal := range typedV { - existingMap[newKey] = newVal - } - default: - panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k)) - } - } - } - default: - variables[k] = v - } - } - } + var err error + variables, err = Variables(opts.Module, opts.Variables) + if err != nil { + return nil, err } } diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 6c2a493c5..d01174ed8 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -3,7 +3,6 @@ package terraform import ( "bytes" "fmt" - "os" "reflect" "sort" "strings" @@ -4314,13 +4313,9 @@ func TestContext2Apply_vars(t *testing.T) { func TestContext2Apply_varsEnv(t *testing.T) { // Set the env var - old_ami := tempEnv(t, "TF_VAR_ami", "baz") - old_list := tempEnv(t, "TF_VAR_list", `["Hello", "World"]`) - old_map := tempEnv(t, "TF_VAR_map", `{"Hello" = "World", "Foo" = "Bar", "Baz" = "Foo"}`) - - defer os.Setenv("TF_VAR_ami", old_ami) - defer os.Setenv("TF_VAR_list", old_list) - defer os.Setenv("TF_VAR_list", old_map) + defer tempEnv(t, "TF_VAR_ami", "baz")() + defer tempEnv(t, "TF_VAR_list", `["Hello", "World"]`)() + defer tempEnv(t, "TF_VAR_map", `{"Hello" = "World", "Foo" = "Bar", "Baz" = "Foo"}`)() m := testModule(t, "apply-vars-env") p := testProvider("aws") diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 469d5376d..e6dc575e5 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -47,11 +47,12 @@ func tempDir(t *testing.T) string { } // tempEnv lets you temporarily set an environment variable. It returns +// a function to defer to reset the old value. // the old value that should be set via a defer. -func tempEnv(t *testing.T, k string, v string) string { +func tempEnv(t *testing.T, k string, v string) func() { old := os.Getenv(k) os.Setenv(k, v) - return old + return func() { os.Setenv(k, old) } } func testConfig(t *testing.T, name string) *config.Config { diff --git a/terraform/test-fixtures/vars-basic/main.tf b/terraform/test-fixtures/vars-basic/main.tf new file mode 100644 index 000000000..66fa77a8b --- /dev/null +++ b/terraform/test-fixtures/vars-basic/main.tf @@ -0,0 +1,14 @@ +variable "a" { + default = "foo" + type = "string" +} + +variable "b" { + default = [] + type = "list" +} + +variable "c" { + default = {} + type = "map" +} diff --git a/terraform/variables.go b/terraform/variables.go new file mode 100644 index 000000000..95b607fd5 --- /dev/null +++ b/terraform/variables.go @@ -0,0 +1,145 @@ +package terraform + +import ( + "fmt" + "os" + "strings" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" +) + +// Variables returns the fully loaded set of variables to use with +// ContextOpts and NewContext, loading any additional variables from +// the environment or any other sources. +// +// The given module tree doesn't need to be loaded. +func Variables( + m *module.Tree, + override map[string]interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + // Variables are loaded in the following sequence. Each additional step + // will override conflicting variable keys from prior steps: + // + // * Take default values from config + // * Take values from TF_VAR_x env vars + // * Take values specified in the "override" param which is usually + // from -var, -var-file, etc. + // + + // First load from the config + for _, v := range m.Config().Variables { + // If the var has no default, ignore + if v.Default == nil { + continue + } + + // If the type isn't a string, we use it as-is since it is a rich type + if v.Type() != config.VariableTypeString { + result[v.Name] = v.Default + continue + } + + // v.Default has already been parsed as HCL but it may be an int type + switch typedDefault := v.Default.(type) { + case string: + if typedDefault == "" { + continue + } + result[v.Name] = typedDefault + case int, int64: + result[v.Name] = fmt.Sprintf("%d", typedDefault) + case float32, float64: + result[v.Name] = fmt.Sprintf("%f", typedDefault) + case bool: + result[v.Name] = fmt.Sprintf("%t", typedDefault) + default: + panic(fmt.Sprintf( + "Unknown default var type: %T\n\n"+ + "THIS IS A BUG. Please report it.", + v.Default)) + } + } + + // Load from env vars + for _, v := range os.Environ() { + if !strings.HasPrefix(v, VarEnvPrefix) { + continue + } + + // Strip off the prefix and get the value after the first "=" + idx := strings.Index(v, "=") + k := v[len(VarEnvPrefix):idx] + v = v[idx+1:] + + // Override the configuration-default values. Note that *not* finding the variable + // in configuration is OK, as we don't want to preclude people from having multiple + // sets of TF_VAR_whatever in their environment even if it is a little weird. + for _, schema := range m.Config().Variables { + if schema.Name != k { + continue + } + + varType := schema.Type() + varVal, err := parseVariableAsHCL(k, v, varType) + if err != nil { + return nil, err + } + + switch varType { + case config.VariableTypeMap: + varSetMap(result, k, varVal) + default: + result[k] = varVal + } + } + } + + // Load from overrides + for k, v := range override { + for _, schema := range m.Config().Variables { + if schema.Name != k { + continue + } + + switch schema.Type() { + case config.VariableTypeMap: + varSetMap(result, k, v) + default: + result[k] = v + } + } + } + + return result, nil +} + +// varSetMap sets or merges the map in "v" with the key "k" in the +// "current" set of variables. This is just a private function to remove +// duplicate logic in Variables +func varSetMap(current map[string]interface{}, k string, v interface{}) { + existing, ok := current[k] + if !ok { + current[k] = v + return + } + + existingMap, ok := existing.(map[string]interface{}) + if !ok { + panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k)) + } + + switch typedV := v.(type) { + case []map[string]interface{}: + for newKey, newVal := range typedV[0] { + existingMap[newKey] = newVal + } + case map[string]interface{}: + for newKey, newVal := range typedV { + existingMap[newKey] = newVal + } + default: + panic(fmt.Sprintf("%s is not a map, this is a bug in Terraform.", k)) + } +} diff --git a/terraform/variables_test.go b/terraform/variables_test.go new file mode 100644 index 000000000..f620fc5c2 --- /dev/null +++ b/terraform/variables_test.go @@ -0,0 +1,114 @@ +package terraform + +import ( + "reflect" + "testing" +) + +func TestVariables(t *testing.T) { + cases := map[string]struct { + Module string + Env map[string]string + Override map[string]interface{} + Error bool + Expected map[string]interface{} + }{ + "config only": { + "vars-basic", + nil, + nil, + false, + map[string]interface{}{ + "a": "foo", + "b": []interface{}{}, + "c": map[string]interface{}{}, + }, + }, + + "env vars": { + "vars-basic", + map[string]string{ + "TF_VAR_a": "bar", + "TF_VAR_b": `["foo", "bar"]`, + "TF_VAR_c": `{"foo" = "bar"}`, + }, + nil, + false, + map[string]interface{}{ + "a": "bar", + "b": []interface{}{"foo", "bar"}, + "c": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + + "override": { + "vars-basic", + nil, + map[string]interface{}{ + "a": "bar", + "b": []interface{}{"foo", "bar"}, + "c": map[string]interface{}{ + "foo": "bar", + }, + }, + false, + map[string]interface{}{ + "a": "bar", + "b": []interface{}{"foo", "bar"}, + "c": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + + "override partial map": { + "vars-basic", + map[string]string{ + "TF_VAR_c": `{"foo" = "a", "bar" = "baz"}`, + }, + map[string]interface{}{ + "c": map[string]interface{}{ + "foo": "bar", + }, + }, + false, + map[string]interface{}{ + "a": "foo", + "b": []interface{}{}, + "c": map[string]interface{}{ + "foo": "bar", + "bar": "baz", + }, + }, + }, + } + + for name, tc := range cases { + if name != "override partial map" { + continue + } + + // Wrapped in a func so we can get defers to work + func() { + // Set the env vars + for k, v := range tc.Env { + defer tempEnv(t, k, v)() + } + + m := testModule(t, tc.Module) + actual, err := Variables(m, tc.Override) + if (err != nil) != tc.Error { + t.Fatalf("%s: err: %s", name, err) + } + if err != nil { + return + } + + if !reflect.DeepEqual(actual, tc.Expected) { + t.Fatalf("%s: expected: %#v\n\ngot: %#v", name, tc.Expected, actual) + } + }() + } +}