github.com/kevinklinger/open_terraform@v1.3.6/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/kevinklinger/open_terraform/noninternal/addrs" 14 "github.com/kevinklinger/open_terraform/noninternal/command/cliconfig" 15 "github.com/kevinklinger/open_terraform/noninternal/getproviders" 16 "github.com/kevinklinger/open_terraform/noninternal/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 }