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:
parent
299aa31b43
commit
1c1e4a4de0
|
@ -94,23 +94,32 @@ func (c *ProvidersCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
reqs, reqDiags := config.ProviderRequirements()
|
reqs, reqDiags := config.ProviderRequirementsByModule()
|
||||||
if reqDiags.HasErrors() {
|
diags = diags.Append(reqDiags)
|
||||||
c.showDiagnostics(configDiags)
|
if diags.HasErrors() {
|
||||||
|
c.showDiagnostics(diags)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
state := s.State()
|
state := s.State()
|
||||||
|
var stateReqs getproviders.Requirements
|
||||||
if state != nil {
|
if state != nil {
|
||||||
stateReqs := state.ProviderRequirements()
|
stateReqs = state.ProviderRequirements()
|
||||||
reqs = reqs.Merge(stateReqs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printRoot := treeprint.New()
|
printRoot := treeprint.New()
|
||||||
providersCommandPopulateTreeNode(printRoot, reqs)
|
c.populateTreeNode(printRoot, reqs)
|
||||||
|
|
||||||
|
c.Ui.Output("\nProviders required by configuration:")
|
||||||
c.Ui.Output(printRoot.String())
|
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)
|
c.showDiagnostics(diags)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return 1
|
return 1
|
||||||
|
@ -118,22 +127,27 @@ func (c *ProvidersCommand) Run(args []string) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func providersCommandPopulateTreeNode(node treeprint.Tree, deps getproviders.Requirements) {
|
func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.ModuleRequirements) {
|
||||||
for fqn, dep := range deps {
|
for fqn, dep := range node.Requirements {
|
||||||
versionsStr := getproviders.VersionConstraintsString(dep)
|
versionsStr := getproviders.VersionConstraintsString(dep)
|
||||||
if versionsStr != "" {
|
if versionsStr != "" {
|
||||||
versionsStr = " " + 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 = `
|
const providersCommandHelp = `
|
||||||
Usage: terraform providers [dir]
|
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
|
This provides an overview of all of the provider requirements across all
|
||||||
understanding why particular provider plugins are needed and why particular
|
referenced modules, as an aid to understanding why particular provider
|
||||||
versions are selected.
|
plugins are needed and why particular versions are selected.
|
||||||
`
|
`
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/helper/copy"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,14 +76,10 @@ func TestProviders_noConfigs(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProviders_modules(t *testing.T) {
|
func TestProviders_modules(t *testing.T) {
|
||||||
cwd, err := os.Getwd()
|
td := tempDir(t)
|
||||||
if err != nil {
|
copy.CopyDir(testFixturePath("providers/modules"), td)
|
||||||
t.Fatalf("err: %s", err)
|
defer os.RemoveAll(td)
|
||||||
}
|
defer testChdir(t, td)()
|
||||||
if err := os.Chdir(testFixturePath("providers/modules")); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
defer os.Chdir(cwd)
|
|
||||||
|
|
||||||
// first run init with mock provider sources to install the module
|
// first run init with mock provider sources to install the module
|
||||||
initUi := new(cli.MockUi)
|
initUi := new(cli.MockUi)
|
||||||
|
@ -120,6 +117,7 @@ func TestProviders_modules(t *testing.T) {
|
||||||
wantOutput := []string{
|
wantOutput := []string{
|
||||||
"provider[registry.terraform.io/hashicorp/foo] 1.0.*", // from required_providers
|
"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/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
|
"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{
|
wantOutput := []string{
|
||||||
"provider[registry.terraform.io/hashicorp/foo] 1.0.*", // from required_providers
|
"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
|
"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)
|
"provider[registry.terraform.io/hashicorp/baz]", // from a resouce in state (only)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,6 @@ provider "bar" {
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
module "child" {
|
module "kiddo" {
|
||||||
source = "./child"
|
source = "./child"
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,15 @@ type Config struct {
|
||||||
Version *version.Version
|
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
|
// NewEmptyConfig constructs a single-node configuration tree with an empty
|
||||||
// root module. This is generally a pretty useless thing to do, so most callers
|
// root module. This is generally a pretty useless thing to do, so most callers
|
||||||
// should instead use BuildConfig.
|
// should instead use BuildConfig.
|
||||||
|
@ -175,12 +184,45 @@ func (c *Config) DescendentForInstance(path addrs.ModuleInstance) *Config {
|
||||||
func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnostics) {
|
func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnostics) {
|
||||||
reqs := make(getproviders.Requirements)
|
reqs := make(getproviders.Requirements)
|
||||||
diags := c.addProviderRequirements(reqs)
|
diags := c.addProviderRequirements(reqs)
|
||||||
|
|
||||||
|
for _, childConfig := range c.Children {
|
||||||
|
moreDiags := childConfig.addProviderRequirements(reqs)
|
||||||
|
diags = append(diags, moreDiags...)
|
||||||
|
}
|
||||||
|
|
||||||
return reqs, diags
|
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
|
// addProviderRequirements is the main part of the ProviderRequirements
|
||||||
// implementation, gradually mutating a shared requirements object to
|
// 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 {
|
func (c *Config) addProviderRequirements(reqs getproviders.Requirements) hcl.Diagnostics {
|
||||||
var diags 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
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@ import (
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
"github.com/google/go-cmp/cmp"
|
"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"
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/internal/getproviders"
|
"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) {
|
func TestConfigProviderForConfigAddr(t *testing.T) {
|
||||||
cfg, diags := testModuleConfigFromDir("testdata/valid-modules/providers-fqns")
|
cfg, diags := testModuleConfigFromDir("testdata/valid-modules/providers-fqns")
|
||||||
assertNoDiagnostics(t, diags)
|
assertNoDiagnostics(t, diags)
|
||||||
|
|
|
@ -16,7 +16,7 @@ terraform {
|
||||||
resource "implied_foo" "bar" {
|
resource "implied_foo" "bar" {
|
||||||
}
|
}
|
||||||
|
|
||||||
module "child" {
|
module "kinder" {
|
||||||
source = "./child"
|
source = "./child"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue