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