diff --git a/config/append.go b/config/append.go new file mode 100644 index 000000000..dd20e8ac0 --- /dev/null +++ b/config/append.go @@ -0,0 +1,58 @@ +package config + +// Append appends one configuration to another. +// +// Append assumes that both configurations will not have +// conflicting variables, resources, etc. If they do, the +// problems will be caught in the validation phase. +// +// It is possible that c1, c2 on their own are not valid. For +// example, a resource in c2 may reference a variable in c1. But +// together, they would be valid. +func Append(c1, c2 *Config) (*Config, error) { + c := new(Config) + + // Append unknown keys, but keep them unique since it is a set + unknowns := make(map[string]struct{}) + for _, k := range c1.unknownKeys { + unknowns[k] = struct{}{} + } + for _, k := range c2.unknownKeys { + unknowns[k] = struct{}{} + } + for k, _ := range unknowns { + c.unknownKeys = append(c.unknownKeys, k) + } + + if len(c1.Outputs) > 0 || len(c2.Outputs) > 0 { + c.Outputs = make( + []*Output, 0, len(c1.Outputs)+len(c2.Outputs)) + c.Outputs = append(c.Outputs, c1.Outputs...) + c.Outputs = append(c.Outputs, c2.Outputs...) + } + + if len(c1.ProviderConfigs) > 0 || len(c2.ProviderConfigs) > 0 { + c.ProviderConfigs = make( + []*ProviderConfig, + 0, len(c1.ProviderConfigs)+len(c2.ProviderConfigs)) + c.ProviderConfigs = append(c.ProviderConfigs, c1.ProviderConfigs...) + c.ProviderConfigs = append(c.ProviderConfigs, c2.ProviderConfigs...) + } + + if len(c1.Resources) > 0 || len(c2.Resources) > 0 { + c.Resources = make( + []*Resource, + 0, len(c1.Resources)+len(c2.Resources)) + c.Resources = append(c.Resources, c1.Resources...) + c.Resources = append(c.Resources, c2.Resources...) + } + + if len(c1.Variables) > 0 || len(c2.Variables) > 0 { + c.Variables = make( + []*Variable, 0, len(c1.Variables)+len(c2.Variables)) + c.Variables = append(c.Variables, c1.Variables...) + c.Variables = append(c.Variables, c2.Variables...) + } + + return c, nil +} diff --git a/config/append_test.go b/config/append_test.go new file mode 100644 index 000000000..4a7fb9d1e --- /dev/null +++ b/config/append_test.go @@ -0,0 +1,83 @@ +package config + +import ( + "reflect" + "testing" +) + +func TestAppend(t *testing.T) { + cases := []struct { + c1, c2, result *Config + err bool + }{ + { + &Config{ + Outputs: []*Output{ + &Output{Name: "foo"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "foo"}, + }, + Resources: []*Resource{ + &Resource{Name: "foo"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo"}, + }, + + unknownKeys: []string{"foo"}, + }, + + &Config{ + Outputs: []*Output{ + &Output{Name: "bar"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "bar"}, + }, + Resources: []*Resource{ + &Resource{Name: "bar"}, + }, + Variables: []*Variable{ + &Variable{Name: "bar"}, + }, + + unknownKeys: []string{"bar"}, + }, + + &Config{ + Outputs: []*Output{ + &Output{Name: "foo"}, + &Output{Name: "bar"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "foo"}, + &ProviderConfig{Name: "bar"}, + }, + Resources: []*Resource{ + &Resource{Name: "foo"}, + &Resource{Name: "bar"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo"}, + &Variable{Name: "bar"}, + }, + + unknownKeys: []string{"foo", "bar"}, + }, + + false, + }, + } + + for i, tc := range cases { + actual, err := Append(tc.c1, tc.c2) + if (err != nil) != tc.err { + t.Fatalf("%d: error fail", i) + } + + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf("%d: bad:\n\n%#v", i, actual) + } + } +} diff --git a/config/config.go b/config/config.go index bcde29439..bb7ab76bf 100644 --- a/config/config.go +++ b/config/config.go @@ -13,10 +13,10 @@ import ( // Config is the configuration that comes from loading a collection // of Terraform templates. type Config struct { - ProviderConfigs map[string]*ProviderConfig + ProviderConfigs []*ProviderConfig Resources []*Resource - Variables map[string]*Variable - Outputs map[string]*Output + Variables []*Variable + Outputs []*Output // The fields below can be filled in by loaders for validation // purposes. @@ -28,6 +28,7 @@ type Config struct { // For example, Terraform needs to set the AWS access keys for the AWS // resource provider. type ProviderConfig struct { + Name string RawConfig *RawConfig } @@ -51,6 +52,7 @@ type Provisioner struct { // Variable is a variable defined within the configuration. type Variable struct { + Name string Default string Description string defaultSet bool @@ -98,9 +100,10 @@ type UserVariable struct { // ProviderConfigName returns the name of the provider configuration in // the given mapping that maps to the proper provider configuration // for this resource. -func ProviderConfigName(t string, pcs map[string]*ProviderConfig) string { +func ProviderConfigName(t string, pcs []*ProviderConfig) string { lk := "" - for k, _ := range pcs { + for _, v := range pcs { + k := v.Name if strings.HasPrefix(t, k) && len(k) > len(lk) { lk = k } @@ -124,6 +127,10 @@ func (c *Config) Validate() error { } vars := c.allVariables() + varMap := make(map[string]*Variable) + for _, v := range c.Variables { + varMap[v.Name] = v + } // Check for references to user variables that do not actually // exist and record those errors. @@ -134,7 +141,7 @@ func (c *Config) Validate() error { continue } - if _, ok := c.Variables[uv.Name]; !ok { + if _, ok := varMap[uv.Name]; !ok { errs = append(errs, fmt.Errorf( "%s: unknown variable referenced: %s", source, @@ -244,6 +251,84 @@ func (c *Config) allVariables() map[string][]InterpolatedVariable { return result } +func (o *Output) mergerName() string { + return o.Name +} + +func (o *Output) mergerMerge(m merger) merger { + o2 := m.(*Output) + + result := *o + result.Name = o2.Name + result.RawConfig = result.RawConfig.merge(o2.RawConfig) + + return &result +} + +func (c *ProviderConfig) mergerName() string { + return c.Name +} + +func (c *ProviderConfig) mergerMerge(m merger) merger { + c2 := m.(*ProviderConfig) + + result := *c + result.Name = c2.Name + result.RawConfig = result.RawConfig.merge(c2.RawConfig) + + return &result +} + +func (r *Resource) mergerName() string { + return fmt.Sprintf("%s.%s", r.Type, r.Name) +} + +func (r *Resource) mergerMerge(m merger) merger { + r2 := m.(*Resource) + + result := *r + result.Name = r2.Name + result.Type = r2.Type + result.RawConfig = result.RawConfig.merge(r2.RawConfig) + + if r2.Count > 0 { + result.Count = r2.Count + } + + if len(r2.Provisioners) > 0 { + result.Provisioners = r2.Provisioners + } + + return &result +} + +// Merge merges two variables to create a new third variable. +func (v *Variable) Merge(v2 *Variable) *Variable { + // Shallow copy the variable + result := *v + + // The names should be the same, but the second name always wins. + result.Name = v2.Name + + if v2.defaultSet { + result.Default = v2.Default + result.defaultSet = true + } + if v2.Description != "" { + result.Description = v2.Description + } + + return &result +} + +func (v *Variable) mergerName() string { + return v.Name +} + +func (v *Variable) mergerMerge(m merger) merger { + return v.Merge(m.(*Variable)) +} + // Required tests whether a variable is required or not. func (v *Variable) Required() bool { return !v.defaultSet diff --git a/config/config_test.go b/config/config_test.go index 1a5ecb8d9..4c66466b1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -151,11 +151,11 @@ func TestNewUserVariable(t *testing.T) { } func TestProviderConfigName(t *testing.T) { - pcs := map[string]*ProviderConfig{ - "aw": new(ProviderConfig), - "aws": new(ProviderConfig), - "a": new(ProviderConfig), - "gce_": new(ProviderConfig), + pcs := []*ProviderConfig{ + &ProviderConfig{Name: "aw"}, + &ProviderConfig{Name: "aws"}, + &ProviderConfig{Name: "a"}, + &ProviderConfig{Name: "gce_"}, } n := ProviderConfigName("aws_instance", pcs) diff --git a/config/import_tree.go b/config/import_tree.go index f9b28c62f..ba7292d2f 100644 --- a/config/import_tree.go +++ b/config/import_tree.go @@ -3,7 +3,6 @@ package config import ( "fmt" "io" - "strings" ) // configurable is an interface that must be implemented by any configuration @@ -33,11 +32,15 @@ type fileLoaderFunc func(path string) (configurable, []string, error) // executes the proper fileLoaderFunc. func loadTree(root string) (*importTree, error) { var f fileLoaderFunc - if strings.HasSuffix(root, ".tf") { + switch ext(root) { + case ".tf": + fallthrough + case ".tf.json": f = loadFileLibucl - } else if strings.HasSuffix(root, ".tf.json") { - f = loadFileLibucl - } else { + default: + } + + if f == nil { return nil, fmt.Errorf( "%s: unknown configuration format. Use '.tf' or '.tf.json' extension", root) diff --git a/config/loader.go b/config/loader.go index 5f0e8bd7e..bfbb3e210 100644 --- a/config/loader.go +++ b/config/loader.go @@ -2,7 +2,11 @@ package config import ( "fmt" + "io" + "os" "path/filepath" + "sort" + "strings" ) // Load loads the Terraform configuration from a given file. @@ -29,28 +33,82 @@ func Load(path string) (*Config, error) { } // LoadDir loads all the Terraform configuration files in a single -// directory and merges them together. -func LoadDir(path string) (*Config, error) { - matches, err := filepath.Glob(filepath.Join(path, "*.tf")) +// directory and appends them together. +// +// Special files known as "override files" can also be present, which +// are merged into the loaded configuration. That is, the non-override +// files are loaded first to create the configuration. Then, the overrides +// are merged into the configuration to create the final configuration. +// +// Files are loaded in lexical order. +func LoadDir(root string) (*Config, error) { + var files, overrides []string + + f, err := os.Open(root) if err != nil { return nil, err } - if len(matches) == 0 { + err = nil + for err != io.EOF { + var fis []os.FileInfo + fis, err = f.Readdir(128) + if err != nil && err != io.EOF { + f.Close() + return nil, err + } + + for _, fi := range fis { + // Ignore directories + if fi.IsDir() { + continue + } + + // Only care about files that are valid to load + name := fi.Name() + extValue := ext(name) + if extValue == "" { + continue + } + + // Determine if we're dealing with an override + nameNoExt := name[:len(name)-len(extValue)] + override := nameNoExt == "override" || + strings.HasSuffix(nameNoExt, "_override") + + path := filepath.Join(root, name) + if override { + overrides = append(overrides, path) + } else { + files = append(files, path) + } + } + } + + // Close the directory, we're done with it + f.Close() + + if len(files) == 0 { return nil, fmt.Errorf( "No Terraform configuration files found in directory: %s", - path) + root) } var result *Config - for _, f := range matches { + + // Sort the files and overrides so we have a deterministic order + sort.Strings(files) + sort.Strings(overrides) + + // Load all the regular files, append them to each other. + for _, f := range files { c, err := Load(f) if err != nil { return nil, err } if result != nil { - result, err = Merge(result, c) + result, err = Append(result, c) if err != nil { return nil, err } @@ -59,5 +117,30 @@ func LoadDir(path string) (*Config, error) { } } + // Load all the overrides, and merge them into the config + for _, f := range overrides { + c, err := Load(f) + if err != nil { + return nil, err + } + + result, err = Merge(result, c) + if err != nil { + return nil, err + } + } + return result, nil } + +// Ext returns the Terraform configuration extension of the given +// path, or a blank string if it is an invalid function. +func ext(path string) string { + if strings.HasSuffix(path, ".tf") { + return ".tf" + } else if strings.HasSuffix(path, ".tf.json") { + return ".tf.json" + } else { + return "" + } +} diff --git a/config/loader_libucl.go b/config/loader_libucl.go index 246a1cf67..c6050d922 100644 --- a/config/loader_libucl.go +++ b/config/loader_libucl.go @@ -45,21 +45,26 @@ func (t *libuclConfigurable) Config() (*Config, error) { // Start building up the actual configuration. We start with // variables. + // TODO(mitchellh): Make function like loadVariablesLibucl so that + // duplicates aren't overriden config := new(Config) - config.Variables = make(map[string]*Variable) - for k, v := range rawConfig.Variable { - defaultSet := false - for _, f := range v.Fields { - if f == "Default" { - defaultSet = true - break + if len(rawConfig.Variable) > 0 { + config.Variables = make([]*Variable, 0, len(rawConfig.Variable)) + for k, v := range rawConfig.Variable { + defaultSet := false + for _, f := range v.Fields { + if f == "Default" { + defaultSet = true + break + } } - } - config.Variables[k] = &Variable{ - Default: v.Default, - Description: v.Description, - defaultSet: defaultSet, + config.Variables = append(config.Variables, &Variable{ + Name: k, + Default: v.Default, + Description: v.Description, + defaultSet: defaultSet, + }) } } @@ -178,7 +183,7 @@ func loadFileLibucl(root string) (configurable, []string, error) { // LoadOutputsLibucl recurses into the given libucl object and turns // it into a mapping of outputs. -func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) { +func loadOutputsLibucl(o *libucl.Object) ([]*Output, error) { objects := make(map[string]*libucl.Object) // Iterate over all the "output" blocks and get the keys along with @@ -196,8 +201,13 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) { } iter.Close() + // If we have none, just return nil + if len(objects) == 0 { + return nil, nil + } + // Go through each object and turn it into an actual result. - result := make(map[string]*Output) + result := make([]*Output, 0, len(objects)) for n, o := range objects { var config map[string]interface{} @@ -213,10 +223,10 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) { err) } - result[n] = &Output{ + result = append(result, &Output{ Name: n, RawConfig: rawConfig, - } + }) } return result, nil @@ -224,7 +234,7 @@ func loadOutputsLibucl(o *libucl.Object) (map[string]*Output, error) { // LoadProvidersLibucl recurses into the given libucl object and turns // it into a mapping of provider configs. -func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) { +func loadProvidersLibucl(o *libucl.Object) ([]*ProviderConfig, error) { objects := make(map[string]*libucl.Object) // Iterate over all the "provider" blocks and get the keys along with @@ -242,8 +252,12 @@ func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) { } iter.Close() + if len(objects) == 0 { + return nil, nil + } + // Go through each object and turn it into an actual result. - result := make(map[string]*ProviderConfig) + result := make([]*ProviderConfig, 0, len(objects)) for n, o := range objects { var config map[string]interface{} @@ -259,9 +273,10 @@ func loadProvidersLibucl(o *libucl.Object) (map[string]*ProviderConfig, error) { err) } - result[n] = &ProviderConfig{ + result = append(result, &ProviderConfig{ + Name: n, RawConfig: rawConfig, - } + }) } return result, nil diff --git a/config/loader_test.go b/config/loader_test.go index d51da23ee..658e1c248 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -116,16 +116,6 @@ func TestLoad_variables(t *testing.T) { if actual != strings.TrimSpace(variablesVariablesStr) { t.Fatalf("bad:\n%s", actual) } - - if !c.Variables["foo"].Required() { - t.Fatal("foo should be required") - } - if c.Variables["bar"].Required() { - t.Fatal("bar should not be required") - } - if c.Variables["baz"].Required() { - t.Fatal("baz should not be required") - } } func TestLoadDir_basic(t *testing.T) { @@ -166,16 +156,64 @@ func TestLoadDir_noConfigs(t *testing.T) { } } -func outputsStr(os map[string]*Output) string { +func TestLoadDir_noMerge(t *testing.T) { + c, err := LoadDir(filepath.Join(fixtureDir, "dir-merge")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + +func TestLoadDir_override(t *testing.T) { + c, err := LoadDir(filepath.Join(fixtureDir, "dir-override")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + actual := variablesStr(c.Variables) + if actual != strings.TrimSpace(dirOverrideVariablesStr) { + t.Fatalf("bad:\n%s", actual) + } + + actual = providerConfigsStr(c.ProviderConfigs) + if actual != strings.TrimSpace(dirOverrideProvidersStr) { + t.Fatalf("bad:\n%s", actual) + } + + actual = resourcesStr(c.Resources) + if actual != strings.TrimSpace(dirOverrideResourcesStr) { + t.Fatalf("bad:\n%s", actual) + } + + actual = outputsStr(c.Outputs) + if actual != strings.TrimSpace(dirOverrideOutputsStr) { + t.Fatalf("bad:\n%s", actual) + } +} + +func outputsStr(os []*Output) string { ns := make([]string, 0, len(os)) - for n, _ := range os { - ns = append(ns, n) + m := make(map[string]*Output) + for _, o := range os { + ns = append(ns, o.Name) + m[o.Name] = o } sort.Strings(ns) result := "" for _, n := range ns { - o := os[n] + o := m[n] result += fmt.Sprintf("%s\n", n) @@ -256,17 +294,19 @@ func TestLoad_connections(t *testing.T) { // This helper turns a provider configs field into a deterministic // string value for comparison in tests. -func providerConfigsStr(pcs map[string]*ProviderConfig) string { +func providerConfigsStr(pcs []*ProviderConfig) string { result := "" ns := make([]string, 0, len(pcs)) - for n, _ := range pcs { - ns = append(ns, n) + m := make(map[string]*ProviderConfig) + for _, n := range pcs { + ns = append(ns, n.Name) + m[n.Name] = n } sort.Strings(ns) for _, n := range ns { - pc := pcs[n] + pc := m[n] result += fmt.Sprintf("%s\n", n) @@ -384,16 +424,18 @@ func resourcesStr(rs []*Resource) string { // This helper turns a variables field into a deterministic // string value for comparison in tests. -func variablesStr(vs map[string]*Variable) string { +func variablesStr(vs []*Variable) string { result := "" ks := make([]string, 0, len(vs)) - for k, _ := range vs { - ks = append(ks, k) + m := make(map[string]*Variable) + for _, v := range vs { + ks = append(ks, v.Name) + m[v.Name] = v } sort.Strings(ks) for _, k := range ks { - v := vs[k] + v := m[k] if v.Default == "" { v.Default = "<>" @@ -402,9 +444,15 @@ func variablesStr(vs map[string]*Variable) string { v.Description = "<>" } + required := "" + if v.Required() { + required = " (required)" + } + result += fmt.Sprintf( - "%s\n %s\n %s\n", + "%s%s\n %s\n %s\n", k, + required, v.Default, v.Description) } @@ -486,8 +534,46 @@ foo bar ` +const dirOverrideOutputsStr = ` +web_ip + vars + resource: aws_instance.web.private_ip +` + +const dirOverrideProvidersStr = ` +aws + access_key + secret_key +do + api_key + vars + user: var.foo +` + +const dirOverrideResourcesStr = ` +aws_instance[db] (x1) + ami + security_groups +aws_instance[web] (x1) + ami + foo + network_interface + security_groups + vars + resource: aws_security_group.firewall.foo + user: var.foo +aws_security_group[firewall] (x5) +` + +const dirOverrideVariablesStr = ` +foo + bar + bar +` + const importProvidersStr = ` aws + bar foo ` @@ -497,7 +583,7 @@ aws_security_group[web] (x1) ` const importVariablesStr = ` -bar +bar (required) <> <> foo @@ -538,7 +624,7 @@ bar baz foo <> -foo +foo (required) <> <> ` diff --git a/config/merge.go b/config/merge.go index be5bb39af..e3f604635 100644 --- a/config/merge.go +++ b/config/merge.go @@ -1,9 +1,5 @@ package config -import ( - "fmt" -) - // Merge merges two configurations into a single configuration. // // Merge allows for the two configurations to have duplicate resources, @@ -24,59 +20,135 @@ func Merge(c1, c2 *Config) (*Config, error) { c.unknownKeys = append(c.unknownKeys, k) } - // Merge variables: Variable merging is quite simple. Set fields in - // later set variables override those earlier. - c.Variables = c1.Variables - for k, v2 := range c2.Variables { - v1, ok := c.Variables[k] - if ok { - if v2.Default == "" { - v2.Default = v1.Default - } - if v2.Description == "" { - v2.Description = v1.Description - } + // NOTE: Everything below is pretty gross. Due to the lack of generics + // in Go, there is some hoop-jumping involved to make this merging a + // little more test-friendly and less repetitive. Ironically, making it + // less repetitive involves being a little repetitive, but I prefer to + // be repetitive with things that are less error prone than things that + // are more error prone (more logic). Type conversions to an interface + // are pretty low-error. + + var m1, m2, mresult []merger + + // Outputs + m1 = make([]merger, 0, len(c1.Outputs)) + m2 = make([]merger, 0, len(c2.Outputs)) + for _, v := range c1.Outputs { + m1 = append(m1, v) + } + for _, v := range c2.Outputs { + m2 = append(m2, v) + } + mresult = mergeSlice(m1, m2) + if len(mresult) > 0 { + c.Outputs = make([]*Output, len(mresult)) + for i, v := range mresult { + c.Outputs[i] = v.(*Output) } - - c.Variables[k] = v2 } - // Merge outputs: If they collide, just take the latest one for now. In - // the future, we might provide smarter merge functionality. - c.Outputs = make(map[string]*Output) - for k, v := range c1.Outputs { - c.Outputs[k] = v + // Provider Configs + m1 = make([]merger, 0, len(c1.ProviderConfigs)) + m2 = make([]merger, 0, len(c2.ProviderConfigs)) + for _, v := range c1.ProviderConfigs { + m1 = append(m1, v) } - for k, v := range c2.Outputs { - c.Outputs[k] = v + for _, v := range c2.ProviderConfigs { + m2 = append(m2, v) + } + mresult = mergeSlice(m1, m2) + if len(mresult) > 0 { + c.ProviderConfigs = make([]*ProviderConfig, len(mresult)) + for i, v := range mresult { + c.ProviderConfigs[i] = v.(*ProviderConfig) + } } - // Merge provider configs: If they collide, we just take the latest one - // for now. In the future, we might provide smarter merge functionality. - c.ProviderConfigs = make(map[string]*ProviderConfig) - for k, v := range c1.ProviderConfigs { - c.ProviderConfigs[k] = v + // Resources + m1 = make([]merger, 0, len(c1.Resources)) + m2 = make([]merger, 0, len(c2.Resources)) + for _, v := range c1.Resources { + m1 = append(m1, v) } - for k, v := range c2.ProviderConfigs { - c.ProviderConfigs[k] = v + for _, v := range c2.Resources { + m2 = append(m2, v) + } + mresult = mergeSlice(m1, m2) + if len(mresult) > 0 { + c.Resources = make([]*Resource, len(mresult)) + for i, v := range mresult { + c.Resources[i] = v.(*Resource) + } } - // Merge resources: If they collide, we just take the latest one - // for now. In the future, we might provide smarter merge functionality. - resources := make(map[string]*Resource) - for _, r := range c1.Resources { - id := fmt.Sprintf("%s[%s]", r.Type, r.Name) - resources[id] = r + // Variables + m1 = make([]merger, 0, len(c1.Variables)) + m2 = make([]merger, 0, len(c2.Variables)) + for _, v := range c1.Variables { + m1 = append(m1, v) } - for _, r := range c2.Resources { - id := fmt.Sprintf("%s[%s]", r.Type, r.Name) - resources[id] = r + for _, v := range c2.Variables { + m2 = append(m2, v) } - - c.Resources = make([]*Resource, 0, len(resources)) - for _, r := range resources { - c.Resources = append(c.Resources, r) + mresult = mergeSlice(m1, m2) + if len(mresult) > 0 { + c.Variables = make([]*Variable, len(mresult)) + for i, v := range mresult { + c.Variables[i] = v.(*Variable) + } } return c, nil } + +// merger is an interface that must be implemented by types that are +// merge-able. This simplifies the implementation of Merge for the various +// components of a Config. +type merger interface { + mergerName() string + mergerMerge(merger) merger +} + +// mergeSlice merges a slice of mergers. +func mergeSlice(m1, m2 []merger) []merger { + r := make([]merger, len(m1), len(m1)+len(m2)) + copy(r, m1) + + m := map[string]struct{}{} + for _, v2 := range m2 { + // If we already saw it, just append it because its a + // duplicate and invalid... + name := v2.mergerName() + if _, ok := m[name]; ok { + r = append(r, v2) + continue + } + m[name] = struct{}{} + + // Find an original to override + var original merger + originalIndex := -1 + for i, v := range m1 { + if v.mergerName() == name { + originalIndex = i + original = v + break + } + } + + var v merger + if original == nil { + v = v2 + } else { + v = original.mergerMerge(v2) + } + + if originalIndex == -1 { + r = append(r, v) + } else { + r[originalIndex] = v + } + } + + return r +} diff --git a/config/merge_test.go b/config/merge_test.go new file mode 100644 index 000000000..fb52677b5 --- /dev/null +++ b/config/merge_test.go @@ -0,0 +1,149 @@ +package config + +import ( + "reflect" + "testing" +) + +func TestMerge(t *testing.T) { + cases := []struct { + c1, c2, result *Config + err bool + }{ + // Normal good case. + { + &Config{ + Outputs: []*Output{ + &Output{Name: "foo"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "foo"}, + }, + Resources: []*Resource{ + &Resource{Name: "foo"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo"}, + }, + + unknownKeys: []string{"foo"}, + }, + + &Config{ + Outputs: []*Output{ + &Output{Name: "bar"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "bar"}, + }, + Resources: []*Resource{ + &Resource{Name: "bar"}, + }, + Variables: []*Variable{ + &Variable{Name: "bar"}, + }, + + unknownKeys: []string{"bar"}, + }, + + &Config{ + Outputs: []*Output{ + &Output{Name: "foo"}, + &Output{Name: "bar"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "foo"}, + &ProviderConfig{Name: "bar"}, + }, + Resources: []*Resource{ + &Resource{Name: "foo"}, + &Resource{Name: "bar"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo"}, + &Variable{Name: "bar"}, + }, + + unknownKeys: []string{"foo", "bar"}, + }, + + false, + }, + + // Test that when merging duplicates, it merges into the + // first, but keeps the duplicates so that errors still + // happen. + { + &Config{ + Outputs: []*Output{ + &Output{Name: "foo"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "foo"}, + }, + Resources: []*Resource{ + &Resource{Name: "foo"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo", Default: "foo"}, + &Variable{Name: "foo"}, + }, + + unknownKeys: []string{"foo"}, + }, + + &Config{ + Outputs: []*Output{ + &Output{Name: "bar"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "bar"}, + }, + Resources: []*Resource{ + &Resource{Name: "bar"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo", Default: "bar", defaultSet: true}, + &Variable{Name: "bar"}, + }, + + unknownKeys: []string{"bar"}, + }, + + &Config{ + Outputs: []*Output{ + &Output{Name: "foo"}, + &Output{Name: "bar"}, + }, + ProviderConfigs: []*ProviderConfig{ + &ProviderConfig{Name: "foo"}, + &ProviderConfig{Name: "bar"}, + }, + Resources: []*Resource{ + &Resource{Name: "foo"}, + &Resource{Name: "bar"}, + }, + Variables: []*Variable{ + &Variable{Name: "foo", Default: "bar", defaultSet: true}, + &Variable{Name: "foo"}, + &Variable{Name: "bar"}, + }, + + unknownKeys: []string{"foo", "bar"}, + }, + + false, + }, + } + + for i, tc := range cases { + actual, err := Merge(tc.c1, tc.c2) + if (err != nil) != tc.err { + t.Fatalf("%d: error fail", i) + } + + if !reflect.DeepEqual(actual, tc.result) { + t.Fatalf("%d: bad:\n\n%#v", i, actual) + } + } +} diff --git a/config/raw_config.go b/config/raw_config.go index 12a43c54b..f16921fd1 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -92,6 +92,25 @@ func (r *RawConfig) init() error { return nil } +func (r *RawConfig) merge(r2 *RawConfig) *RawConfig { + rawRaw, err := copystructure.Copy(r.Raw) + if err != nil { + panic(err) + } + + raw := rawRaw.(map[string]interface{}) + for k, v := range r2.Raw { + raw[k] = v + } + + result, err := NewRawConfig(raw) + if err != nil { + panic(err) + } + + return result +} + // UnknownKeys returns the keys of the configuration that are unknown // because they had interpolated variables that must be computed. func (r *RawConfig) UnknownKeys() []string { diff --git a/config/test-fixtures/dir-merge/one.tf b/config/test-fixtures/dir-merge/one.tf new file mode 100644 index 000000000..c62dc93e6 --- /dev/null +++ b/config/test-fixtures/dir-merge/one.tf @@ -0,0 +1,8 @@ +variable "foo" { + default = "bar"; + description = "bar"; +} + +resource "aws_instance" "db" { + security_groups = "${aws_security_group.firewall.*.id}" +} diff --git a/config/test-fixtures/dir-merge/two.tf b/config/test-fixtures/dir-merge/two.tf new file mode 100644 index 000000000..39f6d7e63 --- /dev/null +++ b/config/test-fixtures/dir-merge/two.tf @@ -0,0 +1,2 @@ +resource "aws_instance" "db" { +} diff --git a/config/test-fixtures/dir-override/foo_override.tf.json b/config/test-fixtures/dir-override/foo_override.tf.json new file mode 100644 index 000000000..a2f07c173 --- /dev/null +++ b/config/test-fixtures/dir-override/foo_override.tf.json @@ -0,0 +1,9 @@ +{ + "resource": { + "aws_instance": { + "web": { + "foo": "bar", + } + } + } +} diff --git a/config/test-fixtures/dir-override/one.tf b/config/test-fixtures/dir-override/one.tf new file mode 100644 index 000000000..a4b59f1ae --- /dev/null +++ b/config/test-fixtures/dir-override/one.tf @@ -0,0 +1,17 @@ +variable "foo" { + default = "bar"; + description = "bar"; +} + +provider "aws" { + access_key = "foo"; + secret_key = "bar"; +} + +resource "aws_instance" "db" { + security_groups = "${aws_security_group.firewall.*.id}" +} + +output "web_ip" { + value = "${aws_instance.web.private_ip}" +} diff --git a/config/test-fixtures/dir-override/override.tf.json b/config/test-fixtures/dir-override/override.tf.json new file mode 100644 index 000000000..a9dcf875e --- /dev/null +++ b/config/test-fixtures/dir-override/override.tf.json @@ -0,0 +1,10 @@ +{ + "resource": { + "aws_instance": { + "db": { + "ami": "foo", + "security_groups": "" + } + } + } +} diff --git a/config/test-fixtures/dir-override/two.tf b/config/test-fixtures/dir-override/two.tf new file mode 100644 index 000000000..c87c5059a --- /dev/null +++ b/config/test-fixtures/dir-override/two.tf @@ -0,0 +1,20 @@ +provider "do" { + api_key = "${var.foo}"; +} + +resource "aws_security_group" "firewall" { + count = 5 +} + +resource aws_instance "web" { + ami = "${var.foo}" + security_groups = [ + "foo", + "${aws_security_group.firewall.foo}" + ] + + network_interface { + device_index = 0 + description = "Main network interface" + } +} diff --git a/terraform/graph.go b/terraform/graph.go index e757c3072..4b993d5a3 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -462,7 +462,8 @@ func graphAddProviderConfigs(g *depgraph.Graph, c *config.Config) { } // Look up the provider config for this resource - pcName := config.ProviderConfigName(resourceNode.Type, c.ProviderConfigs) + pcName := config.ProviderConfigName( + resourceNode.Type, c.ProviderConfigs) if pcName == "" { continue } @@ -470,11 +471,22 @@ func graphAddProviderConfigs(g *depgraph.Graph, c *config.Config) { // We have one, so build the noun if it hasn't already been made pcNoun, ok := pcNouns[pcName] if !ok { + var pc *config.ProviderConfig + for _, v := range c.ProviderConfigs { + if v.Name == pcName { + pc = v + break + } + } + if pc == nil { + panic("pc not found") + } + pcNoun = &depgraph.Noun{ Name: fmt.Sprintf("provider.%s", pcName), Meta: &GraphNodeResourceProvider{ ID: pcName, - Config: c.ProviderConfigs[pcName], + Config: pc, }, } pcNouns[pcName] = pcNoun diff --git a/terraform/plan_test.go b/terraform/plan_test.go index 8a9a28810..59690e4f1 100644 --- a/terraform/plan_test.go +++ b/terraform/plan_test.go @@ -53,6 +53,8 @@ func TestReadWritePlan(t *testing.T) { t.Fatalf("err: %s", err) } + println(reflect.DeepEqual(actual.Config.Variables, plan.Config.Variables)) + if !reflect.DeepEqual(actual, plan) { t.Fatalf("bad: %#v", actual) } diff --git a/terraform/semantics.go b/terraform/semantics.go index 0a40506fd..21b8199e8 100644 --- a/terraform/semantics.go +++ b/terraform/semantics.go @@ -13,9 +13,9 @@ func smcUserVariables(c *config.Config, vs map[string]string) []error { // Check that all required variables are present required := make(map[string]struct{}) - for k, v := range c.Variables { + for _, v := range c.Variables { if v.Required() { - required[k] = struct{}{} + required[v.Name] = struct{}{} } } for k, _ := range vs {