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
|
||||
}
|
||||
|
||||
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.
|
||||
`
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,6 @@ provider "bar" {
|
|||
version = "2.0.0"
|
||||
}
|
||||
|
||||
module "child" {
|
||||
module "kiddo" {
|
||||
source = "./child"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -16,7 +16,7 @@ terraform {
|
|||
resource "implied_foo" "bar" {
|
||||
}
|
||||
|
||||
module "child" {
|
||||
module "kinder" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue