Remove terraform.ResourceProvider, use providercache.Installer instead

Back when we first introduced provider versioning in Terraform 0.10, we
did the provider version resolution in terraform.NewContext because we
weren't sure yet how exactly our versioning model was going to play out
(whether different versions could be selected per provider configuration,
for example) and because we were building around the limitations of our
existing filesystem-based plugin discovery model.

However, the new installer codepath is new able to do all of the
selections up front during installation, so we don't need such a heavy
inversion of control abstraction to get this done: the command package can
select the exact provider versions and pass their factories directly
to terraform.NewContext as a simple static map.

The result of this commit is that CLI commands other than "init" are now
able to consume the local cache directory and selections produced by the
installation process in "terraform init", passing all of the selected
providers down to the terraform.NewContext function for use in
implementing the main operations.

This commit is just enough to get the providers passing into the
terraform.Context. There's still plenty more to do here, including to
repair all of the tests this change has additionally broken.
This commit is contained in:
Martin Atkins 2020-03-30 15:30:56 -07:00
parent 5aa2e5ec8c
commit 549aede792
13 changed files with 235 additions and 482 deletions

View File

@ -112,11 +112,9 @@ func TestLocalProvider(t *testing.T, b *Local, name string, schema *terraform.Pr
} }
// Setup our provider // Setup our provider
b.ContextOpts.ProviderResolver = providers.ResolverFixed( b.ContextOpts.Providers = map[addrs.Provider]providers.Factory{
map[addrs.Provider]providers.Factory{ addrs.NewLegacyProvider(name): providers.FactoryFixed(p),
addrs.NewLegacyProvider(name): providers.FactoryFixed(p), }
},
)
return p return p

View File

