terraform/internal/command/plugins.go

172 lines
5.7 KiB
Go

package command
import (
"fmt"
"log"
"os/exec"
"path/filepath"
"runtime"
plugin "github.com/hashicorp/go-plugin"
"github.com/kardianos/osext"
fileprovisioner "github.com/hashicorp/terraform/internal/builtin/provisioners/file"
localexec "github.com/hashicorp/terraform/internal/builtin/provisioners/local-exec"
remoteexec "github.com/hashicorp/terraform/internal/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/internal/logging"
tfplugin "github.com/hashicorp/terraform/internal/plugin"
"github.com/hashicorp/terraform/internal/plugin/discovery"
"github.com/hashicorp/terraform/internal/provisioners"
)
// NOTE WELL: The logic in this file is primarily about plugin types OTHER THAN
// providers, which use an older set of approaches implemented here.
//
// The provider-related functions live primarily in meta_providers.go, and
// lean on some different underlying mechanisms in order to support automatic
// installation and a hierarchical addressing namespace, neither of which
// are supported for other plugin types.
// store the user-supplied path for plugin discovery
func (m *Meta) storePluginPath(pluginPath []string) error {
if len(pluginPath) == 0 {
return nil
}
m.fixupMissingWorkingDir()
// remove the plugin dir record if the path was set to an empty string
if len(pluginPath) == 1 && (pluginPath[0] == "") {
return m.WorkingDir.SetForcedPluginDirs(nil)
}
return m.WorkingDir.SetForcedPluginDirs(pluginPath)
}
// Load the user-defined plugin search path into Meta.pluginPath if the file
// exists.
func (m *Meta) loadPluginPath() ([]string, error) {
m.fixupMissingWorkingDir()
return m.WorkingDir.ForcedPluginDirs()
}
// 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) provisionerFactories() map[string]provisioners.Factory {
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]provisioners.Factory)
// Wire up the internal provisioners first. These might be overridden
// by discovered provisioners below.
for name, factory := range internalProvisionerFactories() {
factories[name] = factory
}
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()
factories[name] = provisionerFactory(newest)
}
return factories
}
func provisionerFactory(meta discovery.PluginMeta) provisioners.Factory {
return func() (provisioners.Interface, error) {
cfg := &plugin.ClientConfig{
Cmd: exec.Command(meta.Path),
HandshakeConfig: tfplugin.Handshake,
VersionedPlugins: tfplugin.VersionedPlugins,
Managed: true,
Logger: logging.NewLogger("provisioner"),
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
AutoMTLS: enableProviderAutoMTLS,
SyncStdout: logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", meta.Name)),
SyncStderr: logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", meta.Name)),
}
client := plugin.NewClient(cfg)
return newProvisionerClient(client)
}
}
func internalProvisionerFactories() map[string]provisioners.Factory {
return map[string]provisioners.Factory{
"file": provisioners.FactoryFixed(fileprovisioner.New()),
"local-exec": provisioners.FactoryFixed(localexec.New()),
"remote-exec": provisioners.FactoryFixed(remoteexec.New()),
}
}
func newProvisionerClient(client *plugin.Client) (provisioners.Interface, 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
}
// store the client so that the plugin can kill the child process
p := raw.(*tfplugin.GRPCProvisioner)
p.PluginClient = client
return p, nil
}