mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-07 18:00:45 -05:00
Due to how often the state and plan types are referenced throughout Terraform, there isn't a great way to switch them out gradually. As a consequence, this huge commit gets us from the old world to a _compilable_ new world, but still has a large number of known test failures due to key functionality being stubbed out. The stubs here are for anything that interacts with providers, since we now need to do the follow-up work to similarly replace the old terraform.ResourceProvider interface with its replacement in the new "providers" package. That work, along with work to fix the remaining failing tests, will follow in subsequent commits. The aim here was to replace all references to terraform.State and its downstream types with states.State, terraform.Plan with plans.Plan, state.State with statemgr.State, and switch to the new implementations of the state and plan file formats. However, due to the number of times those types are used, this also ended up affecting numerous other parts of core such as terraform.Hook, the backend.Backend interface, and most of the CLI commands. Just as with 5861dbf3fc49b19587a31816eb06f511ab861bb4 before, I apologize in advance to the person who inevitably just found this huge commit while spelunking through the commit history.
404 lines
12 KiB
Go
404 lines
12 KiB
Go
package command
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
plugin "github.com/hashicorp/go-plugin"
|
|
tfplugin "github.com/hashicorp/terraform/plugin"
|
|
"github.com/hashicorp/terraform/plugin/discovery"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/kardianos/osext"
|
|
)
|
|
|
|
// 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
|
|
|
|
// 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[string]terraform.ResourceProviderFactory
|
|
}
|
|
|
|
func choosePlugins(avail discovery.PluginMetaSet, internal map[string]terraform.ResourceProviderFactory, 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[name]; isInternal {
|
|
continue
|
|
}
|
|
|
|
if len(metas) == 0 {
|
|
continue
|
|
}
|
|
ret[name] = metas.Newest()
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (r *multiVersionProviderResolver) ResolveProviders(
|
|
reqd discovery.PluginRequirements,
|
|
) (map[string]terraform.ResourceProviderFactory, []error) {
|
|
factories := make(map[string]terraform.ResourceProviderFactory, len(reqd))
|
|
var errs []error
|
|
|
|
chosen := choosePlugins(r.Available, r.Internal, reqd)
|
|
for name, req := range reqd {
|
|
if factory, isInternal := r.Internal[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[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
|
|
}
|
|
|
|
client := tfplugin.Client(newest)
|
|
factories[name] = providerFactory(client)
|
|
} 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
|
|
func (m *Meta) storePluginPath(pluginPath []string) error {
|
|
if len(pluginPath) == 0 {
|
|
return nil
|
|
}
|
|
|
|
path := filepath.Join(m.DataDir(), PluginPathFile)
|
|
|
|
// remove the plugin dir record if the path was set to an empty string
|
|
if len(pluginPath) == 1 && (pluginPath[0] == "") {
|
|
err := os.Remove(path)
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
js, err := json.MarshalIndent(pluginPath, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if this fails, so will WriteFile
|
|
os.MkdirAll(m.DataDir(), 0755)
|
|
|
|
return ioutil.WriteFile(path, js, 0644)
|
|
}
|
|
|
|
// Load the user-defined plugin search path into Meta.pluginPath if the file
|
|
// exists.
|
|
func (m *Meta) loadPluginPath() ([]string, error) {
|
|
js, err := ioutil.ReadFile(filepath.Join(m.DataDir(), PluginPathFile))
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pluginPath []string
|
|
if err := json.Unmarshal(js, &pluginPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return pluginPath, nil
|
|
}
|
|
|
|
// the default location for automatically installed plugins
|
|
func (m *Meta) pluginDir() string {
|
|
return filepath.Join(m.DataDir(), "plugins", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
|
|
}
|
|
|
|
// pluginDirs return a list of directories to search for plugins.
|
|
//
|
|
// Earlier entries in this slice get priority over later when multiple copies
|
|
// of the same plugin version are found, but newer versions always override
|
|
// older versions where both satisfy the provider version constraints.
|
|
func (m *Meta) pluginDirs(includeAutoInstalled bool) []string {
|
|
// user defined paths take precedence
|
|
if len(m.pluginPath) > 0 {
|
|
return m.pluginPath
|
|
}
|
|
|
|
// When searching the following directories, earlier entries get precedence
|
|
// if the same plugin version is found twice, but newer versions will
|
|
// always get preference below regardless of where they are coming from.
|
|
// TODO: Add auto-install dir, default vendor dir and optional override
|
|
// vendor dir(s).
|
|
dirs := []string{"."}
|
|
|
|
// Look in the same directory as the Terraform executable.
|
|
// If found, this replaces what we found in the config path.
|
|
exePath, err := osext.Executable()
|
|
if err != nil {
|
|
log.Printf("[ERROR] Error discovering exe directory: %s", err)
|
|
} else {
|
|
dirs = append(dirs, filepath.Dir(exePath))
|
|
}
|
|
|
|
// add the user vendor directory
|
|
dirs = append(dirs, DefaultPluginVendorDir)
|
|
|
|
if includeAutoInstalled {
|
|
dirs = append(dirs, m.pluginDir())
|
|
}
|
|
dirs = append(dirs, m.GlobalPluginDirs...)
|
|
|
|
return dirs
|
|
}
|
|
|
|
func (m *Meta) pluginCache() discovery.PluginCache {
|
|
dir := m.PluginCacheDir
|
|
if dir == "" {
|
|
return nil // cache disabled
|
|
}
|
|
|
|
dir = filepath.Join(dir, pluginMachineName)
|
|
|
|
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() terraform.ResourceProviderResolver {
|
|
return &multiVersionProviderResolver{
|
|
Available: m.providerPluginSet(),
|
|
Internal: m.internalProviders(),
|
|
}
|
|
}
|
|
|
|
func (m *Meta) internalProviders() map[string]terraform.ResourceProviderFactory {
|
|
return map[string]terraform.ResourceProviderFactory{
|
|
// FIXME: Re-enable this once the internal provider system is updated
|
|
// for the new provider interface.
|
|
//"terraform": func() (terraform.ResourceProvider, error) {
|
|
// return terraformProvider.Provider(), nil
|
|
//},
|
|
}
|
|
}
|
|
|
|
// filter the requirements returning only the providers that we can't resolve
|
|
func (m *Meta) missingPlugins(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[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.ResourceProvisionerFactory {
|
|
dirs := m.pluginDirs(true)
|
|
plugins := discovery.FindPlugins("provisioner", dirs)
|
|
plugins, _ = plugins.ValidateVersions()
|
|
|
|
// For now our goal is to just find the latest version of each plugin
|
|
// we have on the system. All provisioners should be at version 0.0.0
|
|
// currently, so there should actually only be one instance of each plugin
|
|
// name here, even though the discovery interface forces us to pretend
|
|
// that might not be true.
|
|
|
|
factories := make(map[string]terraform.ResourceProvisionerFactory)
|
|
|
|
// Wire up the internal provisioners first. These might be overridden
|
|
// by discovered provisioners below.
|
|
for name := range InternalProvisioners {
|
|
client, err := internalPluginClient("provisioner", name)
|
|
if err != nil {
|
|
log.Printf("[WARN] failed to build command line for internal plugin %q: %s", name, err)
|
|
continue
|
|
}
|
|
factories[name] = provisionerFactory(client)
|
|
}
|
|
|
|
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] = provisionerFactory(client)
|
|
}
|
|
|
|
return factories
|
|
}
|
|
|
|
func internalPluginClient(kind, name string) (*plugin.Client, error) {
|
|
cmdLine, err := BuildPluginCommandString(kind, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// See the docstring for BuildPluginCommandString for why we need to do
|
|
// this split here.
|
|
cmdArgv := strings.Split(cmdLine, TFSPACE)
|
|
|
|
cfg := &plugin.ClientConfig{
|
|
Cmd: exec.Command(cmdArgv[0], cmdArgv[1:]...),
|
|
HandshakeConfig: tfplugin.Handshake,
|
|
Managed: true,
|
|
Plugins: tfplugin.PluginMap,
|
|
}
|
|
|
|
return plugin.NewClient(cfg), nil
|
|
}
|
|
|
|
func providerFactory(client *plugin.Client) terraform.ResourceProviderFactory {
|
|
return func() (terraform.ResourceProvider, error) {
|
|
// 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
|
|
}
|
|
|
|
return raw.(terraform.ResourceProvider), nil
|
|
}
|
|
}
|
|
|
|
func provisionerFactory(client *plugin.Client) terraform.ResourceProvisionerFactory {
|
|
return func() (terraform.ResourceProvisioner, error) {
|
|
// Request the RPC client so we can get the provisioner
|
|
// so we can build the actual RPC-implemented provisioner.
|
|
rpcClient, err := client.Client()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
raw, err := rpcClient.Dispense(tfplugin.ProvisionerPluginName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return raw.(terraform.ResourceProvisioner), nil
|
|
}
|
|
}
|