command/init: Improve diags for legacy providers
When initializing a configuration which refers to re-namespaced legacy providers, we attempt to detect this and display a diagnostic message. Previously this message would direct the user to run the 0.13upgrade command, but without specifying in which directories. This commit detects which modules are using the providers in question, and for local modules displays a list of upgrade commands which specify the source directories of these modules. For remote modules, we display a separate list noting that they need to be upgraded elsewhere, providing both the local module call name and the module source address.
This commit is contained in:
parent
83d3e3518b
commit
9263b28e99
|
@ -607,6 +607,12 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
|
||||||
ctx := evts.OnContext(context.TODO())
|
ctx := evts.OnContext(context.TODO())
|
||||||
selected, err := inst.EnsureProviderVersions(ctx, reqs, mode)
|
selected, err := inst.EnsureProviderVersions(ctx, reqs, mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Build a map of provider address to modules using the provider,
|
||||||
|
// so that we can later show diagnostics about affected modules
|
||||||
|
reqs, _ := config.ProviderRequirementsByModule()
|
||||||
|
providerToReqs := make(map[addrs.Provider][]*configs.ModuleRequirements)
|
||||||
|
c.populateProviderToReqs(providerToReqs, reqs)
|
||||||
|
|
||||||
// Try to look up any missing providers which may be redirected legacy
|
// Try to look up any missing providers which may be redirected legacy
|
||||||
// providers. If we're successful, construct a "did you mean?" diag to
|
// providers. If we're successful, construct a "did you mean?" diag to
|
||||||
// suggest how to fix this. Otherwise, add a simple error diag
|
// suggest how to fix this. Otherwise, add a simple error diag
|
||||||
|
@ -627,14 +633,63 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(foundProviders) > 0 {
|
if len(foundProviders) > 0 {
|
||||||
|
// Build list of provider suggestions, and track a list of local
|
||||||
|
// and remote modules which need to be upgraded
|
||||||
var providerSuggestions string
|
var providerSuggestions string
|
||||||
|
localModules := make(map[string]struct{})
|
||||||
|
remoteModules := make(map[*configs.ModuleRequirements]struct{})
|
||||||
for missingProvider, foundProvider := range foundProviders {
|
for missingProvider, foundProvider := range foundProviders {
|
||||||
providerSuggestions += fmt.Sprintf(" %s -> %s\n", missingProvider.ForDisplay(), foundProvider.ForDisplay())
|
providerSuggestions += fmt.Sprintf(" %s -> %s\n", missingProvider.ForDisplay(), foundProvider.ForDisplay())
|
||||||
|
exists := struct{}{}
|
||||||
|
for _, reqs := range providerToReqs[missingProvider] {
|
||||||
|
src := reqs.SourceAddr
|
||||||
|
// Treat the root module and any others with local source
|
||||||
|
// addresses as fixable with 0.13upgrade. Remote modules
|
||||||
|
// must be upgraded elsewhere and therefore are listed
|
||||||
|
// separately
|
||||||
|
if src == "" || isLocalSourceAddr(src) {
|
||||||
|
localModules[reqs.SourceDir] = exists
|
||||||
|
} else {
|
||||||
|
remoteModules[reqs] = exists
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create sorted list of 0.13upgrade commands with the affected
|
||||||
|
// source dirs
|
||||||
|
var upgradeCommands []string
|
||||||
|
for dir := range localModules {
|
||||||
|
upgradeCommands = append(upgradeCommands, fmt.Sprintf("terraform 0.13upgrade %s", dir))
|
||||||
|
}
|
||||||
|
sort.Strings(upgradeCommands)
|
||||||
|
command := "command"
|
||||||
|
if len(upgradeCommands) > 1 {
|
||||||
|
command = "commands"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display detailed diagnostic results, including the missing and
|
||||||
|
// found provider FQNs, and the suggested series of upgrade
|
||||||
|
// commands to fix this
|
||||||
|
var detail strings.Builder
|
||||||
|
|
||||||
|
fmt.Fprintf(&detail, "Could not find required providers, but found possible alternatives:\n\n%s\n", providerSuggestions)
|
||||||
|
|
||||||
|
fmt.Fprintf(&detail, "If these suggestions look correct, upgrade your configuration with the following %s:", command)
|
||||||
|
for _, upgradeCommand := range upgradeCommands {
|
||||||
|
fmt.Fprintf(&detail, "\n %s", upgradeCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remoteModules) > 0 {
|
||||||
|
fmt.Fprintf(&detail, "\n\nThe following remote modules must also be upgraded for Terraform 0.13 compatibility:")
|
||||||
|
for remoteModule := range remoteModules {
|
||||||
|
fmt.Fprintf(&detail, "\n- module.%s at %s", remoteModule.Name, remoteModule.SourceAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Failed to install providers",
|
"Failed to install providers",
|
||||||
fmt.Sprintf("Could not find required providers, but found possible alternatives:\n\n%s\nIf these suggestions look correct, upgrade your configuration with the following command:\n terraform 0.13upgrade", providerSuggestions),
|
detail.String(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -675,6 +730,16 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
|
||||||
return true, diags
|
return true, diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *InitCommand) populateProviderToReqs(reqs map[addrs.Provider][]*configs.ModuleRequirements, node *configs.ModuleRequirements) {
|
||||||
|
for fqn := range node.Requirements {
|
||||||
|
reqs[fqn] = append(reqs[fqn], node)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
c.populateProviderToReqs(reqs, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// backendConfigOverrideBody interprets the raw values of -backend-config
|
// backendConfigOverrideBody interprets the raw values of -backend-config
|
||||||
// arguments into a hcl Body that should override the backend settings given
|
// arguments into a hcl Body that should override the backend settings given
|
||||||
// in the configuration.
|
// in the configuration.
|
||||||
|
@ -1045,3 +1110,20 @@ Alternatively, upgrade to the latest version of Terraform for compatibility with
|
||||||
|
|
||||||
// No version of the provider is compatible.
|
// No version of the provider is compatible.
|
||||||
const errProviderVersionIncompatible = `No compatible versions of provider %s were found.`
|
const errProviderVersionIncompatible = `No compatible versions of provider %s were found.`
|
||||||
|
|
||||||
|
// Logic from internal/initwd/getter.go
|
||||||
|
var localSourcePrefixes = []string{
|
||||||
|
"./",
|
||||||
|
"../",
|
||||||
|
".\\",
|
||||||
|
"..\\",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocalSourceAddr(addr string) bool {
|
||||||
|
for _, prefix := range localSourcePrefixes {
|
||||||
|
if strings.HasPrefix(addr, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -951,14 +951,19 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) {
|
||||||
|
|
||||||
// error output is the main focus of this test
|
// error output is the main focus of this test
|
||||||
errOutput := ui.ErrorWriter.String()
|
errOutput := ui.ErrorWriter.String()
|
||||||
if !strings.Contains(errOutput, "Error while installing hashicorp/frob:") {
|
errors := []string{
|
||||||
t.Fatalf("expected error for installing hashicorp/frob: %s", errOutput)
|
"Error while installing hashicorp/frob:",
|
||||||
|
"Could not find required providers, but found possible alternatives",
|
||||||
|
"hashicorp/baz -> terraform-providers/baz",
|
||||||
|
"terraform 0.13upgrade .",
|
||||||
|
"terraform 0.13upgrade child",
|
||||||
|
"The following remote modules must also be upgraded",
|
||||||
|
"- module.dicerolls at acme/bar/random",
|
||||||
}
|
}
|
||||||
if !strings.Contains(errOutput, "Could not find required providers, but found possible alternatives") {
|
for _, want := range errors {
|
||||||
t.Fatalf("expected required provider suggestions: %s", errOutput)
|
if !strings.Contains(errOutput, want) {
|
||||||
}
|
t.Fatalf("expected error %q: %s", want, errOutput)
|
||||||
if !strings.Contains(errOutput, "hashicorp/baz -> terraform-providers/baz") {
|
}
|
||||||
t.Fatalf("expected suggestion for hashicorp/baz: %s", errOutput)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This will try to install hashicorp/baz, fail, and then suggest
|
||||||
|
// terraform-providers/baz
|
||||||
|
provider baz {}
|
||||||
|
|
||||||
|
output "d6" {
|
||||||
|
value = 4 // chosen by fair dice roll, guaranteed to be random
|
||||||
|
}
|
1
command/testdata/init-get-provider-detected-legacy/.terraform/modules/modules.json
vendored
Normal file
1
command/testdata/init-get-provider-detected-legacy/.terraform/modules/modules.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"Modules":[{"Key":"dicerolls","Source":"acme/bar/random","Version":"1.0.0","Dir":".terraform/modules/dicerolls/terraform-random-bar-1.0.0"},{"Key":"","Source":"","Dir":"."}]}
|
|
@ -0,0 +1,3 @@
|
||||||
|
// This will try to install hashicorp/baz, fail, and then suggest
|
||||||
|
// terraform-providers/baz
|
||||||
|
provider baz {}
|
|
@ -8,3 +8,11 @@ provider baz {}
|
||||||
// This will try to install hashicrop/frob, fail, find no suggestions, and
|
// This will try to install hashicrop/frob, fail, find no suggestions, and
|
||||||
// result in an error
|
// result in an error
|
||||||
provider frob {}
|
provider frob {}
|
||||||
|
|
||||||
|
module "some-baz-stuff" {
|
||||||
|
source = "./child"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "dicerolls" {
|
||||||
|
source = "acme/bar/random"
|
||||||
|
}
|
||||||
|
|
|
@ -81,7 +81,9 @@ type Config struct {
|
||||||
// module, along with references to any child modules. This is used to
|
// module, along with references to any child modules. This is used to
|
||||||
// determine which modules require which providers.
|
// determine which modules require which providers.
|
||||||
type ModuleRequirements struct {
|
type ModuleRequirements struct {
|
||||||
Module *Module
|
Name string
|
||||||
|
SourceAddr string
|
||||||
|
SourceDir string
|
||||||
Requirements getproviders.Requirements
|
Requirements getproviders.Requirements
|
||||||
Children map[string]*ModuleRequirements
|
Children map[string]*ModuleRequirements
|
||||||
}
|
}
|
||||||
|
@ -206,12 +208,14 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno
|
||||||
children := make(map[string]*ModuleRequirements)
|
children := make(map[string]*ModuleRequirements)
|
||||||
for name, child := range c.Children {
|
for name, child := range c.Children {
|
||||||
childReqs, childDiags := child.ProviderRequirementsByModule()
|
childReqs, childDiags := child.ProviderRequirementsByModule()
|
||||||
|
childReqs.Name = name
|
||||||
children[name] = childReqs
|
children[name] = childReqs
|
||||||
diags = append(diags, childDiags...)
|
diags = append(diags, childDiags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := &ModuleRequirements{
|
ret := &ModuleRequirements{
|
||||||
Module: c.Module,
|
SourceAddr: c.SourceAddr,
|
||||||
|
SourceDir: c.Module.SourceDir,
|
||||||
Requirements: reqs,
|
Requirements: reqs,
|
||||||
Children: children,
|
Children: children,
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,12 +169,10 @@ func TestConfigProviderRequirementsByModule(t *testing.T) {
|
||||||
|
|
||||||
got, diags := cfg.ProviderRequirementsByModule()
|
got, diags := cfg.ProviderRequirementsByModule()
|
||||||
assertNoDiagnostics(t, diags)
|
assertNoDiagnostics(t, diags)
|
||||||
child, ok := cfg.Children["kinder"]
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf(`could not find child config "kinder" in config children`)
|
|
||||||
}
|
|
||||||
want := &ModuleRequirements{
|
want := &ModuleRequirements{
|
||||||
Module: cfg.Module,
|
Name: "",
|
||||||
|
SourceAddr: "",
|
||||||
|
SourceDir: "testdata/provider-reqs",
|
||||||
Requirements: getproviders.Requirements{
|
Requirements: getproviders.Requirements{
|
||||||
// Only the root module's version is present here
|
// Only the root module's version is present here
|
||||||
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
|
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
|
||||||
|
@ -186,7 +184,9 @@ func TestConfigProviderRequirementsByModule(t *testing.T) {
|
||||||
},
|
},
|
||||||
Children: map[string]*ModuleRequirements{
|
Children: map[string]*ModuleRequirements{
|
||||||
"kinder": {
|
"kinder": {
|
||||||
Module: child.Module,
|
Name: "kinder",
|
||||||
|
SourceAddr: "./child",
|
||||||
|
SourceDir: "testdata/provider-reqs/child",
|
||||||
Requirements: getproviders.Requirements{
|
Requirements: getproviders.Requirements{
|
||||||
nullProvider: getproviders.MustParseVersionConstraints("= 2.0.1"),
|
nullProvider: getproviders.MustParseVersionConstraints("= 2.0.1"),
|
||||||
happycloudProvider: nil,
|
happycloudProvider: nil,
|
||||||
|
|
Loading…
Reference in New Issue