github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/getproviders/didyoumean.go (about)

     1  package getproviders
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"path"
    11  
    12  	"github.com/hashicorp/go-retryablehttp"
    13  	svchost "github.com/hashicorp/terraform-svchost"
    14  	"github.com/hashicorp/terraform/internal/addrs"
    15  )
    16  
    17  // MissingProviderSuggestion takes a provider address that failed installation
    18  // due to the remote registry reporting that it didn't exist, and attempts
    19  // to find another provider that the user might have meant to select.
    20  //
    21  // If the result is equal to the given address then that indicates that there
    22  // is no suggested alternative to offer, either because the function
    23  // successfully determined there is no recorded alternative or because the
    24  // lookup failed somehow. We don't consider a failure to find a suggestion
    25  // as an installation failure, because the caller should already be reporting
    26  // that the provider didn't exist anyway and this is only extra context for
    27  // that error message.
    28  //
    29  // The result of this is a best effort, so any UI presenting it should be
    30  // careful to give it only as a possibility and not necessarily a suitable
    31  // replacement for the given provider.
    32  //
    33  // In practice today this function only knows how to suggest alternatives for
    34  // "default" providers, which is to say ones that are in the hashicorp
    35  // namespace in the Terraform registry. It will always return no result for
    36  // any other provider. That might change in future if we introduce other ways
    37  // to discover provider suggestions.
    38  //
    39  // If the given context is cancelled then this function might not return a
    40  // renaming suggestion even if one would've been available for a completed
    41  // request.
    42  func MissingProviderSuggestion(ctx context.Context, addr addrs.Provider, source Source, reqs Requirements) addrs.Provider {
    43  	if !addrs.IsDefaultProvider(addr) {
    44  		return addr
    45  	}
    46  
    47  	// Before possibly looking up legacy naming, see if the user has another provider
    48  	// named in their requirements that is of the same type, and offer that
    49  	// as a suggestion
    50  	for req := range reqs {
    51  		if req != addr && req.Type == addr.Type {
    52  			return req
    53  		}
    54  	}
    55  
    56  	// Our strategy here, for a default provider, is to use the default
    57  	// registry's special API for looking up "legacy" providers and try looking
    58  	// for a legacy provider whose type name matches the type of the given
    59  	// provider. This should then find a suitable answer for any provider
    60  	// that was originally auto-installable in v0.12 and earlier but moved
    61  	// into a non-default namespace as part of introducing the hierarchical
    62  	// provider namespace.
    63  	//
    64  	// To achieve that, we need to find the direct registry client in
    65  	// particular from the given source, because that is the only Source
    66  	// implementation that can actually handle a legacy provider lookup.
    67  	regSource := findLegacyProviderLookupSource(addr.Hostname, source)
    68  	if regSource == nil {
    69  		// If there's no direct registry source in the installation config
    70  		// then we can't provide a renaming suggestion.
    71  		return addr
    72  	}
    73  
    74  	defaultNS, redirectNS, err := regSource.lookupLegacyProviderNamespace(ctx, addr.Hostname, addr.Type)
    75  	if err != nil {
    76  		return addr
    77  	}
    78  
    79  	switch {
    80  	case redirectNS != "":
    81  		return addrs.Provider{
    82  			Hostname:  addr.Hostname,
    83  			Namespace: redirectNS,
    84  			Type:      addr.Type,
    85  		}
    86  	default:
    87  		return addrs.Provider{
    88  			Hostname:  addr.Hostname,
    89  			Namespace: defaultNS,
    90  			Type:      addr.Type,
    91  		}
    92  	}
    93  }
    94  
    95  // findLegacyProviderLookupSource tries to find a *RegistrySource that can talk
    96  // to the given registry host in the given Source. It might be given directly,
    97  // or it might be given indirectly via a MultiSource where the selector
    98  // includes a wildcard for registry.terraform.io.
    99  //
   100  // Returns nil if the given source does not have any configured way to talk
   101  // directly to the given host.
   102  //
   103  // If the given source contains multiple sources that can talk to the given
   104  // host directly, the first one in the sequence takes preference. In practice
   105  // it's pointless to have two direct installation sources that match the same
   106  // hostname anyway, so this shouldn't arise in normal use.
   107  func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *RegistrySource {
   108  	switch source := source.(type) {
   109  
   110  	case *RegistrySource:
   111  		// Easy case: the source is a registry source directly, and so we'll
   112  		// just use it.
   113  		return source
   114  
   115  	case *MemoizeSource:
   116  		// Also easy: the source is a memoize wrapper, so defer to its
   117  		// underlying source.
   118  		return findLegacyProviderLookupSource(host, source.underlying)
   119  
   120  	case MultiSource:
   121  		// Trickier case: if it's a multisource then we need to scan over
   122  		// its selectors until we find one that is a *RegistrySource _and_
   123  		// that is configured to accept arbitrary providers from the
   124  		// given hostname.
   125  
   126  		// For our matching purposes we'll use an address that would not be
   127  		// valid as a real provider FQN and thus can only match a selector
   128  		// that has no filters at all or a selector that wildcards everything
   129  		// except the hostname, like "registry.terraform.io/*/*"
   130  		matchAddr := addrs.Provider{
   131  			Hostname: host,
   132  			// Other fields are intentionally left empty, to make this invalid
   133  			// as a specific provider address.
   134  		}
   135  
   136  		for _, selector := range source {
   137  			// If this source has suitable matching patterns to install from
   138  			// the given hostname then we'll recursively search inside it
   139  			// for *RegistrySource objects.
   140  			if selector.CanHandleProvider(matchAddr) {
   141  				ret := findLegacyProviderLookupSource(host, selector.Source)
   142  				if ret != nil {
   143  					return ret
   144  				}
   145  			}
   146  		}
   147  
   148  		// If we get here then there were no selectors that are both configured
   149  		// to handle modules from the given hostname and that are registry
   150  		// sources, so we fail.
   151  		return nil
   152  
   153  	default:
   154  		// This source cannot be and cannot contain a *RegistrySource, so
   155  		// we fail.
   156  		return nil
   157  	}
   158  }
   159  
   160  // lookupLegacyProviderNamespace is a special method available only on
   161  // RegistrySource which can deal with legacy provider addresses that contain
   162  // only a type and leave the namespace implied.
   163  //
   164  // It asks the registry at the given hostname to provide a default namespace
   165  // for the given provider type, which can be combined with the given hostname
   166  // and type name to produce a fully-qualified provider address.
   167  //
   168  // Not all unqualified type names can be resolved to a default namespace. If
   169  // the request fails, this method returns an error describing the failure.
   170  //
   171  // This method exists only to allow compatibility with unqualified names
   172  // in older configurations. New configurations should be written so as not to
   173  // depend on it, and this fallback mechanism will likely be removed altogether
   174  // in a future Terraform version.
   175  func (s *RegistrySource) lookupLegacyProviderNamespace(ctx context.Context, hostname svchost.Hostname, typeName string) (string, string, error) {
   176  	client, err := s.registryClient(hostname)
   177  	if err != nil {
   178  		return "", "", err
   179  	}
   180  	return client.legacyProviderDefaultNamespace(ctx, typeName)
   181  }
   182  
   183  // legacyProviderDefaultNamespace returns the raw address strings produced by
   184  // the registry when asked about the given unqualified provider type name.
   185  // The returned namespace string is taken verbatim from the registry's response.
   186  //
   187  // This method exists only to allow compatibility with unqualified names
   188  // in older configurations. New configurations should be written so as not to
   189  // depend on it.
   190  func (c *registryClient) legacyProviderDefaultNamespace(ctx context.Context, typeName string) (string, string, error) {
   191  	endpointPath, err := url.Parse(path.Join("-", typeName, "versions"))
   192  	if err != nil {
   193  		// Should never happen because we're constructing this from
   194  		// already-validated components.
   195  		return "", "", err
   196  	}
   197  	endpointURL := c.baseURL.ResolveReference(endpointPath)
   198  
   199  	req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
   200  	if err != nil {
   201  		return "", "", err
   202  	}
   203  	req = req.WithContext(ctx)
   204  	c.addHeadersToRequest(req.Request)
   205  
   206  	// This is just to give us something to return in error messages. It's
   207  	// not a proper provider address.
   208  	placeholderProviderAddr := addrs.NewLegacyProvider(typeName)
   209  
   210  	resp, err := c.httpClient.Do(req)
   211  	if err != nil {
   212  		return "", "", c.errQueryFailed(placeholderProviderAddr, err)
   213  	}
   214  	defer resp.Body.Close()
   215  
   216  	switch resp.StatusCode {
   217  	case http.StatusOK:
   218  		// Great!
   219  	case http.StatusNotFound:
   220  		return "", "", ErrProviderNotFound{
   221  			Provider: placeholderProviderAddr,
   222  		}
   223  	case http.StatusUnauthorized, http.StatusForbidden:
   224  		return "", "", c.errUnauthorized(placeholderProviderAddr.Hostname)
   225  	default:
   226  		return "", "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status))
   227  	}
   228  
   229  	type ResponseBody struct {
   230  		Id      string `json:"id"`
   231  		MovedTo string `json:"moved_to"`
   232  	}
   233  	var body ResponseBody
   234  
   235  	dec := json.NewDecoder(resp.Body)
   236  	if err := dec.Decode(&body); err != nil {
   237  		return "", "", c.errQueryFailed(placeholderProviderAddr, err)
   238  	}
   239  
   240  	provider, diags := addrs.ParseProviderSourceString(body.Id)
   241  	if diags.HasErrors() {
   242  		return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err())
   243  	}
   244  
   245  	if provider.Type != typeName {
   246  		return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", provider.Type, typeName)
   247  	}
   248  
   249  	var movedTo addrs.Provider
   250  	if body.MovedTo != "" {
   251  		movedTo, diags = addrs.ParseProviderSourceString(body.MovedTo)
   252  		if diags.HasErrors() {
   253  			return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err())
   254  		}
   255  
   256  		if movedTo.Type != typeName {
   257  			return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", movedTo.Type, typeName)
   258  		}
   259  	}
   260  
   261  	return provider.Namespace, movedTo.Namespace, nil
   262  }