github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/meta_providers.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"log"
    11  	"os"
    12  	"os/exec"
    13  	"strings"
    14  
    15  	plugin "github.com/hashicorp/go-plugin"
    16  
    17  	"github.com/terramate-io/tf/addrs"
    18  	terraformProvider "github.com/terramate-io/tf/builtin/providers/terraform"
    19  	"github.com/terramate-io/tf/getproviders"
    20  	"github.com/terramate-io/tf/logging"
    21  	tfplugin "github.com/terramate-io/tf/plugin"
    22  	tfplugin6 "github.com/terramate-io/tf/plugin6"
    23  	"github.com/terramate-io/tf/providercache"
    24  	"github.com/terramate-io/tf/providers"
    25  	"github.com/terramate-io/tf/tfdiags"
    26  )
    27  
    28  // The TF_DISABLE_PLUGIN_TLS environment variable is intended only for use by
    29  // the plugin SDK test framework, to reduce startup overhead when rapidly
    30  // launching and killing lots of instances of the same provider.
    31  //
    32  // This is not intended to be set by end-users.
    33  var enableProviderAutoMTLS = os.Getenv("TF_DISABLE_PLUGIN_TLS") == ""
    34  
    35  // providerInstaller returns an object that knows how to install providers and
    36  // how to recover the selections from a prior installation process.
    37  //
    38  // The resulting provider installer is constructed from the results of
    39  // the other methods providerLocalCacheDir, providerGlobalCacheDir, and
    40  // providerInstallSource.
    41  //
    42  // Only one object returned from this method should be live at any time,
    43  // because objects inside contain caches that must be maintained properly.
    44  // Because this method wraps a result from providerLocalCacheDir, that
    45  // limitation applies also to results from that method.
    46  func (m *Meta) providerInstaller() *providercache.Installer {
    47  	return m.providerInstallerCustomSource(m.providerInstallSource())
    48  }
    49  
    50  // providerInstallerCustomSource is a variant of providerInstaller that
    51  // allows the caller to specify a different installation source than the one
    52  // that would naturally be selected.
    53  //
    54  // The result of this method has the same dependencies and constraints as
    55  // providerInstaller.
    56  //
    57  // The result of providerInstallerCustomSource differs from
    58  // providerInstaller only in how it determines package installation locations
    59  // during EnsureProviderVersions. A caller that doesn't call
    60  // EnsureProviderVersions (anything other than "terraform init") can safely
    61  // just use the providerInstaller method unconditionally.
    62  func (m *Meta) providerInstallerCustomSource(source getproviders.Source) *providercache.Installer {
    63  	targetDir := m.providerLocalCacheDir()
    64  	globalCacheDir := m.providerGlobalCacheDir()
    65  	inst := providercache.NewInstaller(targetDir, source)
    66  	if globalCacheDir != nil {
    67  		inst.SetGlobalCacheDir(globalCacheDir)
    68  		inst.SetGlobalCacheDirMayBreakDependencyLockFile(m.PluginCacheMayBreakDependencyLockFile)
    69  	}
    70  	var builtinProviderTypes []string
    71  	for ty := range m.internalProviders() {
    72  		builtinProviderTypes = append(builtinProviderTypes, ty)
    73  	}
    74  	inst.SetBuiltInProviderTypes(builtinProviderTypes)
    75  	unmanagedProviderTypes := make(map[addrs.Provider]struct{}, len(m.UnmanagedProviders))
    76  	for ty := range m.UnmanagedProviders {
    77  		unmanagedProviderTypes[ty] = struct{}{}
    78  	}
    79  	inst.SetUnmanagedProviderTypes(unmanagedProviderTypes)
    80  	return inst
    81  }
    82  
    83  // providerCustomLocalDirectorySource produces a provider source that consults
    84  // only the given local filesystem directories for plugins to install.
    85  //
    86  // This is used to implement the -plugin-dir option for "terraform init", where
    87  // the result of this method is used instead of what would've been returned
    88  // from m.providerInstallSource.
    89  //
    90  // If the given list of directories is empty then the resulting source will
    91  // have no providers available for installation at all.
    92  func (m *Meta) providerCustomLocalDirectorySource(dirs []string) getproviders.Source {
    93  	var ret getproviders.MultiSource
    94  	for _, dir := range dirs {
    95  		ret = append(ret, getproviders.MultiSourceSelector{
    96  			Source: getproviders.NewFilesystemMirrorSource(dir),
    97  		})
    98  	}
    99  	return ret
   100  }
   101  
   102  // providerLocalCacheDir returns an object representing the
   103  // configuration-specific local cache directory. This is the
   104  // only location consulted for provider plugin packages for Terraform
   105  // operations other than provider installation.
   106  //
   107  // Only the provider installer (in "terraform init") is permitted to make
   108  // modifications to this cache directory. All other commands must treat it
   109  // as read-only.
   110  //
   111  // Only one object returned from this method should be live at any time,
   112  // because objects inside contain caches that must be maintained properly.
   113  func (m *Meta) providerLocalCacheDir() *providercache.Dir {
   114  	m.fixupMissingWorkingDir()
   115  	dir := m.WorkingDir.ProviderLocalCacheDir()
   116  	return providercache.NewDir(dir)
   117  }
   118  
   119  // providerGlobalCacheDir returns an object representing the shared global
   120  // provider cache directory, used as a read-through cache when installing
   121  // new provider plugin packages.
   122  //
   123  // This function may return nil, in which case there is no global cache
   124  // configured and new packages should be downloaded directly into individual
   125  // configuration-specific cache directories.
   126  //
   127  // Only one object returned from this method should be live at any time,
   128  // because objects inside contain caches that must be maintained properly.
   129  func (m *Meta) providerGlobalCacheDir() *providercache.Dir {
   130  	dir := m.PluginCacheDir
   131  	if dir == "" {
   132  		return nil // cache disabled
   133  	}
   134  	return providercache.NewDir(dir)
   135  }
   136  
   137  // providerInstallSource returns an object that knows how to consult one or
   138  // more external sources to determine the availability of and package
   139  // locations for versions of Terraform providers that are available for
   140  // automatic installation.
   141  //
   142  // This returns the standard provider install source that consults a number
   143  // of directories selected either automatically or via the CLI configuration.
   144  // Users may choose to override this during a "terraform init" command by
   145  // specifying one or more -plugin-dir options, in which case the installation
   146  // process will construct its own source consulting only those directories
   147  // and use that instead.
   148  func (m *Meta) providerInstallSource() getproviders.Source {
   149  	// A provider source should always be provided in normal use, but our
   150  	// unit tests might not always populate Meta fully and so we'll be robust
   151  	// by returning a non-nil source that just always answers that no plugins
   152  	// are available.
   153  	if m.ProviderSource == nil {
   154  		// A multi-source with no underlying sources is effectively an
   155  		// always-empty source.
   156  		return getproviders.MultiSource(nil)
   157  	}
   158  	return m.ProviderSource
   159  }
   160  
   161  // providerDevOverrideInitWarnings returns a diagnostics that contains at
   162  // least one warning if and only if there is at least one provider development
   163  // override in effect. If not, the result is always empty. The result never
   164  // contains error diagnostics.
   165  //
   166  // The init command can use this to include a warning that the results
   167  // may differ from what's expected due to the development overrides. For
   168  // other commands, providerDevOverrideRuntimeWarnings should be used.
   169  func (m *Meta) providerDevOverrideInitWarnings() tfdiags.Diagnostics {
   170  	if len(m.ProviderDevOverrides) == 0 {
   171  		return nil
   172  	}
   173  	var detailMsg strings.Builder
   174  	detailMsg.WriteString("The following provider development overrides are set in the CLI configuration:\n")
   175  	for addr, path := range m.ProviderDevOverrides {
   176  		detailMsg.WriteString(fmt.Sprintf(" - %s in %s\n", addr.ForDisplay(), path))
   177  	}
   178  	detailMsg.WriteString("\nSkip terraform init when using provider development overrides. It is not necessary and may error unexpectedly.")
   179  	return tfdiags.Diagnostics{
   180  		tfdiags.Sourceless(
   181  			tfdiags.Warning,
   182  			"Provider development overrides are in effect",
   183  			detailMsg.String(),
   184  		),
   185  	}
   186  }
   187  
   188  // providerDevOverrideRuntimeWarnings returns a diagnostics that contains at
   189  // least one warning if and only if there is at least one provider development
   190  // override in effect. If not, the result is always empty. The result never
   191  // contains error diagnostics.
   192  //
   193  // Certain commands can use this to include a warning that their results
   194  // may differ from what's expected due to the development overrides. It's
   195  // not necessary to bother the user with this warning on every command, but
   196  // it's helpful to return it on commands that have externally-visible side
   197  // effects and on commands that are used to verify conformance to schemas.
   198  //
   199  // See providerDevOverrideInitWarnings for warnings specific to the init
   200  // command.
   201  func (m *Meta) providerDevOverrideRuntimeWarnings() tfdiags.Diagnostics {
   202  	if len(m.ProviderDevOverrides) == 0 {
   203  		return nil
   204  	}
   205  	var detailMsg strings.Builder
   206  	detailMsg.WriteString("The following provider development overrides are set in the CLI configuration:\n")
   207  	for addr, path := range m.ProviderDevOverrides {
   208  		detailMsg.WriteString(fmt.Sprintf(" - %s in %s\n", addr.ForDisplay(), path))
   209  	}
   210  	detailMsg.WriteString("\nThe behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.")
   211  	return tfdiags.Diagnostics{
   212  		tfdiags.Sourceless(
   213  			tfdiags.Warning,
   214  			"Provider development overrides are in effect",
   215  			detailMsg.String(),
   216  		),
   217  	}
   218  }
   219  
   220  // providerFactories uses the selections made previously by an installer in
   221  // the local cache directory (m.providerLocalCacheDir) to produce a map
   222  // from provider addresses to factory functions to create instances of
   223  // those providers.
   224  //
   225  // providerFactories will return an error if the installer's selections cannot
   226  // be honored with what is currently in the cache, such as if a selected
   227  // package has been removed from the cache or if the contents of a selected
   228  // package have been modified outside of the installer. If it returns an error,
   229  // the returned map may be incomplete or invalid, but will be as complete
   230  // as possible given the cause of the error.
   231  func (m *Meta) providerFactories() (map[addrs.Provider]providers.Factory, error) {
   232  	locks, diags := m.lockedDependencies()
   233  	if diags.HasErrors() {
   234  		return nil, fmt.Errorf("failed to read dependency lock file: %s", diags.Err())
   235  	}
   236  
   237  	// We'll always run through all of our providers, even if one of them
   238  	// encounters an error, so that we can potentially report multiple errors
   239  	// where appropriate and so that callers can potentially make use of the
   240  	// partial result we return if e.g. they want to enumerate which providers
   241  	// are available, or call into one of the providers that didn't fail.
   242  	errs := make(map[addrs.Provider]error)
   243  
   244  	// For the providers from the lock file, we expect them to be already
   245  	// available in the provider cache because "terraform init" should already
   246  	// have put them there.
   247  	providerLocks := locks.AllProviders()
   248  	cacheDir := m.providerLocalCacheDir()
   249  
   250  	// The internal providers are _always_ available, even if the configuration
   251  	// doesn't request them, because they don't need any special installation
   252  	// and they'll just be ignored if not used.
   253  	internalFactories := m.internalProviders()
   254  
   255  	// We have two different special cases aimed at provider development
   256  	// use-cases, which are not for "production" use:
   257  	// - The CLI config can specify that a particular provider should always
   258  	// use a plugin from a particular local directory, ignoring anything the
   259  	// lock file or cache directory might have to say about it. This is useful
   260  	// for manual testing of local development builds.
   261  	// - The Terraform SDK test harness (and possibly other callers in future)
   262  	// can ask that we use its own already-started provider servers, which we
   263  	// call "unmanaged" because Terraform isn't responsible for starting
   264  	// and stopping them. This is intended for automated testing where a
   265  	// calling harness is responsible both for starting the provider server
   266  	// and orchestrating one or more non-interactive Terraform runs that then
   267  	// exercise it.
   268  	// Unmanaged providers take precedence over overridden providers because
   269  	// overrides are typically a "session-level" setting while unmanaged
   270  	// providers are typically scoped to a single unattended command.
   271  	devOverrideProviders := m.ProviderDevOverrides
   272  	unmanagedProviders := m.UnmanagedProviders
   273  
   274  	factories := make(map[addrs.Provider]providers.Factory, len(providerLocks)+len(internalFactories)+len(unmanagedProviders))
   275  	for name, factory := range internalFactories {
   276  		factories[addrs.NewBuiltInProvider(name)] = factory
   277  	}
   278  	for provider, lock := range providerLocks {
   279  		reportError := func(thisErr error) {
   280  			errs[provider] = thisErr
   281  			// We'll populate a provider factory that just echoes our error
   282  			// again if called, which allows us to still report a helpful
   283  			// error even if it gets detected downstream somewhere from the
   284  			// caller using our partial result.
   285  			factories[provider] = providerFactoryError(thisErr)
   286  		}
   287  
   288  		if locks.ProviderIsOverridden(provider) {
   289  			// Overridden providers we'll handle with the other separate
   290  			// loops below, for dev overrides etc.
   291  			continue
   292  		}
   293  
   294  		version := lock.Version()
   295  		cached := cacheDir.ProviderVersion(provider, version)
   296  		if cached == nil {
   297  			reportError(fmt.Errorf(
   298  				"there is no package for %s %s cached in %s",
   299  				provider, version, cacheDir.BasePath(),
   300  			))
   301  			continue
   302  		}
   303  		// The cached package must match one of the checksums recorded in
   304  		// the lock file, if any.
   305  		if allowedHashes := lock.PreferredHashes(); len(allowedHashes) != 0 {
   306  			matched, err := cached.MatchesAnyHash(allowedHashes)
   307  			if err != nil {
   308  				reportError(fmt.Errorf(
   309  					"failed to verify checksum of %s %s package cached in in %s: %s",
   310  					provider, version, cacheDir.BasePath(), err,
   311  				))
   312  				continue
   313  			}
   314  			if !matched {
   315  				reportError(fmt.Errorf(
   316  					"the cached package for %s %s (in %s) does not match any of the checksums recorded in the dependency lock file",
   317  					provider, version, cacheDir.BasePath(),
   318  				))
   319  				continue
   320  			}
   321  		}
   322  		factories[provider] = providerFactory(cached)
   323  	}
   324  	for provider, localDir := range devOverrideProviders {
   325  		factories[provider] = devOverrideProviderFactory(provider, localDir)
   326  	}
   327  	for provider, reattach := range unmanagedProviders {
   328  		factories[provider] = unmanagedProviderFactory(provider, reattach)
   329  	}
   330  
   331  	var err error
   332  	if len(errs) > 0 {
   333  		err = providerPluginErrors(errs)
   334  	}
   335  	return factories, err
   336  }
   337  
   338  func (m *Meta) internalProviders() map[string]providers.Factory {
   339  	return map[string]providers.Factory{
   340  		"terraform": func() (providers.Interface, error) {
   341  			return terraformProvider.NewProvider(), nil
   342  		},
   343  	}
   344  }
   345  
   346  // providerFactory produces a provider factory that runs up the executable
   347  // file in the given cache package and uses go-plugin to implement
   348  // providers.Interface against it.
   349  func providerFactory(meta *providercache.CachedProvider) providers.Factory {
   350  	return func() (providers.Interface, error) {
   351  		execFile, err := meta.ExecutableFile()
   352  		if err != nil {
   353  			return nil, err
   354  		}
   355  
   356  		config := &plugin.ClientConfig{
   357  			HandshakeConfig:  tfplugin.Handshake,
   358  			Logger:           logging.NewProviderLogger(""),
   359  			AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   360  			Managed:          true,
   361  			Cmd:              exec.Command(execFile),
   362  			AutoMTLS:         enableProviderAutoMTLS,
   363  			VersionedPlugins: tfplugin.VersionedPlugins,
   364  			SyncStdout:       logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", meta.Provider)),
   365  			SyncStderr:       logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", meta.Provider)),
   366  		}
   367  
   368  		client := plugin.NewClient(config)
   369  		rpcClient, err := client.Client()
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  
   374  		raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
   375  		if err != nil {
   376  			return nil, err
   377  		}
   378  
   379  		// store the client so that the plugin can kill the child process
   380  		protoVer := client.NegotiatedVersion()
   381  		switch protoVer {
   382  		case 5:
   383  			p := raw.(*tfplugin.GRPCProvider)
   384  			p.PluginClient = client
   385  			p.Addr = meta.Provider
   386  			return p, nil
   387  		case 6:
   388  			p := raw.(*tfplugin6.GRPCProvider)
   389  			p.PluginClient = client
   390  			p.Addr = meta.Provider
   391  			return p, nil
   392  		default:
   393  			panic("unsupported protocol version")
   394  		}
   395  	}
   396  }
   397  
   398  func devOverrideProviderFactory(provider addrs.Provider, localDir getproviders.PackageLocalDir) providers.Factory {
   399  	// A dev override is essentially a synthetic cache entry for our purposes
   400  	// here, so that's how we'll construct it. The providerFactory function
   401  	// doesn't actually care about the version, so we can leave it
   402  	// unspecified: overridden providers are not explicitly versioned.
   403  	log.Printf("[DEBUG] Provider %s is overridden to load from %s", provider, localDir)
   404  	return providerFactory(&providercache.CachedProvider{
   405  		Provider:   provider,
   406  		Version:    getproviders.UnspecifiedVersion,
   407  		PackageDir: string(localDir),
   408  	})
   409  }
   410  
   411  // unmanagedProviderFactory produces a provider factory that uses the passed
   412  // reattach information to connect to go-plugin processes that are already
   413  // running, and implements providers.Interface against it.
   414  func unmanagedProviderFactory(provider addrs.Provider, reattach *plugin.ReattachConfig) providers.Factory {
   415  	return func() (providers.Interface, error) {
   416  		config := &plugin.ClientConfig{
   417  			HandshakeConfig:  tfplugin.Handshake,
   418  			Logger:           logging.NewProviderLogger("unmanaged."),
   419  			AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
   420  			Managed:          false,
   421  			Reattach:         reattach,
   422  			SyncStdout:       logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", provider)),
   423  			SyncStderr:       logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", provider)),
   424  		}
   425  
   426  		if reattach.ProtocolVersion == 0 {
   427  			// As of the 0.15 release, sdk.v2 doesn't include the protocol
   428  			// version in the ReattachConfig (only recently added to
   429  			// go-plugin), so client.NegotiatedVersion() always returns 0. We
   430  			// assume that an unmanaged provider reporting protocol version 0 is
   431  			// actually using proto v5 for backwards compatibility.
   432  			if defaultPlugins, ok := tfplugin.VersionedPlugins[5]; ok {
   433  				config.Plugins = defaultPlugins
   434  			} else {
   435  				return nil, errors.New("no supported plugins for protocol 0")
   436  			}
   437  		} else if plugins, ok := tfplugin.VersionedPlugins[reattach.ProtocolVersion]; !ok {
   438  			return nil, fmt.Errorf("no supported plugins for protocol %d", reattach.ProtocolVersion)
   439  		} else {
   440  			config.Plugins = plugins
   441  		}
   442  
   443  		client := plugin.NewClient(config)
   444  		rpcClient, err := client.Client()
   445  		if err != nil {
   446  			return nil, err
   447  		}
   448  
   449  		raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
   450  		if err != nil {
   451  			return nil, err
   452  		}
   453  
   454  		// store the client so that the plugin can kill the child process
   455  		protoVer := client.NegotiatedVersion()
   456  		switch protoVer {
   457  		case 0, 5:
   458  			// As of the 0.15 release, sdk.v2 doesn't include the protocol
   459  			// version in the ReattachConfig (only recently added to
   460  			// go-plugin), so client.NegotiatedVersion() always returns 0. We
   461  			// assume that an unmanaged provider reporting protocol version 0 is
   462  			// actually using proto v5 for backwards compatibility.
   463  			p := raw.(*tfplugin.GRPCProvider)
   464  			p.PluginClient = client
   465  			return p, nil
   466  		case 6:
   467  			p := raw.(*tfplugin6.GRPCProvider)
   468  			p.PluginClient = client
   469  			return p, nil
   470  		default:
   471  			return nil, fmt.Errorf("unsupported protocol version %d", protoVer)
   472  		}
   473  	}
   474  }
   475  
   476  // providerFactoryError is a stub providers.Factory that returns an error
   477  // when called. It's used to allow providerFactories to still produce a
   478  // factory for each available provider in an error case, for situations
   479  // where the caller can do something useful with that partial result.
   480  func providerFactoryError(err error) providers.Factory {
   481  	return func() (providers.Interface, error) {
   482  		return nil, err
   483  	}
   484  }
   485  
   486  // providerPluginErrors is an error implementation we can return from
   487  // Meta.providerFactories to capture potentially multiple errors about the
   488  // locally-cached plugins (or lack thereof) for particular external providers.
   489  //
   490  // Some functions closer to the UI layer can sniff for this error type in order
   491  // to return a more helpful error message.
   492  type providerPluginErrors map[addrs.Provider]error
   493  
   494  func (errs providerPluginErrors) Error() string {
   495  	if len(errs) == 1 {
   496  		for addr, err := range errs {
   497  			return fmt.Sprintf("%s: %s", addr, err)
   498  		}
   499  	}
   500  	var buf bytes.Buffer
   501  	fmt.Fprintf(&buf, "missing or corrupted provider plugins:")
   502  	for addr, err := range errs {
   503  		fmt.Fprintf(&buf, "\n  - %s: %s", addr, err)
   504  	}
   505  	return buf.String()
   506  }