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.
This commit is contained in:
James Bardin 2017-06-22 13:15:30 -04:00
parent 83622cf19b
commit 5be15ed77c
3 changed files with 47 additions and 8 deletions

View File

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

View File

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

View File

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