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

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