mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-02 04:07:22 -06:00
eed605ac05
* internal/initwd: Allow deprecated relative module paths In Terraform 0.11 we deprecated this form but didn't have any explicit warning for it. Now we'll still accept it but generate a warning. In a future major release we will drop this form altogether, since it is ambiguous with registry module source addresses. This codepath is covered by the command/e2etest suite. * e2e: Skip copying .exists file, if present We use this only in the "empty" test fixture in order to let git know that the directory exists. We need to skip copying it so that we can test "terraform init -from-module=...", which expects to find an empty directory. * command/e2etests: Re-enable and fix up the e2etest "acctests" We disabled all of the tests that accessed remote services like the Terraform Registry while they were being updated to support the new protocols we now expect. With those services now in place, we can re-enable these tests. Some details of exactly what output we print, etc, have intentionally changed since these tests were last updated. * e2e: refactor for modern states and plans * command/e2etest: re-enable e2etests and update for tf 0.12 compatibility plugin/discovery: mkdirAll instead of mkdir when creating cache dir
690 lines
24 KiB
Go
690 lines
24 KiB
Go
package discovery
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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 querying the terraform registry.
|
|
|
|
const protocolVersionHeader = "x-terraform-protocol-version"
|
|
|
|
var httpClient *http.Client
|
|
|
|
var errVersionNotFound = errors.New("version not found")
|
|
|
|
func init() {
|
|
httpClient = httpclient.New()
|
|
|
|
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, tfdiags.Diagnostics, error)
|
|
PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
|
|
}
|
|
|
|
// ProviderInstaller is an Installer implementation that knows how to
|
|
// download Terraform providers from the official HashiCorp releases service
|
|
// into a local directory. The files downloaded are compliant with the
|
|
// naming scheme expected by FindPlugins, so the target directory of a
|
|
// provider installer can be used as one of several plugin discovery sources.
|
|
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
|
|
// installing plugins. These use the same labels as the runtime.GOOS and
|
|
// runtime.GOARCH variables respectively, and indeed the values of these
|
|
// are used as defaults if either of these is the empty string.
|
|
OS string
|
|
Arch string
|
|
|
|
// 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
|
|
// and install a Terraform provider matching the given constraints.
|
|
//
|
|
// This method may return one of a number of sentinel errors from this
|
|
// package to indicate issues that are likely to be resolvable via user action:
|
|
//
|
|
// ErrorNoSuchProvider: no provider with the given name exists in the repository.
|
|
// ErrorNoSuitableVersion: the provider exists but no available version matches constraints.
|
|
// ErrorNoVersionCompatible: a plugin was found within the constraints but it is
|
|
// incompatible with the current Terraform version.
|
|
//
|
|
// These errors should be recognized and handled as special cases by the caller
|
|
// to present a suitable user-oriented error message.
|
|
//
|
|
// All other errors indicate an internal problem that is likely _not_ solvable
|
|
// through user action, or at least not within Terraform's scope. Error messages
|
|
// 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, 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 {
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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{}, 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
|
|
}
|
|
|
|
// 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,
|
|
)
|
|
}
|
|
|
|
// 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 {
|
|
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 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())
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// 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 fmt.Errorf("failed to open cached plugin %s: %s", cached, err)
|
|
}
|
|
defer srcFile.Close()
|
|
|
|
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)
|
|
}
|
|
|
|
_, 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)
|
|
}
|
|
|
|
err = destFile.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("error creating %s: %s", targetPath, err)
|
|
}
|
|
}
|
|
|
|
// 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 nil
|
|
}
|
|
|
|
func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) {
|
|
purge := make(PluginMetaSet)
|
|
|
|
present := FindPlugins("provider", []string{i.Dir})
|
|
for meta := range present {
|
|
chosen, ok := used[meta.Name]
|
|
if !ok {
|
|
purge.Add(meta)
|
|
}
|
|
if chosen.Path != meta.Path {
|
|
purge.Add(meta)
|
|
}
|
|
}
|
|
|
|
removed := make(PluginMetaSet)
|
|
var errs error
|
|
for meta := range purge {
|
|
path := meta.Path
|
|
err := os.Remove(path)
|
|
if err != nil {
|
|
errs = multierror.Append(errs, fmt.Errorf(
|
|
"failed to remove unused provider plugin %s: %s",
|
|
path, err,
|
|
))
|
|
} else {
|
|
removed.Add(meta)
|
|
}
|
|
}
|
|
|
|
return removed, errs
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
return checksum, nil
|
|
}
|
|
|
|
func (i *ProviderInstaller) hostname() (string, error) {
|
|
provider := regsrc.NewTerraformProvider("", i.OS, i.Arch)
|
|
svchost, err := provider.SvcHost()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return svchost.ForDisplay(), nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Convert protocols to comparable types
|
|
protoString := strconv.Itoa(int(i.PluginProtocolVersion))
|
|
protocolVersion, err := VersionStr(protoString).Parse()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
|
|
}
|
|
|
|
earliestVersionProtocol, err := VersionStr(earliest.Protocols[0]).Parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Provider protocols are too new, the closest version is the latest compatible version
|
|
return latest, nil
|
|
}
|
|
|
|
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 {
|
|
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())
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
return nil, ErrorNoVersionCompatibleWithPlatform
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
|
|
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 allowed
|
|
}
|
|
|
|
func checksumForFile(sums []byte, name string) string {
|
|
for _, line := range strings.Split(string(sums), "\n") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) > 1 && parts[1] == name {
|
|
return parts[0]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFile(url string) ([]byte, error) {
|
|
resp, err := httpClient.Get(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("%s", resp.Status)
|
|
}
|
|
|
|
data, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return data, err
|
|
}
|
|
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.
|
|
`
|