import (
"errors"
"fmt"
+ "io"
"io/ioutil"
"log"
"net/http"
"os"
+ "path/filepath"
"runtime"
"strconv"
"strings"
- "golang.org/x/net/html"
-
- cleanhttp "github.com/hashicorp/go-cleanhttp"
+ "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()
-var httpClient = cleanhttp.DefaultClient()
+ httpGetter := &getter.HttpGetter{
+ Client: httpClient,
+ Netrc: true,
+ }
+
+ getter.Getters["http"] = httpGetter
+ getter.Getters["https"] = httpGetter
+}
// 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)
}
type ProviderInstaller struct {
Dir string
+ // Cache is used to access and update a local cache of plugins if non-nil.
+ // Can be nil to disable caching.
+ Cache PluginCache
+
PluginProtocolVersion uint
// OS and Arch specify the OS and architecture that should be used when
// Skip checksum and signature verification
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
// 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. The newest version wins!
+ response.ProviderVersionCollection(versions).Sort()
+
+ // 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
+ }
+ }
+
+ // we now have a winning platform-compatible version
+ versionMeta := versions[0]
+ v := VersionStr(versionMeta.Version).MustParse()
+
+ // 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
+ }
+
+ // 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
+ }
+
+ constraintStr := req.String()
+ if constraintStr == "" {
+ constraintStr = "(any version)"
+ }
+
+ return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
+ errMsg, provider, v.String(), tfversion.String(),
+ closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
+ }
+
+ 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
+ }
+
+ // 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
}
- // sort them newest to oldest
- Versions(versions).Sort()
+ // 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,
+ )
+ }
+
+ 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,
+ )
+ }
- // take the first matching plugin we find
- for _, v := range versions {
- url := i.providerURL(provider, v.String())
+ // By now we know we have exactly one meta, and so "Newest" will
+ // return that one.
+ return metas.Newest(), diags, nil
+}
- if !i.SkipVerify {
- sha256, err := i.getProviderChecksum(provider, v.String())
+func (i *ProviderInstaller) install(provider string, version Version, url string) error {
+ if i.Cache != nil {
+ log.Printf("[DEBUG] looking for provider %s %s in plugin cache", provider, version)
+ cached := i.Cache.CachedPluginPath("provider", provider, version)
+ if cached == "" {
+ log.Printf("[DEBUG] %s %s not yet in cache, so downloading %s", provider, version, url)
+ err := getter.Get(i.Cache.InstallDir(), url)
if err != nil {
- return PluginMeta{}, err
+ return err
+ }
+ // should now be in cache
+ cached = i.Cache.CachedPluginPath("provider", provider, version)
+ if cached == "" {
+ // should never happen if the getter is behaving properly
+ // and the plugins are packaged properly.
+ return fmt.Errorf("failed to find downloaded plugin in cache %s", i.Cache.InstallDir())
}
+ }
- // add the checksum parameter for go-getter to verify the download for us.
- if sha256 != "" {
- url = url + "?checksum=sha256:" + sha256
+ // Link or copy the cached binary into our install dir so the
+ // 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)
+
+ // Delete if we can. If there's nothing there already then no harm done.
+ // This is important because we can't create a link if there's
+ // already a file of the same name present.
+ // (any other error here we'll catch below when we try to write here)
+ os.Remove(targetPath)
+
+ // We don't attempt linking on Windows because links are not
+ // comprehensively supported by all tools/apps in Windows and
+ // so we choose to be conservative to avoid creating any
+ // weird issues for Windows users.
+ linkErr := errors.New("link not supported for Windows") // placeholder error, never actually returned
+ if runtime.GOOS != "windows" {
+ // Try hard linking first. Hard links are preferable because this
+ // creates a self-contained directory that doesn't depend on the
+ // cache after install.
+ linkErr = os.Link(cached, targetPath)
+
+ // If that failed, try a symlink. This _does_ depend on the cache
+ // after install, so the user must manage the cache more carefully
+ // in this case, but avoids creating redundant copies of the
+ // plugins on disk.
+ if linkErr != nil {
+ linkErr = os.Symlink(cached, targetPath)
}
}
- log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
- if checkPlugin(url, i.PluginProtocolVersion) {
- log.Printf("[DEBUG] getting provider %q version %q at %s", provider, v, url)
- err := getter.Get(i.Dir, url)
+ // If we still have an error then we'll try a copy as a fallback.
+ // In this case either the OS is Windows or the target filesystem
+ // can't support symlinks.
+ if linkErr != nil {
+ srcFile, err := os.Open(cached)
if err != nil {
- return PluginMeta{}, err
+ return fmt.Errorf("failed to open cached plugin %s: %s", cached, err)
}
+ defer srcFile.Close()
- // 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,
- )
+ destFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
+ if err != nil {
+ return fmt.Errorf("failed to create %s: %s", targetPath, err)
}
- 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,
- )
+ _, err = io.Copy(destFile, srcFile)
+ if err != nil {
+ destFile.Close()
+ return fmt.Errorf("failed to copy cached plugin from %s to %s: %s", cached, targetPath, err)
}
- // By now we know we have exactly one meta, and so "Newest" will
- // return that one.
- return metas.Newest(), nil
+ err = destFile.Close()
+ if err != nil {
+ return fmt.Errorf("error creating %s: %s", targetPath, err)
+ }
}
- log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v)
+ // One way or another, by the time we get here we should have either
+ // a link or a copy of the cached plugin within i.Dir, as expected.
+ } else {
+ log.Printf("[DEBUG] plugin cache is disabled, so downloading %s %s from %s", provider, version, url)
+ err := getter.Get(i.Dir, url)
+ if err != nil {
+ return err
+ }
}
-
- return PluginMeta{}, ErrorNoVersionCompatible
+ return nil
}
func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) {
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("[WARNING] 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 {
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 {
}
return data, nil
}
+
+// 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.
+`