261 lines
7.6 KiB
Go
261 lines
7.6 KiB
Go
package getproviders
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"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 ®istryClient{
|
|
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))
|
|
}
|
|
|
|
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"`
|
|
|
|
// TODO: Other metadata for signature checking
|
|
}
|
|
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{
|
|
ProtocolVersions: protoVersions,
|
|
TargetPlatform: Platform{
|
|
OS: body.OS,
|
|
Arch: body.Arch,
|
|
},
|
|
Filename: body.Filename,
|
|
Location: PackageHTTPURL(downloadURL.String()),
|
|
// SHA256Sum is populated below
|
|
}
|
|
|
|
if len(body.SHA256Sum) != len(ret.SHA256Sum)*2 {
|
|
return PackageMeta{}, c.errQueryFailed(
|
|
provider,
|
|
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
|
|
)
|
|
}
|
|
_, err = hex.Decode(ret.SHA256Sum[:], []byte(body.SHA256Sum))
|
|
if err != nil {
|
|
return PackageMeta{}, c.errQueryFailed(
|
|
provider,
|
|
fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
|
|
)
|
|
}
|
|
|
|
return ret, 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,
|
|
}
|
|
}
|