Merge pull request #25191 from hashicorp/alisdair/better-provider-upgrade-hints-on-init
command/init: Improve diags for legacy providers
This commit is contained in:
commit
08b735984a
|
@ -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