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())
|
||||
selected, err := inst.EnsureProviderVersions(ctx, reqs, mode)
|
||||
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
|
||||
// providers. If we're successful, construct a "did you mean?" diag to
|
||||
// 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 {
|
||||
// Build list of provider suggestions, and track a list of local
|
||||
// and remote modules which need to be upgraded
|
||||
var providerSuggestions string
|
||||
localModules := make(map[string]struct{})
|
||||
remoteModules := make(map[*configs.ModuleRequirements]struct{})
|
||||
for missingProvider, foundProvider := range foundProviders {
|
||||
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(
|
||||
tfdiags.Error,
|
||||
"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
|
||||
}
|
||||
|
||||
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
|
||||
// arguments into a hcl Body that should override the backend settings given
|
||||
// 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.
|
||||
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
|
||||
errOutput := ui.ErrorWriter.String()
|
||||
if !strings.Contains(errOutput, "Error while installing hashicorp/frob:") {
|
||||
t.Fatalf("expected error for installing hashicorp/frob: %s", errOutput)
|
||||
errors := []string{
|
||||
"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") {
|
||||
t.Fatalf("expected required provider suggestions: %s", errOutput)
|
||||
for _, want := range errors {
|
||||
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
|
||||
// result in an error
|
||||
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
|
||||
// determine which modules require which providers.
|
||||
type ModuleRequirements struct {
|
||||
Module *Module
|
||||
Name string
|
||||
SourceAddr string
|
||||
SourceDir string
|
||||
Requirements getproviders.Requirements
|
||||
Children map[string]*ModuleRequirements
|
||||
}
|
||||
|
@ -206,12 +208,14 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno
|
|||
children := make(map[string]*ModuleRequirements)
|
||||
for name, child := range c.Children {
|
||||
childReqs, childDiags := child.ProviderRequirementsByModule()
|
||||
childReqs.Name = name
|
||||
children[name] = childReqs
|
||||
diags = append(diags, childDiags...)
|
||||
}
|
||||
|
||||
ret := &ModuleRequirements{
|
||||
Module: c.Module,
|
||||
SourceAddr: c.SourceAddr,
|
||||
SourceDir: c.Module.SourceDir,
|
||||
Requirements: reqs,
|
||||
Children: children,
|
||||
}
|
||||
|
|
|
@ -169,12 +169,10 @@ func TestConfigProviderRequirementsByModule(t *testing.T) {
|
|||
|
||||
got, diags := cfg.ProviderRequirementsByModule()
|
||||
assertNoDiagnostics(t, diags)
|
||||
child, ok := cfg.Children["kinder"]
|
||||
if !ok {
|
||||
t.Fatalf(`could not find child config "kinder" in config children`)
|
||||
}
|
||||
want := &ModuleRequirements{
|
||||
Module: cfg.Module,
|
||||
Name: "",
|
||||
SourceAddr: "",
|
||||
SourceDir: "testdata/provider-reqs",
|
||||
Requirements: getproviders.Requirements{
|
||||
// Only the root module's version is present here
|
||||
nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"),
|
||||
|
@ -186,7 +184,9 @@ func TestConfigProviderRequirementsByModule(t *testing.T) {
|
|||
},
|
||||
Children: map[string]*ModuleRequirements{
|
||||
"kinder": {
|
||||
Module: child.Module,
|
||||
Name: "kinder",
|
||||
SourceAddr: "./child",
|
||||
SourceDir: "testdata/provider-reqs/child",
|
||||
Requirements: getproviders.Requirements{
|
||||
nullProvider: getproviders.MustParseVersionConstraints("= 2.0.1"),
|
||||
happycloudProvider: nil,
|
||||
|
|
Loading…
Reference in New Issue