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:
Alisdair McDiarmid 2020-06-12 12:31:33 -04:00 committed by GitHub
commit 08b735984a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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)
}
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)
}
}
}

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,