github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/registry/client.go (about)

     1  package registry
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"path"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/hashicorp/go-retryablehttp"
    17  	svchost "github.com/hashicorp/terraform-svchost"
    18  	"github.com/hashicorp/terraform-svchost/disco"
    19  	"github.com/hashicorp/terraform/helper/logging"
    20  	"github.com/hashicorp/terraform/httpclient"
    21  	"github.com/hashicorp/terraform/registry/regsrc"
    22  	"github.com/hashicorp/terraform/registry/response"
    23  	"github.com/hashicorp/terraform/version"
    24  )
    25  
    26  const (
    27  	xTerraformGet      = "X-Terraform-Get"
    28  	xTerraformVersion  = "X-Terraform-Version"
    29  	modulesServiceID   = "modules.v1"
    30  	providersServiceID = "providers.v1"
    31  
    32  	// registryDiscoveryRetryEnvName is the name of the environment variable that
    33  	// can be configured to customize number of retries for module and provider
    34  	// discovery requests with the remote registry.
    35  	registryDiscoveryRetryEnvName = "TF_REGISTRY_DISCOVERY_RETRY"
    36  	defaultRetry                  = 1
    37  
    38  	// registryClientTimeoutEnvName is the name of the environment variable that
    39  	// can be configured to customize the timeout duration (seconds) for module
    40  	// and provider discovery with the remote registry.
    41  	registryClientTimeoutEnvName = "TF_REGISTRY_CLIENT_TIMEOUT"
    42  
    43  	// defaultRequestTimeout is the default timeout duration for requests to the
    44  	// remote registry.
    45  	defaultRequestTimeout = 10 * time.Second
    46  )
    47  
    48  var (
    49  	tfVersion = version.String()
    50  
    51  	discoveryRetry int
    52  	requestTimeout time.Duration
    53  )
    54  
    55  func init() {
    56  	configureDiscoveryRetry()
    57  	configureRequestTimeout()
    58  }
    59  
    60  // Client provides methods to query Terraform Registries.
    61  type Client struct {
    62  	// this is the client to be used for all requests.
    63  	client *retryablehttp.Client
    64  
    65  	// services is a required *disco.Disco, which may have services and
    66  	// credentials pre-loaded.
    67  	services *disco.Disco
    68  
    69  	// retry is the number of retries the client will attempt for each request
    70  	// if it runs into a transient failure with the remote registry.
    71  	retry int
    72  }
    73  
    74  // NewClient returns a new initialized registry client.
    75  func NewClient(services *disco.Disco, client *http.Client) *Client {
    76  	if services == nil {
    77  		services = disco.New()
    78  	}
    79  
    80  	if client == nil {
    81  		client = httpclient.New()
    82  		client.Timeout = requestTimeout
    83  	}
    84  	retryableClient := retryablehttp.NewClient()
    85  	retryableClient.HTTPClient = client
    86  	retryableClient.RetryMax = discoveryRetry
    87  	retryableClient.RequestLogHook = requestLogHook
    88  	retryableClient.ErrorHandler = maxRetryErrorHandler
    89  
    90  	logOutput, err := logging.LogOutput()
    91  	if err != nil {
    92  		log.Printf("[WARN] Failed to set up registry client logger, "+
    93  			"continuing without client logging: %s", err)
    94  	}
    95  	retryableClient.Logger = log.New(logOutput, "", log.Flags())
    96  
    97  	services.Transport = retryableClient.HTTPClient.Transport
    98  
    99  	services.SetUserAgent(httpclient.TerraformUserAgent(version.String()))
   100  
   101  	return &Client{
   102  		client:   retryableClient,
   103  		services: services,
   104  	}
   105  }
   106  
   107  // Discover queries the host, and returns the url for the registry.
   108  func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) {
   109  	service, err := c.services.DiscoverServiceURL(host, serviceID)
   110  	if err != nil {
   111  		return nil, &ServiceUnreachableError{err}
   112  	}
   113  	if !strings.HasSuffix(service.Path, "/") {
   114  		service.Path += "/"
   115  	}
   116  	return service, nil
   117  }
   118  
   119  // ModuleVersions queries the registry for a module, and returns the available versions.
   120  func (c *Client) ModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) {
   121  	host, err := module.SvcHost()
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	service, err := c.Discover(host, modulesServiceID)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	p, err := url.Parse(path.Join(module.Module(), "versions"))
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	service = service.ResolveReference(p)
   137  
   138  	log.Printf("[DEBUG] fetching module versions from %q", service)
   139  
   140  	req, err := retryablehttp.NewRequest("GET", service.String(), nil)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	c.addRequestCreds(host, req.Request)
   146  	req.Header.Set(xTerraformVersion, tfVersion)
   147  
   148  	resp, err := c.client.Do(req)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	defer resp.Body.Close()
   153  
   154  	switch resp.StatusCode {
   155  	case http.StatusOK:
   156  		// OK
   157  	case http.StatusNotFound:
   158  		return nil, &errModuleNotFound{addr: module}
   159  	default:
   160  		return nil, fmt.Errorf("error looking up module versions: %s", resp.Status)
   161  	}
   162  
   163  	var versions response.ModuleVersions
   164  
   165  	dec := json.NewDecoder(resp.Body)
   166  	if err := dec.Decode(&versions); err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	for _, mod := range versions.Modules {
   171  		for _, v := range mod.Versions {
   172  			log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source)
   173  		}
   174  	}
   175  
   176  	return &versions, nil
   177  }
   178  
   179  func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) {
   180  	creds, err := c.services.CredentialsForHost(host)
   181  	if err != nil {
   182  		log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
   183  		return
   184  	}
   185  
   186  	if creds != nil {
   187  		creds.PrepareRequest(req)
   188  	}
   189  }
   190  
   191  // ModuleLocation find the download location for a specific version module.
   192  // This returns a string, because the final location may contain special go-getter syntax.
   193  func (c *Client) ModuleLocation(module *regsrc.Module, version string) (string, error) {
   194  	host, err := module.SvcHost()
   195  	if err != nil {
   196  		return "", err
   197  	}
   198  
   199  	service, err := c.Discover(host, modulesServiceID)
   200  	if err != nil {
   201  		return "", err
   202  	}
   203  
   204  	var p *url.URL
   205  	if version == "" {
   206  		p, err = url.Parse(path.Join(module.Module(), "download"))
   207  	} else {
   208  		p, err = url.Parse(path.Join(module.Module(), version, "download"))
   209  	}
   210  	if err != nil {
   211  		return "", err
   212  	}
   213  	download := service.ResolveReference(p)
   214  
   215  	log.Printf("[DEBUG] looking up module location from %q", download)
   216  
   217  	req, err := retryablehttp.NewRequest("GET", download.String(), nil)
   218  	if err != nil {
   219  		return "", err
   220  	}
   221  
   222  	c.addRequestCreds(host, req.Request)
   223  	req.Header.Set(xTerraformVersion, tfVersion)
   224  
   225  	resp, err := c.client.Do(req)
   226  	if err != nil {
   227  		return "", err
   228  	}
   229  	defer resp.Body.Close()
   230  
   231  	// there should be no body, but save it for logging
   232  	body, err := ioutil.ReadAll(resp.Body)
   233  	if err != nil {
   234  		return "", fmt.Errorf("error reading response body from registry: %s", err)
   235  	}
   236  
   237  	switch resp.StatusCode {
   238  	case http.StatusOK, http.StatusNoContent:
   239  		// OK
   240  	case http.StatusNotFound:
   241  		return "", fmt.Errorf("module %q version %q not found", module, version)
   242  	default:
   243  		// anything else is an error:
   244  		return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
   245  	}
   246  
   247  	// the download location is in the X-Terraform-Get header
   248  	location := resp.Header.Get(xTerraformGet)
   249  	if location == "" {
   250  		return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body)
   251  	}
   252  
   253  	// If location looks like it's trying to be a relative URL, treat it as
   254  	// one.
   255  	//
   256  	// We don't do this for just _any_ location, since the X-Terraform-Get
   257  	// header is a go-getter location rather than a URL, and so not all
   258  	// possible values will parse reasonably as URLs.)
   259  	//
   260  	// When used in conjunction with go-getter we normally require this header
   261  	// to be an absolute URL, but we are more liberal here because third-party
   262  	// registry implementations may not "know" their own absolute URLs if
   263  	// e.g. they are running behind a reverse proxy frontend, or such.
   264  	if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") {
   265  		locationURL, err := url.Parse(location)
   266  		if err != nil {
   267  			return "", fmt.Errorf("invalid relative URL for %q: %s", module, err)
   268  		}
   269  		locationURL = download.ResolveReference(locationURL)
   270  		location = locationURL.String()
   271  	}
   272  
   273  	return location, nil
   274  }
   275  
   276  // TerraformProviderVersions queries the registry for a provider, and returns the available versions.
   277  func (c *Client) TerraformProviderVersions(provider *regsrc.TerraformProvider) (*response.TerraformProviderVersions, error) {
   278  	host, err := provider.SvcHost()
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	service, err := c.Discover(host, providersServiceID)
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	p, err := url.Parse(path.Join(provider.TerraformProvider(), "versions"))
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	service = service.ResolveReference(p)
   294  
   295  	log.Printf("[DEBUG] fetching provider versions from %q", service)
   296  
   297  	req, err := retryablehttp.NewRequest("GET", service.String(), nil)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	c.addRequestCreds(host, req.Request)
   303  	req.Header.Set(xTerraformVersion, tfVersion)
   304  
   305  	resp, err := c.client.Do(req)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	defer resp.Body.Close()
   310  
   311  	switch resp.StatusCode {
   312  	case http.StatusOK:
   313  		// OK
   314  	case http.StatusNotFound:
   315  		return nil, &errProviderNotFound{addr: provider}
   316  	default:
   317  		return nil, fmt.Errorf("error looking up provider versions: %s", resp.Status)
   318  	}
   319  
   320  	var versions response.TerraformProviderVersions
   321  
   322  	dec := json.NewDecoder(resp.Body)
   323  	if err := dec.Decode(&versions); err != nil {
   324  		return nil, err
   325  	}
   326  
   327  	return &versions, nil
   328  }
   329  
   330  // TerraformProviderLocation queries the registry for a provider download metadata
   331  func (c *Client) TerraformProviderLocation(provider *regsrc.TerraformProvider, version string) (*response.TerraformProviderPlatformLocation, error) {
   332  	host, err := provider.SvcHost()
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	service, err := c.Discover(host, providersServiceID)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	p, err := url.Parse(path.Join(
   343  		provider.TerraformProvider(),
   344  		version,
   345  		"download",
   346  		provider.OS,
   347  		provider.Arch,
   348  	))
   349  	if err != nil {
   350  		return nil, err
   351  	}
   352  
   353  	service = service.ResolveReference(p)
   354  
   355  	log.Printf("[DEBUG] fetching provider location from %q", service)
   356  
   357  	req, err := retryablehttp.NewRequest("GET", service.String(), nil)
   358  	if err != nil {
   359  		return nil, err
   360  	}
   361  
   362  	c.addRequestCreds(host, req.Request)
   363  	req.Header.Set(xTerraformVersion, tfVersion)
   364  
   365  	resp, err := c.client.Do(req)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	defer resp.Body.Close()
   370  
   371  	var loc response.TerraformProviderPlatformLocation
   372  
   373  	dec := json.NewDecoder(resp.Body)
   374  	if err := dec.Decode(&loc); err != nil {
   375  		return nil, err
   376  	}
   377  
   378  	switch resp.StatusCode {
   379  	case http.StatusOK, http.StatusNoContent:
   380  		// OK
   381  	case http.StatusNotFound:
   382  		return nil, fmt.Errorf("provider %q version %q not found", provider.TerraformProvider(), version)
   383  	default:
   384  		// anything else is an error:
   385  		return nil, fmt.Errorf("error getting download location for %q: %s", provider.TerraformProvider(), resp.Status)
   386  	}
   387  
   388  	return &loc, nil
   389  }
   390  
   391  // configureDiscoveryRetry configures the number of retries the registry client
   392  // will attempt for requests with retryable errors, like 502 status codes
   393  func configureDiscoveryRetry() {
   394  	discoveryRetry = defaultRetry
   395  
   396  	if v := os.Getenv(registryDiscoveryRetryEnvName); v != "" {
   397  		retry, err := strconv.Atoi(v)
   398  		if err == nil && retry > 0 {
   399  			discoveryRetry = retry
   400  		}
   401  	}
   402  }
   403  
   404  func requestLogHook(logger retryablehttp.Logger, req *http.Request, i int) {
   405  	if i > 0 {
   406  		logger.Printf("[INFO] Previous request to the remote registry failed, attempting retry.")
   407  	}
   408  }
   409  
   410  func maxRetryErrorHandler(resp *http.Response, err error, numTries int) (*http.Response, error) {
   411  	// Close the body per library instructions
   412  	if resp != nil {
   413  		resp.Body.Close()
   414  	}
   415  
   416  	// Additional error detail: if we have a response, use the status code;
   417  	// if we have an error, use that; otherwise nothing. We will never have
   418  	// both response and error.
   419  	var errMsg string
   420  	if resp != nil {
   421  		errMsg = fmt.Sprintf(": %d", resp.StatusCode)
   422  	} else if err != nil {
   423  		errMsg = fmt.Sprintf(": %s", err)
   424  	}
   425  
   426  	// This function is always called with numTries=RetryMax+1. If we made any
   427  	// retry attempts, include that in the error message.
   428  	if numTries > 1 {
   429  		return resp, fmt.Errorf("the request failed after %d attempts, please try again later%s",
   430  			numTries, errMsg)
   431  	}
   432  	return resp, fmt.Errorf("the request failed, please try again later%s", errMsg)
   433  }
   434  
   435  // configureRequestTimeout configures the registry client request timeout from
   436  // environment variables
   437  func configureRequestTimeout() {
   438  	requestTimeout = defaultRequestTimeout
   439  
   440  	if v := os.Getenv(registryClientTimeoutEnvName); v != "" {
   441  		timeout, err := strconv.Atoi(v)
   442  		if err == nil && timeout > 0 {
   443  			requestTimeout = time.Duration(timeout) * time.Second
   444  		}
   445  	}
   446  }