From 5be15ed77cfa1d9a8417c1f69904753a733d28e1 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 22 Jun 2017 13:15:30 -0400 Subject: [PATCH 1/6] have the local backend provide a plugin init msg During plan and apply, because the provider constraints need to be built from a plan, they are not checked until the terraform.Context is created. Since the context is always requested by the backend during the Operation, the backend needs to be responsible for generating contextual error messages for the user. Instead of formatting the ResolveProviders errors during NewContext, return a special error type, ResourceProviderError to signal that init will be required. The backend can then extract and format the errors. --- backend/local/backend.go | 22 ++++++++++++++++++++++ backend/local/backend_local.go | 10 ++++++++++ terraform/resource_provider.go | 23 +++++++++++++++-------- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 595683258..02152c17c 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "sort" + "strings" "sync" "github.com/hashicorp/terraform/backend" @@ -407,3 +408,24 @@ func (b *Local) stateWorkspaceDir() string { return DefaultWorkspaceDir } + +func (b *Local) pluginInitRequired(providerErr *terraform.ResourceProviderError) { + b.CLI.Output(b.Colorize().Color(fmt.Sprintf( + strings.TrimSpace(errPluginInit)+"\n", + "Could not satisfy plugin requirements", + providerErr))) +} + +const errPluginInit = ` +[reset][bold][yellow]Plugin reinitialization required. Please run "terraform init".[reset] +[yellow]Reason: %s + +Plugins are external binaries that Terraform uses to access and manipulate +resources. If this message is showing up, it means that the configuration you +have references plugins which can't be located, don't satisfy the version +constraints, or are otherwise incompatible. + +The errors encountered discovering plugins are: + +%s +` diff --git a/backend/local/backend_local.go b/backend/local/backend_local.go index 7e5c872a1..0b829b611 100644 --- a/backend/local/backend_local.go +++ b/backend/local/backend_local.go @@ -1,6 +1,7 @@ package local import ( + "errors" "fmt" "log" "strings" @@ -57,6 +58,15 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State, } else { tfCtx, err = terraform.NewContext(&opts) } + + // any errors resolving plugins returns this + if rpe, ok := err.(*terraform.ResourceProviderError); ok { + b.pluginInitRequired(rpe) + // we wrote the full UI error here, so return a generic error for flow + // control in the command. + return nil, nil, errors.New("error satisfying plugin requirements") + } + if err != nil { return nil, nil, err } diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index 37d59e480..7d78f67ef 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -1,10 +1,9 @@ package terraform import ( - "bytes" - "errors" "fmt" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/plugin/discovery" ) @@ -162,6 +161,18 @@ type ResourceProvider interface { ReadDataApply(*InstanceInfo, *InstanceDiff) (*InstanceState, error) } +// ResourceProviderError may be returned when creating a Context if the +// required providers cannot be satisfied. This error can then be used to +// format a more useful message for the user. +type ResourceProviderError struct { + Errors []error +} + +func (e *ResourceProviderError) Error() string { + // use multierror to format the default output + return multierror.Append(nil, e.Errors...).Error() +} + // ResourceProviderCloser is an interface that providers that can close // connections that aren't needed anymore must implement. type ResourceProviderCloser interface { @@ -265,13 +276,9 @@ func ProviderHasDataSource(p ResourceProvider, n string) bool { func resourceProviderFactories(resolver ResourceProviderResolver, reqd discovery.PluginRequirements) (map[string]ResourceProviderFactory, error) { ret, errs := resolver.ResolveProviders(reqd) if errs != nil { - errBuf := &bytes.Buffer{} - errBuf.WriteString("Can't satisfy provider requirements with currently-installed plugins:\n\n") - for _, err := range errs { - fmt.Fprintf(errBuf, "* %s\n", err) + return nil, &ResourceProviderError{ + Errors: errs, } - errBuf.WriteString("\nRun 'terraform init' to install the necessary provider plugins.\n") - return nil, errors.New(errBuf.String()) } return ret, nil From 7a955f990c20a70fa2b3006b9c82327cd4375b51 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 22 Jun 2017 13:38:55 -0400 Subject: [PATCH 2/6] make display plugin checksum error to user The error follows a generic message, so can be ignored by users who may not understand the implications. --- backend/local/backend.go | 8 +++----- command/plugins.go | 7 +------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 02152c17c..62b9174d8 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -412,20 +412,18 @@ func (b *Local) stateWorkspaceDir() string { func (b *Local) pluginInitRequired(providerErr *terraform.ResourceProviderError) { b.CLI.Output(b.Colorize().Color(fmt.Sprintf( strings.TrimSpace(errPluginInit)+"\n", - "Could not satisfy plugin requirements", providerErr))) } +// this relies on multierror to format the plugin errors below the copy const errPluginInit = ` [reset][bold][yellow]Plugin reinitialization required. Please run "terraform init".[reset] -[yellow]Reason: %s +[yellow]Reason: Could not satisfy plugin requirements. Plugins are external binaries that Terraform uses to access and manipulate resources. If this message is showing up, it means that the configuration you have references plugins which can't be located, don't satisfy the version constraints, or are otherwise incompatible. -The errors encountered discovering plugins are: - -%s +[reset][red]%s ` diff --git a/command/plugins.go b/command/plugins.go index 6ce642727..f71b2bc9c 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -53,12 +53,7 @@ func (r *multiVersionProviderResolver) ResolveProviders( continue } if !reqd[name].AcceptsSHA256(digest) { - // This generic error message is intended to avoid troubling - // users with implementation details. The main useful point - // here is that they need to run "terraform init" to - // fix this, which is covered by the UI code reporting these - // error messages. - errs = append(errs, fmt.Errorf("provider.%s: installed but not yet initialized", name)) + errs = append(errs, fmt.Errorf("provider.%s: checksum mismatch", name)) continue } From 103ab20b009f417445cd9fd37c536f60b96b83aa Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 22 Jun 2017 14:18:52 -0400 Subject: [PATCH 3/6] add required and found versions to error output Provide the user with feedback showing the version requirements and the versions found when there's is a plugin error --- command/plugins.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/command/plugins.go b/command/plugins.go index f71b2bc9c..dd82ebc8a 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -2,6 +2,7 @@ package command import ( "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -60,7 +61,23 @@ func (r *multiVersionProviderResolver) ResolveProviders( client := tfplugin.Client(newest) factories[name] = providerFactory(client) } else { - errs = append(errs, fmt.Errorf("provider.%s: no suitable version installed", name)) + msg := fmt.Sprintf("provider.%s: no suitable version installed", name) + + if req := reqd[name]; req.Versions.String() != "" { + foundVersions := []string{} + for meta := range r.Available.WithName(name) { + foundVersions = append(foundVersions, fmt.Sprintf("%q", meta.Version)) + } + + found := "none" + if len(foundVersions) > 0 { + found = strings.Join(foundVersions, ", ") + } + + msg += fmt.Sprintf("\n version requirements: %q\n versions found: %s", req.Versions, found) + } + + errs = append(errs, errors.New(msg)) } } From ac937a890dbbc270cfa40c401183fe2cea2b846e Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 22 Jun 2017 15:11:37 -0400 Subject: [PATCH 4/6] improve plugin reinit error text --- backend/local/backend.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/local/backend.go b/backend/local/backend.go index 62b9174d8..054a4659b 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -421,9 +421,12 @@ const errPluginInit = ` [yellow]Reason: Could not satisfy plugin requirements. Plugins are external binaries that Terraform uses to access and manipulate -resources. If this message is showing up, it means that the configuration you -have references plugins which can't be located, don't satisfy the version -constraints, or are otherwise incompatible. +resources. The configuration provided requires plugins which can't be located, +don't satisfy the version constraints, or are otherwise incompatible. [reset][red]%s + +[reset][yellow]Terraform automatically discovers provider requirements from your +configuration, including providers used in child modules. To see the +requirements and constraints from each module, run "terraform providers". ` From 4893fcc24fe55f619fe8e44fb1868fe015104cf8 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 22 Jun 2017 15:28:39 -0400 Subject: [PATCH 5/6] improve plugin error formatting Remove "checksum" from the error, and only indicate that the plugin has changed. Always show requested versions even if it's "any", and found versions of plugins. --- command/plugins.go | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/command/plugins.go b/command/plugins.go index dd82ebc8a..ca94f07b9 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -46,7 +46,7 @@ func (r *multiVersionProviderResolver) ResolveProviders( var errs []error chosen := choosePlugins(r.Available, reqd) - for name := range reqd { + for name, req := range reqd { if newest, available := chosen[name]; available { digest, err := newest.SHA256() if err != nil { @@ -54,7 +54,7 @@ func (r *multiVersionProviderResolver) ResolveProviders( continue } if !reqd[name].AcceptsSHA256(digest) { - errs = append(errs, fmt.Errorf("provider.%s: checksum mismatch", name)) + errs = append(errs, fmt.Errorf("provider.%s: new or changed plugin executable", name)) continue } @@ -63,20 +63,24 @@ func (r *multiVersionProviderResolver) ResolveProviders( } else { msg := fmt.Sprintf("provider.%s: no suitable version installed", name) - if req := reqd[name]; req.Versions.String() != "" { - foundVersions := []string{} - for meta := range r.Available.WithName(name) { - foundVersions = append(foundVersions, fmt.Sprintf("%q", meta.Version)) - } - - found := "none" - if len(foundVersions) > 0 { - found = strings.Join(foundVersions, ", ") - } - - msg += fmt.Sprintf("\n version requirements: %q\n versions found: %s", req.Versions, found) + required := req.Versions.String() + // no version is unconstrained + if required == "" { + required = "(any version)" } + foundVersions := []string{} + for meta := range r.Available.WithName(name) { + foundVersions = append(foundVersions, fmt.Sprintf("%q", meta.Version)) + } + + found := "none" + if len(foundVersions) > 0 { + found = strings.Join(foundVersions, ", ") + } + + msg += fmt.Sprintf("\n version requirements: %q\n versions installed: %s", required, found) + errs = append(errs, errors.New(msg)) } } From b14677bd9a7a6ef38e5c96bd0bb530054f8639b3 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Thu, 22 Jun 2017 15:37:32 -0400 Subject: [PATCH 6/6] look for new error output --- terraform/context_refresh_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terraform/context_refresh_test.go b/terraform/context_refresh_test.go index 0a977b4cc..0e84b8b54 100644 --- a/terraform/context_refresh_test.go +++ b/terraform/context_refresh_test.go @@ -2,6 +2,7 @@ package terraform import ( "reflect" + "regexp" "sort" "strings" "sync" @@ -861,7 +862,7 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { t.Fatal("successfully created context; want error") } - if !strings.Contains(err.Error(), "Can't satisfy provider requirements") { + if !regexp.MustCompile(`provider ".+" is not available`).MatchString(err.Error()) { t.Fatalf("wrong error: %s", err) } }