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.
This commit is contained in:
Kristin Laemmert 2020-04-10 13:26:38 -04:00
parent 0a5fb40fdf
commit 269d511481
9 changed files with 255 additions and 114 deletions

View File

@ -2,9 +2,11 @@ package command
import ( import (
"fmt" "fmt"
"path/filepath"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/moduledeps" "github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/tfdiags"
"github.com/xlab/treeprint" "github.com/xlab/treeprint"
) )
@ -31,7 +33,6 @@ func (c *ProvidersCommand) Run(args []string) int {
return 1 return 1
} }
/*
configPath, err := ModulePath(cmdFlags.Args()) configPath, err := ModulePath(cmdFlags.Args())
if err != nil { if err != nil {
c.Ui.Error(err.Error()) c.Ui.Error(err.Error())
@ -83,22 +84,30 @@ func (c *ProvidersCommand) Run(args []string) int {
// Get the state // Get the state
env := c.Workspace() env := c.Workspace()
state, err := b.StateMgr(env) s, err := b.StateMgr(env)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 return 1
} }
if err := state.RefreshState(); err != nil { if err := s.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 return 1
} }
s := state.State() reqs, reqDiags := config.ProviderRequirements()
depTree := terraform.ConfigTreeDependencies(config, s) if reqDiags.HasErrors() {
depTree.SortDescendents() c.showDiagnostics(configDiags)
return 1
}
state := s.State()
if state != nil {
stateReqs := state.ProviderRequirements()
reqs = reqs.Merge(stateReqs)
}
printRoot := treeprint.New() printRoot := treeprint.New()
providersCommandPopulateTreeNode(printRoot, depTree) providersCommandPopulateTreeNode(printRoot, reqs)
c.Ui.Output(printRoot.String()) c.Ui.Output(printRoot.String())
@ -106,48 +115,25 @@ func (c *ProvidersCommand) Run(args []string) int {
if diags.HasErrors() { if diags.HasErrors() {
return 1 return 1
} }
*/
c.Ui.Output(fmt.Sprintf("terraform providers is temporarily disabled"))
return 0 return 0
} }
func providersCommandPopulateTreeNode(node treeprint.Tree, deps *moduledeps.Module) { func providersCommandPopulateTreeNode(node treeprint.Tree, deps getproviders.Requirements) {
fqns := make([]addrs.Provider, 0, len(deps.Providers)) for fqn, dep := range deps {
for fqn := range deps.Providers { versionsStr := getproviders.VersionConstraintsString(dep)
fqns = append(fqns, fqn)
}
for _, fqn := range fqns {
dep := deps.Providers[fqn]
versionsStr := dep.Constraints.String()
if versionsStr != "" { if versionsStr != "" {
versionsStr = " " + versionsStr versionsStr = " " + versionsStr
} }
var reasonStr string node.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr))
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)
} }
} }
const providersCommandHelp = ` const providersCommandHelp = `
Usage: terraform providers [dir] Usage: terraform providers [dir]
Prints out a tree of modules in the referenced configuration annotated with Prints out a list of providers required by the configuration and state.
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.
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.
` `

View File

@ -9,12 +9,11 @@ import (
) )
func TestProviders(t *testing.T) { func TestProviders(t *testing.T) {
return
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
t.Fatalf("err: %s", err) 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) t.Fatalf("err: %s", err)
} }
defer os.Chdir(cwd) defer os.Chdir(cwd)
@ -31,20 +30,21 @@ func TestProviders(t *testing.T) {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) 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() output := ui.OutputWriter.String()
if !strings.Contains(output, "provider.foo") { for _, want := range wantOutput {
t.Errorf("output missing provider.foo\n\n%s", output) if !strings.Contains(output, want) {
t.Errorf("output missing %s:\n%s", want, 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)
} }
} }
func TestProviders_noConfigs(t *testing.T) { func TestProviders_noConfigs(t *testing.T) {
return
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
t.Fatalf("err: %s", err) 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) 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)
}
}
}

View File

@ -0,0 +1 @@
resource "baz_resource" "baz" {}

View File

@ -0,0 +1,15 @@
terraform {
required_providers {
foo = {
version = "1.0"
}
}
}
provider "bar" {
version = "2.0.0"
}
module "child" {
source = "./child"
}

View File

@ -0,0 +1,11 @@
terraform {
required_providers {
foo = {
version = "1.0"
}
}
}
provider "bar" {
version = "2.0.0"
}

View File

@ -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=="
}
]
}
]
}

View File

@ -125,6 +125,7 @@ func TestConfigProviderRequirements(t *testing.T) {
randomProvider := addrs.NewDefaultProvider("random") randomProvider := addrs.NewDefaultProvider("random")
impliedProvider := addrs.NewDefaultProvider("implied") impliedProvider := addrs.NewDefaultProvider("implied")
terraformProvider := addrs.NewBuiltInProvider("terraform") terraformProvider := addrs.NewBuiltInProvider("terraform")
configuredProvider := addrs.NewDefaultProvider("configured")
got, diags := cfg.ProviderRequirements() got, diags := cfg.ProviderRequirements()
assertNoDiagnostics(t, diags) assertNoDiagnostics(t, diags)
@ -133,6 +134,7 @@ func TestConfigProviderRequirements(t *testing.T) {
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"),
randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"),
tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"),
configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"),
impliedProvider: nil, impliedProvider: nil,
happycloudProvider: nil, happycloudProvider: nil,
terraformProvider: nil, terraformProvider: nil,

View File

@ -26,3 +26,9 @@ module "child" {
# registry.terraform.io/hashicorp/terraform. # registry.terraform.io/hashicorp/terraform.
data "terraform_remote_state" "bar" { 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"
}

View File

@ -14,6 +14,9 @@ used in the current configuration.
Provider dependencies are created in several different ways: 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 * Explicit use of a `provider` block in configuration, optionally including
a version constraint. a version constraint.