| // Copyright (c) HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package main | |
| import ( | |
| "fmt" | |
| "log" | |
| "net/url" | |
| "os" | |
| "path/filepath" | |
| "github.com/apparentlymart/go-userdirs/userdirs" | |
| "github.com/hashicorp/terraform-svchost/disco" | |
| "github.com/hashicorp/terraform/internal/addrs" | |
| "github.com/hashicorp/terraform/internal/command/cliconfig" | |
| "github.com/hashicorp/terraform/internal/getproviders" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| ) | |
| // providerSource constructs a provider source based on a combination of the | |
| // CLI configuration and some default search locations. This will be the | |
| // provider source used for provider installation in the "terraform init" | |
| // command, unless overridden by the special -plugin-dir option. | |
| func providerSource(configs []*cliconfig.ProviderInstallation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) { | |
| if len(configs) == 0 { | |
| // If there's no explicit installation configuration then we'll build | |
| // up an implicit one with direct registry installation along with | |
| // some automatically-selected local filesystem mirrors. | |
| return implicitProviderSource(services), nil | |
| } | |
| // There should only be zero or one configurations, which is checked by | |
| // the validation logic in the cliconfig package. Therefore we'll just | |
| // ignore any additional configurations in here. | |
| config := configs[0] | |
| return explicitProviderSource(config, services) | |
| } | |
| func explicitProviderSource(config *cliconfig.ProviderInstallation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| var searchRules []getproviders.MultiSourceSelector | |
| log.Printf("[DEBUG] Explicit provider installation configuration is set") | |
| for _, methodConfig := range config.Methods { | |
| source, moreDiags := providerSourceForCLIConfigLocation(methodConfig.Location, services) | |
| diags = diags.Append(moreDiags) | |
| if moreDiags.HasErrors() { | |
| continue | |
| } | |
| include, err := getproviders.ParseMultiSourceMatchingPatterns(methodConfig.Include) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid provider source inclusion patterns", | |
| fmt.Sprintf("CLI config specifies invalid provider inclusion patterns: %s.", err), | |
| )) | |
| continue | |
| } | |
| exclude, err := getproviders.ParseMultiSourceMatchingPatterns(methodConfig.Exclude) | |
| if err != nil { | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid provider source exclusion patterns", | |
| fmt.Sprintf("CLI config specifies invalid provider exclusion patterns: %s.", err), | |
| )) | |
| continue | |
| } | |
| searchRules = append(searchRules, getproviders.MultiSourceSelector{ | |
| Source: source, | |
| Include: include, | |
| Exclude: exclude, | |
| }) | |
| log.Printf("[TRACE] Selected provider installation method %#v with includes %s and excludes %s", methodConfig.Location, include, exclude) | |
| } | |
| return getproviders.MultiSource(searchRules), diags | |
| } | |
| // implicitProviderSource builds a default provider source to use if there's | |
| // no explicit provider installation configuration in the CLI config. | |
| // | |
| // This implicit source looks in a number of local filesystem directories and | |
| // directly in a provider's upstream registry. Any providers that have at least | |
| // one version available in a local directory are implicitly excluded from | |
| // direct installation, as if the user had listed them explicitly in the | |
| // "exclude" argument in the direct provider source in the CLI config. | |
| func implicitProviderSource(services *disco.Disco) getproviders.Source { | |
| // The local search directories we use for implicit configuration are: | |
| // - The "terraform.d/plugins" directory in the current working directory, | |
| // which we've historically documented as a place to put plugins as a | |
| // way to include them in bundles uploaded to Terraform Cloud, where | |
| // there has historically otherwise been no way to use custom providers. | |
| // - The "plugins" subdirectory of the CLI config search directory. | |
| // (thats ~/.terraform.d/plugins on Unix systems, equivalents elsewhere) | |
| // - The "plugins" subdirectory of any platform-specific search paths, | |
| // following e.g. the XDG base directory specification on Unix systems, | |
| // Apple's guidelines on OS X, and "known folders" on Windows. | |
| // | |
| // Any provider we find in one of those implicit directories will be | |
| // automatically excluded from direct installation from an upstream | |
| // registry. Anything not available locally will query its primary | |
| // upstream registry. | |
| var searchRules []getproviders.MultiSourceSelector | |
| // We'll track any providers we can find in the local search directories | |
| // along the way, and then exclude them from the registry source we'll | |
| // finally add at the end. | |
| foundLocally := map[addrs.Provider]struct{}{} | |
| addLocalDir := func(dir string) { | |
| // We'll make sure the directory actually exists before we add it, | |
| // because otherwise installation would always fail trying to look | |
| // in non-existent directories. (This is done here rather than in | |
| // the source itself because explicitly-selected directories via the | |
| // CLI config, once we have them, _should_ produce an error if they | |
| // don't exist to help users get their configurations right.) | |
| if info, err := os.Stat(dir); err == nil && info.IsDir() { | |
| log.Printf("[DEBUG] will search for provider plugins in %s", dir) | |
| fsSource := getproviders.NewFilesystemMirrorSource(dir) | |
| // We'll peep into the source to find out what providers it seems | |
| // to be providing, so that we can exclude those from direct | |
| // install. This might fail, in which case we'll just silently | |
| // ignore it and assume it would fail during installation later too | |
| // and therefore effectively doesn't provide _any_ packages. | |
| if available, err := fsSource.AllAvailablePackages(); err == nil { | |
| for found := range available { | |
| foundLocally[found] = struct{}{} | |
| } | |
| } | |
| searchRules = append(searchRules, getproviders.MultiSourceSelector{ | |
| Source: fsSource, | |
| }) | |
| } else { | |
| log.Printf("[DEBUG] ignoring non-existing provider search directory %s", dir) | |
| } | |
| } | |
| addLocalDir("terraform.d/plugins") // our "vendor" directory | |
| cliConfigDir, err := cliconfig.ConfigDir() | |
| if err == nil { | |
| addLocalDir(filepath.Join(cliConfigDir, "plugins")) | |
| } | |
| // This "userdirs" library implements an appropriate user-specific and | |
| // app-specific directory layout for the current platform, such as XDG Base | |
| // Directory on Unix, using the following name strings to construct a | |
| // suitable application-specific subdirectory name following the | |
| // conventions for each platform: | |
| // | |
| // XDG (Unix): lowercase of the first string, "terraform" | |
| // Windows: two-level hierarchy of first two strings, "HashiCorp\Terraform" | |
| // OS X: reverse-DNS unique identifier, "io.terraform". | |
| sysSpecificDirs := userdirs.ForApp("Terraform", "HashiCorp", "io.terraform") | |
| for _, dir := range sysSpecificDirs.DataSearchPaths("plugins") { | |
| addLocalDir(dir) | |
| } | |
| // Anything we found in local directories above is excluded from being | |
| // looked up via the registry source we're about to construct. | |
| var directExcluded getproviders.MultiSourceMatchingPatterns | |
| for addr := range foundLocally { | |
| directExcluded = append(directExcluded, addr) | |
| } | |
| // Last but not least, the main registry source! We'll wrap a caching | |
| // layer around this one to help optimize the several network requests | |
| // we'll end up making to it while treating it as one of several sources | |
| // in a MultiSource (as recommended in the MultiSource docs). | |
| // This one is listed last so that if a particular version is available | |
| // both in one of the above directories _and_ in a remote registry, the | |
| // local copy will take precedence. | |
| searchRules = append(searchRules, getproviders.MultiSourceSelector{ | |
| Source: getproviders.NewMemoizeSource( | |
| getproviders.NewRegistrySource(services), | |
| ), | |
| Exclude: directExcluded, | |
| }) | |
| return getproviders.MultiSource(searchRules) | |
| } | |
| func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocation, services *disco.Disco) (getproviders.Source, tfdiags.Diagnostics) { | |
| if loc == cliconfig.ProviderInstallationDirect { | |
| return getproviders.NewMemoizeSource( | |
| getproviders.NewRegistrySource(services), | |
| ), nil | |
| } | |
| switch loc := loc.(type) { | |
| case cliconfig.ProviderInstallationFilesystemMirror: | |
| return getproviders.NewFilesystemMirrorSource(string(loc)), nil | |
| case cliconfig.ProviderInstallationNetworkMirror: | |
| url, err := url.Parse(string(loc)) | |
| if err != nil { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid URL for provider installation source", | |
| fmt.Sprintf("Cannot parse %q as a URL for a network provider mirror: %s.", string(loc), err), | |
| )) | |
| return nil, diags | |
| } | |
| if url.Scheme != "https" || url.Host == "" { | |
| var diags tfdiags.Diagnostics | |
| diags = diags.Append(tfdiags.Sourceless( | |
| tfdiags.Error, | |
| "Invalid URL for provider installation source", | |
| fmt.Sprintf("Cannot use %q as a URL for a network provider mirror: the mirror must be at an https: URL.", string(loc)), | |
| )) | |
| return nil, diags | |
| } | |
| return getproviders.NewHTTPMirrorSource(url, services.CredentialsSource()), nil | |
| default: | |
| // We should not get here because the set of cases above should | |
| // be comprehensive for all of the | |
| // cliconfig.ProviderInstallationLocation implementations. | |
| panic(fmt.Sprintf("unexpected provider source location type %T", loc)) | |
| } | |
| } | |
| func providerDevOverrides(configs []*cliconfig.ProviderInstallation) map[addrs.Provider]getproviders.PackageLocalDir { | |
| if len(configs) == 0 { | |
| return nil | |
| } | |
| // There should only be zero or one configurations, which is checked by | |
| // the validation logic in the cliconfig package. Therefore we'll just | |
| // ignore any additional configurations in here. | |
| return configs[0].DevOverrides | |
| } |