]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blobdiff - vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
Upgrade to 0.12
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / plugin / discovery / get.go
index 815640f14eb0d7f4e0b0d06904b1f82d784c638a..b1d01fb9ada1459e05242c6558e2e96d09c4e143 100644 (file)
@@ -13,28 +13,27 @@ import (
        "strconv"
        "strings"
 
-       "golang.org/x/net/html"
-
+       "github.com/hashicorp/errwrap"
        getter "github.com/hashicorp/go-getter"
        multierror "github.com/hashicorp/go-multierror"
        "github.com/hashicorp/terraform/httpclient"
+       "github.com/hashicorp/terraform/registry"
+       "github.com/hashicorp/terraform/registry/regsrc"
+       "github.com/hashicorp/terraform/registry/response"
+       "github.com/hashicorp/terraform/svchost/disco"
+       "github.com/hashicorp/terraform/tfdiags"
+       tfversion "github.com/hashicorp/terraform/version"
        "github.com/mitchellh/cli"
 )
 
-// Releases are located by parsing the html listing from releases.hashicorp.com.
-//
-// The URL for releases follows the pattern:
-//    https://releases.hashicorp.com/terraform-provider-name/<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
-//
-// The plugin protocol version will be saved with the release and returned in
-// the header X-TERRAFORM_PROTOCOL_VERSION.
+// Releases are located by querying the terraform registry.
 
 const protocolVersionHeader = "x-terraform-protocol-version"
 
-var releaseHost = "https://releases.hashicorp.com"
-
 var httpClient *http.Client
 
