aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/plugin/discovery/get.go')
-rw-r--r--vendor/github.com/hashicorp/terraform/plugin/discovery/get.go607
1 files changed, 374 insertions, 233 deletions
diff --git a/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go b/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
index 815640f..b1d01fb 100644
--- a/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
+++ b/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
@@ -13,28 +13,27 @@ import (
13 "strconv" 13 "strconv"
14 "strings" 14 "strings"
15 15
16 "golang.org/x/net/html" 16 "github.com/hashicorp/errwrap"
17
18 getter "github.com/hashicorp/go-getter" 17 getter "github.com/hashicorp/go-getter"
19 multierror "github.com/hashicorp/go-multierror" 18 multierror "github.com/hashicorp/go-multierror"
20 "github.com/hashicorp/terraform/httpclient" 19 "github.com/hashicorp/terraform/httpclient"
20 "github.com/hashicorp/terraform/registry"
21 "github.com/hashicorp/terraform/registry/regsrc"
22 "github.com/hashicorp/terraform/registry/response"
23 "github.com/hashicorp/terraform/svchost/disco"
24 "github.com/hashicorp/terraform/tfdiags"
25 tfversion "github.com/hashicorp/terraform/version"
21 "github.com/mitchellh/cli" 26 "github.com/mitchellh/cli"
22) 27)
23 28
24// Releases are located by parsing the html listing from releases.hashicorp.com. 29// Releases are located by querying the terraform registry.
25//
26// The URL for releases follows the pattern:
27// https://releases.hashicorp.com/terraform-provider-name/<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
28//
29// The plugin protocol version will be saved with the release and returned in
30// the header X-TERRAFORM_PROTOCOL_VERSION.
31 30
32const protocolVersionHeader = "x-terraform-protocol-version" 31const protocolVersionHeader = "x-terraform-protocol-version"
33 32
34var releaseHost = "https://releases.hashicorp.com"
35
36var httpClient *http.Client 33var httpClient *http.Client
37 34
35var errVersionNotFound = errors.New("version not found")
36
38func init() { 37func init() {
39 httpClient = httpclient.New() 38 httpClient = httpclient.New()
40 39
@@ -50,7 +49,7 @@ func init() {
50// An Installer maintains a local cache of plugins by downloading plugins 49// An Installer maintains a local cache of plugins by downloading plugins
51// from an online repository. 50// from an online repository.
52type Installer interface { 51type Installer interface {
53 Get(name string, req Constraints) (PluginMeta, error) 52 Get(name string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error)
54 PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error) 53 PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
55} 54}
56 55
@@ -79,6 +78,13 @@ type ProviderInstaller struct {
79 SkipVerify bool 78 SkipVerify bool
80 79
81 Ui cli.Ui // Ui for output 80 Ui cli.Ui // Ui for output
81
82 // Services is a required *disco.Disco, which may have services and
83 // credentials pre-loaded.
84 Services *disco.Disco
85
86 // registry client
87 registry *registry.Client
82} 88}
83 89
84// Get is part of an implementation of type Installer, and attempts to download 90// Get is part of an implementation of type Installer, and attempts to download
@@ -100,96 +106,170 @@ type ProviderInstaller struct {
100// are produced under the assumption that if presented to the user they will 106// are produced under the assumption that if presented to the user they will
101// be presented alongside context about what is being installed, and thus the 107// be presented alongside context about what is being installed, and thus the
102// error messages do not redundantly include such information. 108// error messages do not redundantly include such information.
103func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) { 109func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error) {
104 versions, err := i.listProviderVersions(provider) 110 var diags tfdiags.Diagnostics
111
112 // a little bit of initialization.
113 if i.OS == "" {
114 i.OS = runtime.GOOS
115 }
116 if i.Arch == "" {
117 i.Arch = runtime.GOARCH
118 }
119 if i.registry == nil {
120 i.registry = registry.NewClient(i.Services, nil)
121 }
122
123 // get a full listing of versions for the requested provider
124 allVersions, err := i.listProviderVersions(provider)
125
105 // TODO: return multiple errors 126 // TODO: return multiple errors
106 if err != nil { 127 if err != nil {
107 return PluginMeta{}, err 128 log.Printf("[DEBUG] %s", err)
129 if registry.IsServiceUnreachable(err) {
130 registryHost, err := i.hostname()
131 if err == nil && registryHost == regsrc.PublicRegistryHost.Raw {
132 return PluginMeta{}, diags, ErrorPublicRegistryUnreachable
133 }
134 return PluginMeta{}, diags, ErrorServiceUnreachable
135 }
136 if registry.IsServiceNotProvided(err) {
137 return PluginMeta{}, diags, err
138 }
139 return PluginMeta{}, diags, ErrorNoSuchProvider
108 } 140 }
109 141
110 if len(versions) == 0 { 142 // Add any warnings from the response to diags
111 return PluginMeta{}, ErrorNoSuitableVersion 143 for _, warning := range allVersions.Warnings {
144 hostname, err := i.hostname()
145 if err != nil {
146 return PluginMeta{}, diags, err
147 }
148 diag := tfdiags.SimpleWarning(fmt.Sprintf("%s: %s", hostname, warning))
149 diags = diags.Append(diag)
112 } 150 }
113 151
114 versions = allowedVersions(versions, req) 152 if len(allVersions.Versions) == 0 {
153 return PluginMeta{}, diags, ErrorNoSuitableVersion
154 }
155 providerSource := allVersions.ID
156
157 // Filter the list of plugin versions to those which meet the version constraints
158 versions := allowedVersions(allVersions, req)
115 if len(versions) == 0 { 159 if len(versions) == 0 {
116 return PluginMeta{}, ErrorNoSuitableVersion 160 return PluginMeta{}, diags, ErrorNoSuitableVersion
117 } 161 }
118 162
119 // sort them newest to oldest 163 // sort them newest to oldest. The newest version wins!
120 Versions(versions).Sort() 164 response.ProviderVersionCollection(versions).Sort()
121 165
122 // Ensure that our installation directory exists 166 // if the chosen provider version does not support the requested platform,
123 err = os.MkdirAll(i.Dir, os.ModePerm) 167 // filter the list of acceptable versions to those that support that platform
124 if err != nil { 168 if err := i.checkPlatformCompatibility(versions[0]); err != nil {
125 return PluginMeta{}, fmt.Errorf("failed to create plugin dir %s: %s", i.Dir, err) 169 versions = i.platformCompatibleVersions(versions)
170 if len(versions) == 0 {
171 return PluginMeta{}, diags, ErrorNoVersionCompatibleWithPlatform
172 }
126 } 173 }
127 174
128 // take the first matching plugin we find 175 // we now have a winning platform-compatible version
129 for _, v := range versions { 176 versionMeta := versions[0]
130 url := i.providerURL(provider, v.String()) 177 v := VersionStr(versionMeta.Version).MustParse()
131 178
132 if !i.SkipVerify { 179 // check protocol compatibility
133 sha256, err := i.getProviderChecksum(provider, v.String()) 180 if err := i.checkPluginProtocol(versionMeta); err != nil {
134 if err != nil { 181 closestMatch, err := i.findClosestProtocolCompatibleVersion(allVersions.Versions)
135 return PluginMeta{}, err 182 if err != nil {
136 } 183 // No operation here if we can't find a version with compatible protocol
184 return PluginMeta{}, diags, err
185 }
137 186
138 // add the checksum parameter for go-getter to verify the download for us. 187 // Prompt version suggestion to UI based on closest protocol match
139 if sha256 != "" { 188 var errMsg string
140 url = url + "?checksum=sha256:" + sha256 189 closestVersion := VersionStr(closestMatch.Version).MustParse()
141 } 190 if v.NewerThan(closestVersion) {
191 errMsg = providerProtocolTooNew
192 } else {
193 errMsg = providerProtocolTooOld
142 } 194 }
143 195
144 log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v) 196 constraintStr := req.String()
145 if checkPlugin(url, i.PluginProtocolVersion) { 197 if constraintStr == "" {
146 i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %q (%s)...", provider, v.String())) 198 constraintStr = "(any version)"
147 log.Printf("[DEBUG] getting provider %q version %q", provider, v) 199 }
148 err := i.install(provider, v, url)
149 if err != nil {
150 return PluginMeta{}, err
151 }
152 200
153 // Find what we just installed 201 return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
154 // (This is weird, because go-getter doesn't directly return 202 errMsg, provider, v.String(), tfversion.String(),
155 // information about what was extracted, and we just extracted 203 closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
156 // the archive directly into a shared dir here.) 204 }
157 log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, v)
158 metas := FindPlugins("provider", []string{i.Dir})
159 log.Printf("[DEBUG] all plugins found %#v", metas)
160 metas, _ = metas.ValidateVersions()
161 metas = metas.WithName(provider).WithVersion(v)
162 log.Printf("[DEBUG] filtered plugins %#v", metas)
163 if metas.Count() == 0 {
164 // This should never happen. Suggests that the release archive
165 // contains an executable file whose name doesn't match the
166 // expected convention.
167 return PluginMeta{}, fmt.Errorf(
168 "failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
169 v,
170 )
171 }
172 205
173 if metas.Count() > 1 { 206 downloadURLs, err := i.listProviderDownloadURLs(providerSource, versionMeta.Version)
174 // This should also never happen, and suggests that a 207 providerURL := downloadURLs.DownloadURL
175 // particular version was re-released with a different 208
176 // executable filename. We consider releases as immutable, so 209 if !i.SkipVerify {
177 // this is an error. 210 // Terraform verifies the integrity of a provider release before downloading
178 return PluginMeta{}, fmt.Errorf( 211 // the plugin binary. The digital signature (SHA256SUMS.sig) on the
179 "multiple plugins installed for version %s; this is a bug in Terraform and should be reported", 212 // release distribution (SHA256SUMS) is verified with the public key of the
180 v, 213 // publisher provided in the Terraform Registry response, ensuring that
181 ) 214 // everything is as intended by the publisher. The checksum of the provider
182 } 215 // plugin is expected in the SHA256SUMS file and is double checked to match
216 // the checksum of the original published release to the Registry. This
217 // enforces immutability of releases between the Registry and the plugin's
218 // host location. Lastly, the integrity of the binary is verified upon
219 // download matches the Registry and signed checksum.
220 sha256, err := i.getProviderChecksum(downloadURLs)
221 if err != nil {
222 return PluginMeta{}, diags, err
223 }
183 224
184 // By now we know we have exactly one meta, and so "Newest" will 225 // add the checksum parameter for go-getter to verify the download for us.
185 // return that one. 226 if sha256 != "" {
186 return metas.Newest(), nil 227 providerURL = providerURL + "?checksum=sha256:" + sha256
187 } 228 }
229 }
230
231 printedProviderName := fmt.Sprintf("%q (%s)", provider, providerSource)
232 i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %s %s...", printedProviderName, versionMeta.Version))
233 log.Printf("[DEBUG] getting provider %s version %q", printedProviderName, versionMeta.Version)
234 err = i.install(provider, v, providerURL)
235 if err != nil {
236 return PluginMeta{}, diags, err
237 }
238
239 // Find what we just installed
240 // (This is weird, because go-getter doesn't directly return
241 // information about what was extracted, and we just extracted
242 // the archive directly into a shared dir here.)
243 log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, versionMeta.Version)
244 metas := FindPlugins("provider", []string{i.Dir})
245 log.Printf("[DEBUG] all plugins found %#v", metas)
246 metas, _ = metas.ValidateVersions()
247 metas = metas.WithName(provider).WithVersion(v)
248 log.Printf("[DEBUG] filtered plugins %#v", metas)
249 if metas.Count() == 0 {
250 // This should never happen. Suggests that the release archive
251 // contains an executable file whose name doesn't match the
252 // expected convention.
253 return PluginMeta{}, diags, fmt.Errorf(
254 "failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
255 versionMeta.Version,
256 )
257 }
188 258
189 log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v) 259 if metas.Count() > 1 {
260 // This should also never happen, and suggests that a
261 // particular version was re-released with a different
262 // executable filename. We consider releases as immutable, so
263 // this is an error.
264 return PluginMeta{}, diags, fmt.Errorf(
265 "multiple plugins installed for version %s; this is a bug in Terraform and should be reported",
266 versionMeta.Version,
267 )
190 } 268 }
191 269
192 return PluginMeta{}, ErrorNoVersionCompatible 270 // By now we know we have exactly one meta, and so "Newest" will
271 // return that one.
272 return metas.Newest(), diags, nil
193} 273}
194 274
195func (i *ProviderInstaller) install(provider string, version Version, url string) error { 275func (i *ProviderInstaller) install(provider string, version Version, url string) error {
@@ -215,6 +295,14 @@ func (i *ProviderInstaller) install(provider string, version Version, url string
215 // normal resolution machinery can find it. 295 // normal resolution machinery can find it.
216 filename := filepath.Base(cached) 296 filename := filepath.Base(cached)
217 targetPath := filepath.Join(i.Dir, filename) 297 targetPath := filepath.Join(i.Dir, filename)
298 // check if the target dir exists, and create it if not
299 var err error
300 if _, StatErr := os.Stat(i.Dir); os.IsNotExist(StatErr) {
301 err = os.MkdirAll(i.Dir, 0700)
302 }
303 if err != nil {
304 return err
305 }
218 306
219 log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached) 307 log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached)
220 308
@@ -280,7 +368,6 @@ func (i *ProviderInstaller) install(provider string, version Version, url string
280 return err 368 return err
281 } 369 }
282 } 370 }
283
284 return nil 371 return nil
285} 372}
286 373
@@ -316,182 +403,222 @@ func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaS
316 return removed, errs 403 return removed, errs
317} 404}
318 405
319// Plugins are referred to by the short name, but all URLs and files will use 406func (i *ProviderInstaller) getProviderChecksum(resp *response.TerraformProviderPlatformLocation) (string, error) {
320// the full name prefixed with terraform-<plugin_type>- 407 // Get SHA256SUMS file.
321func (i *ProviderInstaller) providerName(name string) string { 408 shasums, err := getFile(resp.ShasumsURL)
322 return "terraform-provider-" + name 409 if err != nil {
323} 410 log.Printf("[ERROR] error fetching checksums from %q: %s", resp.ShasumsURL, err)
411 return "", ErrorMissingChecksumVerification
412 }
324 413
325func (i *ProviderInstaller) providerFileName(name, version string) string { 414 // Get SHA256SUMS.sig file.
326 os := i.OS 415 signature, err := getFile(resp.ShasumsSignatureURL)
327 arch := i.Arch 416 if err != nil {
328 if os == "" { 417 log.Printf("[ERROR] error fetching checksums signature from %q: %s", resp.ShasumsSignatureURL, err)
329 os = runtime.GOOS 418 return "", ErrorSignatureVerification
330 } 419 }
331 if arch == "" { 420
332 arch = runtime.GOARCH 421 // Verify the GPG signature returned from the Registry.
422 asciiArmor := resp.SigningKeys.GPGASCIIArmor()
423 signer, err := verifySig(shasums, signature, asciiArmor)
424 if err != nil {
425 log.Printf("[ERROR] error verifying signature: %s", err)
426 return "", ErrorSignatureVerification
333 } 427 }
334 return fmt.Sprintf("%s_%s_%s_%s.zip", i.providerName(name), version, os, arch)
335}
336 428
337// providerVersionsURL returns the path to the released versions directory for the provider: 429 // Also verify the GPG signature against the HashiCorp public key. This is
338// https://releases.hashicorp.com/terraform-provider-name/ 430 // a temporary additional check until a more robust key verification
339func (i *ProviderInstaller) providerVersionsURL(name string) string { 431 // process is added in a future release.
340 return releaseHost + "/" + i.providerName(name) + "/" 432 _, err = verifySig(shasums, signature, HashicorpPublicKey)
341} 433 if err != nil {
434 log.Printf("[ERROR] error verifying signature against HashiCorp public key: %s", err)
435 return "", ErrorSignatureVerification
436 }
342 437
343// providerURL returns the full path to the provider file, using the current OS 438 // Display identity for GPG key which succeeded verifying the signature.
344// and ARCH: 439 // This could also be used to display to the user with i.Ui.Info().
345// .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext> 440 identities := []string{}
346func (i *ProviderInstaller) providerURL(name, version string) string { 441 for k := range signer.Identities {
347 return fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, i.providerFileName(name, version)) 442 identities = append(identities, k)
348} 443 }
444 identity := strings.Join(identities, ", ")
445 log.Printf("[DEBUG] verified GPG signature with key from %s", identity)
446
447 // Extract checksum for this os/arch platform binary and verify against Registry
448 checksum := checksumForFile(shasums, resp.Filename)
449 if checksum == "" {
450 log.Printf("[ERROR] missing checksum for %s from source %s", resp.Filename, resp.ShasumsURL)
451 return "", ErrorMissingChecksumVerification
452 } else if checksum != resp.Shasum {
453 log.Printf("[ERROR] unexpected checksum for %s from source %q", resp.Filename, resp.ShasumsURL)
454 return "", ErrorChecksumVerification
455 }
349 456
350func (i *ProviderInstaller) providerChecksumURL(name, version string) string { 457 return checksum, nil
351 fileName := fmt.Sprintf("%s_%s_SHA256SUMS", i.providerName(name), version)
352 u := fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, fileName)
353 return u
354} 458}
355 459
356func (i *ProviderInstaller) getProviderChecksum(name, version string) (string, error) { 460func (i *ProviderInstaller) hostname() (string, error) {
357 checksums, err := getPluginSHA256SUMs(i.providerChecksumURL(name, version)) 461 provider := regsrc.NewTerraformProvider("", i.OS, i.Arch)
462 svchost, err := provider.SvcHost()
358 if err != nil { 463 if err != nil {
359 return "", err 464 return "", err
360 } 465 }
361 466
362 return checksumForFile(checksums, i.providerFileName(name, version)), nil 467 return svchost.ForDisplay(), nil
363} 468}
364 469
365// Return the plugin version by making a HEAD request to the provided url. 470// list all versions available for the named provider
366// If the header is not present, we assume the latest version will be 471func (i *ProviderInstaller) listProviderVersions(name string) (*response.TerraformProviderVersions, error) {
367// compatible, and leave the check for discovery or execution. 472 provider := regsrc.NewTerraformProvider(name, i.OS, i.Arch)
368func checkPlugin(url string, pluginProtocolVersion uint) bool { 473 versions, err := i.registry.TerraformProviderVersions(provider)
369 resp, err := httpClient.Head(url) 474 return versions, err
370 if err != nil { 475}
371 log.Printf("[ERROR] error fetching plugin headers: %s", err)
372 return false
373 }
374 476
375 if resp.StatusCode != http.StatusOK { 477func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) {
376 log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status) 478 urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version)
377 return false 479 if urls == nil {
480 return nil, fmt.Errorf("No download urls found for provider %s", name)
378 } 481 }
482 return urls, err
483}
484
485// findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
486// Prerelease versions are filtered.
487func (i *ProviderInstaller) findClosestProtocolCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
488 // Loop through all the provider versions to find the earliest and latest
489 // versions that match the installer protocol to then select the closest of the two
490 var latest, earliest *response.TerraformProviderVersion
491 for _, version := range versions {
492 // Prereleases are filtered and will not be suggested
493 v, err := VersionStr(version.Version).Parse()
494 if err != nil || v.IsPrerelease() {
495 continue
496 }
379 497
380 proto := resp.Header.Get(protocolVersionHeader) 498 if err := i.checkPluginProtocol(version); err == nil {
381 if proto == "" { 499 if earliest == nil {
382 // The header isn't present, but we don't make this error fatal since 500 // Found the first provider version with compatible protocol
383 // the latest version will probably work. 501 earliest = version
384 log.Printf("[WARN] missing %s from: %s", protocolVersionHeader, url) 502 }
385 return true 503 // Update the latest protocol compatible version
504 latest = version
505 }
506 }
507 if earliest == nil {
508 // No compatible protocol was found for any version
509 return nil, ErrorNoVersionCompatible
386 } 510 }
387 511
388 protoVersion, err := strconv.Atoi(proto) 512 // Convert protocols to comparable types
513 protoString := strconv.Itoa(int(i.PluginProtocolVersion))
514 protocolVersion, err := VersionStr(protoString).Parse()
389 if err != nil { 515 if err != nil {
390 log.Printf("[ERROR] invalid ProtocolVersion: %s", proto) 516 return nil, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
391 return false
392 } 517 }
393 518
394 return protoVersion == int(pluginProtocolVersion) 519 earliestVersionProtocol, err := VersionStr(earliest.Protocols[0]).Parse()
395}
396
397// list the version available for the named plugin
398func (i *ProviderInstaller) listProviderVersions(name string) ([]Version, error) {
399 versions, err := listPluginVersions(i.providerVersionsURL(name))
400 if err != nil { 520 if err != nil {
401 // listPluginVersions returns a verbose error message indicating
402 // what was being accessed and what failed
403 return nil, err 521 return nil, err
404 } 522 }
405 return versions, nil
406}
407
408var errVersionNotFound = errors.New("version not found")
409 523
410// take the list of available versions for a plugin, and filter out those that 524 // Compare installer protocol version with the first protocol listed of the earliest match
411// don't fit the constraints. 525 // [A, B] where A is assumed the earliest compatible major version of the protocol pair
412func allowedVersions(available []Version, required Constraints) []Version { 526 if protocolVersion.NewerThan(earliestVersionProtocol) {
413 var allowed []Version 527 // Provider protocols are too old, the closest version is the earliest compatible version
414 528 return earliest, nil
415 for _, v := range available {
416 if required.Allows(v) {
417 allowed = append(allowed, v)
418 }
419 } 529 }
420 530
421 return allowed 531 // Provider protocols are too new, the closest version is the latest compatible version
532 return latest, nil
422} 533}
423 534
424// return a list of the plugin versions at the given URL 535func (i *ProviderInstaller) checkPluginProtocol(versionMeta *response.TerraformProviderVersion) error {
425func listPluginVersions(url string) ([]Version, error) { 536 // TODO: should this be a different error? We should probably differentiate between
426 resp, err := httpClient.Get(url) 537 // no compatible versions and no protocol versions listed at all
538 if len(versionMeta.Protocols) == 0 {
539 return fmt.Errorf("no plugin protocol versions listed")
540 }
541
542 protoString := strconv.Itoa(int(i.PluginProtocolVersion))
543 protocolVersion, err := VersionStr(protoString).Parse()
427 if err != nil { 544 if err != nil {
428 // http library produces a verbose error message that includes the 545 return fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
429 // URL being accessed, etc. 546 }
430 return nil, err 547 protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse()
548 if err != nil {
549 // This should not fail if the preceding function succeeded.
550 return fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String())
431 } 551 }
432 defer resp.Body.Close()
433 552
434 if resp.StatusCode != http.StatusOK { 553 for _, p := range versionMeta.Protocols {
435 body, _ := ioutil.ReadAll(resp.Body) 554 proPro, err := VersionStr(p).Parse()
436 log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body) 555 if err != nil {
437 556 // invalid protocol reported by the registry. Move along.
438 switch resp.StatusCode { 557 log.Printf("[WARN] invalid provider protocol version %q found in the registry", versionMeta.Version)
439 case http.StatusNotFound, http.StatusForbidden: 558 continue
440 // These are treated as indicative of the given name not being 559 }
441 // a valid provider name at all. 560 // success!
442 return nil, ErrorNoSuchProvider 561 if protocolConstraint.Allows(proPro) {
443 562 return nil
444 default:
445 // All other errors are assumed to be operational problems.
446 return nil, fmt.Errorf("error accessing %s: %s", url, resp.Status)
447 } 563 }
448
449 } 564 }
450 565
451 body, err := html.Parse(resp.Body) 566 return ErrorNoVersionCompatible
452 if err != nil { 567}
453 log.Fatal(err) 568
569// REVIEWER QUESTION (again): this ends up swallowing a bunch of errors from
570// checkPluginProtocol. Do they need to be percolated up better, or would
571// debug messages would suffice in these situations?
572func (i *ProviderInstaller) findPlatformCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
573 for _, version := range versions {
574 if err := i.checkPlatformCompatibility(version); err == nil {
575 return version, nil
576 }
454 } 577 }
455 578
456 names := []string{} 579 return nil, ErrorNoVersionCompatibleWithPlatform
580}
457 581
458 // all we need to do is list links on the directory listing page that look like plugins 582// platformCompatibleVersions returns a list of provider versions that are
459 var f func(*html.Node) 583// compatible with the requested platform.
460 f = func(n *html.Node) { 584func (i *ProviderInstaller) platformCompatibleVersions(versions []*response.TerraformProviderVersion) []*response.TerraformProviderVersion {
461 if n.Type == html.ElementNode && n.Data == "a" { 585 var v []*response.TerraformProviderVersion
462 c := n.FirstChild 586 for _, version := range versions {
463 if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") { 587 if err := i.checkPlatformCompatibility(version); err == nil {
464 names = append(names, c.Data) 588 v = append(v, version)
465 return
466 }
467 }
468 for c := n.FirstChild; c != nil; c = c.NextSibling {
469 f(c)
470 } 589 }
471 } 590 }
472 f(body) 591 return v
592}
473 593
474 return versionsFromNames(names), nil 594func (i *ProviderInstaller) checkPlatformCompatibility(versionMeta *response.TerraformProviderVersion) error {
595 if len(versionMeta.Platforms) == 0 {
596 return fmt.Errorf("no supported provider platforms listed")
597 }
598 for _, p := range versionMeta.Platforms {
599 if p.Arch == i.Arch && p.OS == i.OS {
600 return nil
601 }
602 }
603 return fmt.Errorf("version %s does not support the requested platform %s_%s", versionMeta.Version, i.OS, i.Arch)
475} 604}
476 605
477// parse the list of directory names into a sorted list of available versions 606// take the list of available versions for a plugin, and filter out those that
478func versionsFromNames(names []string) []Version { 607// don't fit the constraints.
479 var versions []Version 608func allowedVersions(available *response.TerraformProviderVersions, required Constraints) []*response.TerraformProviderVersion {
480 for _, name := range names { 609 var allowed []*response.TerraformProviderVersion
481 parts := strings.SplitN(name, "_", 2)
482 if len(parts) == 2 && parts[1] != "" {
483 v, err := VersionStr(parts[1]).Parse()
484 if err != nil {
485 // filter invalid versions scraped from the page
486 log.Printf("[WARN] invalid version found for %q: %s", name, err)
487 continue
488 }
489 610
490 versions = append(versions, v) 611 for _, v := range available.Versions {
612 version, err := VersionStr(v.Version).Parse()
613 if err != nil {
614 log.Printf("[WARN] invalid version found for %q: %s", available.ID, err)
615 continue
616 }
617 if required.Allows(version) {
618 allowed = append(allowed, v)
491 } 619 }
492 } 620 }
493 621 return allowed
494 return versions
495} 622}
496 623
497func checksumForFile(sums []byte, name string) string { 624func checksumForFile(sums []byte, name string) string {
@@ -504,27 +631,6 @@ func checksumForFile(sums []byte, name string) string {
504 return "" 631 return ""
505} 632}
506 633
507// fetch the SHA256SUMS file provided, and verify its signature.
508func getPluginSHA256SUMs(sumsURL string) ([]byte, error) {
509 sigURL := sumsURL + ".sig"
510
511 sums, err := getFile(sumsURL)
512 if err != nil {
513 return nil, fmt.Errorf("error fetching checksums: %s", err)
514 }
515
516 sig, err := getFile(sigURL)
517 if err != nil {
518 return nil, fmt.Errorf("error fetching checksums signature: %s", err)
519 }
520
521 if err := verifySig(sums, sig); err != nil {
522 return nil, err
523 }
524
525 return sums, nil
526}
527
528func getFile(url string) ([]byte, error) { 634func getFile(url string) ([]byte, error) {
529 resp, err := httpClient.Get(url) 635 resp, err := httpClient.Get(url)
530 if err != nil { 636 if err != nil {
@@ -543,6 +649,41 @@ func getFile(url string) ([]byte, error) {
543 return data, nil 649 return data, nil
544} 650}
545 651
546func GetReleaseHost() string { 652// providerProtocolTooOld is a message sent to the CLI UI if the provider's
547 return releaseHost 653// supported protocol versions are too old for the user's version of terraform,
548} 654// but an older version of the provider is compatible.
655const providerProtocolTooOld = `
656[reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]
657
658Provider version %s is the earliest compatible version. Select it with
659the following version constraint:
660
661 version = %q
662
663Terraform checked all of the plugin versions matching the given constraint:
664 %s
665
666Consult the documentation for this provider for more information on
667compatibility between provider and Terraform versions.
668`
669
670// providerProtocolTooNew is a message sent to the CLI UI if the provider's
671// supported protocol versions are too new for the user's version of terraform,
672// and the user could either upgrade terraform or choose an older version of the
673// provider
674const providerProtocolTooNew = `
675[reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]
676
677Provider version %s is the latest compatible version. Select it with
678the following constraint:
679
680 version = %q
681
682Terraform checked all of the plugin versions matching the given constraint:
683 %s
684
685Consult the documentation for this provider for more information on
686compatibility between provider and Terraform versions.
687
688Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases.
689`