package command import ( "encoding/json" "errors" "fmt" "io/ioutil" "log" "os" "os/exec" "path/filepath" "runtime" "strings" plugin "github.com/hashicorp/go-plugin" "github.com/kardianos/osext" "github.com/hashicorp/terraform/addrs" terraformProvider "github.com/hashicorp/terraform/builtin/providers/terraform" tfplugin "github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin/discovery" "github.com/hashicorp/terraform/providers" "github.com/hashicorp/terraform/provisioners" "github.com/hashicorp/terraform/terraform" ) // 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[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 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() 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 { 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.ProvisionerFactory) // Wire up the internal provisioners first. These might be overridden // by discovered provisioners below. for name := range InternalProvisioners { factories[name] = internalProvisionerFactory(discovery.PluginMeta{Name: name}) } 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 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, VersionedPlugins: tfplugin.VersionedPlugins, AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, } 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 { return func() (provisioners.Interface, error) { client := tfplugin.Client(meta) return newProvisionerClient(client) } } func internalProvisionerFactory(meta discovery.PluginMeta) terraform.ProvisionerFactory { return func() (provisioners.Interface, error) { client, err := internalPluginClient("provisioner", meta.Name) if err != nil { return nil, fmt.Errorf("[WARN] failed to build command line for internal plugin %q: %s", meta.Name, err) } return newProvisionerClient(client) } } 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 }