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 (
"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.
`

View File

@ -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)
}
}
}

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")
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,

View File

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

View File

@ -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.