diff --git a/command/providers.go b/command/providers.go index b90946493..797082dfd 100644 --- a/command/providers.go +++ b/command/providers.go @@ -94,23 +94,32 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } - reqs, reqDiags := config.ProviderRequirements() - if reqDiags.HasErrors() { - c.showDiagnostics(configDiags) + reqs, reqDiags := config.ProviderRequirementsByModule() + diags = diags.Append(reqDiags) + if diags.HasErrors() { + c.showDiagnostics(diags) return 1 } state := s.State() + var stateReqs getproviders.Requirements if state != nil { - stateReqs := state.ProviderRequirements() - reqs = reqs.Merge(stateReqs) + stateReqs = state.ProviderRequirements() } printRoot := treeprint.New() - providersCommandPopulateTreeNode(printRoot, reqs) + c.populateTreeNode(printRoot, reqs) + c.Ui.Output("\nProviders required by configuration:") c.Ui.Output(printRoot.String()) + if len(stateReqs) > 0 { + c.Ui.Output("Providers required by state:\n") + for fqn := range stateReqs { + c.Ui.Output(fmt.Sprintf(" provider[%s]\n", fqn.String())) + } + } + c.showDiagnostics(diags) if diags.HasErrors() { return 1 @@ -118,22 +127,27 @@ func (c *ProvidersCommand) Run(args []string) int { return 0 } -func providersCommandPopulateTreeNode(node treeprint.Tree, deps getproviders.Requirements) { - for fqn, dep := range deps { +func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.ModuleRequirements) { + for fqn, dep := range node.Requirements { versionsStr := getproviders.VersionConstraintsString(dep) if versionsStr != "" { versionsStr = " " + versionsStr } - node.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr)) + tree.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr)) + } + for name, childNode := range node.Children { + branch := tree.AddBranch(fmt.Sprintf("module.%s", name)) + c.populateTreeNode(branch, childNode) } } const providersCommandHelp = ` Usage: terraform providers [dir] - Prints out a list of providers required by the configuration and state. + Prints out a tree of modules in the referenced configuration annotated with + their provider requirements. - This provides an overview of all of the provider requirements as an aid to - understanding why particular provider plugins are needed and why particular - versions are selected. + This provides an overview of all of the provider requirements across all + referenced modules, as an aid to understanding why particular provider + plugins are needed and why particular versions are selected. ` diff --git a/command/providers_test.go b/command/providers_test.go index c0fed4df7..802fdf571 100644 --- a/command/providers_test.go +++ b/command/providers_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/helper/copy" "github.com/mitchellh/cli" ) @@ -75,14 +76,10 @@ func TestProviders_noConfigs(t *testing.T) { } func TestProviders_modules(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(testFixturePath("providers/modules")); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) + td := tempDir(t) + copy.CopyDir(testFixturePath("providers/modules"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() // first run init with mock provider sources to install the module initUi := new(cli.MockUi) @@ -120,7 +117,8 @@ func TestProviders_modules(t *testing.T) { wantOutput := []string{ "provider[registry.terraform.io/hashicorp/foo] 1.0.*", // from required_providers "provider[registry.terraform.io/hashicorp/bar] 2.0.0", // from provider config - "provider[registry.terraform.io/hashicorp/baz]", // implied by a resource in the child module + "── module.kiddo", // tree node for child module + "provider[registry.terraform.io/hashicorp/baz]", // implied by a resource in the child module } output := ui.OutputWriter.String() @@ -156,6 +154,7 @@ func TestProviders_state(t *testing.T) { wantOutput := []string{ "provider[registry.terraform.io/hashicorp/foo] 1.0.*", // from required_providers "provider[registry.terraform.io/hashicorp/bar] 2.0.0", // from a provider config block + "Providers required by state", // header for state providers "provider[registry.terraform.io/hashicorp/baz]", // from a resouce in state (only) } diff --git a/command/testdata/providers/modules/main.tf b/command/testdata/providers/modules/main.tf index 9e0546a84..37e179307 100644 --- a/command/testdata/providers/modules/main.tf +++ b/command/testdata/providers/modules/main.tf @@ -10,6 +10,6 @@ provider "bar" { version = "2.0.0" } -module "child" { +module "kiddo" { source = "./child" } diff --git a/configs/config.go b/configs/config.go index a99e90cad..fadc78c5f 100644 --- a/configs/config.go +++ b/configs/config.go @@ -77,6 +77,15 @@ type Config struct { Version *version.Version } +// ModuleRequirements represents the provider requirements for an individual +// module, along with references to any child modules. This is used to +// determine which modules require which providers. +type ModuleRequirements struct { + Module *Module + Requirements getproviders.Requirements + Children map[string]*ModuleRequirements +} + // NewEmptyConfig constructs a single-node configuration tree with an empty // root module. This is generally a pretty useless thing to do, so most callers // should instead use BuildConfig. @@ -175,12 +184,45 @@ func (c *Config) DescendentForInstance(path addrs.ModuleInstance) *Config { func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnostics) { reqs := make(getproviders.Requirements) diags := c.addProviderRequirements(reqs) + + for _, childConfig := range c.Children { + moreDiags := childConfig.addProviderRequirements(reqs) + diags = append(diags, moreDiags...) + } + return reqs, diags } +// ProviderRequirementsByModule searches the full tree of modules under the +// receiver for both explicit and implicit dependencies on providers, +// constructing a tree where the requirements are broken out by module. +// +// If the returned diagnostics includes errors then the resulting Requirements +// may be incomplete. +func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagnostics) { + reqs := make(getproviders.Requirements) + diags := c.addProviderRequirements(reqs) + + children := make(map[string]*ModuleRequirements) + for name, child := range c.Children { + childReqs, childDiags := child.ProviderRequirementsByModule() + children[name] = childReqs + diags = append(diags, childDiags...) + } + + ret := &ModuleRequirements{ + Module: c.Module, + Requirements: reqs, + Children: children, + } + + return ret, diags +} + // addProviderRequirements is the main part of the ProviderRequirements // implementation, gradually mutating a shared requirements object to -// eventually return. +// eventually return. This function only adds requirements for the top-level +// module. func (c *Config) addProviderRequirements(reqs getproviders.Requirements) hcl.Diagnostics { var diags hcl.Diagnostics @@ -235,13 +277,6 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements) hcl.Dia } } - // ...and now we'll recursively visit all of the child modules to merge - // in their requirements too. - for _, childConfig := range c.Children { - moreDiags := childConfig.addProviderRequirements(reqs) - diags = append(diags, moreDiags...) - } - return diags } diff --git a/configs/config_test.go b/configs/config_test.go index b2cefe751..f3481ae6f 100644 --- a/configs/config_test.go +++ b/configs/config_test.go @@ -5,7 +5,11 @@ import ( "github.com/go-test/deep" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/zclconf/go-cty/cty" + version "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2/hclsyntax" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/internal/getproviders" @@ -145,6 +149,59 @@ func TestConfigProviderRequirements(t *testing.T) { } } +func TestConfigProviderRequirementsByModule(t *testing.T) { + cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs") + assertNoDiagnostics(t, diags) + + tlsProvider := addrs.NewProvider( + addrs.DefaultRegistryHost, + "hashicorp", "tls", + ) + happycloudProvider := addrs.NewProvider( + svchost.Hostname("tf.example.com"), + "awesomecorp", "happycloud", + ) + nullProvider := addrs.NewDefaultProvider("null") + randomProvider := addrs.NewDefaultProvider("random") + impliedProvider := addrs.NewDefaultProvider("implied") + terraformProvider := addrs.NewBuiltInProvider("terraform") + configuredProvider := addrs.NewDefaultProvider("configured") + + got, diags := cfg.ProviderRequirementsByModule() + assertNoDiagnostics(t, diags) + child, ok := cfg.Children["kinder"] + if !ok { + t.Fatalf(`could not find child config "kinder" in config children`) + } + want := &ModuleRequirements{ + Module: cfg.Module, + Requirements: getproviders.Requirements{ + // Only the root module's version is present here + nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"), + randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), + tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), + configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"), + impliedProvider: nil, + terraformProvider: nil, + }, + Children: map[string]*ModuleRequirements{ + "kinder": { + Module: child.Module, + Requirements: getproviders.Requirements{ + nullProvider: getproviders.MustParseVersionConstraints("= 2.0.1"), + happycloudProvider: nil, + }, + Children: map[string]*ModuleRequirements{}, + }, + }, + } + + ignore := cmpopts.IgnoreUnexported(version.Constraint{}, cty.Value{}, hclsyntax.Body{}) + if diff := cmp.Diff(want, got, ignore); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + func TestConfigProviderForConfigAddr(t *testing.T) { cfg, diags := testModuleConfigFromDir("testdata/valid-modules/providers-fqns") assertNoDiagnostics(t, diags) diff --git a/configs/testdata/provider-reqs/provider-reqs-root.tf b/configs/testdata/provider-reqs/provider-reqs-root.tf index 217658203..03a562598 100644 --- a/configs/testdata/provider-reqs/provider-reqs-root.tf +++ b/configs/testdata/provider-reqs/provider-reqs-root.tf @@ -16,7 +16,7 @@ terraform { resource "implied_foo" "bar" { } -module "child" { +module "kinder" { source = "./child" }