diff --git a/command/init.go b/command/init.go index 7fa88b24b..4297f6ab2 100644 --- a/command/init.go +++ b/command/init.go @@ -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 +} diff --git a/command/init_test.go b/command/init_test.go index 194049d8b..7ba7a6815 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -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) - } - if !strings.Contains(errOutput, "hashicorp/baz -> terraform-providers/baz") { - t.Fatalf("expected suggestion for hashicorp/baz: %s", errOutput) + for _, want := range errors { + if !strings.Contains(errOutput, want) { + t.Fatalf("expected error %q: %s", want, errOutput) + } } } diff --git a/command/testdata/init-get-provider-detected-legacy/.terraform/modules/dicerolls/terraform-random-bar-1.0.0/main.tf b/command/testdata/init-get-provider-detected-legacy/.terraform/modules/dicerolls/terraform-random-bar-1.0.0/main.tf new file mode 100644 index 000000000..ae4c998a2 --- /dev/null +++ b/command/testdata/init-get-provider-detected-legacy/.terraform/modules/dicerolls/terraform-random-bar-1.0.0/main.tf @@ -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 +} diff --git a/command/testdata/init-get-provider-detected-legacy/.terraform/modules/modules.json b/command/testdata/init-get-provider-detected-legacy/.terraform/modules/modules.json new file mode 100644 index 000000000..8ee988105 --- /dev/null +++ b/command/testdata/init-get-provider-detected-legacy/.terraform/modules/modules.json @@ -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":"."}]} diff --git a/command/testdata/init-get-provider-detected-legacy/child/main.tf b/command/testdata/init-get-provider-detected-legacy/child/main.tf new file mode 100644 index 000000000..6c8b883f4 --- /dev/null +++ b/command/testdata/init-get-provider-detected-legacy/child/main.tf @@ -0,0 +1,3 @@ +// This will try to install hashicorp/baz, fail, and then suggest +// terraform-providers/baz +provider baz {} diff --git a/command/testdata/init-get-provider-detected-legacy/main.tf b/command/testdata/init-get-provider-detected-legacy/main.tf index 467ecf3be..4ba7ef4d3 100644 --- a/command/testdata/init-get-provider-detected-legacy/main.tf +++ b/command/testdata/init-get-provider-detected-legacy/main.tf @@ -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" +} diff --git a/configs/config.go b/configs/config.go index fadc78c5f..82282a50e 100644 --- a/configs/config.go +++ b/configs/config.go @@ -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, } diff --git a/configs/config_test.go b/configs/config_test.go index f3481ae6f..6cfca7850 100644 --- a/configs/config_test.go +++ b/configs/config_test.go @@ -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,