diff --git a/config/config.go b/config/config.go index 102e18163..c055189a5 100644 --- a/config/config.go +++ b/config/config.go @@ -15,6 +15,7 @@ import ( // Config is the configuration that comes from loading a collection // of Terraform templates. type Config struct { + Modules []*Module ProviderConfigs []*ProviderConfig Resources []*Resource Variables []*Variable @@ -25,6 +26,17 @@ type Config struct { unknownKeys []string } +// Module is a module used within a configuration. +// +// This does not represent a module itself, this represents a module +// call-site within an existing configuration. +type Module struct { + Name string + Type string + Source string + RawConfig *RawConfig +} + // ProviderConfig is the configuration for a resource provider. // // For example, Terraform needs to set the AWS access keys for the AWS @@ -92,6 +104,11 @@ func ProviderConfigName(t string, pcs []*ProviderConfig) string { return lk } +// A unique identifier for this module. +func (r *Module) Id() string { + return fmt.Sprintf("%s.%s", r.Type, r.Name) +} + // A unique identifier for this resource. func (r *Resource) Id() string { return fmt.Sprintf("%s.%s", r.Type, r.Name) diff --git a/config/loader_hcl.go b/config/loader_hcl.go index 380d50294..13337006a 100644 --- a/config/loader_hcl.go +++ b/config/loader_hcl.go @@ -17,6 +17,7 @@ type hclConfigurable struct { func (t *hclConfigurable) Config() (*Config, error) { validKeys := map[string]struct{}{ + "module": struct{}{}, "output": struct{}{}, "provider": struct{}{}, "resource": struct{}{}, @@ -69,6 +70,15 @@ func (t *hclConfigurable) Config() (*Config, error) { } } + // Build the modules + if modules := t.Object.Get("module", false); modules != nil { + var err error + config.Modules, err = loadModulesHcl(modules) + if err != nil { + return nil, err + } + } + // Build the provider configs if providers := t.Object.Get("provider", false); providers != nil { var err error @@ -177,6 +187,81 @@ func loadFileHcl(root string) (configurable, []string, error) { return result, nil, nil } +// Given a handle to a HCL object, this recurses into the structure +// and pulls out a list of modules. +// +// The resulting modules may not be unique, but each module +// represents exactly one module definition in the HCL configuration. +// We leave it up to another pass to merge them together. +func loadModulesHcl(os *hclobj.Object) ([]*Module, error) { + var allTypes []*hclobj.Object + + // See loadResourcesHcl for why this exists. Don't touch this. + for _, o1 := range os.Elem(false) { + // Iterate the inner to get the list of types + for _, o2 := range o1.Elem(true) { + // Iterate all of this type to get _all_ the types + for _, o3 := range o2.Elem(false) { + allTypes = append(allTypes, o3) + } + } + } + + // Where all the results will go + var result []*Module + + // Now go over all the types and their children in order to get + // all of the actual resources. + for _, t := range allTypes { + for _, obj := range t.Elem(true) { + k := obj.Key + + var config map[string]interface{} + if err := hcl.DecodeObject(&config, obj); err != nil { + return nil, fmt.Errorf( + "Error reading config for %s[%s]: %s", + t.Key, + k, + err) + } + + // Remove the fields we handle specially + delete(config, "source") + + rawConfig, err := NewRawConfig(config) + if err != nil { + return nil, fmt.Errorf( + "Error reading config for %s[%s]: %s", + t.Key, + k, + err) + } + + // If we have a count, then figure it out + var source string + if o := obj.Get("source", false); o != nil { + err = hcl.DecodeObject(&source, o) + if err != nil { + return nil, fmt.Errorf( + "Error parsing source for %s[%s]: %s", + t.Key, + k, + err) + } + } + + result = append(result, &Module{ + Name: k, + Type: t.Key, + Source: source, + RawConfig: rawConfig, + }) + } + } + + return result, nil +} + // LoadOutputsHcl recurses into the given HCL object and turns // it into a mapping of outputs. func loadOutputsHcl(os *hclobj.Object) ([]*Output, error) { diff --git a/config/loader_test.go b/config/loader_test.go index b20096d6f..e4843e5c6 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -106,6 +106,22 @@ func TestLoadBasic_json(t *testing.T) { } } +func TestLoadBasic_modules(t *testing.T) { + c, err := Load(filepath.Join(fixtureDir, "modules.tf")) + if err != nil { + t.Fatalf("err: %s", err) + } + + if c == nil { + t.Fatal("config should not be nil") + } + + actual := modulesStr(c.Modules) + if actual != strings.TrimSpace(modulesModulesStr) { + t.Fatalf("bad:\n%s", actual) + } +} + func TestLoad_variables(t *testing.T) { c, err := Load(filepath.Join(fixtureDir, "variables.tf")) if err != nil { @@ -302,6 +318,41 @@ func TestLoad_connections(t *testing.T) { } } +func modulesStr(ms []*Module) string { + result := "" + order := make([]int, 0, len(ms)) + ks := make([]string, 0, len(ms)) + mapping := make(map[string]int) + for i, m := range ms { + k := m.Id() + ks = append(ks, k) + mapping[k] = i + } + sort.Strings(ks) + for _, k := range ks { + order = append(order, mapping[k]) + } + + for _, i := range order { + m := ms[i] + result += fmt.Sprintf("%s\n", m.Id()) + + ks := make([]string, 0, len(m.RawConfig.Raw)) + for k, _ := range m.RawConfig.Raw { + ks = append(ks, k) + } + sort.Strings(ks) + + result += fmt.Sprintf(" source = %s\n", m.Source) + + for _, k := range ks { + result += fmt.Sprintf(" %s\n", k) + } + } + + return strings.TrimSpace(result) +} + // This helper turns a provider configs field into a deterministic // string value for comparison in tests. func providerConfigsStr(pcs []*ProviderConfig) string { @@ -611,6 +662,12 @@ foo bar ` +const modulesModulesStr = ` +foo.bar + source = baz + memory +` + const provisionerResourcesStr = ` aws_instance[web] (x1) ami diff --git a/config/test-fixtures/modules.tf b/config/test-fixtures/modules.tf new file mode 100644 index 000000000..48e7ad09e --- /dev/null +++ b/config/test-fixtures/modules.tf @@ -0,0 +1,4 @@ +module "foo" "bar" { + memory = "1G" + source = "baz" +}