plugin/discovery: Remove dead code
Provider installation is now handled in the internal/getproviders package instead.
This commit is contained in:
parent
01f91316da
commit
8b279b6f34
|
@ -1,64 +0,0 @@
|
|||
package discovery
|
||||
|
||||
// Error is a type used to describe situations that the caller must handle
|
||||
// since they indicate some form of user error.
|
||||
//
|
||||
// The functions and methods that return these specialized errors indicate so
|
||||
// in their documentation. The Error type should not itself be used directly,
|
||||
// but rather errors should be compared using the == operator with the
|
||||
// error constants in this package.
|
||||
//
|
||||
// Values of this type are _not_ used when the error being reported is an
|
||||
// operational error (server unavailable, etc) or indicative of a bug in
|
||||
// this package or its caller.
|
||||
type Error string
|
||||
|
||||
// ErrorNoSuitableVersion indicates that a suitable version (meeting given
|
||||
// constraints) is not available.
|
||||
const ErrorNoSuitableVersion = Error("no suitable version is available")
|
||||
|
||||
// ErrorNoVersionCompatible indicates that all of the available versions
|
||||
// that otherwise met constraints are not compatible with the current
|
||||
// version of Terraform.
|
||||
const ErrorNoVersionCompatible = Error("no available version is compatible with this version of Terraform")
|
||||
|
||||
// ErrorVersionIncompatible indicates that all of the versions within the
|
||||
// constraints are not compatible with the current version of Terrafrom, though
|
||||
// there does exist a version outside of the constaints that is compatible.
|
||||
const ErrorVersionIncompatible = Error("incompatible provider version")
|
||||
|
||||
// ErrorNoSuchProvider indicates that no provider exists with a name given
|
||||
const ErrorNoSuchProvider = Error("no provider exists with the given name")
|
||||
|
||||
// ErrorNoVersionCompatibleWithPlatform indicates that all of the available
|
||||
// versions that otherwise met constraints are not compatible with the
|
||||
// requested platform
|
||||
const ErrorNoVersionCompatibleWithPlatform = Error("no available version is compatible for the requested platform")
|
||||
|
||||
// ErrorMissingChecksumVerification indicates that either the provider
|
||||
// distribution is missing the SHA256SUMS file or the checksum file does
|
||||
// not contain a checksum for the binary plugin
|
||||
const ErrorMissingChecksumVerification = Error("unable to verify checksum")
|
||||
|
||||
// ErrorChecksumVerification indicates that the current checksum of the
|
||||
// provider plugin has changed since the initial release and is not trusted
|
||||
// to download
|
||||
const ErrorChecksumVerification = Error("unexpected plugin checksum")
|
||||
|
||||
// ErrorSignatureVerification indicates that the digital signature for a
|
||||
// provider distribution could not be verified for one of the following
|
||||
// reasons: missing signature file, missing public key, or the signature
|
||||
// was not signed by any known key for the publisher
|
||||
const ErrorSignatureVerification = Error("unable to verify signature")
|
||||
|
||||
// ErrorServiceUnreachable indicates that the network was unable to connect
|
||||
// to the registry service
|
||||
const ErrorServiceUnreachable = Error("registry service is unreachable")
|
||||
|
||||
// ErrorPublicRegistryUnreachable indicates that the network was unable to connect
|
||||
// to the public registry in particular, so we can show a link to the statuspage
|
||||
const ErrorPublicRegistryUnreachable = Error("registry service is unreachable, check https://status.hashicorp.com/ for status updates")
|
||||
|
||||
func (err Error) Error() string {
|
||||
return string(err)
|
||||
}
|
|
@ -1,693 +0,0 @@
|
|||
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-svchost/disco"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"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/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(provider addrs.Provider, 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 addrs.Provider, 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.LegacyString(), v.String(), tfversion.String(),
|
||||
closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
|
||||
}
|
||||
|
||||
downloadURLs, err := i.listProviderDownloadURLs(providerSource, versionMeta.Version)
|
||||
if err != nil {
|
||||
return PluginMeta{}, diags, err
|
||||
}
|
||||
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.LegacyString(), 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.LegacyString(), versionMeta.Version)
|
||||
metas := FindPlugins("provider", []string{i.Dir})
|
||||
log.Printf("[DEBUG] all plugins found %#v", metas)
|
||||
metas, _ = metas.ValidateVersions()
|
||||
metas = metas.WithName(provider.Type).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 addrs.Provider, version Version, url string) error {
|
||||
if i.Cache != nil {
|
||||
log.Printf("[DEBUG] looking for provider %s %s in plugin cache", provider.LegacyString(), version)
|
||||
cached := i.Cache.CachedPluginPath("provider", provider.Type, version)
|
||||
if cached == "" {
|
||||
log.Printf("[DEBUG] %s %s not yet in cache, so downloading %s", provider.LegacyString(), 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.Type, 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.LegacyString(), 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.LegacyString(), 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(provider addrs.Provider) (*response.TerraformProviderVersions, error) {
|
||||
req := regsrc.NewTerraformProvider(provider.Type, i.OS, i.Arch)
|
||||
versions, err := i.registry.TerraformProviderVersions(req)
|
||||
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.
|
||||
`
|
|
@ -1,778 +0,0 @@
|
|||
package discovery
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
svchost "github.com/hashicorp/terraform-svchost"
|
||||
"github.com/hashicorp/terraform-svchost/disco"
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/httpclient"
|
||||
"github.com/hashicorp/terraform/registry"
|
||||
"github.com/hashicorp/terraform/registry/response"
|
||||
"github.com/hashicorp/terraform/version"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
const testProviderFile = "test provider binary"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
server := testReleaseServer()
|
||||
l, err := net.Listen("tcp", "127.0.0.1:8080")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// NewUnstartedServer creates a listener. Close that listener and replace
|
||||
// with the one we created.
|
||||
server.Listener.Close()
|
||||
server.Listener = l
|
||||
server.Start()
|
||||
defer server.Close()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// return the directory listing for the "test" provider
|
||||
func testListingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
if len(parts) != 6 {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
provider := parts[4]
|
||||
if provider == "test" {
|
||||
js, err := json.Marshal(versionList)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
http.Error(w, ErrorNoSuchProvider.Error(), http.StatusNotFound)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// return the download URLs for the "test" provider
|
||||
func testDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
js, err := json.Marshal(downloadURLs)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write(js)
|
||||
}
|
||||
|
||||
func testChecksumHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// this exact plugin has a signature and checksum file
|
||||
if r.URL.Path == "/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS" {
|
||||
http.ServeFile(w, r, "testdata/terraform-provider-template_0.1.0_SHA256SUMS")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig" {
|
||||
http.ServeFile(w, r, "testdata/terraform-provider-template_0.1.0_SHA256SUMS.sig")
|
||||
return
|
||||
}
|
||||
|
||||
// this this checksum file is corrupt and doesn't match the sig
|
||||
if r.URL.Path == "/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS" {
|
||||
http.ServeFile(w, r, "testdata/terraform-provider-badsig_0.1.0_SHA256SUMS")
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS.sig" {
|
||||
http.ServeFile(w, r, "testdata/terraform-provider-badsig_0.1.0_SHA256SUMS.sig")
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "signtaure files not found", http.StatusNotFound)
|
||||
}
|
||||
|
||||
// returns a 200 for a valid provider url, using the patch number for the
|
||||
// plugin protocol version.
|
||||
func testHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/versions") {
|
||||
testListingHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(r.URL.Path, "/download") {
|
||||
testDownloadHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
if len(parts) != 7 {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// write a dummy file
|
||||
z := zip.NewWriter(w)
|
||||
fn := fmt.Sprintf("%s_v%s", parts[4], parts[5])
|
||||
f, err := z.Create(fn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
io.WriteString(f, testProviderFile)
|
||||
z.Close()
|
||||
}
|
||||
|
||||
func testReleaseServer() *httptest.Server {
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/v1/providers/-/", testHandler)
|
||||
handler.HandleFunc("/v1/providers/terraform-providers/", testHandler)
|
||||
handler.HandleFunc("/terraform-provider-template/", testChecksumHandler)
|
||||
handler.HandleFunc("/terraform-provider-badsig/", testChecksumHandler)
|
||||
handler.HandleFunc("/.well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, `{"modules.v1":"http://localhost/v1/modules/", "providers.v1":"http://localhost/v1/providers/"}`)
|
||||
})
|
||||
|
||||
return httptest.NewUnstartedServer(handler)
|
||||
}
|
||||
|
||||
func TestVersionListing(t *testing.T) {
|
||||
server := testReleaseServer()
|
||||
server.Start()
|
||||
defer server.Close()
|
||||
|
||||
i := newProviderInstaller(server)
|
||||
|
||||
allVersions, err := i.listProviderVersions(addrs.Provider{Type: "test"})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var versions []*response.TerraformProviderVersion
|
||||
|
||||
for _, v := range allVersions.Versions {
|
||||
versions = append(versions, v)
|
||||
}
|
||||
|
||||
response.ProviderVersionCollection(versions).Sort()
|
||||
|
||||
expected := []*response.TerraformProviderVersion{
|
||||
{Version: "1.2.4"},
|
||||
{Version: "1.2.3"},
|
||||
{Version: "1.2.1"},
|
||||
}
|
||||
|
||||
if len(versions) != len(expected) {
|
||||
t.Fatalf("Received wrong number of versions. expected: %#v, got: %#v", expected, versions)
|
||||
}
|
||||
|
||||
for i, v := range versions {
|
||||
if v.Version != expected[i].Version {
|
||||
t.Fatalf("incorrect version: %#v, expected %#v", v, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckProtocolVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
VersionMeta *response.TerraformProviderVersion
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
&response.TerraformProviderVersion{
|
||||
Protocols: []string{"1", "2"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&response.TerraformProviderVersion{
|
||||
Protocols: []string{"4"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&response.TerraformProviderVersion{
|
||||
Protocols: []string{"4.2"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&response.TerraformProviderVersion{
|
||||
Protocols: []string{"4.0", "5.2"},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&response.TerraformProviderVersion{
|
||||
Protocols: []string{"5.0", "6.1"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
server := testReleaseServer()
|
||||
server.Start()
|
||||
defer server.Close()
|
||||
i := newProviderInstaller(server)
|
||||
|
||||
for _, test := range tests {
|
||||
err := i.checkPluginProtocol(test.VersionMeta)
|
||||
if test.Err {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindClosestProtocolCompatibleVersion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Name string
|
||||
PluginProtocolVersion uint
|
||||
ProviderVersions []*response.TerraformProviderVersion
|
||||
ExpectedVersion string
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"no compatible version",
|
||||
5,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "1.0.0",
|
||||
Protocols: []string{"4.0"},
|
||||
},
|
||||
},
|
||||
"",
|
||||
true,
|
||||
}, {
|
||||
"equal, suggests latest",
|
||||
4,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "1.0.0",
|
||||
Protocols: []string{"4.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "1.5.0",
|
||||
Protocols: []string{"4.0"},
|
||||
},
|
||||
},
|
||||
"1.5.0",
|
||||
false,
|
||||
}, {
|
||||
"provider protocol too old, suggests earliest",
|
||||
5,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "1.0.0",
|
||||
Protocols: []string{"4.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.5.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "3.0.0",
|
||||
Protocols: []string{"5.0"},
|
||||
},
|
||||
},
|
||||
"2.0.0",
|
||||
false,
|
||||
}, {
|
||||
"provider protocol too new, suggests latest",
|
||||
4,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "1.0.0",
|
||||
Protocols: []string{"4.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.5.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "3.0.0",
|
||||
Protocols: []string{"5.0"},
|
||||
},
|
||||
},
|
||||
"2.5.0",
|
||||
false,
|
||||
}, {
|
||||
"compatible prereleses are filtered",
|
||||
5,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0-alpha",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
},
|
||||
"",
|
||||
true,
|
||||
}, {
|
||||
"suggests latest non-prerelease",
|
||||
4,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0-alpha",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.5.0-pre",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.5.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
},
|
||||
"2.5.0",
|
||||
false,
|
||||
}, {
|
||||
"suggests earliest non-prerelease",
|
||||
5,
|
||||
[]*response.TerraformProviderVersion{
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0-alpha",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.0.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "2.6.0",
|
||||
Protocols: []string{"4.0", "5.0"},
|
||||
},
|
||||
&response.TerraformProviderVersion{
|
||||
Version: "3.0.0",
|
||||
Protocols: []string{"5.0"},
|
||||
},
|
||||
},
|
||||
"2.0.0",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
i := ProviderInstaller{
|
||||
Ui: cli.NewMockUi(),
|
||||
PluginProtocolVersion: tc.PluginProtocolVersion,
|
||||
}
|
||||
|
||||
closestMatch, err := i.findClosestProtocolCompatibleVersion(tc.ProviderVersions)
|
||||
if err != nil {
|
||||
if !tc.Err {
|
||||
t.Fatalf("unexpected error: %q", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tc.ExpectedVersion != closestMatch.Version {
|
||||
t.Errorf("Expected %q, got %q", tc.ExpectedVersion, closestMatch.Version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderInstallerGet(t *testing.T) {
|
||||
server := testReleaseServer()
|
||||
server.Start()
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "tf-plugin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// attempt to use an incompatible protocol version
|
||||
i := &ProviderInstaller{
|
||||
Dir: tmpDir,
|
||||
PluginProtocolVersion: 5,
|
||||
SkipVerify: true,
|
||||
Ui: cli.NewMockUi(),
|
||||
registry: registry.NewClient(Disco(server), nil),
|
||||
}
|
||||
|
||||
_, _, err = i.Get(addrs.NewLegacyProvider("test"), AllVersions)
|
||||
|
||||
if err != ErrorNoVersionCompatibleWithPlatform {
|
||||
t.Fatal("want error for incompatible version")
|
||||
}
|
||||
|
||||
i = &ProviderInstaller{
|
||||
Dir: tmpDir,
|
||||
PluginProtocolVersion: 4,
|
||||
SkipVerify: true,
|
||||
Ui: cli.NewMockUi(),
|
||||
registry: registry.NewClient(Disco(server), nil),
|
||||
OS: "mockos",
|
||||
Arch: "mockarch",
|
||||
}
|
||||
|
||||
{
|
||||
_, _, err := i.Get(addrs.NewLegacyProvider("test"), ConstraintStr(">9.0.0").MustParse())
|
||||
if err != ErrorNoSuitableVersion {
|
||||
t.Fatal("want error for mismatching constraints")
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
provider := addrs.NewLegacyProvider("nonexist")
|
||||
_, _, err := i.Get(provider, AllVersions)
|
||||
if err != ErrorNoSuchProvider {
|
||||
t.Fatal("want error for no such provider")
|
||||
}
|
||||
}
|
||||
|
||||
gotMeta, _, err := i.Get(addrs.NewLegacyProvider("test"), AllVersions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// we should have version 1.2.4
|
||||
dest := filepath.Join(tmpDir, "terraform-provider-test_v1.2.4")
|
||||
|
||||
wantMeta := PluginMeta{
|
||||
Name: "test",
|
||||
Version: VersionStr("1.2.4"),
|
||||
Path: dest,
|
||||
}
|
||||
if !reflect.DeepEqual(gotMeta, wantMeta) {
|
||||
t.Errorf("wrong result meta\ngot: %#v\nwant: %#v", gotMeta, wantMeta)
|
||||
}
|
||||
|
||||
f, err := ioutil.ReadFile(dest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// provider should have been unzipped
|
||||
if string(f) != testProviderFile {
|
||||
t.Fatalf("test provider contains: %q", f)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// test that the provider installer can install plugins from a plugin cache dir
|
||||
// into a target directory that does not exist.
|
||||
// https://github.com/hashicorp/terraform/issues/20532
|
||||
func TestProviderInstallerGet_cache(t *testing.T) {
|
||||
server := testReleaseServer()
|
||||
server.Start()
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "tf-plugin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cache := NewLocalPluginCache(filepath.Join(tmpDir, "cache"))
|
||||
targetDir := filepath.Join(tmpDir, "non-existant-dir")
|
||||
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
i := &ProviderInstaller{
|
||||
Dir: targetDir,
|
||||
Cache: cache,
|
||||
PluginProtocolVersion: 4,
|
||||
SkipVerify: true,
|
||||
Ui: cli.NewMockUi(),
|
||||
registry: registry.NewClient(Disco(server), nil),
|
||||
OS: "mockos",
|
||||
Arch: "mockarch",
|
||||
}
|
||||
|
||||
gotMeta, _, err := i.Get(addrs.NewLegacyProvider("test"), AllVersions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// we should have version 1.2.4
|
||||
dest := filepath.Join(targetDir, "terraform-provider-test_v1.2.4")
|
||||
|
||||
wantMeta := PluginMeta{
|
||||
Name: "test",
|
||||
Version: VersionStr("1.2.4"),
|
||||
Path: dest,
|
||||
}
|
||||
if !reflect.DeepEqual(gotMeta, wantMeta) {
|
||||
t.Errorf("wrong result meta\ngot: %#v\nwant: %#v", gotMeta, wantMeta)
|
||||
}
|
||||
|
||||
f, err := ioutil.ReadFile(dest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// provider should have been unzipped
|
||||
if string(f) != testProviderFile {
|
||||
t.Fatalf("test provider contains: %q", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderInstallerPurgeUnused(t *testing.T) {
|
||||
server := testReleaseServer()
|
||||
defer server.Close()
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "tf-plugin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
unwantedPath := filepath.Join(tmpDir, "terraform-provider-test_v0.0.1_x2")
|
||||
wantedPath := filepath.Join(tmpDir, "terraform-provider-test_v1.2.3_x3")
|
||||
|
||||
f, err := os.Create(unwantedPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
f, err = os.Create(wantedPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.Close()
|
||||
|
||||
i := &ProviderInstaller{
|
||||
Dir: tmpDir,
|
||||
PluginProtocolVersion: 3,
|
||||
SkipVerify: true,
|
||||
Ui: cli.NewMockUi(),
|
||||
registry: registry.NewClient(Disco(server), nil),
|
||||
}
|
||||
purged, err := i.PurgeUnused(map[string]PluginMeta{
|
||||
"test": PluginMeta{
|
||||
Name: "test",
|
||||
Version: VersionStr("1.2.3"),
|
||||
Path: wantedPath,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := purged.Count(), 1; got != want {
|
||||
t.Errorf("wrong purged count %d; want %d", got, want)
|
||||
}
|
||||
if got, want := purged.Newest().Path, unwantedPath; got != want {
|
||||
t.Errorf("wrong purged path %s; want %s", got, want)
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotFilenames := make([]string, len(files))
|
||||
for i, info := range files {
|
||||
gotFilenames[i] = info.Name()
|
||||
}
|
||||
wantFilenames := []string{"terraform-provider-test_v1.2.3_x3"}
|
||||
|
||||
if !reflect.DeepEqual(gotFilenames, wantFilenames) {
|
||||
t.Errorf("wrong filenames after purge\ngot: %#v\nwant: %#v", gotFilenames, wantFilenames)
|
||||
}
|
||||
}
|
||||
|
||||
// Test fetching a provider's checksum file while verifying its signature.
|
||||
func TestProviderChecksum(t *testing.T) {
|
||||
hashicorpKey, err := ioutil.ReadFile("testdata/hashicorp.asc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
Resp *response.TerraformProviderPlatformLocation
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"good",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
|
||||
Shasum: "3c3e7df78b1f0161a3f941c271d5501f7b5e5f2c53738e7a371459712f5d4726",
|
||||
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS",
|
||||
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig",
|
||||
SigningKeys: response.SigningKeyList{
|
||||
GPGKeys: []*response.GPGKey{
|
||||
&response.GPGKey{
|
||||
ASCIIArmor: string(hashicorpKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"bad",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
|
||||
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS",
|
||||
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-badsig/0.1.0/terraform-provider-badsig_0.1.0_SHA256SUMS.sig",
|
||||
SigningKeys: response.SigningKeyList{
|
||||
GPGKeys: []*response.GPGKey{
|
||||
&response.GPGKey{
|
||||
ASCIIArmor: string(hashicorpKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"no keys",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
|
||||
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS",
|
||||
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig",
|
||||
SigningKeys: response.SigningKeyList{
|
||||
GPGKeys: []*response.GPGKey{},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"mismatch checksum",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64.zip",
|
||||
Shasum: "force mismatch",
|
||||
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS",
|
||||
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig",
|
||||
SigningKeys: response.SigningKeyList{
|
||||
GPGKeys: []*response.GPGKey{
|
||||
&response.GPGKey{
|
||||
ASCIIArmor: string(hashicorpKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"missing checksum for file",
|
||||
&response.TerraformProviderPlatformLocation{
|
||||
Filename: "terraform-provider-template_0.1.0_darwin_amd64_missing_checksum.zip",
|
||||
Shasum: "checksum",
|
||||
ShasumsURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS",
|
||||
ShasumsSignatureURL: "http://127.0.0.1:8080/terraform-provider-template/0.1.0/terraform-provider-template_0.1.0_SHA256SUMS.sig",
|
||||
SigningKeys: response.SigningKeyList{
|
||||
GPGKeys: []*response.GPGKey{
|
||||
&response.GPGKey{
|
||||
ASCIIArmor: string(hashicorpKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
i := ProviderInstaller{}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
sha256sum, err := i.getProviderChecksum(test.Resp)
|
||||
if test.Err {
|
||||
if err == nil {
|
||||
t.Fatal("succeeded; want error")
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// get the expected checksum for our os/arch
|
||||
sumData, err := ioutil.ReadFile("testdata/terraform-provider-template_0.1.0_SHA256SUMS")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := checksumForFile(sumData, test.Resp.Filename)
|
||||
|
||||
if sha256sum != expected {
|
||||
t.Fatalf("expected: %s\ngot %s\n", sha256sum, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// newProviderInstaller returns a minimally-initialized ProviderInstaller
|
||||
func newProviderInstaller(s *httptest.Server) ProviderInstaller {
|
||||
return ProviderInstaller{
|
||||
registry: registry.NewClient(Disco(s), nil),
|
||||
OS: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
PluginProtocolVersion: 4,
|
||||
}
|
||||
}
|
||||
|
||||
// Disco return a *disco.Disco mapping registry.terraform.io, localhost,
|
||||
// localhost.localdomain, and example.com to the test server.
|
||||
func Disco(s *httptest.Server) *disco.Disco {
|
||||
services := map[string]interface{}{
|
||||
// Note that both with and without trailing slashes are supported behaviours
|
||||
"modules.v1": fmt.Sprintf("%s/v1/modules", s.URL),
|
||||
"providers.v1": fmt.Sprintf("%s/v1/providers", s.URL),
|
||||
}
|
||||
d := disco.New()
|
||||
d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
|
||||
|
||||
d.ForceHostServices(svchost.Hostname("registry.terraform.io"), services)
|
||||
d.ForceHostServices(svchost.Hostname("localhost"), services)
|
||||
d.ForceHostServices(svchost.Hostname("localhost.localdomain"), services)
|
||||
d.ForceHostServices(svchost.Hostname("example.com"), services)
|
||||
return d
|
||||
}
|
||||
|
||||
var versionList = response.TerraformProvider{
|
||||
ID: "terraform-providers/test",
|
||||
Versions: []*response.TerraformProviderVersion{
|
||||
{Version: "1.2.1"},
|
||||
{Version: "1.2.3"},
|
||||
{
|
||||
Version: "1.2.4",
|
||||
Protocols: []string{"4"},
|
||||
Platforms: []*response.TerraformProviderPlatform{
|
||||
{
|
||||
OS: "mockos",
|
||||
Arch: "mockarch",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var downloadURLs = response.TerraformProviderPlatformLocation{
|
||||
ShasumsURL: "https://registry.terraform.io/terraform-provider-template/1.2.4/terraform-provider-test_1.2.4_SHA256SUMS",
|
||||
ShasumsSignatureURL: "https://registry.terraform.io/terraform-provider-template/1.2.4/terraform-provider-test_1.2.4_SHA256SUMS.sig",
|
||||
Filename: "terraform-provider-template_1.2.4_darwin_amd64.zip",
|
||||
DownloadURL: "http://127.0.0.1:8080/v1/providers/terraform-providers/terraform-provider-test/1.2.4/terraform-provider-test_1.2.4_darwin_amd64.zip",
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package discovery
|
||||
|
||||
// HashicorpPublicKey is the HashiCorp public key, also available at
|
||||
// https://www.hashicorp.com/security
|
||||
const HashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
|
||||
W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
|
||||
fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
|
||||
3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
|
||||
KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
|
||||
SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
|
||||
cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JATgEEwECACIFAlMORM0CGwMG
|
||||
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFGFLYc0j/xMyWIIAIPhcVqiQ59n
|
||||
Jc07gjUX0SWBJAxEG1lKxfzS4Xp+57h2xxTpdotGQ1fZwsihaIqow337YHQI3q0i
|
||||
SqV534Ms+j/tU7X8sq11xFJIeEVG8PASRCwmryUwghFKPlHETQ8jJ+Y8+1asRydi
|
||||
psP3B/5Mjhqv/uOK+Vy3zAyIpyDOMtIpOVfjSpCplVRdtSTFWBu9Em7j5I2HMn1w
|
||||
sJZnJgXKpybpibGiiTtmnFLOwibmprSu04rsnP4ncdC2XRD4wIjoyA+4PKgX3sCO
|
||||
klEzKryWYBmLkJOMDdo52LttP3279s7XrkLEE7ia0fXa2c12EQ0f0DQ1tGUvyVEW
|
||||
WmJVccm5bq25AQ0EUw5EzQEIANaPUY04/g7AmYkOMjaCZ6iTp9hB5Rsj/4ee/ln9
|
||||
wArzRO9+3eejLWh53FoN1rO+su7tiXJA5YAzVy6tuolrqjM8DBztPxdLBbEi4V+j
|
||||
2tK0dATdBQBHEh3OJApO2UBtcjaZBT31zrG9K55D+CrcgIVEHAKY8Cb4kLBkb5wM
|
||||
skn+DrASKU0BNIV1qRsxfiUdQHZfSqtp004nrql1lbFMLFEuiY8FZrkkQ9qduixo
|
||||
mTT6f34/oiY+Jam3zCK7RDN/OjuWheIPGj/Qbx9JuNiwgX6yRj7OE1tjUx6d8g9y
|
||||
0H1fmLJbb3WZZbuuGFnK6qrE3bGeY8+AWaJAZ37wpWh1p0cAEQEAAYkBHwQYAQIA
|
||||
CQUCUw5EzQIbDAAKCRBRhS2HNI/8TJntCAClU7TOO/X053eKF1jqNW4A1qpxctVc
|
||||
z8eTcY8Om5O4f6a/rfxfNFKn9Qyja/OG1xWNobETy7MiMXYjaa8uUx5iFy6kMVaP
|
||||
0BXJ59NLZjMARGw6lVTYDTIvzqqqwLxgliSDfSnqUhubGwvykANPO+93BBx89MRG
|
||||
unNoYGXtPlhNFrAsB1VR8+EyKLv2HQtGCPSFBhrjuzH3gxGibNDDdFQLxxuJWepJ
|
||||
EK1UbTS4ms0NgZ2Uknqn1WRU1Ki7rE4sTy68iZtWpKQXZEJa0IGnuI2sSINGcXCJ
|
||||
oEIgXTMyCILo34Fa/C6VCm2WBgz9zZO8/rHIiQm1J5zqz0DrDwKBUM9C
|
||||
=LYpS
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
|
@ -1,19 +0,0 @@
|
|||
package discovery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/openpgp"
|
||||
)
|
||||
|
||||
// Verify the data using the provided openpgp detached signature and the
|
||||
// embedded hashicorp public key.
|
||||
func verifySig(data, sig []byte, armor string) (*openpgp.Entity, error) {
|
||||
el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(armor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openpgp.CheckDetachedSignature(el, bytes.NewReader(data), bytes.NewReader(sig))
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v1
|
||||
|
||||
mQENBFMORM0BCADBRyKO1MhCirazOSVwcfTr1xUxjPvfxD3hjUwHtjsOy/bT6p9f
|
||||
W2mRPfwnq2JB5As+paL3UGDsSRDnK9KAxQb0NNF4+eVhr/EJ18s3wwXXDMjpIifq
|
||||
fIm2WyH3G+aRLTLPIpscUNKDyxFOUbsmgXAmJ46Re1fn8uKxKRHbfa39aeuEYWFA
|
||||
3drdL1WoUngvED7f+RnKBK2G6ZEpO+LDovQk19xGjiMTtPJrjMjZJ3QXqPvx5wca
|
||||
KSZLr4lMTuoTI/ZXyZy5bD4tShiZz6KcyX27cD70q2iRcEZ0poLKHyEIDAi3TM5k
|
||||
SwbbWBFd5RNPOR0qzrb/0p9ksKK48IIfH2FvABEBAAG0K0hhc2hpQ29ycCBTZWN1
|
||||
cml0eSA8c2VjdXJpdHlAaGFzaGljb3JwLmNvbT6JATgEEwECACIFAlMORM0CGwMG
|
||||
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEFGFLYc0j/xMyWIIAIPhcVqiQ59n
|
||||
Jc07gjUX0SWBJAxEG1lKxfzS4Xp+57h2xxTpdotGQ1fZwsihaIqow337YHQI3q0i
|
||||
SqV534Ms+j/tU7X8sq11xFJIeEVG8PASRCwmryUwghFKPlHETQ8jJ+Y8+1asRydi
|
||||
psP3B/5Mjhqv/uOK+Vy3zAyIpyDOMtIpOVfjSpCplVRdtSTFWBu9Em7j5I2HMn1w
|
||||
sJZnJgXKpybpibGiiTtmnFLOwibmprSu04rsnP4ncdC2XRD4wIjoyA+4PKgX3sCO
|
||||
klEzKryWYBmLkJOMDdo52LttP3279s7XrkLEE7ia0fXa2c12EQ0f0DQ1tGUvyVEW
|
||||
WmJVccm5bq25AQ0EUw5EzQEIANaPUY04/g7AmYkOMjaCZ6iTp9hB5Rsj/4ee/ln9
|
||||
wArzRO9+3eejLWh53FoN1rO+su7tiXJA5YAzVy6tuolrqjM8DBztPxdLBbEi4V+j
|
||||
2tK0dATdBQBHEh3OJApO2UBtcjaZBT31zrG9K55D+CrcgIVEHAKY8Cb4kLBkb5wM
|
||||
skn+DrASKU0BNIV1qRsxfiUdQHZfSqtp004nrql1lbFMLFEuiY8FZrkkQ9qduixo
|
||||
mTT6f34/oiY+Jam3zCK7RDN/OjuWheIPGj/Qbx9JuNiwgX6yRj7OE1tjUx6d8g9y
|
||||
0H1fmLJbb3WZZbuuGFnK6qrE3bGeY8+AWaJAZ37wpWh1p0cAEQEAAYkBHwQYAQIA
|
||||
CQUCUw5EzQIbDAAKCRBRhS2HNI/8TJntCAClU7TOO/X053eKF1jqNW4A1qpxctVc
|
||||
z8eTcY8Om5O4f6a/rfxfNFKn9Qyja/OG1xWNobETy7MiMXYjaa8uUx5iFy6kMVaP
|
||||
0BXJ59NLZjMARGw6lVTYDTIvzqqqwLxgliSDfSnqUhubGwvykANPO+93BBx89MRG
|
||||
unNoYGXtPlhNFrAsB1VR8+EyKLv2HQtGCPSFBhrjuzH3gxGibNDDdFQLxxuJWepJ
|
||||
EK1UbTS4ms0NgZ2Uknqn1WRU1Ki7rE4sTy68iZtWpKQXZEJa0IGnuI2sSINGcXCJ
|
||||
oEIgXTMyCILo34Fa/C6VCm2WBgz9zZO8/rHIiQm1J5zqz0DrDwKBUM9C
|
||||
=LYpS
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
@ -1,12 +0,0 @@
|
|||
XX3X7df78b1f0161a3f941c271d55X1f7b5e5f2c53738e7a37145XX12f5d4726 terraform-provider-template_0.1.0_darwin_amd64.zip
|
||||
XXXXfe878e2dXb2ed0a7da1d0eb6X6Xe4703d3df93ebf22bc12aXf5X1bb38b7c terraform-provider-template_0.1.0_freebsd_386.zip
|
||||
XXXX9268ebfX8Xb63e53b2a476cX21aXf18c52e303673e2219eXc0dcXcc25622 terraform-provider-template_0.1.0_freebsd_amd64.zip
|
||||
XXXX0c5ef0X43X47ecf93c313aXd58b3X8b8df8a10d2fb5dbeX3f7ac2X81cee7 terraform-provider-template_0.1.0_freebsd_arm.zip
|
||||
XXXfXXa6dX5ddbX6903c8733cX4b69893X4f088ceb96560c7Xc876df49Xce2f4 terraform-provider-template_0.1.0_linux_386.zip
|
||||
XXX8bXX1Xe2e077X88a68e4aX271c49e2dX22b149f440ff7X362581ec11Xe380 terraform-provider-template_0.1.0_linux_amd64.zip
|
||||
XXX0969XXb34e8fcXXf7653Xd8bb42654cbX49c1d3902d8X729d3b1792daX9fe terraform-provider-template_0.1.0_linux_arm.zip
|
||||
XXX8eca7X33808ec5eX027X83c42824ac9c0Xf5a458299Xc9ae86f4a04d76X4b terraform-provider-template_0.1.0_openbsd_386.zip
|
||||
XXX18466c1590fc3cceXeXd619b29d6ea4ec1X3aab976X9dc64d1f5652d5c4Xf terraform-provider-template_0.1.0_openbsd_amd64.zip
|
||||
XXXe603de6fd57310175X842002c0cc53472c4Xf1cf5X8d306884009fd80d22X terraform-provider-template_0.1.0_solaris_amd64.zip
|
||||
XXX7a87ae47c383991f31774be8dfb70b7786cfXf22X497fe2d8b48dfcfe5ca1 terraform-provider-template_0.1.0_windows_386.zip
|
||||
XXf12267bf26a5754f740e28f445cf015e66f59aXXX681564ac45888ebd83ff0 terraform-provider-template_0.1.0_windows_amd64.zip
|
Binary file not shown.
|
@ -1,12 +0,0 @@
|
|||
3c3e7df78b1f0161a3f941c271d5501f7b5e5f2c53738e7a371459712f5d4726 terraform-provider-template_0.1.0_darwin_amd64.zip
|
||||
83fefe878e2dfb2ed0a7da1d0eb6e62e4703d3df93ebf22bc12a5f571bb38b7c terraform-provider-template_0.1.0_freebsd_386.zip
|
||||
a19c9268ebf089b63e53b2a476cf21a4f18c52e303673e2219edc0dc8cc25622 terraform-provider-template_0.1.0_freebsd_amd64.zip
|
||||
158b0c5ef0f43d47ecf93c313a6d58b398b8df8a10d2fb5dbed3f7ac2b81cee7 terraform-provider-template_0.1.0_freebsd_arm.zip
|
||||
27ef86a6d15ddb46903c8733c84b69893e4f088ceb96560c76c876df49bce2f4 terraform-provider-template_0.1.0_linux_386.zip
|
||||
7018b681ee2e077588a68e4a2271c49e2da22b149f440ff7a362581ec113e380 terraform-provider-template_0.1.0_linux_amd64.zip
|
||||
c810969a5b34e8fc94f7653fd8bb42654cb449c1d3902d8f729d3b1792da99fe terraform-provider-template_0.1.0_linux_arm.zip
|
||||
77b8eca7d33808ec5e1027d83c42824ac9c05f5a4582997c9ae86f4a04d7664b terraform-provider-template_0.1.0_openbsd_386.zip
|
||||
d4d18466c1590fc3cce5efd619b29d6ea4ec113aab97639dc64d1f5652d5c4af terraform-provider-template_0.1.0_openbsd_amd64.zip
|
||||
00be603de6fd573101757842002c0cc53472c44f1cf568d306884009fd80d224 terraform-provider-template_0.1.0_solaris_amd64.zip
|
||||
2457a87ae47c383991f31774be8dfb70b7786cfef220497fe2d8b48dfcfe5ca1 terraform-provider-template_0.1.0_windows_386.zip
|
||||
38f12267bf26a5754f740e28f445cf015e66f59a89b681564ac45888ebd83ff0 terraform-provider-template_0.1.0_windows_amd64.zip
|
Binary file not shown.
Loading…
Reference in New Issue