internal/getproviders: HTTPMirrorSource implementation
We previously had this just stubbed out because it was a stretch goal for the v0.13.0 release and it ultimately didn't make it in. Here we fill out the existing stub -- with a minor change to its interface so it can access credentials -- with a client implementation that is compatible with the directory structure produced by the "terraform providers mirror" subcommand, were the result to be published on a static file server.
This commit is contained in:
parent
146e983c36
commit
2bd2a9a923
|
@ -2,6 +2,7 @@ package getproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
svchost "github.com/hashicorp/terraform-svchost"
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
@ -123,9 +124,23 @@ type ErrPlatformNotSupported struct {
|
||||||
Provider addrs.Provider
|
Provider addrs.Provider
|
||||||
Version Version
|
Version Version
|
||||||
Platform Platform
|
Platform Platform
|
||||||
|
|
||||||
|
// MirrorURL, if non-nil, is the base URL of the mirror that serviced
|
||||||
|
// the request in place of the provider's origin registry. MirrorURL
|
||||||
|
// is nil for a direct query.
|
||||||
|
MirrorURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrPlatformNotSupported) Error() string {
|
func (err ErrPlatformNotSupported) Error() string {
|
||||||
|
if err.MirrorURL != nil {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"provider mirror %s does not have a package of %s %s for %s",
|
||||||
|
err.MirrorURL.String(),
|
||||||
|
err.Provider,
|
||||||
|
err.Version,
|
||||||
|
err.Platform,
|
||||||
|
)
|
||||||
|
}
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"provider %s %s is not available for %s",
|
"provider %s %s is not available for %s",
|
||||||
err.Provider,
|
err.Provider,
|
||||||
|
@ -166,9 +181,22 @@ func (err ErrProtocolNotSupported) Error() string {
|
||||||
type ErrQueryFailed struct {
|
type ErrQueryFailed struct {
|
||||||
Provider addrs.Provider
|
Provider addrs.Provider
|
||||||
Wrapped error
|
Wrapped error
|
||||||
|
|
||||||
|
// MirrorURL, if non-nil, is the base URL of the mirror that serviced
|
||||||
|
// the request in place of the provider's origin registry. MirrorURL
|
||||||
|
// is nil for a direct query.
|
||||||
|
MirrorURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrQueryFailed) Error() string {
|
func (err ErrQueryFailed) Error() string {
|
||||||
|
if err.MirrorURL != nil {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"failed to query provider mirror %s for %s: %s",
|
||||||
|
err.MirrorURL.String(),
|
||||||
|
err.Provider.String(),
|
||||||
|
err.Wrapped.Error(),
|
||||||
|
)
|
||||||
|
}
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"could not query provider registry for %s: %s",
|
"could not query provider registry for %s: %s",
|
||||||
err.Provider.String(),
|
err.Provider.String(),
|
||||||
|
|
|
@ -51,6 +51,23 @@ func PackageMatchesHash(loc PackageLocation, want string) (bool, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreferredHash examines all of the given hash strings and returns the one
|
||||||
|
// that the current version of Terraform considers to provide the strongest
|
||||||
|
// verification.
|
||||||
|
//
|
||||||
|
// Returns an empty string if none of the given hashes are of a supported
|
||||||
|
// format. If PreferredHash returns a non-empty string then it will be one
|
||||||
|
// of the hash strings in "given", and that hash is the one that must pass
|
||||||
|
// verification in order for a package to be considered valid.
|
||||||
|
func PreferredHash(given []string) string {
|
||||||
|
for _, s := range given {
|
||||||
|
if strings.HasPrefix(s, "h1:") {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// PackageHashV1 computes a hash of the contents of the package at the given
|
// PackageHashV1 computes a hash of the contents of the package at the given
|
||||||
// location using hash algorithm 1.
|
// location using hash algorithm 1.
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,16 +1,33 @@
|
||||||
package getproviders
|
package getproviders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
|
svcauth "github.com/hashicorp/terraform-svchost/auth"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
"github.com/hashicorp/terraform/helper/logging"
|
||||||
|
"github.com/hashicorp/terraform/httpclient"
|
||||||
|
"github.com/hashicorp/terraform/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPMirrorSource is a source that reads provider metadata from a provider
|
// HTTPMirrorSource is a source that reads provider metadata from a provider
|
||||||
// mirror that is accessible over the HTTP provider mirror protocol.
|
// mirror that is accessible over the HTTP provider mirror protocol.
|
||||||
type HTTPMirrorSource struct {
|
type HTTPMirrorSource struct {
|
||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
|
creds svcauth.CredentialsSource
|
||||||
|
httpClient *retryablehttp.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Source = (*HTTPMirrorSource)(nil)
|
var _ Source = (*HTTPMirrorSource)(nil)
|
||||||
|
@ -18,25 +35,380 @@ var _ Source = (*HTTPMirrorSource)(nil)
|
||||||
// NewHTTPMirrorSource constructs and returns a new network mirror source with
|
// NewHTTPMirrorSource constructs and returns a new network mirror source with
|
||||||
// the given base URL. The relative URL offsets defined by the HTTP mirror
|
// the given base URL. The relative URL offsets defined by the HTTP mirror
|
||||||
// protocol will be resolve relative to the given URL.
|
// protocol will be resolve relative to the given URL.
|
||||||
func NewHTTPMirrorSource(baseURL *url.URL) *HTTPMirrorSource {
|
//
|
||||||
|
// The given URL must use the "https" scheme, or this function will panic.
|
||||||
|
// (When the URL comes from user input, such as in the CLI config, it's the
|
||||||
|
// UI/config layer's responsibility to validate this and return a suitable
|
||||||
|
// error message for the end-user audience.)
|
||||||
|
func NewHTTPMirrorSource(baseURL *url.URL, creds svcauth.CredentialsSource) *HTTPMirrorSource {
|
||||||
|
httpClient := httpclient.New()
|
||||||
|
httpClient.Timeout = requestTimeout
|
||||||
|
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||||
|
// If we get redirected more than five times we'll assume we're
|
||||||
|
// in a redirect loop and bail out, rather than hanging forever.
|
||||||
|
if len(via) > 5 {
|
||||||
|
return fmt.Errorf("too many redirects")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return newHTTPMirrorSourceWithHTTPClient(baseURL, creds, httpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPMirrorSourceWithHTTPClient(baseURL *url.URL, creds svcauth.CredentialsSource, httpClient *http.Client) *HTTPMirrorSource {
|
||||||
|
if baseURL.Scheme != "https" {
|
||||||
|
panic("non-https URL for HTTP mirror")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We borrow the retry settings and behaviors from the registry client,
|
||||||
|
// because our needs here are very similar to those of the registry client.
|
||||||
|
retryableClient := retryablehttp.NewClient()
|
||||||
|
retryableClient.HTTPClient = httpClient
|
||||||
|
retryableClient.RetryMax = discoveryRetry
|
||||||
|
retryableClient.RequestLogHook = requestLogHook
|
||||||
|
retryableClient.ErrorHandler = maxRetryErrorHandler
|
||||||
|
|
||||||
|
logOutput, err := logging.LogOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Failed to set up provider HTTP mirror logger, so continuing without client logging: %s", err)
|
||||||
|
}
|
||||||
|
retryableClient.Logger = log.New(logOutput, "", log.Flags())
|
||||||
|
|
||||||
return &HTTPMirrorSource{
|
return &HTTPMirrorSource{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
creds: creds,
|
||||||
|
httpClient: retryableClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvailableVersions retrieves the available versions for the given provider
|
// AvailableVersions retrieves the available versions for the given provider
|
||||||
// from the object's underlying HTTP mirror service.
|
// from the object's underlying HTTP mirror service.
|
||||||
func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) {
|
func (s *HTTPMirrorSource) AvailableVersions(provider addrs.Provider) (VersionList, Warnings, error) {
|
||||||
return nil, nil, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform")
|
log.Printf("[DEBUG] Querying available versions of provider %s at network mirror %s", provider.String(), s.baseURL.String())
|
||||||
|
|
||||||
|
endpointPath := path.Join(
|
||||||
|
provider.Hostname.String(),
|
||||||
|
provider.Namespace,
|
||||||
|
provider.Type,
|
||||||
|
"index.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
statusCode, body, finalURL, err := s.get(endpointPath)
|
||||||
|
defer func() {
|
||||||
|
if body != nil {
|
||||||
|
body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, s.errQueryFailed(provider, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch statusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
// Great!
|
||||||
|
case http.StatusNotFound:
|
||||||
|
return nil, nil, ErrProviderNotFound{
|
||||||
|
Provider: provider,
|
||||||
|
}
|
||||||
|
case http.StatusUnauthorized, http.StatusForbidden:
|
||||||
|
return nil, nil, s.errUnauthorized(finalURL)
|
||||||
|
default:
|
||||||
|
return nil, nil, s.errQueryFailed(provider, fmt.Errorf("server returned unsuccessful status %d", statusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here then the response had status OK and so our body
|
||||||
|
// will be non-nil and should contain some JSON for us to parse.
|
||||||
|
type ResponseBody struct {
|
||||||
|
Versions map[string]struct{} `json:"versions"`
|
||||||
|
}
|
||||||
|
var bodyContent ResponseBody
|
||||||
|
|
||||||
|
dec := json.NewDecoder(body)
|
||||||
|
if err := dec.Decode(&bodyContent); err != nil {
|
||||||
|
return nil, nil, s.errQueryFailed(provider, fmt.Errorf("invalid response content from mirror server: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bodyContent.Versions) == 0 {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
ret := make(VersionList, 0, len(bodyContent.Versions))
|
||||||
|
for versionStr := range bodyContent.Versions {
|
||||||
|
version, err := ParseVersion(versionStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] Ignoring invalid %s version string %q in provider mirror response", provider, versionStr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Sort()
|
||||||
|
return ret, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageMeta retrieves metadata for the requested provider package
|
// PackageMeta retrieves metadata for the requested provider package
|
||||||
// from the object's underlying HTTP mirror service.
|
// from the object's underlying HTTP mirror service.
|
||||||
func (s *HTTPMirrorSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
|
func (s *HTTPMirrorSource) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
|
||||||
return PackageMeta{}, fmt.Errorf("Network-based provider mirrors are not supported in this version of Terraform")
|
log.Printf("[DEBUG] Finding package URL for %s v%s on %s via network mirror %s", provider.String(), version.String(), target.String(), s.baseURL.String())
|
||||||
|
|
||||||
|
endpointPath := path.Join(
|
||||||
|
provider.Hostname.String(),
|
||||||
|
provider.Namespace,
|
||||||
|
provider.Type,
|
||||||
|
version.String()+".json",
|
||||||
|
)
|
||||||
|
|
||||||
|
statusCode, body, finalURL, err := s.get(endpointPath)
|
||||||
|
defer func() {
|
||||||
|
if body != nil {
|
||||||
|
body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return PackageMeta{}, s.errQueryFailed(provider, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch statusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
// Great!
|
||||||
|
case http.StatusNotFound:
|
||||||
|
// A 404 Not Found for a version we previously saw in index.json is
|
||||||
|
// a protocol error, so we'll report this as "query failed.
|
||||||
|
return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("provider mirror does not have archive index for previously-reported %s version %s", provider, version))
|
||||||
|
case http.StatusUnauthorized, http.StatusForbidden:
|
||||||
|
return PackageMeta{}, s.errUnauthorized(finalURL)
|
||||||
|
default:
|
||||||
|
return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("server returned unsuccessful status %d", statusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here then the response had status OK and so our body
|
||||||
|
// will be non-nil and should contain some JSON for us to parse.
|
||||||
|
type ResponseArchiveMeta struct {
|
||||||
|
RelativeURL string `json:"url"`
|
||||||
|
Hashes []string
|
||||||
|
}
|
||||||
|
type ResponseBody struct {
|
||||||
|
Archives map[string]*ResponseArchiveMeta `json:"archives"`
|
||||||
|
}
|
||||||
|
var bodyContent ResponseBody
|
||||||
|
|
||||||
|
dec := json.NewDecoder(body)
|
||||||
|
if err := dec.Decode(&bodyContent); err != nil {
|
||||||
|
return PackageMeta{}, s.errQueryFailed(provider, fmt.Errorf("invalid response content from mirror server: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveMeta, ok := bodyContent.Archives[target.String()]
|
||||||
|
if !ok {
|
||||||
|
return PackageMeta{}, ErrPlatformNotSupported{
|
||||||
|
Provider: provider,
|
||||||
|
Version: version,
|
||||||
|
Platform: target,
|
||||||
|
MirrorURL: s.baseURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relURL, err := url.Parse(archiveMeta.RelativeURL)
|
||||||
|
if err != nil {
|
||||||
|
return PackageMeta{}, s.errQueryFailed(
|
||||||
|
provider,
|
||||||
|
fmt.Errorf("provider mirror returned invalid URL %q: %s", archiveMeta.RelativeURL, err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
absURL := finalURL.ResolveReference(relURL)
|
||||||
|
|
||||||
|
ret := PackageMeta{
|
||||||
|
Provider: provider,
|
||||||
|
Version: version,
|
||||||
|
TargetPlatform: target,
|
||||||
|
|
||||||
|
Location: PackageHTTPURL(absURL.String()),
|
||||||
|
Filename: path.Base(absURL.Path),
|
||||||
|
}
|
||||||
|
// A network mirror might not provide any hashes at all, in which case
|
||||||
|
// the package has no source-defined authentication whatsoever.
|
||||||
|
if len(archiveMeta.Hashes) > 0 {
|
||||||
|
ret.Authentication = NewPackageHashAuthentication(archiveMeta.Hashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForDisplay returns a string description of the source for user-facing output.
|
// ForDisplay returns a string description of the source for user-facing output.
|
||||||
func (s *HTTPMirrorSource) ForDisplay(provider addrs.Provider) string {
|
func (s *HTTPMirrorSource) ForDisplay(provider addrs.Provider) string {
|
||||||
return "Network-based provider mirrors are not supported in this version of Terraform"
|
return "provider mirror at " + s.baseURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mirrorHost extracts the hostname portion of the configured base URL and
|
||||||
|
// returns it as a svchost.Hostname, normalized in the usual ways.
|
||||||
|
//
|
||||||
|
// If the returned error is non-nil then the given hostname doesn't comply
|
||||||
|
// with the IETF RFC 5891 section 5.3 and 5.4 validation rules, and thus cannot
|
||||||
|
// be interpreted as a valid Terraform service host. The IDNA validation errors
|
||||||
|
// are unfortunately usually not very user-friendly, but they are also
|
||||||
|
// relatively rare because the IDNA normalization rules are quite tolerant.
|
||||||
|
func (s *HTTPMirrorSource) mirrorHost() (svchost.Hostname, error) {
|
||||||
|
return svchostFromURL(s.baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mirrorHostCredentials returns the HostCredentials, if any, for the hostname
|
||||||
|
// included in the mirror base URL.
|
||||||
|
//
|
||||||
|
// It might return an error if the mirror base URL is invalid, or if the
|
||||||
|
// credentials lookup itself fails.
|
||||||
|
func (s *HTTPMirrorSource) mirrorHostCredentials() (svcauth.HostCredentials, error) {
|
||||||
|
hostname, err := s.mirrorHost()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid provider mirror base URL %s: %s", s.baseURL.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.creds == nil {
|
||||||
|
// No host-specific credentials, then.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.creds.ForHost(hostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get is the shared functionality for querying a JSON index from a mirror.
|
||||||
|
//
|
||||||
|
// It only handles the raw HTTP request. The "body" return value is the
|
||||||
|
// reader from the response if and only if the response status code is 200 OK
|
||||||
|
// and the Content-Type is application/json. In all other cases it's nil.
|
||||||
|
// If body is non-nil then the caller must close it after reading it.
|
||||||
|
//
|
||||||
|
// If the "finalURL" return value is not empty then it's the URL that actually
|
||||||
|
// produced the returned response, possibly after following some redirects.
|
||||||
|
func (s *HTTPMirrorSource) get(relativePath string) (statusCode int, body io.ReadCloser, finalURL *url.URL, error error) {
|
||||||
|
endpointPath, err := url.Parse(relativePath)
|
||||||
|
if err != nil {
|
||||||
|
// Should never happen because the caller should validate all of the
|
||||||
|
// components it's including in the path.
|
||||||
|
return 0, nil, nil, err
|
||||||
|
}
|
||||||
|
endpointURL := s.baseURL.ResolveReference(endpointPath)
|
||||||
|
|
||||||
|
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, endpointURL, err
|
||||||
|
}
|
||||||
|
req.Request.Header.Set(terraformVersionHeader, version.String())
|
||||||
|
creds, err := s.mirrorHostCredentials()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, endpointURL, fmt.Errorf("failed to determine request credentials: %s", err)
|
||||||
|
}
|
||||||
|
if creds != nil {
|
||||||
|
// Note that if the initial requests gets redirected elsewhere
|
||||||
|
// then the credentials will still be included in the new request,
|
||||||
|
// even if they are on a different hostname. This is intentional
|
||||||
|
// and consistent with how we handle credentials for other
|
||||||
|
// Terraform-native services, because the user model is to configure
|
||||||
|
// credentials for the "friendly hostname" they configured, not for
|
||||||
|
// whatever hostname ends up ultimately serving the request as an
|
||||||
|
// implementation detail.
|
||||||
|
creds.PrepareRequest(req.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, endpointURL, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// If we're not returning the body then we'll close it
|
||||||
|
// before we return.
|
||||||
|
if body == nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// After this point, our final URL return value should always be the
|
||||||
|
// one from resp.Request, because that takes into account any redirects
|
||||||
|
// we followed along the way.
|
||||||
|
finalURL = resp.Request.URL
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
// If and only if we get an OK response, we'll check that the response
|
||||||
|
// type is JSON and return the body reader.
|
||||||
|
ct := resp.Header.Get("Content-Type")
|
||||||
|
mt, params, err := mime.ParseMediaType(ct)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, finalURL, fmt.Errorf("response has invalid Content-Type: %s", err)
|
||||||
|
}
|
||||||
|
if mt != "application/json" {
|
||||||
|
return 0, nil, finalURL, fmt.Errorf("response has invalid Content-Type: must be application/json")
|
||||||
|
}
|
||||||
|
for name := range params {
|
||||||
|
// The application/json content-type has no defined parameters,
|
||||||
|
// but some servers are configured to include a redundant "charset"
|
||||||
|
// parameter anyway, presumably out of a sense of completeness.
|
||||||
|
// We'll ignore them but warn that we're ignoring them in case the
|
||||||
|
// subsequent parsing fails due to the server trying to use an
|
||||||
|
// unsupported character encoding. (RFC 7159 defines its own
|
||||||
|
// JSON-specific character encoding rules.)
|
||||||
|
log.Printf("[WARN] Network mirror returned %q as part of its JSON content type, which is not defined. Ignoring.", name)
|
||||||
|
}
|
||||||
|
body = resp.Body
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.StatusCode, body, finalURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPMirrorSource) errQueryFailed(provider addrs.Provider, err error) error {
|
||||||
|
return ErrQueryFailed{
|
||||||
|
Provider: provider,
|
||||||
|
Wrapped: err,
|
||||||
|
MirrorURL: s.baseURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HTTPMirrorSource) errUnauthorized(finalURL *url.URL) error {
|
||||||
|
hostname, err := svchostFromURL(finalURL)
|
||||||
|
if err != nil {
|
||||||
|
// Again, weird but we'll tolerate it.
|
||||||
|
return fmt.Errorf("invalid credentials for %s", finalURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrUnauthorized{
|
||||||
|
Hostname: hostname,
|
||||||
|
|
||||||
|
// We can't easily tell from here whether we had credentials or
|
||||||
|
// not, so for now we'll just assume we did because "host rejected
|
||||||
|
// the given credentials" is, hopefully, still understandable in
|
||||||
|
// the event that there were none. (If this ends up being confusing
|
||||||
|
// in practice then we'll need to do some refactoring of how
|
||||||
|
// we handle credentials in this source.)
|
||||||
|
HaveCredentials: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func svchostFromURL(u *url.URL) (svchost.Hostname, error) {
|
||||||
|
raw := u.Host
|
||||||
|
|
||||||
|
// When "friendly hostnames" appear in Terraform-specific identifiers we
|
||||||
|
// typically constrain their syntax more strictly than the
|
||||||
|
// Internationalized Domain Name specifications call for, such as
|
||||||
|
// forbidding direct use of punycode, but in this case we're just
|
||||||
|
// working with a standard http: or https: URL and so we'll first use the
|
||||||
|
// IDNA "lookup" rules directly, with no additional notational constraints,
|
||||||
|
// to effectively normalize away the differences that would normally
|
||||||
|
// produce an error.
|
||||||
|
var portPortion string
|
||||||
|
if colonPos := strings.Index(raw, ":"); colonPos != -1 {
|
||||||
|
raw, portPortion = raw[:colonPos], raw[colonPos:]
|
||||||
|
}
|
||||||
|
// HTTPMirrorSource requires all URLs to be https URLs, because running
|
||||||
|
// a network mirror over HTTP would potentially transmit any configured
|
||||||
|
// credentials in cleartext. Therefore we don't need to do any special
|
||||||
|
// handling of default ports here, because svchost.Hostname already
|
||||||
|
// considers the absense of a port to represent the standard HTTPS port
|
||||||
|
// 443, and will normalize away an explicit specification of port 443
|
||||||
|
// in svchost.ForComparison below.
|
||||||
|
|
||||||
|
normalized, err := idna.Display.ToUnicode(raw)
|
||||||
|
if err != nil {
|
||||||
|
return svchost.Hostname(""), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If ToUnicode succeeded above then "normalized" is now a hostname in the
|
||||||
|
// normalized IDNA form, with any direct punycode already interpreted and
|
||||||
|
// the case folding and other normalization rules applied. It should
|
||||||
|
// therefore now be accepted by svchost.ForComparison with no additional
|
||||||
|
// errors, but the port portion can still potentially be invalid.
|
||||||
|
return svchost.ForComparison(normalized + portPortion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,314 @@
|
||||||
|
package getproviders
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
svchost "github.com/hashicorp/terraform-svchost"
|
||||||
|
svcauth "github.com/hashicorp/terraform-svchost/auth"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/addrs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPMirrorSource(t *testing.T) {
|
||||||
|
// For mirrors we require a HTTPS server, so we'll use httptest to create
|
||||||
|
// one. However, that means we need to instantiate the source in an unusual
|
||||||
|
// way to force it to use the test client that is configured to trust the
|
||||||
|
// test server.
|
||||||
|
httpServer := httptest.NewTLSServer(http.HandlerFunc(testHTTPMirrorSourceHandler))
|
||||||
|
defer httpServer.Close()
|
||||||
|
httpClient := httpServer.Client()
|
||||||
|
baseURL, err := url.Parse(httpServer.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("httptest.NewTLSServer returned a server with an invalid URL")
|
||||||
|
}
|
||||||
|
creds := svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
|
||||||
|
svchost.Hostname(baseURL.Host): {
|
||||||
|
"token": "placeholder-token",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
source := newHTTPMirrorSourceWithHTTPClient(baseURL, creds, httpClient)
|
||||||
|
|
||||||
|
existingProvider := addrs.MustParseProviderSourceString("terraform.io/test/exists")
|
||||||
|
missingProvider := addrs.MustParseProviderSourceString("terraform.io/test/missing")
|
||||||
|
failingProvider := addrs.MustParseProviderSourceString("terraform.io/test/fails")
|
||||||
|
redirectingProvider := addrs.MustParseProviderSourceString("terraform.io/test/redirects")
|
||||||
|
redirectLoopProvider := addrs.MustParseProviderSourceString("terraform.io/test/redirect-loop")
|
||||||
|
tosPlatform := Platform{OS: "tos", Arch: "m68k"}
|
||||||
|
|
||||||
|
t.Run("AvailableVersions for provider that exists", func(t *testing.T) {
|
||||||
|
got, _, err := source.AvailableVersions(existingProvider)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
want := VersionList{
|
||||||
|
MustParseVersion("1.0.0"),
|
||||||
|
MustParseVersion("1.0.1"),
|
||||||
|
MustParseVersion("1.0.2-beta.1"),
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("wrong result\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AvailableVersions for provider that doesn't exist", func(t *testing.T) {
|
||||||
|
_, _, err := source.AvailableVersions(missingProvider)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case ErrProviderNotFound:
|
||||||
|
if got, want := err.Provider, missingProvider; got != want {
|
||||||
|
t.Errorf("wrong provider in error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("wrong error type %T; want ErrProviderNotFound", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AvailableVersions without required credentials", func(t *testing.T) {
|
||||||
|
unauthSource := newHTTPMirrorSourceWithHTTPClient(baseURL, nil, httpClient)
|
||||||
|
_, _, err := unauthSource.AvailableVersions(existingProvider)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case ErrUnauthorized:
|
||||||
|
if got, want := string(err.Hostname), baseURL.Host; got != want {
|
||||||
|
t.Errorf("wrong hostname in error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("wrong error type %T; want ErrUnauthorized", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AvailableVersions when the response is a server error", func(t *testing.T) {
|
||||||
|
_, _, err := source.AvailableVersions(failingProvider)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case ErrQueryFailed:
|
||||||
|
if got, want := err.Provider, failingProvider; got != want {
|
||||||
|
t.Errorf("wrong provider in error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if err.MirrorURL != source.baseURL {
|
||||||
|
t.Errorf("error does not refer to the mirror URL")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("wrong error type %T; want ErrQueryFailed", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AvailableVersions for provider that redirects", func(t *testing.T) {
|
||||||
|
got, _, err := source.AvailableVersions(redirectingProvider)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
want := VersionList{
|
||||||
|
MustParseVersion("1.0.0"),
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("wrong result\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AvailableVersions for provider that redirects too much", func(t *testing.T) {
|
||||||
|
_, _, err := source.AvailableVersions(redirectLoopProvider)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("succeeded; expected error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("PackageMeta for a version that exists and has a hash", func(t *testing.T) {
|
||||||
|
version := MustParseVersion("1.0.0")
|
||||||
|
got, err := source.PackageMeta(existingProvider, version, tosPlatform)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := PackageMeta{
|
||||||
|
Provider: existingProvider,
|
||||||
|
Version: version,
|
||||||
|
TargetPlatform: tosPlatform,
|
||||||
|
Filename: "terraform-provider-test_v1.0.0_tos_m68k.zip",
|
||||||
|
Location: PackageHTTPURL(httpServer.URL + "/terraform.io/test/exists/terraform-provider-test_v1.0.0_tos_m68k.zip"),
|
||||||
|
Authentication: packageHashAuthentication{
|
||||||
|
RequiredHash: "h1:placeholder-hash",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("wrong result\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("PackageMeta for a version that exists and has no hash", func(t *testing.T) {
|
||||||
|
version := MustParseVersion("1.0.1")
|
||||||
|
got, err := source.PackageMeta(existingProvider, version, tosPlatform)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := PackageMeta{
|
||||||
|
Provider: existingProvider,
|
||||||
|
Version: version,
|
||||||
|
TargetPlatform: tosPlatform,
|
||||||
|
Filename: "terraform-provider-test_v1.0.1_tos_m68k.zip",
|
||||||
|
Location: PackageHTTPURL(httpServer.URL + "/terraform.io/test/exists/terraform-provider-test_v1.0.1_tos_m68k.zip"),
|
||||||
|
Authentication: nil,
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("wrong result\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("PackageMeta for a version that exists but has no archives", func(t *testing.T) {
|
||||||
|
version := MustParseVersion("1.0.2-beta.1")
|
||||||
|
_, err := source.PackageMeta(existingProvider, version, tosPlatform)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case ErrPlatformNotSupported:
|
||||||
|
if got, want := err.Provider, existingProvider; got != want {
|
||||||
|
t.Errorf("wrong provider in error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if got, want := err.Platform, tosPlatform; got != want {
|
||||||
|
t.Errorf("wrong platform in error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if err.MirrorURL != source.baseURL {
|
||||||
|
t.Errorf("error does not contain the mirror URL")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("wrong error type %T; want ErrPlatformNotSupported", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("PackageMeta with redirect to a version that exists", func(t *testing.T) {
|
||||||
|
version := MustParseVersion("1.0.0")
|
||||||
|
got, err := source.PackageMeta(redirectingProvider, version, tosPlatform)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := PackageMeta{
|
||||||
|
Provider: redirectingProvider,
|
||||||
|
Version: version,
|
||||||
|
TargetPlatform: tosPlatform,
|
||||||
|
Filename: "terraform-provider-test.zip",
|
||||||
|
|
||||||
|
// NOTE: The final URL is interpreted relative to the redirect
|
||||||
|
// target, not relative to what we originally requested.
|
||||||
|
Location: PackageHTTPURL(httpServer.URL + "/redirect-target/terraform-provider-test.zip"),
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
|
t.Errorf("wrong result\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("PackageMeta when the response is a server error", func(t *testing.T) {
|
||||||
|
version := MustParseVersion("1.0.0")
|
||||||
|
_, err := source.PackageMeta(failingProvider, version, tosPlatform)
|
||||||
|
switch err := err.(type) {
|
||||||
|
case ErrQueryFailed:
|
||||||
|
if got, want := err.Provider, failingProvider; got != want {
|
||||||
|
t.Errorf("wrong provider in error\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
if err.MirrorURL != source.baseURL {
|
||||||
|
t.Errorf("error does not contain the mirror URL")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Fatalf("wrong error type %T; want ErrQueryFailed", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHTTPMirrorSourceHandler(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
if auth := req.Header.Get("authorization"); auth != "Bearer placeholder-token" {
|
||||||
|
resp.WriteHeader(401)
|
||||||
|
fmt.Fprintln(resp, "incorrect auth token")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.URL.Path {
|
||||||
|
case "/terraform.io/test/exists/index.json":
|
||||||
|
resp.Header().Add("Content-Type", "application/json; ignored=yes")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
fmt.Fprint(resp, `
|
||||||
|
{
|
||||||
|
"versions": {
|
||||||
|
"1.0.0": {},
|
||||||
|
"1.0.1": {},
|
||||||
|
"1.0.2-beta.1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
case "/terraform.io/test/fails/index.json", "/terraform.io/test/fails/1.0.0.json":
|
||||||
|
resp.WriteHeader(500)
|
||||||
|
fmt.Fprint(resp, "server error")
|
||||||
|
|
||||||
|
case "/terraform.io/test/exists/1.0.0.json":
|
||||||
|
resp.Header().Add("Content-Type", "application/json; ignored=yes")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
fmt.Fprint(resp, `
|
||||||
|
{
|
||||||
|
"archives": {
|
||||||
|
"tos_m68k": {
|
||||||
|
"url": "terraform-provider-test_v1.0.0_tos_m68k.zip",
|
||||||
|
"hashes": [
|
||||||
|
"h1:placeholder-hash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
case "/terraform.io/test/exists/1.0.1.json":
|
||||||
|
resp.Header().Add("Content-Type", "application/json; ignored=yes")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
fmt.Fprint(resp, `
|
||||||
|
{
|
||||||
|
"archives": {
|
||||||
|
"tos_m68k": {
|
||||||
|
"url": "terraform-provider-test_v1.0.1_tos_m68k.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
case "/terraform.io/test/exists/1.0.2-beta.1.json":
|
||||||
|
resp.Header().Add("Content-Type", "application/json; ignored=yes")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
fmt.Fprint(resp, `
|
||||||
|
{
|
||||||
|
"archives": {}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
case "/terraform.io/test/redirects/index.json":
|
||||||
|
resp.Header().Add("location", "/redirect-target/index.json")
|
||||||
|
resp.WriteHeader(301)
|
||||||
|
fmt.Fprint(resp, "redirect")
|
||||||
|
|
||||||
|
case "/redirect-target/index.json":
|
||||||
|
resp.Header().Add("Content-Type", "application/json")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
fmt.Fprint(resp, `
|
||||||
|
{
|
||||||
|
"versions": {
|
||||||
|
"1.0.0": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
case "/terraform.io/test/redirects/1.0.0.json":
|
||||||
|
resp.Header().Add("location", "/redirect-target/1.0.0.json")
|
||||||
|
resp.WriteHeader(301)
|
||||||
|
fmt.Fprint(resp, "redirect")
|
||||||
|
|
||||||
|
case "/redirect-target/1.0.0.json":
|
||||||
|
resp.Header().Add("Content-Type", "application/json")
|
||||||
|
resp.WriteHeader(200)
|
||||||
|
fmt.Fprint(resp, `
|
||||||
|
{
|
||||||
|
"archives": {
|
||||||
|
"tos_m68k": {
|
||||||
|
"url": "terraform-provider-test.zip"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
case "/terraform.io/test/redirect-loop/index.json":
|
||||||
|
// This is intentionally redirecting to itself, to create a loop.
|
||||||
|
resp.Header().Add("location", req.URL.Path)
|
||||||
|
resp.WriteHeader(301)
|
||||||
|
fmt.Fprint(resp, "redirect loop")
|
||||||
|
|
||||||
|
default:
|
||||||
|
resp.WriteHeader(404)
|
||||||
|
fmt.Fprintln(resp, "not found")
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,7 +89,7 @@ func TestConfigureRegistryClientTimeout(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// testServices starts up a local HTTP server running a fake provider registry
|
// testRegistryServices starts up a local HTTP server running a fake provider registry
|
||||||
// service and returns a service discovery object pre-configured to consider
|
// service and returns a service discovery object pre-configured to consider
|
||||||
// the host "example.com" to be served by the fake registry service.
|
// the host "example.com" to be served by the fake registry service.
|
||||||
//
|
//
|
||||||
|
@ -103,7 +103,7 @@ func TestConfigureRegistryClientTimeout(t *testing.T) {
|
||||||
// The second return value is a function to call at the end of a test function
|
// The second return value is a function to call at the end of a test function
|
||||||
// to shut down the test server. After you call that function, the discovery
|
// to shut down the test server. After you call that function, the discovery
|
||||||
// object becomes useless.
|
// object becomes useless.
|
||||||
func testServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup func()) {
|
func testRegistryServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup func()) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler))
|
server := httptest.NewServer(http.HandlerFunc(fakeRegistryHandler))
|
||||||
|
|
||||||
services = disco.New()
|
services = disco.New()
|
||||||
|
@ -141,7 +141,7 @@ func testServices(t *testing.T) (services *disco.Disco, baseURL string, cleanup
|
||||||
// As with testServices, the second return value is a function to call at the end
|
// As with testServices, the second return value is a function to call at the end
|
||||||
// of your test in order to shut down the test server.
|
// of your test in order to shut down the test server.
|
||||||
func testRegistrySource(t *testing.T) (source *RegistrySource, baseURL string, cleanup func()) {
|
func testRegistrySource(t *testing.T) (source *RegistrySource, baseURL string, cleanup func()) {
|
||||||
services, baseURL, close := testServices(t)
|
services, baseURL, close := testRegistryServices(t)
|
||||||
source = NewRegistrySource(services)
|
source = NewRegistrySource(services)
|
||||||
return source, baseURL, close
|
return source, baseURL, close
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,7 +216,7 @@ func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocati
|
||||||
))
|
))
|
||||||
return nil, diags
|
return nil, diags
|
||||||
}
|
}
|
||||||
return getproviders.NewHTTPMirrorSource(url), nil
|
return getproviders.NewHTTPMirrorSource(url, services.CredentialsSource()), nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// We should not get here because the set of cases above should
|
// We should not get here because the set of cases above should
|
||||||
|
|
|
@ -238,6 +238,21 @@ The following are the two supported installation method types:
|
||||||
You can include multiple `filesystem_mirror` blocks in order to specify
|
You can include multiple `filesystem_mirror` blocks in order to specify
|
||||||
several different directories to search.
|
several different directories to search.
|
||||||
|
|
||||||
|
* `network_mirror`: consult a particular HTTPS server for copies of providers,
|
||||||
|
regardless of which registry host they belong to. This method requires the
|
||||||
|
additional argument `url` to indicate the mirror base URL, which should
|
||||||
|
use the `https:` scheme and end with a trailing slash.
|
||||||
|
|
||||||
|
Terraform expects the given URL to be a base URL for an implementation of
|
||||||
|
[the provider network mirror protocol](/docs/internals/providor-network-mirror-protocol.html),
|
||||||
|
which is designed to be relatively easy to implement using typical static
|
||||||
|
website hosting mechanisms.
|
||||||
|
|
||||||
|
~> **Warning:** Don't configure `network_mirror` URLs that you do not trust.
|
||||||
|
Provider mirror servers are subject to TLS certificate checks to verify
|
||||||
|
identity, but a network mirror with a TLS certificate can potentially serve
|
||||||
|
modified copies of upstream providers with malicious content.
|
||||||
|
|
||||||
Terraform will try all of the specified methods whose include and exclude
|
Terraform will try all of the specified methods whose include and exclude
|
||||||
patterns match a given provider, and select the newest version available across
|
patterns match a given provider, and select the newest version available across
|
||||||
all of those methods that matches the version constraint given in each
|
all of those methods that matches the version constraint given in each
|
||||||
|
|
|
@ -36,9 +36,15 @@ Usage: `terraform providers mirror [options] <target-dir>`
|
||||||
|
|
||||||
A single target directory is required. Terraform will create under that
|
A single target directory is required. Terraform will create under that
|
||||||
directory the path structure that is expected for filesystem-based provider
|
directory the path structure that is expected for filesystem-based provider
|
||||||
plugin mirrors, populating it both with `.zip` files containing the plugins
|
plugin mirrors, populating it with `.zip` files containing the plugins
|
||||||
themselves and `.json` index files that describe what is available in the
|
themselves.
|
||||||
directory.
|
|
||||||
|
Terraform will also generate various `.json` index files which contain suitable
|
||||||
|
responses to implement
|
||||||
|
[the network mirror protocol](/docs/internals/providor-network-mirror-protocol.html),
|
||||||
|
if you upload the resulting directory to a static website host. Terraform
|
||||||
|
ignores those index files when using the directory as a filesystem mirror,
|
||||||
|
because the directory entries themselves are authoritative in that case.
|
||||||
|
|
||||||
This command supports the following additional option:
|
This command supports the following additional option:
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Provider Network Mirror Protocol"
|
||||||
|
sidebar_current: "docs-internals-provider-network-mirror-protocol"
|
||||||
|
description: |-
|
||||||
|
The provider network mirror protocol is implemented by a server intending
|
||||||
|
to provide a mirror or read-through caching proxy for Terraform providers,
|
||||||
|
as an alternative distribution source from the provider's origin provider
|
||||||
|
registry.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Provider Network Mirror Protocol
|
||||||
|
|
||||||
|
-> Provider network mirrors are supported only in Terraform CLI v0.13.2 and later. Prior versions do not support this protocol.
|
||||||
|
|
||||||
|
The provider network mirror protocol is an optional protocol which you can
|
||||||
|
implement to provide an alternative installation source for Terraform providers,
|
||||||
|
regardless of their origin registries.
|
||||||
|
|
||||||
|
Terraform uses network mirrors only if you activate them explicitly in
|
||||||
|
[the CLI configuration's `provider_installation` block](/docs/commands/cli-config.html#provider-installation).
|
||||||
|
When enabled, a network mirror can serve providers belonging to any registry
|
||||||
|
hostname, which can allow an organization to serve all of the Terraform
|
||||||
|
providers they intend to use from an internal server, rather than from each
|
||||||
|
provider's origin registry.
|
||||||
|
|
||||||
|
This is _not_ the protocol that should be implemented by a host intending to
|
||||||
|
serve as an origin registry for Terraform Providers. To provide an origin
|
||||||
|
registry (whose hostname would then be included in the source addresses of the
|
||||||
|
providers it hosts), implement
|
||||||
|
[the provider registry protocol](./provider-registry-protocol.html)
|
||||||
|
instead.
|
||||||
|
|
||||||
|
## Provider Addresses
|
||||||
|
|
||||||
|
Each Terraform provider has an associated address which uniquely identifies it
|
||||||
|
within Terraform. A provider address has the syntax `hostname/namespace/type`,
|
||||||
|
which is described in more detail in
|
||||||
|
[the Provider Requirements documentation](/docs/configuration/provider-requirements.html).
|
||||||
|
|
||||||
|
By default, the `hostname` portion of a provider address serves both as part
|
||||||
|
of its unique identifier _and_ as the location of the registry to retrieve it
|
||||||
|
from. However, when you configure Terraform to install providers from a network
|
||||||
|
mirror, the `hostname` serves _only_ as an identifier and no longer as
|
||||||
|
an installation source. A provider mirror can therefore serve providers
|
||||||
|
belonging to a variety of different provider registry hostnames, including
|
||||||
|
providers from the public Terraform Registry at `registry.terraform.io`, from a
|
||||||
|
single server.
|
||||||
|
|
||||||
|
In the relative URL patterns later in this document, the placeholder `:hostname`
|
||||||
|
refers to the hostname from the address of the provider being requested, not
|
||||||
|
the hostname where the provider network mirror is deployed.
|
||||||
|
|
||||||
|
## Protocol Base URL
|
||||||
|
|
||||||
|
Most Terraform-native services use
|
||||||
|
[the remote service discovery protocol](./remote-service-discovery.html) so
|
||||||
|
that the physical location of the endpoints can potentially be separated from
|
||||||
|
the hostname used in identifiers. The Provider Network Mirror protocol does
|
||||||
|
_not_ use the service discovery indirection, because a network mirror location
|
||||||
|
is only a physical location and is never used as part of the identifier of a
|
||||||
|
dependency in a Terraform configuration.
|
||||||
|
|
||||||
|
Instead, the provider installation section of the CLI configuration accepts
|
||||||
|
a base URL directly. The given URL must use the scheme `https:`, and should
|
||||||
|
end with a trailing slash so that the relative URLs of the individual operation
|
||||||
|
endpoints will be resolved beneath it.
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
provider_installation {
|
||||||
|
network_mirror {
|
||||||
|
url = "https://terraform.example.com/providers/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Terraform uses the base URL only as a stem to resolve the operation endpoint
|
||||||
|
URLs against, and so it will never access the base URL directly. You can
|
||||||
|
therefore, if desired, publish human-readable usage documentation for your
|
||||||
|
network mirror at that URL.
|
||||||
|
|
||||||
|
The following sections describe the various operations that a provider
|
||||||
|
network mirror server must implement to be compatible with Terraform CLI's
|
||||||
|
provider installer. The indicated URLs are all relative to the given base URL,
|
||||||
|
as described above.
|
||||||
|
|
||||||
|
The URLs are shown with the convention that a path portion with a colon `:`
|
||||||
|
prefix is a placeholder for a dynamically-selected value, while all other
|
||||||
|
path portions are literal. For example, in `:hostname/:namespace/:type/index.json`,
|
||||||
|
the first three path portions are placeholders while the third is literally
|
||||||
|
the string "index.json".
|
||||||
|
|
||||||
|
The example requests in the following sections will assume the example mirror
|
||||||
|
base URL from the above CLI configuration example.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
If the CLI configuration includes
|
||||||
|
[credentials](/docs/commands/cli-config.html#credentials) for the hostname
|
||||||
|
given in the network mirror base URL, Terraform will include those credentials
|
||||||
|
in its requests for operations described below.
|
||||||
|
|
||||||
|
If the given URL uses a non-standard port number (other than 443) then the
|
||||||
|
credentials must be associated with a hostname that includes the port number,
|
||||||
|
such as `terraform.example.com:8443`.
|
||||||
|
|
||||||
|
Terraform does _not_ send credentials when retrieving the archives whose
|
||||||
|
URLs are given in the "List Available Installation Packages" response below.
|
||||||
|
If a particular mirror considers the distribution packages themselves to be
|
||||||
|
sensitive then it must use cryptographically-secure, user-specific, and
|
||||||
|
time-limited URLs in the metadata response. Strategies for doing so are out
|
||||||
|
of scope of this protocol documentation.
|
||||||
|
|
||||||
|
## List Available Versions
|
||||||
|
|
||||||
|
This operation determines which versions are currently available for a
|
||||||
|
particular provider.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
|--------|-----------------------------------------|--------------------|
|
||||||
|
| `GET` | `:hostname/:namespace/:type/index.json` | `application/json` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
* `hostname` (required): the hostname portion of the address of the requested
|
||||||
|
provider.
|
||||||
|
* `namespace` (required): the namespace portion of the address of the requested
|
||||||
|
provider.
|
||||||
|
* `type` (required): the type portion of the address of the requested provider.
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
curl 'https://terraform.example.com/providers/registry.terraform.io/hashicorp/random/index.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"versions": {
|
||||||
|
"2.0.0": {},
|
||||||
|
"2.0.1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Properties
|
||||||
|
|
||||||
|
A successful result is a JSON object containing a single property `versions`,
|
||||||
|
which must be a JSON object.
|
||||||
|
|
||||||
|
Each of the property names of the `versions` object represents an available
|
||||||
|
version number. The property values must be objects, but no properties are
|
||||||
|
currently defined for those objects. Future versions of this protocol may
|
||||||
|
define optional per-version properties for Terraform to use as installation
|
||||||
|
hints, so implementations of the current version should leave those objects
|
||||||
|
empty.
|
||||||
|
|
||||||
|
Return `404 Not Found` to signal that the mirror does not have a provider
|
||||||
|
with the given address.
|
||||||
|
|
||||||
|
## List Available Installation Packages
|
||||||
|
|
||||||
|
This operation returns download URLs and associated metadata for the
|
||||||
|
distribution packages for a particular version of a provider.
|
||||||
|
|
||||||
|
Each distribution package is associated with a particular operating system
|
||||||
|
and architecture. A network mirror may host only a subset of the available
|
||||||
|
packages for a provider version, if the users of the mirror are known to all
|
||||||
|
use only a subset of the target platforms that Terraform supports.
|
||||||
|
|
||||||
|
Terraform CLI uses this operation after it has selected the newest available
|
||||||
|
version matching the configured version constraints, in order to find a zip
|
||||||
|
archive containing the plugin itself.
|
||||||
|
|
||||||
|
| Method | Path | Produces |
|
||||||
|
|--------|--------------------------------------------|--------------------|
|
||||||
|
| `GET` | `:hostname/:namespace/:type/:version.json` | `application/json` |
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
* `hostname` (required): the hostname portion of the address of the requested
|
||||||
|
provider.
|
||||||
|
* `namespace` (required): the namespace portion of the address of the requested
|
||||||
|
provider.
|
||||||
|
* `type` (required): the type portion of the address of the requested provider.
|
||||||
|
* `version` (required): the version selected to download. This will exactly
|
||||||
|
match one of the version strings returned from a previous call to
|
||||||
|
[List Available Versions](#list-available-versions).
|
||||||
|
|
||||||
|
### Sample Request
|
||||||
|
|
||||||
|
```
|
||||||
|
curl 'https://terraform.example.com/providers/registry.terraform.io/hashicorp/random/2.0.0.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"archives": {
|
||||||
|
"darwin_amd64": {
|
||||||
|
"url": "terraform-provider-random_2.0.0_darwin_amd64.zip",
|
||||||
|
"hashes": [
|
||||||
|
"h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs="
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux_amd64": {
|
||||||
|
"url": "terraform-provider-random_2.0.0_linux_amd64.zip",
|
||||||
|
"hashes": [
|
||||||
|
"h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ="
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Properties
|
||||||
|
|
||||||
|
A successful result is a JSON object with a property called `archives`, which
|
||||||
|
must be a JSON object.
|
||||||
|
|
||||||
|
Each of the property names of the `archives` object is a target platform
|
||||||
|
identifier, which consists of an operating system and architecture concatenated
|
||||||
|
with an underscore (`_`).
|
||||||
|
|
||||||
|
Each property value in the `archives` object is itself a nested object with
|
||||||
|
the following properties:
|
||||||
|
|
||||||
|
* `url` (required): a string specifying the URL from which Terraform should
|
||||||
|
download the `.zip` archive containing the requested provider plugin version.
|
||||||
|
|
||||||
|
Terraform resolves the URL relative to the URL from which the current
|
||||||
|
JSON document was returned, so the examples above containing only a
|
||||||
|
filename would cause Terraform to construct a URL like:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://terraform.example.com/providers/registry.terraform.io/hashicorp/random/terraform-provider-random_2.0.0_darwin_amd64.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
* `hashes` (optional): a JSON array of strings containing one or more hash
|
||||||
|
values for the indicated archive. These hashes use Terraform's provider
|
||||||
|
package hashing algorithm. At present, the easiest way to populate these
|
||||||
|
is to construct a mirror's JSON indices using the `terraform providers mirror`
|
||||||
|
command, as described in a later section, which will include the calculated
|
||||||
|
hashes of each provider.
|
||||||
|
|
||||||
|
If the response includes at least one hash, Terraform will select the hash
|
||||||
|
whose algorithm it considers to be strongest and verify that the downloaded
|
||||||
|
package matches that hash. If the response does not include a `hashes`
|
||||||
|
property then Terraform will install the indicated archive with no
|
||||||
|
verification.
|
||||||
|
|
||||||
|
Terraform CLI will only attempt to download versions that it has previously
|
||||||
|
seen in response to [List Available Versions](#list-available-versions).
|
||||||
|
|
||||||
|
## Provider Mirror as a Static Website
|
||||||
|
|
||||||
|
The provider mirror protocol is designed so that it can potentially implemented
|
||||||
|
by placing files on typical static website hosting services. When using this
|
||||||
|
strategy, implement the JSON index responses described above as `.json` files
|
||||||
|
in the appropriate nested subdirectories, and ensure that your system is
|
||||||
|
configured to serve `.json` files with the `application/json` media type.
|
||||||
|
|
||||||
|
As a convenience, Terraform CLI includes
|
||||||
|
[the `terraform providers mirror` subcommand](https://www.terraform.io/docs/commands/providers/mirror.html),
|
||||||
|
which will analyze the current configuration for the providers it requires,
|
||||||
|
download the packages for those providers from their origin registries, and
|
||||||
|
place them into a local directory suitable for use as a mirror.
|
||||||
|
|
||||||
|
The `terraform providers mirror` subcommand also generates `index.json` and
|
||||||
|
version-specific `.json` files that can, when placed in a static website hosting
|
||||||
|
system, produce responses compatible with the provider mirror protocol.
|
||||||
|
|
||||||
|
If you wish to create a mirror with providers for a number of different
|
||||||
|
Terraform configurations, run `terraform providers mirror` in each configuration
|
||||||
|
in turn while providing the same output directory each time. Terraform will
|
||||||
|
then merge together all of the requirements into a single set of JSON indices.
|
|
@ -499,6 +499,10 @@
|
||||||
<a href="/docs/internals/module-registry-protocol.html">Module Registry Protocol</a>
|
<a href="/docs/internals/module-registry-protocol.html">Module Registry Protocol</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-internals-provider-network-mirror-protocol") %>>
|
||||||
|
<a href="/docs/internals/provider-network-mirror-protocol.html">Provider Network Mirror Protocol</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li<%= sidebar_current("docs-internals-provider-registry-protocol") %>>
|
<li<%= sidebar_current("docs-internals-provider-registry-protocol") %>>
|
||||||
<a href="/docs/internals/provider-registry-protocol.html">Provider Registry Protocol</a>
|
<a href="/docs/internals/provider-registry-protocol.html">Provider Registry Protocol</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue