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 0ef8b116d..4ec23330b 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. @@ -30,20 +34,67 @@ 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")) +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 @@ -59,5 +110,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_test.go b/config/loader_test.go index 21e870e43..cdb34cfb8 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -171,6 +171,37 @@ func TestLoadDir_noMerge(t *testing.T) { } } +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)) m := make(map[string]*Output) @@ -503,6 +534,42 @@ 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 + 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 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" + } +}