diff --git a/command/meta.go b/command/meta.go index 40d13aa1e..4e3233362 100644 --- a/command/meta.go +++ b/command/meta.go @@ -119,8 +119,8 @@ type PluginOverrides struct { } type testingOverrides struct { - Providers map[string]terraform.ResourceProviderFactory - Provisioners map[string]terraform.ResourceProvisionerFactory + ProviderResolver terraform.ResourceProviderResolver + Provisioners map[string]terraform.ResourceProvisionerFactory } // initStatePaths is used to initialize the default values for @@ -237,10 +237,10 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { // and just work with what we've been given, thus allowing the tests // to provide mock providers and provisioners. if m.testingOverrides != nil { - opts.Providers = m.testingOverrides.Providers + opts.ProviderResolver = m.testingOverrides.ProviderResolver opts.Provisioners = m.testingOverrides.Provisioners } else { - opts.Providers = m.providerFactories() + opts.ProviderResolver = m.providerResolver() opts.Provisioners = m.provisionerFactories() } diff --git a/command/plugins.go b/command/plugins.go index fa4e48ab4..a0f94bc1f 100644 --- a/command/plugins.go +++ b/command/plugins.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "log" "os/exec" "strings" @@ -11,7 +12,35 @@ import ( "github.com/hashicorp/terraform/terraform" ) -func (m *Meta) providerFactories() map[string]terraform.ResourceProviderFactory { +// multiVersionProviderResolver is an implementation of +// terraform.ResourceProviderResolver that matches the given version constraints +// against a set of versioned provider plugins to find the newest version of +// each that satisfies the given constraints. +type multiVersionProviderResolver struct { + Available discovery.PluginMetaSet +} + +func (r *multiVersionProviderResolver) ResolveProviders( + reqd discovery.PluginRequirements, +) (map[string]terraform.ResourceProviderFactory, []error) { + factories := make(map[string]terraform.ResourceProviderFactory, len(reqd)) + var errs []error + + candidates := r.Available.ConstrainVersions(reqd) + for name := range reqd { + if metas := candidates[name]; metas != nil { + newest := metas.Newest() + client := tfplugin.Client(newest) + factories[name] = providerFactory(client) + } else { + errs = append(errs, fmt.Errorf("provider.%s: no suitable version installed", name)) + } + } + + return factories, errs +} + +func (m *Meta) providerResolver() terraform.ResourceProviderResolver { var dirs []string // When searching the following directories, earlier entries get precedence @@ -25,36 +54,9 @@ func (m *Meta) providerFactories() map[string]terraform.ResourceProviderFactory plugins := discovery.FindPlugins("provider", dirs) plugins, _ = plugins.ValidateVersions() - // For now our goal is to just find the latest version of each plugin - // we have on the system, emulating our pre-versioning behavior. - // TODO: Reorganize how providers are handled so that we can use - // version constraints from configuration to select which plugins - // we will use when multiple are available. - - factories := make(map[string]terraform.ResourceProviderFactory) - - // Wire up the internal provisioners first. These might be overridden - // by discovered providers below. - for name := range InternalProviders { - client, err := internalPluginClient("provider", name) - if err != nil { - log.Printf("[WARN] failed to build command line for internal plugin %q: %s", name, err) - continue - } - factories[name] = providerFactory(client) + return &multiVersionProviderResolver{ + Available: plugins, } - - byName := plugins.ByName() - for name, metas := range byName { - // Since we validated versions above and we partitioned the sets - // by name, we're guaranteed that the metas in our set all have - // valid versions and that there's at least one meta. - newest := metas.Newest() - client := tfplugin.Client(newest) - factories[name] = providerFactory(client) - } - - return factories } func (m *Meta) provisionerFactories() map[string]terraform.ResourceProvisionerFactory { diff --git a/terraform/context.go b/terraform/context.go index 306128edf..e344b306d 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -57,7 +57,7 @@ type ContextOpts struct { Parallelism int State *State StateFutureAllowed bool - Providers map[string]ResourceProviderFactory + ProviderResolver ResourceProviderResolver Provisioners map[string]ResourceProvisionerFactory Shadow bool Targets []string @@ -166,7 +166,6 @@ func NewContext(opts *ContextOpts) (*Context, error) { // set by environment variables if necessary. This includes // values taken from -var-file in addition. variables := make(map[string]interface{}) - if opts.Module != nil { var err error variables, err = Variables(opts.Module, opts.Variables) @@ -175,6 +174,20 @@ func NewContext(opts *ContextOpts) (*Context, error) { } } + // Bind available provider plugins to the constraints in config + var providers map[string]ResourceProviderFactory + if opts.ProviderResolver != nil { + var err error + deps := moduleTreeDependencies(opts.Module, state) + reqd := deps.AllPluginRequirements() + providers, err = resourceProviderFactories(opts.ProviderResolver, reqd) + if err != nil { + return nil, err + } + } else { + providers = make(map[string]ResourceProviderFactory) + } + diff := opts.Diff if diff == nil { diff = &Diff{} @@ -182,7 +195,7 @@ func NewContext(opts *ContextOpts) (*Context, error) { return &Context{ components: &basicComponentFactory{ - providers: opts.Providers, + providers: providers, provisioners: opts.Provisioners, }, destroy: opts.Destroy, diff --git a/terraform/resource_provider.go b/terraform/resource_provider.go index 949d426e7..37d59e480 100644 --- a/terraform/resource_provider.go +++ b/terraform/resource_provider.go @@ -1,6 +1,8 @@ package terraform import ( + "bytes" + "errors" "fmt" "github.com/hashicorp/terraform/plugin/discovery" @@ -252,3 +254,25 @@ func ProviderHasDataSource(p ResourceProvider, n string) bool { return false } + +// resourceProviderFactories matches available plugins to the given version +// requirements to produce a map of compatible provider plugins if possible, +// or an error if the currently-available plugins are insufficient. +// +// This should be called only with configurations that have passed calls +// to config.Validate(), which ensures that all of the given version +// constraints are valid. It will panic if any invalid constraints are present. +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) + } + errBuf.WriteString("\nRun 'terraform init' to install the necessary provider plugins.\n") + return nil, errors.New(errBuf.String()) + } + + return ret, nil +}