github.com/pulumi/terraform@v1.4.0/provider_source.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"net/url"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/apparentlymart/go-userdirs/userdirs"
    11  	"github.com/hashicorp/terraform-svchost/disco"
    12  
    13  	"github.com/pulumi/terraform/pkg/addrs"
    14  	"github.com/pulumi/terraform/pkg/command/cliconfig"
    15  	"github.com/pulumi/terraform/pkg/getproviders"
    16  	"github.com/pulumi/terraform/pkg/tfdiags"
    17  )
    18  
    19  // providerSource constructs a provider source based on a combination of the
    20  // CLI configuration and some default search locations. This will be the
    21  // provider source used for provider installation in the "terraform init"
    22  // command, unless overridden by the special -plugin-dir option.
    23  func providerSource(configs []*cliconfig.ProviderInstallation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) {
    24  	if len(configs) == 0 {
    25  		// If there's no explicit installation configuration then we'll build
    26  		// up an implicit one with direct registry installation along with
    27  		// some automatically-selected local filesystem mirrors.
    28  		return implicitProviderSource(services), nil
    29  	}
    30  
    31  	// There should only be zero or one configurations, which is checked by
    32  	// the validation logic in the cliconfig package. Therefore we'll just
    33  	// ignore any additional configurations in here.
    34  	config := configs[0]
    35  	return explicitProviderSource(config, services)
    36  }
    37  
    38  func explicitProviderSource(config *cliconfig.ProviderInstallation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) {
    39  	var diags tfdiags.Diagnostics
    40  	var searchRules []getproviders.MultiSourceSelector
    41  
    42  	log.Printf("[DEBUG] Explicit provider installation configuration is set")
    43  	for _, methodConfig := range config.Methods {
    44  		source, moreDiags := providerSourceForCLIConfigLocation(methodConfig.Location, services)
    45  		diags = diags.Append(moreDiags)
    46  		if moreDiags.HasErrors() {
    47  			continue
    48  		}
    49  
    50  		include, err := getproviders.ParseMultiSourceMatchingPatterns(methodConfig.Include)
    51  		if err != nil {
    52  			diags = diags.Append(tfdiags.Sourceless(
    53  				tfdiags.Error,
    54  				"Invalid provider source inclusion patterns",
    55  				fmt.Sprintf("CLI config specifies invalid provider inclusion patterns: %s.", err),
    56  			))
    57  			continue
    58  		}
    59  		exclude, err := getproviders.ParseMultiSourceMatchingPatterns(methodConfig.Exclude)
    60  		if err != nil {
    61  			diags = diags.Append(tfdiags.Sourceless(
    62  				tfdiags.Error,
    63  				"Invalid provider source exclusion patterns",
    64  				fmt.Sprintf("CLI config specifies invalid provider exclusion patterns: %s.", err),
    65  			))
    66  			continue
    67  		}
    68  
    69  		searchRules = append(searchRules, getproviders.MultiSourceSelector{
    70  			Source:  source,
    71  			Include: include,
    72  			Exclude: exclude,
    73  		})
    74  
    75  		log.Printf("[TRACE] Selected provider installation method %#v with includes %s and excludes %s", methodConfig.Location, include, exclude)
    76  	}
    77  
    78  	return getproviders.MultiSource(searchRules), diags
    79  }
    80  
    81  // implicitProviderSource builds a default provider source to use if there's
    82  // no explicit provider installation configuration in the CLI config.
    83  //
    84  // This implicit source looks in a number of local filesystem directories and
    85  // directly in a provider's upstream registry. Any providers that have at least
    86  // one version available in a local directory are implicitly excluded from
    87  // direct installation, as if the user had listed them explicitly in the
    88  // "exclude" argument in the direct provider source in the CLI config.
    89  func implicitProviderSource(services *disco.Disco) getproviders.Source {
    90  	// The local search directories we use for implicit configuration are:
    91  	// - The "terraform.d/plugins" directory in the current working directory,
    92  	//   which we've historically documented as a place to put plugins as a
    93  	//   way to include them in bundles uploaded to Terraform Cloud, where
    94  	//   there has historically otherwise been no way to use custom providers.
    95  	// - The "plugins" subdirectory of the CLI config search directory.
    96  	//   (thats ~/.terraform.d/plugins on Unix systems, equivalents elsewhere)
    97  	// - The "plugins" subdirectory of any platform-specific search paths,
    98  	//   following e.g. the XDG base directory specification on Unix systems,
    99  	//   Apple's guidelines on OS X, and "known folders" on Windows.
   100  	//
   101  	// Any provider we find in one of those implicit directories will be
   102  	// automatically excluded from direct installation from an upstream
   103  	// registry. Anything not available locally will query its primary
   104  	// upstream registry.
   105  	var searchRules []getproviders.MultiSourceSelector
   106  
   107  	// We'll track any providers we can find in the local search directories
   108  	// along the way, and then exclude them from the registry source we'll
   109  	// finally add at the end.
   110  	foundLocally := map[addrs.Provider]struct{}{}
   111  
   112  	addLocalDir := func(dir string) {
   113  		// We'll make sure the directory actually exists before we add it,
   114  		// because otherwise installation would always fail trying to look
   115  		// in non-existent directories. (This is done here rather than in
   116  		// the source itself because explicitly-selected directories via the
   117  		// CLI config, once we have them, _should_ produce an error if they
   118  		// don't exist to help users get their configurations right.)
   119  		if info, err := os.Stat(dir); err == nil && info.IsDir() {
   120  			log.Printf("[DEBUG] will search for provider plugins in %s", dir)
   121  			fsSource := getproviders.NewFilesystemMirrorSource(dir)
   122  
   123  			// We'll peep into the source to find out what providers it seems
   124  			// to be providing, so that we can exclude those from direct
   125  			// install. This might fail, in which case we'll just silently
   126  			// ignore it and assume it would fail during installation later too
   127  			// and therefore effectively doesn't provide _any_ packages.
   128  			if available, err := fsSource.AllAvailablePackages(); err == nil {
   129  				for found := range available {
   130  					foundLocally[found] = struct{}{}
   131  				}
   132  			}
   133  
   134  			searchRules = append(searchRules, getproviders.MultiSourceSelector{
   135  				Source: fsSource,
   136  			})
   137  
   138  		} else {
   139  			log.Printf("[DEBUG] ignoring non-existing provider search directory %s", dir)
   140  		}
   141  	}
   142  
   143  	addLocalDir("terraform.d/plugins") // our "vendor" directory
   144  	cliConfigDir, err := cliconfig.ConfigDir()
   145  	if err == nil {
   146  		addLocalDir(filepath.Join(cliConfigDir, "plugins"))
   147  	}
   148  
   149  	// This "userdirs" library implements an appropriate user-specific and
   150  	// app-specific directory layout for the current platform, such as XDG Base
   151  	// Directory on Unix, using the following name strings to construct a
   152  	// suitable application-specific subdirectory name following the
   153  	// conventions for each platform:
   154  	//
   155  	//   XDG (Unix): lowercase of the first string, "terraform"
   156  	//   Windows:    two-level hierarchy of first two strings, "HashiCorp\Terraform"
   157  	//   OS X:       reverse-DNS unique identifier, "io.terraform".
   158  	sysSpecificDirs := userdirs.ForApp("Terraform", "HashiCorp", "io.terraform")
   159  	for _, dir := range sysSpecificDirs.DataSearchPaths("plugins") {
   160  		addLocalDir(dir)
   161  	}
   162  
   163  	// Anything we found in local directories above is excluded from being
   164  	// looked up via the registry source we're about to construct.
   165  	var directExcluded getproviders.MultiSourceMatchingPatterns
   166  	for addr := range foundLocally {
   167  		directExcluded = append(directExcluded, addr)
   168  	}
   169  
   170  	// Last but not least, the main registry source! We'll wrap a caching
   171  	// layer around this one to help optimize the several network requests
   172  	// we'll end up making to it while treating it as one of several sources
   173  	// in a MultiSource (as recommended in the MultiSource docs).
   174  	// This one is listed last so that if a particular version is available
   175  	// both in one of the above directories _and_ in a remote registry, the
   176  	// local copy will take precedence.
   177  	searchRules = append(searchRules, getproviders.MultiSourceSelector{
   178  		Source: getproviders.NewMemoizeSource(
   179  			getproviders.NewRegistrySource(services),
   180  		),
   181  		Exclude: directExcluded,
   182  	})
   183  
   184  	return getproviders.MultiSource(searchRules)
   185  }
   186  
   187  func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) {
   188  	if loc == cliconfig.ProviderInstallationDirect {
   189  		return getproviders.NewMemoizeSource(
   190  			getproviders.NewRegistrySource(services),
   191  		), nil
   192  	}
   193  
   194  	switch loc := loc.(type) {
   195  
   196  	case cliconfig.ProviderInstallationFilesystemMirror:
   197  		return getproviders.NewFilesystemMirrorSource(string(loc)), nil
   198  
   199  	case cliconfig.ProviderInstallationNetworkMirror:
   200  		url, err := url.Parse(string(loc))
   201  		if err != nil {
   202  			var diags tfdiags.Diagnostics
   203  			diags = diags.Append(tfdiags.Sourceless(
   204  				tfdiags.Error,
   205  				"Invalid URL for provider installation source",
   206  				fmt.Sprintf("Cannot parse %q as a URL for a network provider mirror: %s.", string(loc), err),
   207  			))
   208  			return nil, diags
   209  		}
   210  		if url.Scheme != "https" || url.Host == "" {
   211  			var diags tfdiags.Diagnostics
   212  			diags = diags.Append(tfdiags.Sourceless(
   213  				tfdiags.Error,
   214  				"Invalid URL for provider installation source",
   215  				fmt.Sprintf("Cannot use %q as a URL for a network provider mirror: the mirror must be at an https: URL.", string(loc)),
   216  			))
   217  			return nil, diags
   218  		}
   219  		return getproviders.NewHTTPMirrorSource(url, services.CredentialsSource()), nil
   220  
   221  	default:
   222  		// We should not get here because the set of cases above should
   223  		// be comprehensive for all of the
   224  		// cliconfig.ProviderInstallationLocation implementations.
   225  		panic(fmt.Sprintf("unexpected provider source location type %T", loc))
   226  	}
   227  }
   228  
   229  func providerDevOverrides(configs []*cliconfig.ProviderInstallation) map[addrs.Provider]getproviders.PackageLocalDir {
   230  	if len(configs) == 0 {
   231  		return nil
   232  	}
   233  
   234  	// There should only be zero or one configurations, which is checked by
   235  	// the validation logic in the cliconfig package. Therefore we'll just
   236  	// ignore any additional configurations in here.
   237  	return configs[0].DevOverrides
   238  }