github.com/Hashicorp/terraform@v0.11.12-beta1/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/httpclient"
    15  	"github.com/hashicorp/terraform/registry/regsrc"
    16  	"github.com/hashicorp/terraform/registry/response"
    17  	"github.com/hashicorp/terraform/svchost"
    18  	"github.com/hashicorp/terraform/svchost/disco"
    19  	"github.com/hashicorp/terraform/version"
    20  )
    21  
    22  const (
    23  	xTerraformGet     = "X-Terraform-Get"
    24  	xTerraformVersion = "X-Terraform-Version"
    25  	requestTimeout    = 10 * time.Second
    26  	serviceID         = "modules.v1"
    27  )
    28  
    29  var tfVersion = version.String()
    30  
    31  // Client provides methods to query Terraform Registries.
    32  type Client struct {
    33  	// this is the client to be used for all requests.
    34  	client *http.Client
    35  
    36  	// services is a required *disco.Disco, which may have services and
    37  	// credentials pre-loaded.
    38  	services *disco.Disco
    39  }
    40  
    41  // NewClient returns a new initialized registry client.
    42  func NewClient(services *disco.Disco, client *http.Client) *Client {
    43  	if services == nil {
    44  		services = disco.New()
    45  	}
    46  
    47  	if client == nil {
    48  		client = httpclient.New()
    49  		client.Timeout = requestTimeout
    50  	}
    51  
    52  	services.Transport = client.Transport
    53  
    54  	return &Client{
    55  		client:   client,
    56  		services: services,
    57  	}
    58  }
    59  
    60  // Discover queries the host, and returns the url for the registry.
    61  func (c *Client) Discover(host svchost.Hostname) (*url.URL, error) {
    62  	service, err := c.services.DiscoverServiceURL(host, serviceID)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  	if !strings.HasSuffix(service.Path, "/") {
    67  		service.Path += "/"
    68  	}
    69  	return service, nil
    70  }
    71  
    72  // Versions queries the registry for a module, and returns the available versions.
    73  func (c *Client) Versions(module *regsrc.Module) (*response.ModuleVersions, error) {
    74  	host, err := module.SvcHost()
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	service, err := c.Discover(host)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	p, err := url.Parse(path.Join(module.Module(), "versions"))
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	service = service.ResolveReference(p)
    90  
    91  	log.Printf("[DEBUG] fetching module versions from %q", service)
    92  
    93  	req, err := http.NewRequest("GET", service.String(), nil)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	c.addRequestCreds(host, req)
    99  	req.Header.Set(xTerraformVersion, tfVersion)
   100  
   101  	resp, err := c.client.Do(req)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	defer resp.Body.Close()
   106  
   107  	switch resp.StatusCode {
   108  	case http.StatusOK:
   109  		// OK
   110  	case http.StatusNotFound:
   111  		return nil, &errModuleNotFound{addr: module}
   112  	default:
   113  		return nil, fmt.Errorf("error looking up module versions: %s", resp.Status)
   114  	}
   115  
   116  	var versions response.ModuleVersions
   117  
   118  	dec := json.NewDecoder(resp.Body)
   119  	if err := dec.Decode(&versions); err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	for _, mod := range versions.Modules {
   124  		for _, v := range mod.Versions {
   125  			log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source)
   126  		}
   127  	}
   128  
   129  	return &versions, nil
   130  }
   131  
   132  func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) {
   133  	creds, err := c.services.CredentialsForHost(host)
   134  	if err != nil {
   135  		log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
   136  		return
   137  	}
   138  
   139  	if creds != nil {
   140  		creds.PrepareRequest(req)
   141  	}
   142  }
   143  
   144  // Location find the download location for a specific version module.
   145  // This returns a string, because the final location may contain special go-getter syntax.
   146  func (c *Client) Location(module *regsrc.Module, version string) (string, error) {
   147  	host, err := module.SvcHost()
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  
   152  	service, err := c.Discover(host)
   153  	if err != nil {
   154  		return "", err
   155  	}
   156  
   157  	var p *url.URL
   158  	if version == "" {
   159  		p, err = url.Parse(path.Join(module.Module(), "download"))
   160  	} else {
   161  		p, err = url.Parse(path.Join(module.Module(), version, "download"))
   162  	}
   163  	if err != nil {
   164  		return "", err
   165  	}
   166  	download := service.ResolveReference(p)
   167  
   168  	log.Printf("[DEBUG] looking up module location from %q", download)
   169  
   170  	req, err := http.NewRequest("GET", download.String(), nil)
   171  	if err != nil {
   172  		return "", err
   173  	}
   174  
   175  	c.addRequestCreds(host, req)
   176  	req.Header.Set(xTerraformVersion, tfVersion)
   177  
   178  	resp, err := c.client.Do(req)
   179  	if err != nil {
   180  		return "", err
   181  	}
   182  	defer resp.Body.Close()
   183  
   184  	// there should be no body, but save it for logging
   185  	body, err := ioutil.ReadAll(resp.Body)
   186  	if err != nil {
   187  		return "", fmt.Errorf("error reading response body from registry: %s", err)
   188  	}
   189  
   190  	switch resp.StatusCode {
   191  	case http.StatusOK, http.StatusNoContent:
   192  		// OK
   193  	case http.StatusNotFound:
   194  		return "", fmt.Errorf("module %q version %q not found", module, version)
   195  	default:
   196  		// anything else is an error:
   197  		return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
   198  	}
   199  
   200  	// the download location is in the X-Terraform-Get header
   201  	location := resp.Header.Get(xTerraformGet)
   202  	if location == "" {
   203  		return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body)
   204  	}
   205  
   206  	// If location looks like it's trying to be a relative URL, treat it as
   207  	// one.
   208  	//
   209  	// We don't do this for just _any_ location, since the X-Terraform-Get
   210  	// header is a go-getter location rather than a URL, and so not all
   211  	// possible values will parse reasonably as URLs.)
   212  	//
   213  	// When used in conjunction with go-getter we normally require this header
   214  	// to be an absolute URL, but we are more liberal here because third-party
   215  	// registry implementations may not "know" their own absolute URLs if
   216  	// e.g. they are running behind a reverse proxy frontend, or such.
   217  	if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") {
   218  		locationURL, err := url.Parse(location)
   219  		if err != nil {
   220  			return "", fmt.Errorf("invalid relative URL for %q: %s", module, err)
   221  		}
   222  		locationURL = download.ResolveReference(locationURL)
   223  		location = locationURL.String()
   224  	}
   225  
   226  	return location, nil
   227  }