254 lines
9.2 KiB
Go
254 lines
9.2 KiB
Go
package getproviders
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
svchost "github.com/hashicorp/terraform-svchost"
|
|
"github.com/hashicorp/terraform/addrs"
|
|
)
|
|
|
|
// MissingProviderSuggestion takes a provider address that failed installation
|
|
// due to the remote registry reporting that it didn't exist, and attempts
|
|
// to find another provider that the user might have meant to select.
|
|
//
|
|
// If the result is equal to the given address then that indicates that there
|
|
// is no suggested alternative to offer, either because the function
|
|
// successfully determined there is no recorded alternative or because the
|
|
// lookup failed somehow. We don't consider a failure to find a suggestion
|
|
// as an installation failure, because the caller should already be reporting
|
|
// that the provider didn't exist anyway and this is only extra context for
|
|
// that error message.
|
|
//
|
|
// The result of this is a best effort, so any UI presenting it should be
|
|
// careful to give it only as a possibility and not necessarily a suitable
|
|
// replacement for the given provider.
|
|
//
|
|
// In practice today this function only knows how to suggest alternatives for
|
|
// "default" providers, which is to say ones that are in the hashicorp
|
|
// namespace in the Terraform registry. It will always return no result for
|
|
// any other provider. That might change in future if we introduce other ways
|
|
// to discover provider suggestions.
|
|
//
|
|
// If the given context is cancelled then this function might not return a
|
|
// renaming suggestion even if one would've been available for a completed
|
|
// request.
|
|
func MissingProviderSuggestion(ctx context.Context, addr addrs.Provider, source Source) addrs.Provider {
|
|
if !addr.IsDefault() {
|
|
return addr
|
|
}
|
|
|
|
// Our strategy here, for a default provider, is to use the default
|
|
// registry's special API for looking up "legacy" providers and try looking
|
|
// for a legacy provider whose type name matches the type of the given
|
|
// provider. This should then find a suitable answer for any provider
|
|
// that was originally auto-installable in v0.12 and earlier but moved
|
|
// into a non-default namespace as part of introducing the hierarchical
|
|
// provider namespace.
|
|
//
|
|
// To achieve that, we need to find the direct registry client in
|
|
// particular from the given source, because that is the only Source
|
|
// implementation that can actually handle a legacy provider lookup.
|
|
regSource := findLegacyProviderLookupSource(addr.Hostname, source)
|
|
if regSource == nil {
|
|
// If there's no direct registry source in the installation config
|
|
// then we can't provide a renaming suggestion.
|
|
return addr
|
|
}
|
|
|
|
defaultNS, redirectNS, err := regSource.lookupLegacyProviderNamespace(ctx, addr.Hostname, addr.Type)
|
|
if err != nil {
|
|
return addr
|
|
}
|
|
|
|
switch {
|
|
case redirectNS != "":
|
|
return addrs.Provider{
|
|
Hostname: addr.Hostname,
|
|
Namespace: redirectNS,
|
|
Type: addr.Type,
|
|
}
|
|
default:
|
|
return addrs.Provider{
|
|
Hostname: addr.Hostname,
|
|
Namespace: defaultNS,
|
|
Type: addr.Type,
|
|
}
|
|
}
|
|
}
|
|
|
|
// findLegacyProviderLookupSource tries to find a *RegistrySource that can talk
|
|
// to the given registry host in the given Source. It might be given directly,
|
|
// or it might be given indirectly via a MultiSource where the selector
|
|
// includes a wildcard for registry.terraform.io.
|
|
//
|
|
// Returns nil if the given source does not have any configured way to talk
|
|
// directly to the given host.
|
|
//
|
|
// If the given source contains multiple sources that can talk to the given
|
|
// host directly, the first one in the sequence takes preference. In practice
|
|
// it's pointless to have two direct installation sources that match the same
|
|
// hostname anyway, so this shouldn't arise in normal use.
|
|
func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *RegistrySource {
|
|
switch source := source.(type) {
|
|
|
|
case *RegistrySource:
|
|
// Easy case: the source is a registry source directly, and so we'll
|
|
// just use it.
|
|
return source
|
|
|
|
case *MemoizeSource:
|
|
// Also easy: the source is a memoize wrapper, so defer to its
|
|
// underlying source.
|
|
return findLegacyProviderLookupSource(host, source.underlying)
|
|
|
|
case MultiSource:
|
|
// Trickier case: if it's a multisource then we need to scan over
|
|
// its selectors until we find one that is a *RegistrySource _and_
|
|
// that is configured to accept arbitrary providers from the
|
|
// given hostname.
|
|
|
|
// For our matching purposes we'll use an address that would not be
|
|
// valid as a real provider FQN and thus can only match a selector
|
|
// that has no filters at all or a selector that wildcards everything
|
|
// except the hostname, like "registry.terraform.io/*/*"
|
|
matchAddr := addrs.Provider{
|
|
Hostname: host,
|
|
// Other fields are intentionally left empty, to make this invalid
|
|
// as a specific provider address.
|
|
}
|
|
|
|
for _, selector := range source {
|
|
// If this source has suitable matching patterns to install from
|
|
// the given hostname then we'll recursively search inside it
|
|
// for *RegistrySource objects.
|
|
if selector.CanHandleProvider(matchAddr) {
|
|
ret := findLegacyProviderLookupSource(host, selector.Source)
|
|
if ret != nil {
|
|
return ret
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we get here then there were no selectors that are both configured
|
|
// to handle modules from the given hostname and that are registry
|
|
// sources, so we fail.
|
|
return nil
|
|
|
|
default:
|
|
// This source cannot be and cannot contain a *RegistrySource, so
|
|
// we fail.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// lookupLegacyProviderNamespace is a special method available only on
|
|
// RegistrySource which can deal with legacy provider addresses that contain
|
|
// only a type and leave the namespace implied.
|
|
//
|
|
// It asks the registry at the given hostname to provide a default namespace
|
|
// for the given provider type, which can be combined with the given hostname
|
|
// and type name to produce a fully-qualified provider address.
|
|
//
|
|
// Not all unqualified type names can be resolved to a default namespace. If
|
|
// the request fails, this method returns an error describing the failure.
|
|
//
|
|
// 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, and this fallback mechanism will likely be removed altogether
|
|
// in a future Terraform version.
|
|
func (s *RegistrySource) lookupLegacyProviderNamespace(ctx context.Context, hostname svchost.Hostname, typeName string) (string, string, error) {
|
|
client, err := s.registryClient(hostname)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return client.legacyProviderDefaultNamespace(ctx, typeName)
|
|
}
|
|
|
|
// legacyProviderDefaultNamespace 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(ctx context.Context, typeName string) (string, string, error) {
|
|
endpointPath, err := url.Parse(path.Join("-", typeName, "versions"))
|
|
if err != nil {
|
|
// Should never happen because we're constructing this from
|
|
// already-validated components.
|
|
return "", "", err
|
|
}
|
|
endpointURL := c.baseURL.ResolveReference(endpointPath)
|
|
|
|
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
req = req.WithContext(ctx)
|
|
c.addHeadersToRequest(req.Request)
|
|
|
|
// 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 "", "", ErrProviderNotFound{
|
|
Provider: placeholderProviderAddr,
|
|
}
|
|
case http.StatusUnauthorized, http.StatusForbidden:
|
|
return "", "", c.errUnauthorized(placeholderProviderAddr.Hostname)
|
|
default:
|
|
return "", "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status))
|
|
}
|
|
|
|
type ResponseBody struct {
|
|
Id string `json:"id"`
|
|
MovedTo string `json:"moved_to"`
|
|
}
|
|
var body ResponseBody
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
if err := dec.Decode(&body); err != nil {
|
|
return "", "", c.errQueryFailed(placeholderProviderAddr, err)
|
|
}
|
|
|
|
provider, diags := addrs.ParseProviderSourceString(body.Id)
|
|
if diags.HasErrors() {
|
|
return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err())
|
|
}
|
|
|
|
if provider.Type != typeName {
|
|
return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", provider.Type, typeName)
|
|
}
|
|
|
|
var movedTo addrs.Provider
|
|
if body.MovedTo != "" {
|
|
movedTo, diags = addrs.ParseProviderSourceString(body.MovedTo)
|
|
if diags.HasErrors() {
|
|
return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err())
|
|
}
|
|
|
|
if movedTo.Type != typeName {
|
|
return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", movedTo.Type, typeName)
|
|
}
|
|
}
|
|
|
|
return provider.Namespace, movedTo.Namespace, nil
|
|
}
|