github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/internal/getproviders/registry_client.go (about)

     1  package getproviders
     2  
     3  import (
     4  	"encoding/hex"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"net/url"
    10  	"path"
    11  	"time"
    12  
    13  	"github.com/apparentlymart/go-versions/versions"
    14  	svchost "github.com/hashicorp/terraform-svchost"
    15  	svcauth "github.com/hashicorp/terraform-svchost/auth"
    16  
    17  	"github.com/hashicorp/terraform/addrs"
    18  	"github.com/hashicorp/terraform/httpclient"
    19  	"github.com/hashicorp/terraform/version"
    20  )
    21  
    22  const terraformVersionHeader = "X-Terraform-Version"
    23  
    24  // registryClient is a client for the provider registry protocol that is
    25  // specialized only for the needs of this package. It's not intended as a
    26  // general registry API client.
    27  type registryClient struct {
    28  	baseURL *url.URL
    29  	creds   svcauth.HostCredentials
    30  
    31  	httpClient *http.Client
    32  }
    33  
    34  func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registryClient {
    35  	httpClient := httpclient.New()
    36  	httpClient.Timeout = 10 * time.Second
    37  
    38  	return &registryClient{
    39  		baseURL:    baseURL,
    40  		creds:      creds,
    41  		httpClient: httpClient,
    42  	}
    43  }
    44  
    45  // ProviderVersions returns the raw version strings produced by the registry
    46  // for the given provider.
    47  //
    48  // The returned error will be ErrProviderNotKnown if the registry responds
    49  // with 404 Not Found to indicate that the namespace or provider type are
    50  // not known, ErrUnauthorized if the registry responds with 401 or 403 status
    51  // codes, or ErrQueryFailed for any other protocol or operational problem.
    52  func (c *registryClient) ProviderVersions(addr addrs.Provider) ([]string, error) {
    53  	endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions"))
    54  	if err != nil {
    55  		// Should never happen because we're constructing this from
    56  		// already-validated components.
    57  		return nil, err
    58  	}
    59  	endpointURL := c.baseURL.ResolveReference(endpointPath)
    60  
    61  	req, err := http.NewRequest("GET", endpointURL.String(), nil)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	c.addHeadersToRequest(req)
    66  
    67  	resp, err := c.httpClient.Do(req)
    68  	if err != nil {
    69  		return nil, c.errQueryFailed(addr, err)
    70  	}
    71  	defer resp.Body.Close()
    72  
    73  	switch resp.StatusCode {
    74  	case http.StatusOK:
    75  		// Great!
    76  	case http.StatusNotFound:
    77  		return nil, ErrProviderNotKnown{
    78  			Provider: addr,
    79  		}
    80  	case http.StatusUnauthorized, http.StatusForbidden:
    81  		return nil, c.errUnauthorized(addr.Hostname)
    82  	default:
    83  		return nil, c.errQueryFailed(addr, errors.New(resp.Status))
    84  	}
    85  
    86  	// We ignore everything except the version numbers here because our goal
    87  	// is to find out which versions are available _at all_. Which ones are
    88  	// compatible with the current Terraform becomes relevant only once we've
    89  	// selected one, at which point we'll return an error if the selected one
    90  	// is incompatible.
    91  	//
    92  	// We intentionally produce an error on incompatibility, rather than
    93  	// silently ignoring an incompatible version, in order to give the user
    94  	// explicit feedback about why their selection wasn't valid and allow them
    95  	// to decide whether to fix that by changing the selection or by some other
    96  	// action such as upgrading Terraform, using a different OS to run
    97  	// Terraform, etc. Changes that affect compatibility are considered
    98  	// breaking changes from a provider API standpoint, so provider teams
    99  	// should change compatibility only in new major versions.
   100  	type ResponseBody struct {
   101  		Versions []struct {
   102  			Version string `json:"version"`
   103  		} `json:"versions"`
   104  	}
   105  	var body ResponseBody
   106  
   107  	dec := json.NewDecoder(resp.Body)
   108  	if err := dec.Decode(&body); err != nil {
   109  		return nil, c.errQueryFailed(addr, err)
   110  	}
   111  
   112  	if len(body.Versions) == 0 {
   113  		return nil, nil
   114  	}
   115  
   116  	ret := make([]string, len(body.Versions))
   117  	for i, v := range body.Versions {
   118  		ret[i] = v.Version
   119  	}
   120  	return ret, nil
   121  }
   122  
   123  // PackageMeta returns metadata about a distribution package for a
   124  // provider.
   125  //
   126  // The returned error will be ErrPlatformNotSupported if the registry responds
   127  // with 404 Not Found, under the assumption that the caller previously checked
   128  // that the provider and version are valid. It will return ErrUnauthorized if
   129  // the registry responds with 401 or 403 status codes, or ErrQueryFailed for
   130  // any other protocol or operational problem.
   131  func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
   132  	endpointPath, err := url.Parse(path.Join(
   133  		provider.Namespace,
   134  		provider.Type,
   135  		version.String(),
   136  		"download",
   137  		target.OS,
   138  		target.Arch,
   139  	))
   140  	if err != nil {
   141  		// Should never happen because we're constructing this from
   142  		// already-validated components.
   143  		return PackageMeta{}, err
   144  	}
   145  	endpointURL := c.baseURL.ResolveReference(endpointPath)
   146  
   147  	req, err := http.NewRequest("GET", endpointURL.String(), nil)
   148  	if err != nil {
   149  		return PackageMeta{}, err
   150  	}
   151  	c.addHeadersToRequest(req)
   152  
   153  	resp, err := c.httpClient.Do(req)
   154  	if err != nil {
   155  		return PackageMeta{}, c.errQueryFailed(provider, err)
   156  	}
   157  	defer resp.Body.Close()
   158  
   159  	switch resp.StatusCode {
   160  	case http.StatusOK:
   161  		// Great!
   162  	case http.StatusNotFound:
   163  		return PackageMeta{}, ErrPlatformNotSupported{
   164  			Provider: provider,
   165  			Version:  version,
   166  			Platform: target,
   167  		}
   168  	case http.StatusUnauthorized, http.StatusForbidden:
   169  		return PackageMeta{}, c.errUnauthorized(provider.Hostname)
   170  	default:
   171  		return PackageMeta{}, c.errQueryFailed(provider, errors.New(resp.Status))
   172  	}
   173  
   174  	type ResponseBody struct {
   175  		Protocols   []string `json:"protocols"`
   176  		OS          string   `json:"os"`
   177  		Arch        string   `json:"arch"`
   178  		Filename    string   `json:"filename"`
   179  		DownloadURL string   `json:"download_url"`
   180  		SHA256Sum   string   `json:"shasum"`
   181  
   182  		// TODO: Other metadata for signature checking
   183  	}
   184  	var body ResponseBody
   185  
   186  	dec := json.NewDecoder(resp.Body)
   187  	if err := dec.Decode(&body); err != nil {
   188  		return PackageMeta{}, c.errQueryFailed(provider, err)
   189  	}
   190  
   191  	var protoVersions VersionList
   192  	for _, versionStr := range body.Protocols {
   193  		v, err := versions.ParseVersion(versionStr)
   194  		if err != nil {
   195  			return PackageMeta{}, c.errQueryFailed(
   196  				provider,
   197  				fmt.Errorf("registry response includes invalid version string %q: %s", versionStr, err),
   198  			)
   199  		}
   200  		protoVersions = append(protoVersions, v)
   201  	}
   202  	protoVersions.Sort()
   203  
   204  	downloadURL, err := url.Parse(body.DownloadURL)
   205  	if err != nil {
   206  		return PackageMeta{}, fmt.Errorf("registry response includes invalid download URL: %s", err)
   207  	}
   208  	downloadURL = resp.Request.URL.ResolveReference(downloadURL)
   209  	if downloadURL.Scheme != "http" && downloadURL.Scheme != "https" {
   210  		return PackageMeta{}, fmt.Errorf("registry response includes invalid download URL: must use http or https scheme")
   211  	}
   212  
   213  	ret := PackageMeta{
   214  		ProtocolVersions: protoVersions,
   215  		TargetPlatform: Platform{
   216  			OS:   body.OS,
   217  			Arch: body.Arch,
   218  		},
   219  		Filename: body.Filename,
   220  		Location: PackageHTTPURL(downloadURL.String()),
   221  		// SHA256Sum is populated below
   222  	}
   223  
   224  	if len(body.SHA256Sum) != len(ret.SHA256Sum)*2 {
   225  		return PackageMeta{}, c.errQueryFailed(
   226  			provider,
   227  			fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
   228  		)
   229  	}
   230  	_, err = hex.Decode(ret.SHA256Sum[:], []byte(body.SHA256Sum))
   231  	if err != nil {
   232  		return PackageMeta{}, c.errQueryFailed(
   233  			provider,
   234  			fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err),
   235  		)
   236  	}
   237  
   238  	return ret, nil
   239  }
   240  
   241  // LegacyProviderCanonicalAddress returns the raw address strings produced by
   242  // the registry when asked about the given unqualified provider type name.
   243  // The returned namespace string is taken verbatim from the registry's response.
   244  //
   245  // This method exists only to allow compatibility with unqualified names
   246  // in older configurations. New configurations should be written so as not to
   247  // depend on it.
   248  func (c *registryClient) LegacyProviderDefaultNamespace(typeName string) (string, error) {
   249  	endpointPath, err := url.Parse(path.Join("-", typeName))
   250  	if err != nil {
   251  		// Should never happen because we're constructing this from
   252  		// already-validated components.
   253  		return "", err
   254  	}
   255  	endpointURL := c.baseURL.ResolveReference(endpointPath)
   256  
   257  	req, err := http.NewRequest("GET", endpointURL.String(), nil)
   258  	if err != nil {
   259  		return "", err
   260  	}
   261  	c.addHeadersToRequest(req)
   262  
   263  	// This is just to give us something to return in error messages. It's
   264  	// not a proper provider address.
   265  	placeholderProviderAddr := addrs.NewLegacyProvider(typeName)
   266  
   267  	resp, err := c.httpClient.Do(req)
   268  	if err != nil {
   269  		return "", c.errQueryFailed(placeholderProviderAddr, err)
   270  	}
   271  	defer resp.Body.Close()
   272  
   273  	switch resp.StatusCode {
   274  	case http.StatusOK:
   275  		// Great!
   276  	case http.StatusNotFound:
   277  		return "", ErrProviderNotKnown{
   278  			Provider: placeholderProviderAddr,
   279  		}
   280  	case http.StatusUnauthorized, http.StatusForbidden:
   281  		return "", c.errUnauthorized(placeholderProviderAddr.Hostname)
   282  	default:
   283  		return "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status))
   284  	}
   285  
   286  	type ResponseBody struct {
   287  		Namespace string
   288  	}
   289  	var body ResponseBody
   290  
   291  	dec := json.NewDecoder(resp.Body)
   292  	if err := dec.Decode(&body); err != nil {
   293  		return "", c.errQueryFailed(placeholderProviderAddr, err)
   294  	}
   295  
   296  	return body.Namespace, nil
   297  }
   298  
   299  func (c *registryClient) addHeadersToRequest(req *http.Request) {
   300  	if c.creds != nil {
   301  		c.creds.PrepareRequest(req)
   302  	}
   303  	req.Header.Set(terraformVersionHeader, version.String())
   304  }
   305  
   306  func (c *registryClient) errQueryFailed(provider addrs.Provider, err error) error {
   307  	return ErrQueryFailed{
   308  		Provider: provider,
   309  		Wrapped:  err,
   310  	}
   311  }
   312  
   313  func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error {
   314  	return ErrUnauthorized{
   315  		Hostname:        hostname,
   316  		HaveCredentials: c.creds != nil,
   317  	}
   318  }