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:
Alisdair McDiarmid 2020-06-09 13:33:07 -04:00
parent 83d3e3518b
commit 9263b28e99
8 changed files with 126 additions and 16 deletions

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View 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":"."}]}

View File

@ -0,0 +1,3 @@
// This will try to install hashicorp/baz, fail, and then suggest
// terraform-providers/baz
provider baz {}

View File

@ -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"
}

View File

@ -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,
}

View File

@ -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,