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

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