terraform/internal/getproviders/registry_client.go

392 lines
12 KiB
Go
Raw Normal View History

package getproviders
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
2020-04-08 22:22:07 +02:00
"io/ioutil"
"net/http"
"net/url"
"path"
"time"
"github.com/apparentlymart/go-versions/versions"
svchost "github.com/hashicorp/terraform-svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/httpclient"
"github.com/hashicorp/terraform/version"
)
const terraformVersionHeader = "X-Terraform-Version"
// registryClient is a client for the provider registry protocol that is
// specialized only for the needs of this package. It's not intended as a
// general registry API client.
type registryClient struct {
baseURL *url.URL
creds svcauth.HostCredentials
httpClient *http.Client
}
func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registryClient {
httpClient := httpclient.New()
httpClient.Timeout = 10 * time.Second
return &registryClient{
baseURL: baseURL,
creds: creds,
httpClient: httpClient,
}
}
// ProviderVersions returns the raw version strings produced by the registry
// for the given provider.
//
// The returned error will be ErrProviderNotKnown if the registry responds
// with 404 Not Found to indicate that the namespace or provider type are
// not known, ErrUnauthorized if the registry responds with 401 or 403 status
// codes, or ErrQueryFailed for any other protocol or operational problem.
func (c *registryClient) ProviderVersions(addr addrs.Provider) ([]string, error) {
endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions"))
if err != nil {
// Should never happen because we're constructing this from
// already-validated components.
return nil, err
}
endpointURL := c.baseURL.ResolveReference(endpointPath)
req, err := http.NewRequest("GET", endpointURL.String(), nil)
if err != nil {
return nil, err
}
c.addHeadersToRequest(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, c.errQueryFailed(addr, err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// Great!
case http.StatusNotFound:
return nil, ErrProviderNotKnown{
Provider: addr,
}
case http.StatusUnauthorized, http.StatusForbidden:
return nil, c.errUnauthorized(addr.Hostname)
default:
return nil, c.errQueryFailed(addr, errors.New(resp.Status))
}
// We ignore everything except the version numbers here because our goal
// is to find out which versions are available _at all_. Which ones are
// compatible with the current Terraform becomes relevant only once we've
// selected one, at which point we'll return an error if the selected one
// is incompatible.
//
// We intentionally produce an error on incompatibility, rather than
// silently ignoring an incompatible version, in order to give the user
// explicit feedback about why their selection wasn't valid and allow them
// to decide whether to fix that by changing the selection or by some other
// action such as upgrading Terraform, using a different OS to run
// Terraform, etc. Changes that affect compatibility are considered
// breaking changes from a provider API standpoint, so provider teams
// should change compatibility only in new major versions.
type ResponseBody struct {
Versions []struct {
Version string `json:"version"`
} `json:"versions"`
}
var body ResponseBody
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&body); err != nil {
return nil, c.errQueryFailed(addr, err)
}
if len(body.Versions) == 0 {
return nil, nil
}
ret := make([]string, len(body.Versions))
for i, v := range body.Versions {
ret[i] = v.Version
}
return ret, nil
}
// PackageMeta returns metadata about a distribution package for a
// provider.
//
// The returned error will be ErrPlatformNotSupported if the registry responds
// with 404 Not Found, under the assumption that the caller previously checked
// that the provider and version are valid. It will return ErrUnauthorized if
// the registry responds with 401 or 403 status codes, or ErrQueryFailed for
// any other protocol or operational problem.
func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
endpointPath, err := url.Parse(path.Join(
provider.Namespace,
provider.Type,
version.String(),
"download",
target.OS,
target.Arch,
))
if err != nil {
// Should never happen because we're constructing this from
// already-validated components.
return PackageMeta{}, err
}
endpointURL := c.baseURL.ResolveReference(endpointPath)
req, err := http.NewRequest("GET", endpointURL.String(), nil)
if err != nil {
return PackageMeta{}, err
}
c.addHeadersToRequest(req)
resp, err := c.httpClient.Do(req)
if err != nil {
return PackageMeta{}, c.errQueryFailed(provider, err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// Great!
case http.StatusNotFound:
return PackageMeta{}, ErrPlatformNotSupported{
Provider: provider,
Version: version,
Platform: target,
}
case http.StatusUnauthorized, http.StatusForbidden:
return PackageMeta{}, c.errUnauthorized(provider.Hostname)
default:
return PackageMeta{}, c.errQueryFailed(provider, errors.New(resp.Status))
}
2020-04-08 22:22:07 +02:00
type SigningKeyList struct {
GPGPublicKeys []*SigningKey `json:"gpg_public_keys"`
}
type ResponseBody struct {
Protocols []string `json:"protocols"`
OS string `json:"os"`
Arch string `json:"arch"`
Filename string `json:"filename"`
DownloadURL string `json:"download_url"`
SHA256Sum string `json:"shasum"`
2020-04-08 22:22:07 +02:00
SHA256SumsURL string `json:"shasums_url"`
SHA256SumsSignatureURL string `json:"shasums_signature_url"`
SigningKeys SigningKeyList `json:"signing_keys"`
}
var body ResponseBody
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&body); err != nil {
return PackageMeta{}, c.errQueryFailed(provider, err)
}
var protoVersions VersionList
for _, versionStr := range body.Protocols {
v, err := versions.ParseVersion(versionStr)
if err != nil {
return PackageMeta{}, c.errQueryFailed(
provider,
fmt.Errorf("registry response includes invalid version string %q: %s", versionStr, err),
)
}
protoVersions = append(protoVersions, v)
}
protoVersions.Sort()
downloadURL, err := url.Parse(body.DownloadURL)
if err != nil {
return PackageMeta{}, fmt.Errorf("registry response includes invalid download URL: %s", err)
}
downloadURL = resp.Request.URL.ResolveReference(downloadURL)
if downloadURL.Scheme != "http" && downloadURL.Scheme != "https" {
return PackageMeta{}, fmt.Errorf("registry response includes invalid download URL: must use http or https scheme")
}
ret := PackageMeta{
Provider: provider,
Version: version,
ProtocolVersions: protoVersions,
TargetPlatform: Platform{
OS: body.OS,
Arch: body.Arch,
},
Filename: body.Filename,
Location: PackageHTTPURL(downloadURL.String()),
// "Authentication" is populated below
}
if len(body.SHA256Sum) != sha256.Size*2 { // *2 because it's hex-encoded
return PackageMeta{}, c.errQueryFailed(
provider,
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
)
}
2020-04-08 22:22:07 +02:00
var checksum [sha256.Size]byte
_, err = hex.Decode(checksum[:], []byte(body.SHA256Sum))
if err != nil {
return PackageMeta{}, c.errQueryFailed(
provider,
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
)
}
2020-04-08 22:22:07 +02:00
shasumsURL, err := url.Parse(body.SHA256SumsURL)
if err != nil {
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS URL: %s", err)
}
shasumsURL = resp.Request.URL.ResolveReference(shasumsURL)
if shasumsURL.Scheme != "http" && shasumsURL.Scheme != "https" {
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS URL: must use http or https scheme")
}
document, err := c.getFile(shasumsURL)
if err != nil {
return PackageMeta{}, c.errQueryFailed(
provider,
fmt.Errorf("failed to retrieve authentication checksums for provider: %s", err),
)
}
signatureURL, err := url.Parse(body.SHA256SumsSignatureURL)
if err != nil {
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS signature URL: %s", err)
}
signatureURL = resp.Request.URL.ResolveReference(signatureURL)
if signatureURL.Scheme != "http" && signatureURL.Scheme != "https" {
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS signature URL: must use http or https scheme")
}
signature, err := c.getFile(signatureURL)
if err != nil {
return PackageMeta{}, c.errQueryFailed(
provider,
fmt.Errorf("failed to retrieve cryptographic signature for provider: %s", err),
)
}
keys := make([]SigningKey, len(body.SigningKeys.GPGPublicKeys))
for i, key := range body.SigningKeys.GPGPublicKeys {
keys[i] = *key
}
ret.Authentication = PackageAuthenticationAll(
NewMatchingChecksumAuthentication(document, body.Filename, checksum),
NewArchiveChecksumAuthentication(checksum),
NewSignatureAuthentication(document, signature, keys),
)
return ret, nil
}
internal/getproviders: LookupLegacyProvider This is a temporary helper so that we can potentially ship the new provider installer without making a breaking change by relying on the old default namespace lookup API on the default registry to find a proper FQN for a legacy provider provider address during installation. If it's given a non-legacy provider address then it just returns the given address verbatim, so any codepath using it will also correctly handle explicit full provider addresses. This also means it will automatically self-disable once we stop using addrs.NewLegacyProvider in the config loader, because there will therefore no longer be any legacy provider addresses in the config to resolve. (They'll be "default" provider addresses instead, assumed to be under registry.terraform.io/hashicorp/* ) It's not decided yet whether we will actually introduce the new provider in a minor release, but even if we don't this API function will likely be useful for a hypothetical automatic upgrade tool to introduce explicit full provider addresses into existing modules that currently rely on the equivalent to this lookup in the current provider installer. This is dead code for now, but my intent is that it would either be called as part of new provider installation to produce an address suitable to pass to Source.AvailableVersions, or it would be called from the aforementioned hypothetical upgrade tool. Whatever happens, these functions can be removed no later than one whole major release after the new provider installer is introduced, when everyone's had the opportunity to update their legacy unqualified addresses.
2020-01-22 01:01:49 +01:00
// LegacyProviderCanonicalAddress returns the raw address strings produced by
// the registry when asked about the given unqualified provider type name.
// The returned namespace string is taken verbatim from the registry's response.
//
// This method exists only to allow compatibility with unqualified names
// in older configurations. New configurations should be written so as not to
// depend on it.
func (c *registryClient) LegacyProviderDefaultNamespace(typeName string) (string, error) {
endpointPath, err := url.Parse(path.Join("-", typeName))
if err != nil {
// Should never happen because we're constructing this from
// already-validated components.
return "", err
}
endpointURL := c.baseURL.ResolveReference(endpointPath)
req, err := http.NewRequest("GET", endpointURL.String(), nil)
if err != nil {
return "", err
}
c.addHeadersToRequest(req)
// This is just to give us something to return in error messages. It's
// not a proper provider address.
placeholderProviderAddr := addrs.NewLegacyProvider(typeName)
resp, err := c.httpClient.Do(req)
if err != nil {
return "", c.errQueryFailed(placeholderProviderAddr, err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
// Great!
case http.StatusNotFound:
return "", ErrProviderNotKnown{
Provider: placeholderProviderAddr,
}
case http.StatusUnauthorized, http.StatusForbidden:
return "", c.errUnauthorized(placeholderProviderAddr.Hostname)
default:
return "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status))
}
type ResponseBody struct {
Namespace string
}
var body ResponseBody
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&body); err != nil {
return "", c.errQueryFailed(placeholderProviderAddr, err)
}
return body.Namespace, nil
}
func (c *registryClient) addHeadersToRequest(req *http.Request) {
if c.creds != nil {
c.creds.PrepareRequest(req)
}
req.Header.Set(terraformVersionHeader, version.String())
}
func (c *registryClient) errQueryFailed(provider addrs.Provider, err error) error {
return ErrQueryFailed{
Provider: provider,
Wrapped: err,
}
}
func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error {
return ErrUnauthorized{
Hostname: hostname,
HaveCredentials: c.creds != nil,
}
}
2020-04-08 22:22:07 +02:00
func (c *registryClient) getFile(url *url.URL) ([]byte, error) {
resp, err := c.httpClient.Get(url.String())
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
}