@ -206,7 +206,7 @@ func TestApply_parallelism(t *testing.T) {
providerFactories[addrs.NewLegacyProvider(name)] = providers.FactoryFixed(provider) providerFactories[addrs.NewLegacyProvider(name)] = providers.FactoryFixed(provider)
} }
testingOverrides := &testingOverrides{ testingOverrides := &testingOverrides{
ProviderResolver: providers.ResolverFixed(providerFactories), Providers: providerFactories,
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)

View File

@ -119,21 +119,17 @@ func testFixturePath(name string) string {
func metaOverridesForProvider(p providers.Interface) *testingOverrides { func metaOverridesForProvider(p providers.Interface) *testingOverrides {
return &testingOverrides{ return &testingOverrides{
ProviderResolver: providers.ResolverFixed( Providers: map[addrs.Provider]providers.Factory{
map[addrs.Provider]providers.Factory{ addrs.NewLegacyProvider("test"): providers.FactoryFixed(p),
addrs.NewLegacyProvider("test"): providers.FactoryFixed(p), },
},
),
} }
} }
func metaOverridesForProviderAndProvisioner(p providers.Interface, pr provisioners.Interface) *testingOverrides { func metaOverridesForProviderAndProvisioner(p providers.Interface, pr provisioners.Interface) *testingOverrides {
return &testingOverrides{ return &testingOverrides{
ProviderResolver: providers.ResolverFixed( Providers: map[addrs.Provider]providers.Factory{
map[addrs.Provider]providers.Factory{ addrs.NewLegacyProvider("test"): providers.FactoryFixed(p),
addrs.NewLegacyProvider("test"): providers.FactoryFixed(p), },
},
),
Provisioners: map[string]provisioners.Factory{ Provisioners: map[string]provisioners.Factory{
"shell": provisioners.FactoryFixed(pr), "shell": provisioners.FactoryFixed(pr),
}, },

View File

@ -20,7 +20,6 @@ import (
"github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/providercache" "github.com/hashicorp/terraform/internal/providercache"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -32,12 +31,6 @@ type InitCommand struct {
// getPlugins is for the -get-plugins flag // getPlugins is for the -get-plugins flag
getPlugins bool getPlugins bool
// providerInstaller is used to download and install providers that
// aren't found locally. This uses a discovery.ProviderInstaller instance
// by default, but it can be overridden here as a way to mock fetching
// providers for tests.
providerInstaller discovery.Installer
} }
func (c *InitCommand) Run(args []string) int { func (c *InitCommand) Run(args []string) int {
@ -73,18 +66,6 @@ func (c *InitCommand) Run(args []string) int {
c.getPlugins = false c.getPlugins = false
} }
// set providerInstaller if we don't have a test version already
if c.providerInstaller == nil {
c.providerInstaller = &discovery.ProviderInstaller{
Dir: c.pluginDir(),
Cache: c.pluginCache(),
PluginProtocolVersion: discovery.PluginInstallProtocolVersion,
SkipVerify: !flagVerifyPlugins,
Ui: c.Ui,
Services: c.Services,
}
}
// Validate the arg count // Validate the arg count
args = cmdFlags.Args() args = cmdFlags.Args()
if len(args) > 1 { if len(args) > 1 {
@ -456,15 +437,9 @@ func (c *InitCommand) getProviders(earlyConfig *earlyconfig.Config, state *state
// TODO: If the user gave at least one -plugin-dir option on the command // TODO: If the user gave at least one -plugin-dir option on the command
// line, we should construct a one-off getproviders.Source that consults // line, we should construct a one-off getproviders.Source that consults
// only those directories and use that instead of c.providerInstallSource() // only those directories and pass that to c.providerInstallerCustomSource
// here. // instead.
targetDir := c.providerLocalCacheDir() inst := c.providerInstaller()
globalCacheDir := c.providerGlobalCacheDir()
source := c.providerInstallSource()
inst := providercache.NewInstaller(targetDir, source)
if globalCacheDir != nil {
inst.SetGlobalCacheDir(globalCacheDir)
}
// Because we're currently just streaming a series of events sequentially // Because we're currently just streaming a series of events sequentially
// into the terminal, we're showing only a subset of the events to keep // into the terminal, we're showing only a subset of the events to keep

View File

@ -187,8 +187,8 @@ type PluginOverrides struct {
} }
type testingOverrides struct { type testingOverrides struct {
ProviderResolver providers.Resolver Providers map[addrs.Provider]providers.Factory
Provisioners map[string]provisioners.Factory Provisioners map[string]provisioners.Factory
} }
// initStatePaths is used to initialize the default values for // initStatePaths is used to initialize the default values for
@ -350,10 +350,22 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
// and just work with what we've been given, thus allowing the tests // and just work with what we've been given, thus allowing the tests
// to provide mock providers and provisioners. // to provide mock providers and provisioners.
if m.testingOverrides != nil { if m.testingOverrides != nil {
opts.ProviderResolver = m.testingOverrides.ProviderResolver opts.Providers = m.testingOverrides.Providers
opts.Provisioners = m.testingOverrides.Provisioners opts.Provisioners = m.testingOverrides.Provisioners
} else { } else {
opts.ProviderResolver = m.providerResolver() providerFactories, err := m.providerFactories()
if err != nil {
// providerFactories can fail if the plugin selections file is
// invalid in some way, but we don't have any way to report that
// from here so we'll just behave as if no providers are available
// in that case. However, we will produce a warning in case this
// shows up unexpectedly and prompts a bug report.
// This situation shouldn't arise commonly in practice because
// the selections file is generated programmatically.
log.Printf("[WARN] Failed to determine selected providers: %s", err)
providerFactories = nil
}
opts.Providers = providerFactories
opts.Provisioners = m.provisionerFactories() opts.Provisioners = m.provisionerFactories()
} }

View File

@ -1,12 +1,66 @@
package command package command
import ( import (
"fmt"
"os"
"os/exec"
"path/filepath" "path/filepath"
hclog "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/addrs"
terraformProvider "github.com/hashicorp/terraform/builtin/providers/terraform"
"github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/providercache" "github.com/hashicorp/terraform/internal/providercache"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/providers"
) )
// The TF_DISABLE_PLUGIN_TLS environment variable is intended only for use by
// the plugin SDK test framework, to reduce startup overhead when rapidly
// launching and killing lots of instances of the same provider.
//
// This is not intended to be set by end-users.
var enableProviderAutoMTLS = os.Getenv("TF_DISABLE_PLUGIN_TLS") == ""
// providerInstaller returns an object that knows how to install providers and
// how to recover the selections from a prior installation process.
//
// The resulting provider installer is constructed from the results of
// the other methods providerLocalCacheDir, providerGlobalCacheDir, and
// providerInstallSource.
//
// Only one object returned from this method should be live at any time,
// because objects inside contain caches that must be maintained properly.
// Because this method wraps a result from providerLocalCacheDir, that
// limitation applies also to results from that method.
func (m *Meta) providerInstaller() *providercache.Installer {
return m.providerInstallerCustomSource(m.providerInstallSource())
}
// providerInstallerCustomSource is a variant of providerInstaller that
// allows the caller to specify a different installation source than the one
// that would naturally be selected.
//
// The result of this method has the same dependencies and constraints as
// providerInstaller.
//
// The result of providerInstallerCustomSource differs from
// providerInstaller only in how it determines package installation locations
// during EnsureProviderVersions. A caller that doesn't call
// EnsureProviderVersions (anything other than "terraform init") can safely
// just use the providerInstaller method unconditionally.
func (m *Meta) providerInstallerCustomSource(source getproviders.Source) *providercache.Installer {
targetDir := m.providerLocalCacheDir()
globalCacheDir := m.providerGlobalCacheDir()
inst := providercache.NewInstaller(targetDir, source)
if globalCacheDir != nil {
inst.SetGlobalCacheDir(globalCacheDir)
}
return inst
}
// providerLocalCacheDir returns an object representing the // providerLocalCacheDir returns an object representing the
// configuration-specific local cache directory. This is the // configuration-specific local cache directory. This is the
// only location consulted for provider plugin packages for Terraform // only location consulted for provider plugin packages for Terraform
@ -15,6 +69,9 @@ import (
// Only the provider installer (in "terraform init") is permitted to make // Only the provider installer (in "terraform init") is permitted to make
// modifications to this cache directory. All other commands must treat it // modifications to this cache directory. All other commands must treat it
// as read-only. // as read-only.
//
// Only one object returned from this method should be live at any time,
// because objects inside contain caches that must be maintained properly.
func (m *Meta) providerLocalCacheDir() *providercache.Dir { func (m *Meta) providerLocalCacheDir() *providercache.Dir {
dir := filepath.Join(m.DataDir(), "plugins") dir := filepath.Join(m.DataDir(), "plugins")
if dir == "" { if dir == "" {
@ -30,6 +87,9 @@ func (m *Meta) providerLocalCacheDir() *providercache.Dir {
// This function may return nil, in which case there is no global cache // This function may return nil, in which case there is no global cache
// configured and new packages should be downloaded directly into individual // configured and new packages should be downloaded directly into individual
// configuration-specific cache directories. // configuration-specific cache directories.
//
// Only one object returned from this method should be live at any time,
// because objects inside contain caches that must be maintained properly.
func (m *Meta) providerGlobalCacheDir() *providercache.Dir { func (m *Meta) providerGlobalCacheDir() *providercache.Dir {
dir := m.PluginCacheDir dir := m.PluginCacheDir
if dir == "" { if dir == "" {
@ -61,3 +121,85 @@ func (m *Meta) providerInstallSource() getproviders.Source {
} }
return m.ProviderSource return m.ProviderSource
} }
// providerFactories uses the selections made previously by an installer in
// the local cache directory (m.providerLocalCacheDir) to produce a map
// from provider addresses to factory functions to create instances of
// those providers.
//
// providerFactories will return an error if the installer's selections cannot
// be honored with what is currently in the cache, such as if a selected
// package has been removed from the cache or if the contents of a selected
// package have been modified outside of the installer. If it returns an error,
// the returned map may be incomplete or invalid.
func (m *Meta) providerFactories() (map[addrs.Provider]providers.Factory, error) {
// We don't have to worry about potentially calling
// providerInstallerCustomSource here because we're only using this
// installer for its SelectedPackages method, which does not consult
// any provider sources.
inst := m.providerInstaller()
selected, err := inst.SelectedPackages()
if err != nil {
return nil, fmt.Errorf("failed to recall provider packages selected by earlier 'terraform init': %s", err)
}
// The internal providers are _always_ available, even if the configuration
// doesn't request them, because they don't need any special installation
// and they'll just be ignored if not used.
internalFactories := m.internalProviders()
factories := make(map[addrs.Provider]providers.Factory, len(selected)+len(internalFactories))
for name, factory := range internalFactories {
factories[addrs.NewBuiltInProvider(name)] = factory
}
for provider, cached := range selected {
factories[provider] = providerFactory(cached)
}
return factories, nil
}
func (m *Meta) internalProviders() map[string]providers.Factory {
return map[string]providers.Factory{
"terraform": func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
}
}
// providerFactory produces a provider factory that runs up the executable
// file in the given cache package and uses go-plugin to implement
// providers.Interface against it.
func providerFactory(meta *providercache.CachedProvider) providers.Factory {
return func() (providers.Interface, error) {
logger := hclog.New(&hclog.LoggerOptions{
Name: "plugin",
Level: hclog.Trace,
Output: os.Stderr,
})
config := &plugin.ClientConfig{
Cmd: exec.Command(meta.ExecutableFile),
HandshakeConfig: tfplugin.Handshake,
VersionedPlugins: tfplugin.VersionedPlugins,
Managed: true,
Logger: logger,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
AutoMTLS: enableProviderAutoMTLS,
}
client := plugin.NewClient(config)
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
// store the client so that the plugin can kill the child process
p := raw.(*tfplugin.GRPCProvider)
p.PluginClient = client
return p, nil
}
}

View File

@ -2,7 +2,6 @@ package command
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -15,104 +14,19 @@ import (
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
"github.com/kardianos/osext" "github.com/kardianos/osext"
"github.com/hashicorp/terraform/addrs"
terraformProvider "github.com/hashicorp/terraform/builtin/providers/terraform"
tfplugin "github.com/hashicorp/terraform/plugin" tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/provisioners" "github.com/hashicorp/terraform/provisioners"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// multiVersionProviderResolver is an implementation of // NOTE WELL: The logic in this file is primarily about plugin types OTHER THAN
// terraform.ResourceProviderResolver that matches the given version constraints // providers, which use an older set of approaches implemented here.
// against a set of versioned provider plugins to find the newest version of //
// each that satisfies the given constraints. // The provider-related functions live primarily in meta_providers.go, and
type multiVersionProviderResolver struct { // lean on some different underlying mechanisms in order to support automatic
Available discovery.PluginMetaSet // installation and a heirarchical addressing namespace, neither of which
// are supported for other plugin types.
// Internal is a map that overrides the usual plugin selection process
// for internal plugins. These plugins do not support version constraints
// (will produce an error if one is set). This should be used only in
// exceptional circumstances since it forces the provider's release
// schedule to be tied to that of Terraform Core.
Internal map[addrs.Provider]providers.Factory
}
func chooseProviders(avail discovery.PluginMetaSet, internal map[addrs.Provider]providers.Factory, reqd discovery.PluginRequirements) map[string]discovery.PluginMeta {
candidates := avail.ConstrainVersions(reqd)
ret := map[string]discovery.PluginMeta{}
for name, metas := range candidates {
// If the provider is in our internal map then we ignore any
// discovered plugins for it since these are dealt with separately.
if _, isInternal := internal[addrs.NewLegacyProvider(name)]; isInternal {
continue
}
if len(metas) == 0 {
continue
}
ret[name] = metas.Newest()
}
return ret
}
func (r *multiVersionProviderResolver) ResolveProviders(
reqd discovery.PluginRequirements,
) (map[addrs.Provider]providers.Factory, []error) {
factories := make(map[addrs.Provider]providers.Factory, len(reqd))
var errs []error
chosen := chooseProviders(r.Available, r.Internal, reqd)
for name, req := range reqd {
if factory, isInternal := r.Internal[addrs.NewLegacyProvider(name)]; isInternal {
if !req.Versions.Unconstrained() {
errs = append(errs, fmt.Errorf("provider.%s: this provider is built in to Terraform and so it does not support version constraints", name))
continue
}
factories[addrs.NewLegacyProvider(name)] = factory
continue
}
if newest, available := chosen[name]; available {
digest, err := newest.SHA256()
if err != nil {
errs = append(errs, fmt.Errorf("provider.%s: failed to load plugin to verify its signature: %s", name, err))
continue
}
if !reqd[name].AcceptsSHA256(digest) {
errs = append(errs, fmt.Errorf("provider.%s: new or changed plugin executable", name))
continue
}
factories[addrs.NewLegacyProvider(name)] = providerFactory(newest)
} else {
msg := fmt.Sprintf("provider.%s: no suitable version installed", name)
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))
}
}
return factories, errs
}
// store the user-supplied path for plugin discovery // store the user-supplied path for plugin discovery
func (m *Meta) storePluginPath(pluginPath []string) error { func (m *Meta) storePluginPath(pluginPath []string) error {
@ -216,101 +130,6 @@ func (m *Meta) pluginCache() discovery.PluginCache {
return discovery.NewLocalPluginCache(dir) return discovery.NewLocalPluginCache(dir)
} }
// providerPluginSet returns the set of valid providers that were discovered in
// the defined search paths.
func (m *Meta) providerPluginSet() discovery.PluginMetaSet {
plugins := discovery.FindPlugins("provider", m.pluginDirs(true))
// Add providers defined in the legacy .terraformrc,
if m.PluginOverrides != nil {
for k, v := range m.PluginOverrides.Providers {
log.Printf("[DEBUG] found plugin override in .terraformrc: %q, %q", k, v)
}
plugins = plugins.OverridePaths(m.PluginOverrides.Providers)
}
plugins, _ = plugins.ValidateVersions()
for p := range plugins {
log.Printf("[DEBUG] found valid plugin: %q, %q, %q", p.Name, p.Version, p.Path)
}
return plugins
}
// providerPluginAutoInstalledSet returns the set of providers that exist
// within the auto-install directory.
func (m *Meta) providerPluginAutoInstalledSet() discovery.PluginMetaSet {
plugins := discovery.FindPlugins("provider", []string{m.pluginDir()})
plugins, _ = plugins.ValidateVersions()
for p := range plugins {
log.Printf("[DEBUG] found valid plugin: %q", p.Name)
}
return plugins
}
// providerPluginManuallyInstalledSet returns the set of providers that exist
// in all locations *except* the auto-install directory.
func (m *Meta) providerPluginManuallyInstalledSet() discovery.PluginMetaSet {
plugins := discovery.FindPlugins("provider", m.pluginDirs(false))
// Add providers defined in the legacy .terraformrc,
if m.PluginOverrides != nil {
for k, v := range m.PluginOverrides.Providers {
log.Printf("[DEBUG] found plugin override in .terraformrc: %q, %q", k, v)
}
plugins = plugins.OverridePaths(m.PluginOverrides.Providers)
}
plugins, _ = plugins.ValidateVersions()
for p := range plugins {
log.Printf("[DEBUG] found valid plugin: %q, %q, %q", p.Name, p.Version, p.Path)
}
return plugins
}
func (m *Meta) providerResolver() providers.Resolver {
return &multiVersionProviderResolver{
Available: m.providerPluginSet(),
Internal: m.internalProviders(),
}
}
func (m *Meta) internalProviders() map[addrs.Provider]providers.Factory {
return map[addrs.Provider]providers.Factory{
addrs.NewLegacyProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
}
}
// filter the requirements returning only the providers that we can't resolve
func (m *Meta) missingProviders(avail discovery.PluginMetaSet, reqd discovery.PluginRequirements) discovery.PluginRequirements {
missing := make(discovery.PluginRequirements)
candidates := avail.ConstrainVersions(reqd)
internal := m.internalProviders()
for name, versionSet := range reqd {
// internal providers can't be missing
if _, ok := internal[addrs.NewLegacyProvider(name)]; ok {
continue
}
log.Printf("[DEBUG] plugin requirements: %q=%q", name, versionSet.Versions)
if metas := candidates[name]; metas.Count() == 0 {
missing[name] = versionSet
}
}
return missing
}
func (m *Meta) provisionerFactories() map[string]terraform.ProvisionerFactory { func (m *Meta) provisionerFactories() map[string]terraform.ProvisionerFactory {
dirs := m.pluginDirs(true) dirs := m.pluginDirs(true)
plugins := discovery.FindPlugins("provisioner", dirs) plugins := discovery.FindPlugins("provisioner", dirs)
@ -364,28 +183,6 @@ func internalPluginClient(kind, name string) (*plugin.Client, error) {
return plugin.NewClient(cfg), nil return plugin.NewClient(cfg), nil
} }
func providerFactory(meta discovery.PluginMeta) providers.Factory {
return func() (providers.Interface, error) {
client := tfplugin.Client(meta)
// Request the RPC client so we can get the provider
// so we can build the actual RPC-implemented provider.
rpcClient, err := client.Client()
if err != nil {
return nil, err
}
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
// store the client so that the plugin can kill the child process
p := raw.(*tfplugin.GRPCProvider)
p.PluginClient = client
return p, nil
}
}
func provisionerFactory(meta discovery.PluginMeta) terraform.ProvisionerFactory { func provisionerFactory(meta discovery.PluginMeta) terraform.ProvisionerFactory {
return func() (provisioners.Interface, error) { return func() (provisioners.Interface, error) {
client := tfplugin.Client(meta) client := tfplugin.Client(meta)

View File

@ -229,11 +229,9 @@ func TestStateShow_configured_provider(t *testing.T) {
c := &StateShowCommand{ c := &StateShowCommand{
Meta: Meta{ Meta: Meta{
testingOverrides: &testingOverrides{ testingOverrides: &testingOverrides{
ProviderResolver: providers.ResolverFixed( Providers: map[addrs.Provider]providers.Factory{
map[addrs.Provider]providers.Factory{ addrs.NewLegacyProvider("test-beta"): providers.FactoryFixed(p),
addrs.NewLegacyProvider("test-beta"): providers.FactoryFixed(p), },
},
),
}, },
Ui: ui, Ui: ui,
}, },

View File

@ -55,33 +55,19 @@ func (c *VersionCommand) Run(args []string) int {
// Generally-speaking this is a best-effort thing that will give us a good // Generally-speaking this is a best-effort thing that will give us a good
// result in the usual case where the user successfully ran "terraform init" // result in the usual case where the user successfully ran "terraform init"
// and then hit a problem running _another_ command. // and then hit a problem running _another_ command.
providerPlugins := c.providerPluginSet() providerInstaller := c.providerInstaller()
pluginsLockFile := c.providerPluginsLock() providerSelections, err := providerInstaller.SelectedPackages()
pluginsLock := pluginsLockFile.Read()
var pluginVersions []string var pluginVersions []string
for meta := range providerPlugins { if err != nil {
name := meta.Name // we'll just ignore it and show no plugins at all, then.
wantHash, wanted := pluginsLock[name] providerSelections = nil
if !wanted { }
// Ignore providers that aren't used by the current config at all for providerAddr, cached := range providerSelections {
continue version := cached.Version.String()
} if version == "0.0.0" {
gotHash, err := meta.SHA256() pluginVersions = append(pluginVersions, fmt.Sprintf("+ provider %s (unversioned)", providerAddr))
if err != nil {
// if we can't read the file to hash it, ignore it.
continue
}
if !bytes.Equal(gotHash, wantHash) {
// Not the plugin we've locked, so ignore it.
continue
}
// If we get here then we've found a selected plugin, so we'll print
// out its details.
if meta.Version == "0.0.0" {
pluginVersions = append(pluginVersions, fmt.Sprintf("+ provider.%s (unversioned)", name))
} else { } else {
pluginVersions = append(pluginVersions, fmt.Sprintf("+ provider.%s v%s", name, meta.Version)) pluginVersions = append(pluginVersions, fmt.Sprintf("+ provider %s v%s", providerAddr, version))
} }
} }
if len(pluginVersions) != 0 { if len(pluginVersions) != 0 {

View File

@ -481,10 +481,19 @@ func Test(t TestT, c TestCase) {
c.PreCheck() c.PreCheck()
} }
providerFactories, err := testProviderFactories(c)
if err != nil {
t.Fatal(err)
}
// get instances of all providers, so we can use the individual // get instances of all providers, so we can use the individual
// resources to shim the state during the tests. // resources to shim the state during the tests.
providers := make(map[string]terraform.ResourceProvider) providers := make(map[string]terraform.ResourceProvider)
for name, pf := range testProviderFactories(c) { legacyProviderFactories, err := testProviderFactoriesLegacy(c)
if err != nil {
t.Fatal(err)
}
for name, pf := range legacyProviderFactories {
p, err := pf() p, err := pf()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -492,12 +501,7 @@ func Test(t TestT, c TestCase) {
providers[name] = p providers[name] = p
} }
providerResolver, err := testProviderResolver(c) opts := terraform.ContextOpts{Providers: providerFactories}
if err != nil {
t.Fatal(err)
}
opts := terraform.ContextOpts{ProviderResolver: providerResolver}
// A single state variable to track the lifecycle, starting with no state // A single state variable to track the lifecycle, starting with no state
var state *terraform.State var state *terraform.State
@ -650,10 +654,14 @@ func testProviderConfig(c TestCase) string {
return strings.Join(lines, "") return strings.Join(lines, "")
} }
// testProviderFactories combines the fixed Providers and // testProviderFactoriesLegacy is like testProviderFactories but it returns
// ResourceProviderFactory functions into a single map of // providers implementing the legacy interface terraform.ResourceProvider,
// ResourceProviderFactory functions. // rather than the current providers.Interface.
func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFactory { //
// It also identifies all providers as legacy-style single names rather than
// full addresses, for compatibility with legacy code that doesn't understand
// FQNs.
func testProviderFactoriesLegacy(c TestCase) (map[string]terraform.ResourceProviderFactory, error) {
ctxProviders := make(map[string]terraform.ResourceProviderFactory) ctxProviders := make(map[string]terraform.ResourceProviderFactory)
for k, pf := range c.ProviderFactories { for k, pf := range c.ProviderFactories {
ctxProviders[k] = pf ctxProviders[k] = pf
@ -663,24 +671,25 @@ func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFact
for k, p := range c.Providers { for k, p := range c.Providers {
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
} }
return ctxProviders return ctxProviders, nil
} }
// testProviderResolver is a helper to build a ResourceProviderResolver // testProviderFactories combines the fixed Providers and
// with pre instantiated ResourceProviders, so that we can reset them for the // ResourceProviderFactory functions into a single map of
// test, while only calling the factory function once. // ResourceProviderFactory functions.
// Any errors are stored so that they can be returned by the factory in func testProviderFactories(c TestCase) (map[addrs.Provider]providers.Factory, error) {
// terraform to match non-test behavior. ctxProviders, err := testProviderFactoriesLegacy(c)
func testProviderResolver(c TestCase) (providers.Resolver, error) { if err != nil {
ctxProviders := testProviderFactories(c) return nil, err
}
// wrap the old provider factories in the test grpc server so they can be // We additionally wrap all of the factories as a GRPCTestProvider, which
// called from terraform. // allows them to appear as a new-style providers.Interface, rather than
// the legacy terraform.ResourceProvider.
newProviders := make(map[addrs.Provider]providers.Factory) newProviders := make(map[addrs.Provider]providers.Factory)
for legacyName, pf := range ctxProviders {
for k, pf := range ctxProviders {
factory := pf // must copy to ensure each closure sees its own value factory := pf // must copy to ensure each closure sees its own value
newProviders[addrs.NewLegacyProvider(k)] = func() (providers.Interface, error) { newProviders[addrs.NewLegacyProvider(legacyName)] = func() (providers.Interface, error) {
p, err := factory() p, err := factory()
if err != nil { if err != nil {
return nil, err return nil, err
@ -693,7 +702,7 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
} }
} }
return providers.ResolverFixed(newProviders), nil return newProviders, nil
} }
// UnitTest is a helper to force the acceptance testing harness to run in the // UnitTest is a helper to force the acceptance testing harness to run in the

View File

@ -1,56 +1,5 @@
package providers package providers
import (
"fmt"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/plugin/discovery"
)
// Resolver is an interface implemented by objects that are able to resolve
// a given set of resource provider version constraints into Factory
// callbacks.
type Resolver interface {
// Given a constraint map, return a Factory for each requested provider.
// If some or all of the constraints cannot be satisfied, return a non-nil
// slice of errors describing the problems.
ResolveProviders(reqd discovery.PluginRequirements) (map[addrs.Provider]Factory, []error)
}
// ResolverFunc wraps a callback function and turns it into a Resolver
// implementation, for convenience in situations where a function and its
// associated closure are sufficient as a resolver implementation.
type ResolverFunc func(reqd discovery.PluginRequirements) (map[addrs.Provider]Factory, []error)
// ResolveProviders implements Resolver by calling the
// wrapped function.
func (f ResolverFunc) ResolveProviders(reqd discovery.PluginRequirements) (map[addrs.Provider]Factory, []error) {
return f(reqd)
}
// ResolverFixed returns a Resolver that has a fixed set of provider factories
// provided by the caller. The returned resolver ignores version constraints
// entirely and just returns the given factory for each requested provider
// name.
//
// This function is primarily used in tests, to provide mock providers or
// in-process providers under test.
func ResolverFixed(factories map[addrs.Provider]Factory) Resolver {
return ResolverFunc(func(reqd discovery.PluginRequirements) (map[addrs.Provider]Factory, []error) {
ret := make(map[addrs.Provider]Factory, len(reqd))
var errs []error
for name := range reqd {
fqn := addrs.NewLegacyProvider(name)
if factory, exists := factories[fqn]; exists {
ret[fqn] = factory
} else {
errs = append(errs, fmt.Errorf("provider %q is not available", name))
}
}
return ret, errs
})
}
// Factory is a function type that creates a new instance of a resource // Factory is a function type that creates a new instance of a resource
// provider, or returns an error if that is impossible. // provider, or returns an error if that is impossible.
type Factory func() (Interface, error) type Factory func() (Interface, error)

View File

@ -55,10 +55,10 @@ type ContextOpts struct {
Meta *ContextMeta Meta *ContextMeta
Destroy bool Destroy bool
Hooks []Hook Hooks []Hook
Parallelism int Parallelism int
ProviderResolver providers.Resolver Providers map[addrs.Provider]providers.Factory
Provisioners map[string]ProvisionerFactory Provisioners map[string]provisioners.Factory
// If non-nil, will apply as additional constraints on the provider // If non-nil, will apply as additional constraints on the provider
// plugins that will be requested from the provider resolver. // plugins that will be requested from the provider resolver.
@ -169,28 +169,8 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) {
// override the defaults. // override the defaults.
variables = variables.Override(opts.Variables) variables = variables.Override(opts.Variables)
// Bind available provider plugins to the constraints in config
var providerFactories map[addrs.Provider]providers.Factory
if opts.ProviderResolver != nil {
deps := ConfigTreeDependencies(opts.Config, state)
reqd := deps.AllProviderRequirements()
if opts.ProviderSHA256s != nil && !opts.SkipProviderVerify {
reqd.LockExecutables(opts.ProviderSHA256s)
}
log.Printf("[TRACE] terraform.NewContext: resolving provider version selections")
var providerDiags tfdiags.Diagnostics
providerFactories, providerDiags = resourceProviderFactories(opts.ProviderResolver, reqd)
diags = diags.Append(providerDiags)
if diags.HasErrors() {
return nil, diags
}
} else {
providerFactories = make(map[addrs.Provider]providers.Factory)
}
components := &basicComponentFactory{ components := &basicComponentFactory{
providers: providerFactories, providers: opts.Providers,
provisioners: opts.Provisioners, provisioners: opts.Provisioners,
} }

View File

@ -1,24 +1,10 @@
package terraform package terraform
import ( // ResourceProvider is a legacy interface for providers.
"fmt"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/plugin/discovery"
"github.com/hashicorp/terraform/providers"
)
// ResourceProvider is an interface that must be implemented by any
// resource provider: the thing that creates and manages the resources in
// a Terraform configuration.
// //
// Important implementation note: All returned pointers, such as // This is retained only for compatibility with legacy code. The current
// *ResourceConfig, *InstanceState, *InstanceDiff, etc. must not point to // interface for providers is providers.Interface, in the sibling directory
// shared data. Terraform is highly parallel and assumes that this data is safe // named "providers".
// to read/write in parallel so it must be unique references. Note that it is
// safe to return arguments as results, however.
type ResourceProvider interface { type ResourceProvider interface {
/********************************************************************* /*********************************************************************
* Functions related to the provider * Functions related to the provider
@ -203,53 +189,6 @@ type DataSource struct {
SchemaAvailable bool SchemaAvailable bool
} }
// ResourceProviderResolver is an interface implemented by objects that are
// able to resolve a given set of resource provider version constraints
// into ResourceProviderFactory callbacks.
type ResourceProviderResolver interface {
// Given a constraint map, return a ResourceProviderFactory for each
// requested provider. If some or all of the constraints cannot be
// satisfied, return a non-nil slice of errors describing the problems.
ResolveProviders(reqd discovery.PluginRequirements) (map[addrs.Provider]ResourceProviderFactory, []error)
}
// ResourceProviderResolverFunc wraps a callback function and turns it into
// a ResourceProviderResolver implementation, for convenience in situations
// where a function and its associated closure are sufficient as a resolver
// implementation.
type ResourceProviderResolverFunc func(reqd discovery.PluginRequirements) (map[addrs.Provider]ResourceProviderFactory, []error)
// ResolveProviders implements ResourceProviderResolver by calling the
// wrapped function.
func (f ResourceProviderResolverFunc) ResolveProviders(reqd discovery.PluginRequirements) (map[addrs.Provider]ResourceProviderFactory, []error) {
return f(reqd)
}
// ResourceProviderResolverFixed returns a ResourceProviderResolver that
// has a fixed set of provider factories provided by the caller. The returned
// resolver ignores version constraints entirely and just returns the given
// factory for each requested provider name.
//
// This function is primarily used in tests, to provide mock providers or
// in-process providers under test.
func ResourceProviderResolverFixed(factories map[addrs.Provider]ResourceProviderFactory) ResourceProviderResolver {
return ResourceProviderResolverFunc(func(reqd discovery.PluginRequirements) (map[addrs.Provider]ResourceProviderFactory, []error) {
ret := make(map[addrs.Provider]ResourceProviderFactory, len(reqd))
var errs []error
for name := range reqd {
// FIXME: discovery.PluginRequirements should use addrs.Provider as
// the map keys instead of a string
fqn := addrs.NewLegacyProvider(name)
if factory, exists := factories[fqn]; exists {
ret[fqn] = factory
} else {
errs = append(errs, fmt.Errorf("provider %q is not available", name))
}
}
return ret, errs
})
}
// ResourceProviderFactory is a function type that creates a new instance // ResourceProviderFactory is a function type that creates a new instance
// of a resource provider. // of a resource provider.
type ResourceProviderFactory func() (ResourceProvider, error) type ResourceProviderFactory func() (ResourceProvider, error)
@ -282,34 +221,6 @@ func ProviderHasDataSource(p ResourceProvider, n string) bool {
return false 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 providers.Resolver, reqd discovery.PluginRequirements) (map[addrs.Provider]providers.Factory, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ret, errs := resolver.ResolveProviders(reqd)
if errs != nil {
diags = diags.Append(
tfdiags.Sourceless(tfdiags.Error,
"Could not satisfy plugin requirements",
errPluginInit,
),
)
for _, err := range errs {
diags = diags.Append(err)
}
return nil, diags
}
return ret, nil
}
const errPluginInit = ` const errPluginInit = `
Plugin reinitialization required. Please run "terraform init". Plugin reinitialization required. Please run "terraform init".