+var errVersionNotFound = errors.New("version not found")
+
 func init() {
        httpClient = httpclient.New()
 
@@ -50,7 +49,7 @@ func init() {
 // An Installer maintains a local cache of plugins by downloading plugins
 // from an online repository.
 type Installer interface {
-       Get(name string, req Constraints) (PluginMeta, error)
+       Get(name string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error)
        PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
 }
 
@@ -79,6 +78,13 @@ type ProviderInstaller struct {
        SkipVerify bool
 
        Ui cli.Ui // Ui for output
+
+       // Services is a required *disco.Disco, which may have services and
+       // credentials pre-loaded.
+       Services *disco.Disco
+
+       // registry client
+       registry *registry.Client
 }
 
 // Get is part of an implementation of type Installer, and attempts to download
@@ -100,96 +106,170 @@ type ProviderInstaller struct {
 // are produced under the assumption that if presented to the user they will
 // be presented alongside context about what is being installed, and thus the
 // error messages do not redundantly include such information.
-func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) {
-       versions, err := i.listProviderVersions(provider)
+func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error) {
+       var diags tfdiags.Diagnostics
+
+       // a little bit of initialization.
+       if i.OS == "" {
+               i.OS = runtime.GOOS
+       }
+       if i.Arch == "" {
+               i.Arch = runtime.GOARCH
+       }
+       if i.registry == nil {
+               i.registry = registry.NewClient(i.Services, nil)
+       }
+
+       // get a full listing of versions for the requested provider
+       allVersions, err := i.listProviderVersions(provider)
+
        // TODO: return multiple errors
        if err != nil {
-               return PluginMeta{}, err
+               log.Printf("[DEBUG] %s", err)
+               if registry.IsServiceUnreachable(err) {
+                       registryHost, err := i.hostname()
+                       if err == nil && registryHost == regsrc.PublicRegistryHost.Raw {
+                               return PluginMeta{}, diags, ErrorPublicRegistryUnreachable
+                       }
+                       return PluginMeta{}, diags, ErrorServiceUnreachable
+               }
+               if registry.IsServiceNotProvided(err) {
+                       return PluginMeta{}, diags, err
+               }
+               return PluginMeta{}, diags, ErrorNoSuchProvider
        }
 
-       if len(versions) == 0 {
-               return PluginMeta{}, ErrorNoSuitableVersion
+       // Add any warnings from the response to diags
+       for _, warning := range allVersions.Warnings {
+               hostname, err := i.hostname()
+               if err != nil {
+                       return PluginMeta{}, diags, err
+               }
+               diag := tfdiags.SimpleWarning(fmt.Sprintf("%s: %s", hostname, warning))
+               diags = diags.Append(diag)
        }
 
-       versions = allowedVersions(versions, req)
+       if len(allVersions.Versions) == 0 {
+               return PluginMeta{}, diags, ErrorNoSuitableVersion
+       }
+       providerSource := allVersions.ID
+
+       // Filter the list of plugin versions to those which meet the version constraints
+       versions := allowedVersions(allVersions, req)
        if len(versions) == 0 {
-               return PluginMeta{}, ErrorNoSuitableVersion
+               return PluginMeta{}, diags, ErrorNoSuitableVersion
        }
 
-       // sort them newest to oldest
-       Versions(versions).Sort()
+       // sort them newest to oldest. The newest version wins!
+       response.ProviderVersionCollection(versions).Sort()
 
-       // Ensure that our installation directory exists
-       err = os.MkdirAll(i.Dir, os.ModePerm)
-       if err != nil {
-               return PluginMeta{}, fmt.Errorf("failed to create plugin dir %s: %s", i.Dir, err)
+       // if the chosen provider version does not support the requested platform,
+       // filter the list of acceptable versions to those that support that platform
+       if err := i.checkPlatformCompatibility(versions[0]); err != nil {
+               versions = i.platformCompatibleVersions(versions)
+               if len(versions) == 0 {
+                       return PluginMeta{}, diags, ErrorNoVersionCompatibleWithPlatform
+               }
        }
 
-       // take the first matching plugin we find
-       for _, v := range versions {
-               url := i.providerURL(provider, v.String())
+       // we now have a winning platform-compatible version
+       versionMeta := versions[0]
+       v := VersionStr(versionMeta.Version).MustParse()
 
-               if !i.SkipVerify {
-                       sha256, err := i.getProviderChecksum(provider, v.String())
-                       if err != nil {
-                               return PluginMeta{}, err
-                       }
+       // check protocol compatibility
+       if err := i.checkPluginProtocol(versionMeta); err != nil {
+               closestMatch, err := i.findClosestProtocolCompatibleVersion(allVersions.Versions)
+               if err != nil {
+                       // No operation here if we can't find a version with compatible protocol
+                       return PluginMeta{}, diags, err
+               }
 
-                       // add the checksum parameter for go-getter to verify the download for us.
-                       if sha256 != "" {
-                               url = url + "?checksum=sha256:" + sha256
-                       }
+               // Prompt version suggestion to UI based on closest protocol match
+               var errMsg string
+               closestVersion := VersionStr(closestMatch.Version).MustParse()
+               if v.NewerThan(closestVersion) {
+                       errMsg = providerProtocolTooNew
+               } else {
+                       errMsg = providerProtocolTooOld
                }
 
-               log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
-               if checkPlugin(url, i.PluginProtocolVersion) {
-                       i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %q (%s)...", provider, v.String()))
-                       log.Printf("[DEBUG] getting provider %q version %q", provider, v)
-                       err := i.install(provider, v, url)
-                       if err != nil {
-                               return PluginMeta{}, err
-                       }
+               constraintStr := req.String()
+               if constraintStr == "" {
+                       constraintStr = "(any version)"
+               }
 
-                       // Find what we just installed
-                       // (This is weird, because go-getter doesn't directly return
-                       //  information about what was extracted, and we just extracted
-                       //  the archive directly into a shared dir here.)
-                       log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, v)
-                       metas := FindPlugins("provider", []string{i.Dir})
-                       log.Printf("[DEBUG] all plugins found %#v", metas)
-                       metas, _ = metas.ValidateVersions()
-                       metas = metas.WithName(provider).WithVersion(v)
-                       log.Printf("[DEBUG] filtered plugins %#v", metas)
-                       if metas.Count() == 0 {
-                               // This should never happen. Suggests that the release archive
-                               // contains an executable file whose name doesn't match the
-                               // expected convention.
-                               return PluginMeta{}, fmt.Errorf(
-                                       "failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
-                                       v,
-                               )
-                       }
+               return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
+                       errMsg, provider, v.String(), tfversion.String(),
+                       closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
+       }
 
-                       if metas.Count() > 1 {
-                               // This should also never happen, and suggests that a
-                               // particular version was re-released with a different
-                               // executable filename. We consider releases as immutable, so
-                               // this is an error.
-                               return PluginMeta{}, fmt.Errorf(
-                                       "multiple plugins installed for version %s; this is a bug in Terraform and should be reported",
-                                       v,
-                               )
-                       }
+       downloadURLs, err := i.listProviderDownloadURLs(providerSource, versionMeta.Version)
+       providerURL := downloadURLs.DownloadURL
+
+       if !i.SkipVerify {
+               // Terraform verifies the integrity of a provider release before downloading
+               // the plugin binary. The digital signature (SHA256SUMS.sig) on the
+               // release distribution (SHA256SUMS) is verified with the public key of the
+               // publisher provided in the Terraform Registry response, ensuring that
+               // everything is as intended by the publisher. The checksum of the provider
+               // plugin is expected in the SHA256SUMS file and is double checked to match
+               // the checksum of the original published release to the Registry. This
+               // enforces immutability of releases between the Registry and the plugin's
+               // host location. Lastly, the integrity of the binary is verified upon
+               // download matches the Registry and signed checksum.
+               sha256, err := i.getProviderChecksum(downloadURLs)
+               if err != nil {
+                       return PluginMeta{}, diags, err
+               }
 
-                       // By now we know we have exactly one meta, and so "Newest" will
-                       // return that one.
-                       return metas.Newest(), nil
+               // add the checksum parameter for go-getter to verify the download for us.
+               if sha256 != "" {
+                       providerURL = providerURL + "?checksum=sha256:" + sha256
                }
+       }
+
+       printedProviderName := fmt.Sprintf("%q (%s)", provider, providerSource)
+       i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %s %s...", printedProviderName, versionMeta.Version))
+       log.Printf("[DEBUG] getting provider %s version %q", printedProviderName, versionMeta.Version)
+       err = i.install(provider, v, providerURL)
+       if err != nil {
+               return PluginMeta{}, diags, err
+       }
+
+       // Find what we just installed
+       // (This is weird, because go-getter doesn't directly return
+       //  information about what was extracted, and we just extracted
+       //  the archive directly into a shared dir here.)
+       log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, versionMeta.Version)
+       metas := FindPlugins("provider", []string{i.Dir})
+       log.Printf("[DEBUG] all plugins found %#v", metas)
+       metas, _ = metas.ValidateVersions()
+       metas = metas.WithName(provider).WithVersion(v)
+       log.Printf("[DEBUG] filtered plugins %#v", metas)
+       if metas.Count() == 0 {
+               // This should never happen. Suggests that the release archive
+               // contains an executable file whose name doesn't match the
+               // expected convention.
+               return PluginMeta{}, diags, fmt.Errorf(
+                       "failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
+                       versionMeta.Version,
+               )
+       }
 
-               log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v)
+       if metas.Count() > 1 {
+               // This should also never happen, and suggests that a
+               // particular version was re-released with a different
+               // executable filename. We consider releases as immutable, so
+               // this is an error.
+               return PluginMeta{}, diags, fmt.Errorf(
+                       "multiple plugins installed for version %s; this is a bug in Terraform and should be reported",
+                       versionMeta.Version,
+               )
        }
 
-       return PluginMeta{}, ErrorNoVersionCompatible
+       // By now we know we have exactly one meta, and so "Newest" will
+       // return that one.
+       return metas.Newest(), diags, nil
 }
 
 func (i *ProviderInstaller) install(provider string, version Version, url string) error {
@@ -215,6 +295,14 @@ func (i *ProviderInstaller) install(provider string, version Version, url string
                // normal resolution machinery can find it.
                filename := filepath.Base(cached)
                targetPath := filepath.Join(i.Dir, filename)
+               // check if the target dir exists, and create it if not
+               var err error
+               if _, StatErr := os.Stat(i.Dir); os.IsNotExist(StatErr) {
+                       err = os.MkdirAll(i.Dir, 0700)
+               }
+               if err != nil {
+                       return err
+               }
 
                log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached)
 
@@ -280,7 +368,6 @@ func (i *ProviderInstaller) install(provider string, version Version, url string
                        return err
                }
        }
-
        return nil
 }
 
@@ -316,182 +403,222 @@ func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaS
        return removed, errs
 }
 
-// Plugins are referred to by the short name, but all URLs and files will use
-// the full name prefixed with terraform-<plugin_type>-
-func (i *ProviderInstaller) providerName(name string) string {
-       return "terraform-provider-" + name
-}
+func (i *ProviderInstaller) getProviderChecksum(resp *response.TerraformProviderPlatformLocation) (string, error) {
+       // Get SHA256SUMS file.
+       shasums, err := getFile(resp.ShasumsURL)
+       if err != nil {
+               log.Printf("[ERROR] error fetching checksums from %q: %s", resp.ShasumsURL, err)
+               return "", ErrorMissingChecksumVerification
+       }
 
-func (i *ProviderInstaller) providerFileName(name, version string) string {
-       os := i.OS
-       arch := i.Arch
-       if os == "" {
-               os = runtime.GOOS
+       // Get SHA256SUMS.sig file.
+       signature, err := getFile(resp.ShasumsSignatureURL)
+       if err != nil {
+               log.Printf("[ERROR] error fetching checksums signature from %q: %s", resp.ShasumsSignatureURL, err)
+               return "", ErrorSignatureVerification
        }
-       if arch == "" {
-               arch = runtime.GOARCH
+
+       // Verify the GPG signature returned from the Registry.
+       asciiArmor := resp.SigningKeys.GPGASCIIArmor()
+       signer, err := verifySig(shasums, signature, asciiArmor)
+       if err != nil {
+               log.Printf("[ERROR] error verifying signature: %s", err)
+               return "", ErrorSignatureVerification
        }
-       return fmt.Sprintf("%s_%s_%s_%s.zip", i.providerName(name), version, os, arch)
-}
 
-// providerVersionsURL returns the path to the released versions directory for the provider:
-// https://releases.hashicorp.com/terraform-provider-name/
-func (i *ProviderInstaller) providerVersionsURL(name string) string {
-       return releaseHost + "/" + i.providerName(name) + "/"
-}
+       // Also verify the GPG signature against the HashiCorp public key. This is
+       // a temporary additional check until a more robust key verification
+       // process is added in a future release.
+       _, err = verifySig(shasums, signature, HashicorpPublicKey)
+       if err != nil {
+               log.Printf("[ERROR] error verifying signature against HashiCorp public key: %s", err)
+               return "", ErrorSignatureVerification
+       }
 
-// providerURL returns the full path to the provider file, using the current OS
-// and ARCH:
-// .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
-func (i *ProviderInstaller) providerURL(name, version string) string {
-       return fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, i.providerFileName(name, version))
-}
+       // Display identity for GPG key which succeeded verifying the signature.
+       // This could also be used to display to the user with i.Ui.Info().
+       identities := []string{}
+       for k := range signer.Identities {
+               identities = append(identities, k)
+       }
+       identity := strings.Join(identities, ", ")
+       log.Printf("[DEBUG] verified GPG signature with key from %s", identity)
+
+       // Extract checksum for this os/arch platform binary and verify against Registry
+       checksum := checksumForFile(shasums, resp.Filename)
+       if checksum == "" {
+               log.Printf("[ERROR] missing checksum for %s from source %s", resp.Filename, resp.ShasumsURL)
+               return "", ErrorMissingChecksumVerification
+       } else if checksum != resp.Shasum {
+               log.Printf("[ERROR] unexpected checksum for %s from source %q", resp.Filename, resp.ShasumsURL)
+               return "", ErrorChecksumVerification
+       }
 
-func (i *ProviderInstaller) providerChecksumURL(name, version string) string {
-       fileName := fmt.Sprintf("%s_%s_SHA256SUMS", i.providerName(name), version)
-       u := fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, fileName)
-       return u
+       return checksum, nil
 }
 
-func (i *ProviderInstaller) getProviderChecksum(name, version string) (string, error) {
-       checksums, err := getPluginSHA256SUMs(i.providerChecksumURL(name, version))
+func (i *ProviderInstaller) hostname() (string, error) {
+       provider := regsrc.NewTerraformProvider("", i.OS, i.Arch)
+       svchost, err := provider.SvcHost()
        if err != nil {
                return "", err
        }
 
-       return checksumForFile(checksums, i.providerFileName(name, version)), nil
+       return svchost.ForDisplay(), nil
 }
 
-// Return the plugin version by making a HEAD request to the provided url.
-// If the header is not present, we assume the latest version will be
-// compatible, and leave the check for discovery or execution.
-func checkPlugin(url string, pluginProtocolVersion uint) bool {
-       resp, err := httpClient.Head(url)
-       if err != nil {
-               log.Printf("[ERROR] error fetching plugin headers: %s", err)
-               return false
-       }
+// list all versions available for the named provider
+func (i *ProviderInstaller) listProviderVersions(name string) (*response.TerraformProviderVersions, error) {
+       provider := regsrc.NewTerraformProvider(name, i.OS, i.Arch)
+       versions, err := i.registry.TerraformProviderVersions(provider)
+       return versions, err
+}
 
-       if resp.StatusCode != http.StatusOK {
-               log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status)
-               return false
+func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) {
+       urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version)
+       if urls == nil {
+               return nil, fmt.Errorf("No download urls found for provider %s", name)
        }
+       return urls, err
+}
+
+// findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
+// Prerelease versions are filtered.
+func (i *ProviderInstaller) findClosestProtocolCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
+       // Loop through all the provider versions to find the earliest and latest
+       // versions that match the installer protocol to then select the closest of the two
+       var latest, earliest *response.TerraformProviderVersion
+       for _, version := range versions {
+               // Prereleases are filtered and will not be suggested
+               v, err := VersionStr(version.Version).Parse()
+               if err != nil || v.IsPrerelease() {
+                       continue
+               }
 
-       proto := resp.Header.Get(protocolVersionHeader)
-       if proto == "" {
-               // The header isn't present, but we don't make this error fatal since
-               // the latest version will probably work.
-               log.Printf("[WARN] missing %s from: %s", protocolVersionHeader, url)
-               return true
+               if err := i.checkPluginProtocol(version); err == nil {
+                       if earliest == nil {
+                               // Found the first provider version with compatible protocol
+                               earliest = version
+                       }
+                       // Update the latest protocol compatible version
+                       latest = version
+               }
+       }
+       if earliest == nil {
+               // No compatible protocol was found for any version
+               return nil, ErrorNoVersionCompatible
        }
 
-       protoVersion, err := strconv.Atoi(proto)
+       // Convert protocols to comparable types
+       protoString := strconv.Itoa(int(i.PluginProtocolVersion))
+       protocolVersion, err := VersionStr(protoString).Parse()
        if err != nil {
-               log.Printf("[ERROR] invalid ProtocolVersion: %s", proto)
-               return false
+               return nil, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
        }
 
-       return protoVersion == int(pluginProtocolVersion)
-}
-
-// list the version available for the named plugin
-func (i *ProviderInstaller) listProviderVersions(name string) ([]Version, error) {
-       versions, err := listPluginVersions(i.providerVersionsURL(name))
+       earliestVersionProtocol, err := VersionStr(earliest.Protocols[0]).Parse()
        if err != nil {
-               // listPluginVersions returns a verbose error message indicating
-               // what was being accessed and what failed
                return nil, err
        }
-       return versions, nil
-}
-
-var errVersionNotFound = errors.New("version not found")
 
-// take the list of available versions for a plugin, and filter out those that
-// don't fit the constraints.
-func allowedVersions(available []Version, required Constraints) []Version {
-       var allowed []Version
-
-       for _, v := range available {
-               if required.Allows(v) {
-                       allowed = append(allowed, v)
-               }
+       // Compare installer protocol version with the first protocol listed of the earliest match
+       // [A, B] where A is assumed the earliest compatible major version of the protocol pair
+       if protocolVersion.NewerThan(earliestVersionProtocol) {
+               // Provider protocols are too old, the closest version is the earliest compatible version
+               return earliest, nil
        }
 
-       return allowed
+       // Provider protocols are too new, the closest version is the latest compatible version
+       return latest, nil
 }
 
-// return a list of the plugin versions at the given URL
-func listPluginVersions(url string) ([]Version, error) {
-       resp, err := httpClient.Get(url)
+func (i *ProviderInstaller) checkPluginProtocol(versionMeta *response.TerraformProviderVersion) error {
+       // TODO: should this be a different error? We should probably differentiate between
+       // no compatible versions and no protocol versions listed at all
+       if len(versionMeta.Protocols) == 0 {
+               return fmt.Errorf("no plugin protocol versions listed")
+       }
+
+       protoString := strconv.Itoa(int(i.PluginProtocolVersion))
+       protocolVersion, err := VersionStr(protoString).Parse()
        if err != nil {
-               // http library produces a verbose error message that includes the
-               // URL being accessed, etc.
-               return nil, err
+               return fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
+       }
+       protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse()
+       if err != nil {
+               // This should not fail if the preceding function succeeded.
+               return fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String())
        }
-       defer resp.Body.Close()
 
-       if resp.StatusCode != http.StatusOK {
-               body, _ := ioutil.ReadAll(resp.Body)
-               log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body)
-
-               switch resp.StatusCode {
-               case http.StatusNotFound, http.StatusForbidden:
-                       // These are treated as indicative of the given name not being
-                       // a valid provider name at all.
-                       return nil, ErrorNoSuchProvider
-
-               default:
-                       // All other errors are assumed to be operational problems.
-                       return nil, fmt.Errorf("error accessing %s: %s", url, resp.Status)
+       for _, p := range versionMeta.Protocols {
+               proPro, err := VersionStr(p).Parse()
+               if err != nil {
+                       // invalid protocol reported by the registry. Move along.
+                       log.Printf("[WARN] invalid provider protocol version %q found in the registry", versionMeta.Version)
+                       continue
+               }
+               // success!
+               if protocolConstraint.Allows(proPro) {
+                       return nil
                }
-
        }
 
-       body, err := html.Parse(resp.Body)
-       if err != nil {
-               log.Fatal(err)
+       return ErrorNoVersionCompatible
+}
+
+// REVIEWER QUESTION (again): this ends up swallowing a bunch of errors from
+// checkPluginProtocol. Do they need to be percolated up better, or would
+// debug messages would suffice in these situations?
+func (i *ProviderInstaller) findPlatformCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
+       for _, version := range versions {
+               if err := i.checkPlatformCompatibility(version); err == nil {
+                       return version, nil
+               }
        }
 
-       names := []string{}
+       return nil, ErrorNoVersionCompatibleWithPlatform
+}
 
-       // all we need to do is list links on the directory listing page that look like plugins
-       var f func(*html.Node)
-       f = func(n *html.Node) {
-               if n.Type == html.ElementNode && n.Data == "a" {
-                       c := n.FirstChild
-                       if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") {
-                               names = append(names, c.Data)
-                               return
-                       }
-               }
-               for c := n.FirstChild; c != nil; c = c.NextSibling {
-                       f(c)
+// platformCompatibleVersions returns a list of provider versions that are
+// compatible with the requested platform.
+func (i *ProviderInstaller) platformCompatibleVersions(versions []*response.TerraformProviderVersion) []*response.TerraformProviderVersion {
+       var v []*response.TerraformProviderVersion
+       for _, version := range versions {
+               if err := i.checkPlatformCompatibility(version); err == nil {
+                       v = append(v, version)
                }
        }
-       f(body)
+       return v
+}
 
-       return versionsFromNames(names), nil
+func (i *ProviderInstaller) checkPlatformCompatibility(versionMeta *response.TerraformProviderVersion) error {
+       if len(versionMeta.Platforms) == 0 {
+               return fmt.Errorf("no supported provider platforms listed")
+       }
+       for _, p := range versionMeta.Platforms {
+               if p.Arch == i.Arch && p.OS == i.OS {
+                       return nil
+               }
+       }
+       return fmt.Errorf("version %s does not support the requested platform %s_%s", versionMeta.Version, i.OS, i.Arch)
 }
 
-// parse the list of directory names into a sorted list of available versions
-func versionsFromNames(names []string) []Version {
-       var versions []Version
-       for _, name := range names {
-               parts := strings.SplitN(name, "_", 2)
-               if len(parts) == 2 && parts[1] != "" {
-                       v, err := VersionStr(parts[1]).Parse()
-                       if err != nil {
-                               // filter invalid versions scraped from the page
-                               log.Printf("[WARN] invalid version found for %q: %s", name, err)
-                               continue
-                       }
+// take the list of available versions for a plugin, and filter out those that
+// don't fit the constraints.
+func allowedVersions(available *response.TerraformProviderVersions, required Constraints) []*response.TerraformProviderVersion {
+       var allowed []*response.TerraformProviderVersion
 
-                       versions = append(versions, v)
+       for _, v := range available.Versions {
+               version, err := VersionStr(v.Version).Parse()
+               if err != nil {
+                       log.Printf("[WARN] invalid version found for %q: %s", available.ID, err)
+                       continue
+               }
+               if required.Allows(version) {
+                       allowed = append(allowed, v)
                }
        }
-
-       return versions
+       return allowed
 }
 
 func checksumForFile(sums []byte, name string) string {
@@ -504,27 +631,6 @@ func checksumForFile(sums []byte, name string) string {
        return ""
 }
 
-// fetch the SHA256SUMS file provided, and verify its signature.
-func getPluginSHA256SUMs(sumsURL string) ([]byte, error) {
-       sigURL := sumsURL + ".sig"
-
-       sums, err := getFile(sumsURL)
-       if err != nil {
-               return nil, fmt.Errorf("error fetching checksums: %s", err)
-       }
-
-       sig, err := getFile(sigURL)
-       if err != nil {
-               return nil, fmt.Errorf("error fetching checksums signature: %s", err)
-       }
-
-       if err := verifySig(sums, sig); err != nil {
-               return nil, err
-       }
-
-       return sums, nil
-}
-
 func getFile(url string) ([]byte, error) {
        resp, err := httpClient.Get(url)
        if err != nil {
@@ -543,6 +649,41 @@ func getFile(url string) ([]byte, error) {
        return data, nil
 }
 
-func GetReleaseHost() string {
-       return releaseHost
-}
+// providerProtocolTooOld is a message sent to the CLI UI if the provider's
+// supported protocol versions are too old for the user's version of terraform,
+// but an older version of the provider is compatible.
+const providerProtocolTooOld = `
+[reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]
+
+Provider version %s is the earliest compatible version. Select it with 
+the following version constraint:
+
+       version = %q
+
+Terraform checked all of the plugin versions matching the given constraint:
+    %s
+
+Consult the documentation for this provider for more information on
+compatibility between provider and Terraform versions.
+`
+
+// providerProtocolTooNew is a message sent to the CLI UI if the provider's
+// supported protocol versions are too new for the user's version of terraform,
+// and the user could either upgrade terraform or choose an older version of the
+// provider
+const providerProtocolTooNew = `
+[reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]
+
+Provider version %s is the latest compatible version. Select it with 
+the following constraint:
+
+    version = %q
+
+Terraform checked all of the plugin versions matching the given constraint:
+    %s
+
+Consult the documentation for this provider for more information on
+compatibility between provider and Terraform versions.
+
+Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases.
+`