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:
parent
0a5fb40fdf
commit
269d511481
|
@ -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,123 +33,107 @@ 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())
|
return 1
|
||||||
return 1
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var diags tfdiags.Diagnostics
|
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())
|
|
||||||
|
|
||||||
|
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)
|
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
|
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.
|
||||||
`
|
`
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
resource "baz_resource" "baz" {}
|
|
@ -0,0 +1,15 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
foo = {
|
||||||
|
version = "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "bar" {
|
||||||
|
version = "2.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "child" {
|
||||||
|
source = "./child"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
foo = {
|
||||||
|
version = "1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "bar" {
|
||||||
|
version = "2.0.0"
|
||||||
|
}
|
|
@ -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=="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue