From 9263b28e993b173aab8327688d523379b51af21a Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Tue, 9 Jun 2020 13:33:07 -0400 Subject: [PATCH] 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. --- command/init.go | 84 ++++++++++++++++++- command/init_test.go | 19 +++-- .../terraform-random-bar-1.0.0/main.tf | 7 ++ .../.terraform/modules/modules.json | 1 + .../child/main.tf | 3 + .../init-get-provider-detected-legacy/main.tf | 8 ++ configs/config.go | 8 +- configs/config_test.go | 12 +-- 8 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 command/testdata/init-get-provider-detected-legacy/.terraform/modules/dicerolls/terraform-random-bar-1.0.0/main.tf create mode 100644 command/testdata/init-get-provider-detected-legacy/.terraform/modules/modules.json create mode 100644 command/testdata/init-get-provider-detected-legacy/child/main.tf 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,