From 269d51148162476180963132147ffbfc918cdac9 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Fri, 10 Apr 2020 13:26:38 -0400 Subject: [PATCH] command/providers: refactor with new provider types and functions The providers command has been refactored to use the modern provider types and ProviderRequirements() functions. This resulted in a breaking change to the output: it no longer outputs the providers by module and no longer prints `(inherited)` or `(from state)` to show why a provider is included. We decided that at this time it was best to stick with the existing functions and make this change, but if we get feedback from the community we will revisit. Additional tests to exercise providers in modules and providers from state have been included. --- command/providers.go | 192 ++++++++---------- command/providers_test.go | 115 ++++++++++- .../testdata/providers/modules/child/main.tf | 1 + command/testdata/providers/modules/main.tf | 15 ++ command/testdata/providers/state/main.tf | 11 + .../providers/state/terraform.tfstate | 24 +++ configs/config_test.go | 2 + .../provider-reqs/provider-reqs-root.tf | 6 + website/docs/commands/providers.html.markdown | 3 + 9 files changed, 255 insertions(+), 114 deletions(-) create mode 100644 command/testdata/providers/modules/child/main.tf create mode 100644 command/testdata/providers/modules/main.tf create mode 100644 command/testdata/providers/state/main.tf create mode 100644 command/testdata/providers/state/terraform.tfstate diff --git a/command/providers.go b/command/providers.go index 1cbe397b46..b909464935 100644 --- a/command/providers.go +++ b/command/providers.go @@ -2,9 +2,11 @@ package command import ( "fmt" + "path/filepath" - "github.com/hashicorp/terraform/addrs" - "github.com/hashicorp/terraform/moduledeps" + "github.com/hashicorp/terraform/configs" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/tfdiags" "github.com/xlab/treeprint" ) @@ -31,123 +33,107 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } - /* - configPath, err := ModulePath(cmdFlags.Args()) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } + configPath, err := ModulePath(cmdFlags.Args()) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } - var diags tfdiags.Diagnostics - - empty, err := configs.IsEmptyDir(configPath) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error validating configuration directory", - fmt.Sprintf("Terraform encountered an unexpected error while verifying that the given configuration directory is valid: %s.", err), - )) - c.showDiagnostics(diags) - return 1 - } - if empty { - absPath, err := filepath.Abs(configPath) - if err != nil { - absPath = configPath - } - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "No configuration files", - fmt.Sprintf("The directory %s contains no Terraform configuration files.", absPath), - )) - c.showDiagnostics(diags) - return 1 - } - - config, configDiags := c.loadConfig(configPath) - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - - // Load the backend - b, backendDiags := c.Backend(&BackendOpts{ - Config: config.Module.Backend, - }) - diags = diags.Append(backendDiags) - if backendDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - - // Get the state - env := c.Workspace() - state, err := b.StateMgr(env) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) - return 1 - } - if err := state.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) - return 1 - } - - s := state.State() - depTree := terraform.ConfigTreeDependencies(config, s) - depTree.SortDescendents() - - printRoot := treeprint.New() - providersCommandPopulateTreeNode(printRoot, depTree) - - c.Ui.Output(printRoot.String()) + var diags tfdiags.Diagnostics + empty, err := configs.IsEmptyDir(configPath) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Error validating configuration directory", + fmt.Sprintf("Terraform encountered an unexpected error while verifying that the given configuration directory is valid: %s.", err), + )) c.showDiagnostics(diags) - if diags.HasErrors() { - return 1 + return 1 + } + if empty { + absPath, err := filepath.Abs(configPath) + if err != nil { + absPath = configPath } - */ + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No configuration files", + fmt.Sprintf("The directory %s contains no Terraform configuration files.", absPath), + )) + c.showDiagnostics(diags) + return 1 + } - c.Ui.Output(fmt.Sprintf("terraform providers is temporarily disabled")) + config, configDiags := c.loadConfig(configPath) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Load the backend + b, backendDiags := c.Backend(&BackendOpts{ + Config: config.Module.Backend, + }) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Get the state + env := c.Workspace() + s, err := b.StateMgr(env) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + if err := s.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + reqs, reqDiags := config.ProviderRequirements() + if reqDiags.HasErrors() { + c.showDiagnostics(configDiags) + return 1 + } + + state := s.State() + if state != nil { + stateReqs := state.ProviderRequirements() + reqs = reqs.Merge(stateReqs) + } + + printRoot := treeprint.New() + providersCommandPopulateTreeNode(printRoot, reqs) + + c.Ui.Output(printRoot.String()) + + c.showDiagnostics(diags) + if diags.HasErrors() { + return 1 + } return 0 } -func providersCommandPopulateTreeNode(node treeprint.Tree, deps *moduledeps.Module) { - fqns := make([]addrs.Provider, 0, len(deps.Providers)) - for fqn := range deps.Providers { - fqns = append(fqns, fqn) - } - - for _, fqn := range fqns { - dep := deps.Providers[fqn] - versionsStr := dep.Constraints.String() +func providersCommandPopulateTreeNode(node treeprint.Tree, deps getproviders.Requirements) { + for fqn, dep := range deps { + versionsStr := getproviders.VersionConstraintsString(dep) if versionsStr != "" { versionsStr = " " + versionsStr } - var reasonStr string - switch dep.Reason { - case moduledeps.ProviderDependencyInherited: - reasonStr = " (inherited)" - case moduledeps.ProviderDependencyFromState: - reasonStr = " (from state)" - } - node.AddNode(fmt.Sprintf("provider.%s%s%s", fqn.LegacyString(), versionsStr, reasonStr)) - } - - for _, child := range deps.Children { - childNode := node.AddBranch(fmt.Sprintf("module.%s", child.Name)) - providersCommandPopulateTreeNode(childNode, child) + node.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr)) } } const providersCommandHelp = ` Usage: terraform providers [dir] - 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 across all - referenced modules, as an aid to understanding why particular provider - plugins are needed and why particular versions are selected. + Prints out a list of providers required by the configuration and state. + 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. ` diff --git a/command/providers_test.go b/command/providers_test.go index 103590611e..c0fed4df74 100644 --- a/command/providers_test.go +++ b/command/providers_test.go @@ -9,12 +9,11 @@ import ( ) func TestProviders(t *testing.T) { - return cwd, err := os.Getwd() if err != nil { t.Fatalf("err: %s", err) } - if err := os.Chdir(testFixturePath("providers")); err != nil { + if err := os.Chdir(testFixturePath("providers/basic")); err != nil { t.Fatalf("err: %s", err) } defer os.Chdir(cwd) @@ -31,20 +30,21 @@ func TestProviders(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } + wantOutput := []string{ + "provider[registry.terraform.io/hashicorp/foo]", + "provider[registry.terraform.io/hashicorp/bar]", + "provider[registry.terraform.io/hashicorp/baz]", + } + output := ui.OutputWriter.String() - if !strings.Contains(output, "provider.foo") { - t.Errorf("output missing provider.foo\n\n%s", output) - } - if !strings.Contains(output, "provider.bar") { - t.Errorf("output missing provider.bar\n\n%s", output) - } - if !strings.Contains(output, "provider.baz") { - t.Errorf("output missing provider.baz\n\n%s", output) + for _, want := range wantOutput { + if !strings.Contains(output, want) { + t.Errorf("output missing %s:\n%s", want, output) + } } } func TestProviders_noConfigs(t *testing.T) { - return cwd, err := os.Getwd() if err != nil { t.Fatalf("err: %s", err) @@ -73,3 +73,96 @@ func TestProviders_noConfigs(t *testing.T) { t.Errorf("Expected error message: %s\nGiven output: %s", expectedErrMsg, output) } } + +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) + + // first run init with mock provider sources to install the module + initUi := new(cli.MockUi) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "foo": {"1.0.0"}, + "bar": {"2.0.0"}, + "baz": {"1.2.2"}, + }) + defer close() + m := Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: initUi, + ProviderSource: providerSource, + } + ic := &InitCommand{ + Meta: m, + } + if code := ic.Run([]string{}); code != 0 { + t.Fatalf("init failed\n%s", initUi.ErrorWriter) + } + + // Providers command + ui := new(cli.MockUi) + c := &ProvidersCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + 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 + } + + output := ui.OutputWriter.String() + for _, want := range wantOutput { + if !strings.Contains(output, want) { + t.Errorf("output missing %s:\n%s", want, output) + } + } +} + +func TestProviders_state(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("providers/state")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + ui := new(cli.MockUi) + c := &ProvidersCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + 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 + "provider[registry.terraform.io/hashicorp/baz]", // from a resouce in state (only) + } + + output := ui.OutputWriter.String() + for _, want := range wantOutput { + if !strings.Contains(output, want) { + t.Errorf("output missing %s:\n%s", want, output) + } + } +} diff --git a/command/testdata/providers/modules/child/main.tf b/command/testdata/providers/modules/child/main.tf new file mode 100644 index 0000000000..e5bd70220b --- /dev/null +++ b/command/testdata/providers/modules/child/main.tf @@ -0,0 +1 @@ +resource "baz_resource" "baz" {} diff --git a/command/testdata/providers/modules/main.tf b/command/testdata/providers/modules/main.tf new file mode 100644 index 0000000000..9e0546a845 --- /dev/null +++ b/command/testdata/providers/modules/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + foo = { + version = "1.0" + } + } +} + +provider "bar" { + version = "2.0.0" +} + +module "child" { + source = "./child" +} diff --git a/command/testdata/providers/state/main.tf b/command/testdata/providers/state/main.tf new file mode 100644 index 0000000000..b34c855be6 --- /dev/null +++ b/command/testdata/providers/state/main.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + foo = { + version = "1.0" + } + } +} + +provider "bar" { + version = "2.0.0" +} diff --git a/command/testdata/providers/state/terraform.tfstate b/command/testdata/providers/state/terraform.tfstate new file mode 100644 index 0000000000..7e58182894 --- /dev/null +++ b/command/testdata/providers/state/terraform.tfstate @@ -0,0 +1,24 @@ +{ + "version": 4, + "terraform_version": "0.13.0", + "serial": 1, + "lineage": "00bfda35-ad61-ec8d-c013-14b0320bc416", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "baz_instance", + "name": "example", + "provider": "provider[\"registry.terraform.io/hashicorp/baz\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "621124146446964903" + }, + "private": "bnVsbA==" + } + ] + } + ] +} diff --git a/configs/config_test.go b/configs/config_test.go index 53033b1b78..e33e236da4 100644 --- a/configs/config_test.go +++ b/configs/config_test.go @@ -125,6 +125,7 @@ func TestConfigProviderRequirements(t *testing.T) { randomProvider := addrs.NewDefaultProvider("random") impliedProvider := addrs.NewDefaultProvider("implied") terraformProvider := addrs.NewBuiltInProvider("terraform") + configuredProvider := addrs.NewDefaultProvider("configured") got, diags := cfg.ProviderRequirements() assertNoDiagnostics(t, diags) @@ -133,6 +134,7 @@ func TestConfigProviderRequirements(t *testing.T) { nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), + configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"), impliedProvider: nil, happycloudProvider: nil, terraformProvider: nil, diff --git a/configs/testdata/provider-reqs/provider-reqs-root.tf b/configs/testdata/provider-reqs/provider-reqs-root.tf index 9427d431c1..217658203d 100644 --- a/configs/testdata/provider-reqs/provider-reqs-root.tf +++ b/configs/testdata/provider-reqs/provider-reqs-root.tf @@ -26,3 +26,9 @@ module "child" { # registry.terraform.io/hashicorp/terraform. data "terraform_remote_state" "bar" { } + +# There is no provider in required_providers called "configured", so the version +# constraint should come from this configuration block. +provider "configured" { + version = "~> 1.4" +} diff --git a/website/docs/commands/providers.html.markdown b/website/docs/commands/providers.html.markdown index 85dddce99b..d1b34cb7f7 100644 --- a/website/docs/commands/providers.html.markdown +++ b/website/docs/commands/providers.html.markdown @@ -14,6 +14,9 @@ used in the current configuration. Provider dependencies are created in several different ways: +* Explicit use of a `terraform.required_providers` block in configuration, + optionally including a version constraint. + * Explicit use of a `provider` block in configuration, optionally including a version constraint.