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.