command/providers: Show provider requirements tree

Providers can be required from multiple sources. The previous
implementation of the providers sub-command displayed only a flat list
of provider requirements, which made it difficult to see which modules
required each provider.

This commit reintroduces the tree display of provider requirements, and
adds a separate output block for providers required by existing state.
This commit is contained in:
Alisdair McDiarmid 2020-06-09 13:32:56 -04:00
parent 299aa31b43
commit 1c1e4a4de0
6 changed files with 137 additions and 32 deletions

View File

@ -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.
`

View File

@ -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,6 +117,7 @@ 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
"── module.kiddo", // tree node for child module
"provider[registry.terraform.io/hashicorp/baz]", // implied by a resource in the child module
}
@ -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)
}

View File

@ -10,6 +10,6 @@ provider "bar" {
version = "2.0.0"
}
module "child" {
module "kiddo" {
source = "./child"
}

View File

@ -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
}

View File

@ -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)

View File

@ -16,7 +16,7 @@ terraform {
resource "implied_foo" "bar" {
}
module "child" {
module "kinder" {
source = "./child"
}