Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

command/providers: Show provider requirements tree #25190

Merged
merged 1 commit into from Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 27 additions & 13 deletions command/providers.go
Expand Up @@ -94,46 +94,60 @@ 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
}
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a specific reason or benefit to make this a method on the ProvidersCommand? I'm not opposed per se, but I don't see it using anything from the ProvidersCommand to explain the change - without this other commands could call this function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no good reason. I'm happy to change it back if you prefer!

In prototyping this feature I added another function to this file, and since it was also very providers-command specific I named it providersCommandWhatever as well. Then that made me realize that if we want to signal that the purpose of the function is specific to this command, making it a method is probably clearer than a long name prefix, so I did that for both. Then I removed that function and forgot about this one.

I think we have both patterns elsewhere in the command package: methods which don't reference the receiver, and package-scoped functions which are really only related to one struct. I slightly prefer the former but only at a 2/5 level.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, my approval stands 😄

At the absolute "worst" (ie, no problem at all, a totally normal and fine situation), we make a future change if we decide to use some of that functionality in another command (and even then the method would likely remain, and the other bits factored out).

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.
`
17 changes: 8 additions & 9 deletions command/providers_test.go
Expand Up @@ -5,6 +5,7 @@ import (
"strings"
"testing"

"github.com/hashicorp/terraform/helper/copy"
"github.com/mitchellh/cli"
)

Expand Down Expand Up @@ -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)()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing a bug which left dangling .terraform state when running tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 thank you! that had even been pointed out to me and i forgot 🤦


// first run init with mock provider sources to install the module
initUi := new(cli.MockUi)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion command/testdata/providers/modules/main.tf
Expand Up @@ -10,6 +10,6 @@ provider "bar" {
version = "2.0.0"
}

module "child" {
module "kiddo" {
source = "./child"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed so that the module didn't match the source directory, so we can verify that the name is shown in the tree.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love kiddo/kinder, excellent choices

}
51 changes: 43 additions & 8 deletions configs/config.go
Expand Up @@ -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
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type and the method which returns an instance of it below are intended to be used in an upcoming improvement to the init command's diagnostics when providers fail to install. That's why the Module member is here, and also why it's in the configs package instead of command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for this extra context!


// 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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}

Expand Down
57 changes: 57 additions & 0 deletions configs/config_test.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion configs/testdata/provider-reqs/provider-reqs-root.tf
Expand Up @@ -16,7 +16,7 @@ terraform {
resource "implied_foo" "bar" {
}

module "child" {
module "kinder" {
source = "./child"
}

Expand Down