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 }