mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-27 09:21:14 -06:00
84214437b3
This replaces this plugin system with the extracted hashicorp/go-plugin library. This doesn't introduce any new features such as binary flattening but opens us up to that a lot more easily and removes a lot of code from TF in favor of the upstream lib. This will introduce a protocol change that will cause all existing plugins to have to be recompiled to work properly. There is no actual API changes so they just have to recompile, but it is technically backwards incompatible.
297 lines
7.2 KiB
Go
297 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-plugin"
|
|
"github.com/hashicorp/hcl"
|
|
tfplugin "github.com/hashicorp/terraform/plugin"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/kardianos/osext"
|
|
)
|
|
|
|
// Config is the structure of the configuration for the Terraform CLI.
|
|
//
|
|
// This is not the configuration for Terraform itself. That is in the
|
|
// "config" package.
|
|
type Config struct {
|
|
Providers map[string]string
|
|
Provisioners map[string]string
|
|
|
|
DisableCheckpoint bool `hcl:"disable_checkpoint"`
|
|
DisableCheckpointSignature bool `hcl:"disable_checkpoint_signature"`
|
|
}
|
|
|
|
// BuiltinConfig is the built-in defaults for the configuration. These
|
|
// can be overridden by user configurations.
|
|
var BuiltinConfig Config
|
|
|
|
// ContextOpts are the global ContextOpts we use to initialize the CLI.
|
|
var ContextOpts terraform.ContextOpts
|
|
|
|
// ConfigFile returns the default path to the configuration file.
|
|
//
|
|
// On Unix-like systems this is the ".terraformrc" file in the home directory.
|
|
// On Windows, this is the "terraform.rc" file in the application data
|
|
// directory.
|
|
func ConfigFile() (string, error) {
|
|
return configFile()
|
|
}
|
|
|
|
// ConfigDir returns the configuration directory for Terraform.
|
|
func ConfigDir() (string, error) {
|
|
return configDir()
|
|
}
|
|
|
|
// LoadConfig loads the CLI configuration from ".terraformrc" files.
|
|
func LoadConfig(path string) (*Config, error) {
|
|
// Read the HCL file and prepare for parsing
|
|
d, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Error reading %s: %s", path, err)
|
|
}
|
|
|
|
// Parse it
|
|
obj, err := hcl.Parse(string(d))
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"Error parsing %s: %s", path, err)
|
|
}
|
|
|
|
// Build up the result
|
|
var result Config
|
|
if err := hcl.DecodeObject(&result, obj); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// Discover discovers plugins.
|
|
//
|
|
// This looks in the directory of the executable and the CWD, in that
|
|
// order for priority.
|
|
func (c *Config) Discover() error {
|
|
// Look in the cwd.
|
|
if err := c.discover("."); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Look in the plugins directory. This will override any found
|
|
// in the current directory.
|
|
dir, err := ConfigDir()
|
|
if err != nil {
|
|
log.Printf("[ERR] Error loading config directory: %s", err)
|
|
} else {
|
|
if err := c.discover(filepath.Join(dir, "plugins")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Next, look in the same directory as the executable. Any conflicts
|
|
// will overwrite those found in our current directory.
|
|
exePath, err := osext.Executable()
|
|
if err != nil {
|
|
log.Printf("[ERR] Error loading exe directory: %s", err)
|
|
} else {
|
|
if err := c.discover(filepath.Dir(exePath)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Merge merges two configurations and returns a third entirely
|
|
// new configuration with the two merged.
|
|
func (c1 *Config) Merge(c2 *Config) *Config {
|
|
var result Config
|
|
result.Providers = make(map[string]string)
|
|
result.Provisioners = make(map[string]string)
|
|
for k, v := range c1.Providers {
|
|
result.Providers[k] = v
|
|
}
|
|
for k, v := range c2.Providers {
|
|
result.Providers[k] = v
|
|
}
|
|
for k, v := range c1.Provisioners {
|
|
result.Provisioners[k] = v
|
|
}
|
|
for k, v := range c2.Provisioners {
|
|
result.Provisioners[k] = v
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
func (c *Config) discover(path string) error {
|
|
var err error
|
|
|
|
if !filepath.IsAbs(path) {
|
|
path, err = filepath.Abs(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = c.discoverSingle(
|
|
filepath.Join(path, "terraform-provider-*"), &c.Providers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = c.discoverSingle(
|
|
filepath.Join(path, "terraform-provisioner-*"), &c.Provisioners)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) discoverSingle(glob string, m *map[string]string) error {
|
|
matches, err := filepath.Glob(glob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if *m == nil {
|
|
*m = make(map[string]string)
|
|
}
|
|
|
|
for _, match := range matches {
|
|
file := filepath.Base(match)
|
|
|
|
// If the filename has a ".", trim up to there
|
|
if idx := strings.Index(file, "."); idx >= 0 {
|
|
file = file[:idx]
|
|
}
|
|
|
|
// Look for foo-bar-baz. The plugin name is "baz"
|
|
parts := strings.SplitN(file, "-", 3)
|
|
if len(parts) != 3 {
|
|
continue
|
|
}
|
|
|
|
log.Printf("[DEBUG] Discovered plugin: %s = %s", parts[2], match)
|
|
(*m)[parts[2]] = match
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ProviderFactories returns the mapping of prefixes to
|
|
// ResourceProviderFactory that can be used to instantiate a
|
|
// binary-based plugin.
|
|
func (c *Config) ProviderFactories() map[string]terraform.ResourceProviderFactory {
|
|
result := make(map[string]terraform.ResourceProviderFactory)
|
|
for k, v := range c.Providers {
|
|
result[k] = c.providerFactory(v)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory {
|
|
// Build the plugin client configuration and init the plugin
|
|
var config plugin.ClientConfig
|
|
config.Cmd = pluginCmd(path)
|
|
config.HandshakeConfig = tfplugin.Handshake
|
|
config.Managed = true
|
|
config.Plugins = tfplugin.PluginMap
|
|
client := plugin.NewClient(&config)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// ProvisionerFactories returns the mapping of prefixes to
|
|
// ResourceProvisionerFactory that can be used to instantiate a
|
|
// binary-based plugin.
|
|
func (c *Config) ProvisionerFactories() map[string]terraform.ResourceProvisionerFactory {
|
|
result := make(map[string]terraform.ResourceProvisionerFactory)
|
|
for k, v := range c.Provisioners {
|
|
result[k] = c.provisionerFactory(v)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFactory {
|
|
// Build the plugin client configuration and init the plugin
|
|
var config plugin.ClientConfig
|
|
config.HandshakeConfig = tfplugin.Handshake
|
|
config.Cmd = pluginCmd(path)
|
|
config.Managed = true
|
|
config.Plugins = tfplugin.PluginMap
|
|
client := plugin.NewClient(&config)
|
|
|
|
return func() (terraform.ResourceProvisioner, error) {
|
|
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
|
|
}
|
|
}
|
|
|
|
func pluginCmd(path string) *exec.Cmd {
|
|
cmdPath := ""
|
|
|
|
// If the path doesn't contain a separator, look in the same
|
|
// directory as the Terraform executable first.
|
|
if !strings.ContainsRune(path, os.PathSeparator) {
|
|
exePath, err := osext.Executable()
|
|
if err == nil {
|
|
temp := filepath.Join(
|
|
filepath.Dir(exePath),
|
|
filepath.Base(path))
|
|
|
|
if _, err := os.Stat(temp); err == nil {
|
|
cmdPath = temp
|
|
}
|
|
}
|
|
|
|
// If we still haven't found the executable, look for it
|
|
// in the PATH.
|
|
if v, err := exec.LookPath(path); err == nil {
|
|
cmdPath = v
|
|
}
|
|
}
|
|
|
|
// If we still don't have a path, then just set it to the original
|
|
// given path.
|
|
if cmdPath == "" {
|
|
cmdPath = path
|
|
}
|
|
|
|
// Build the command to execute the plugin
|
|
return exec.Command(cmdPath)
|
|
}
|