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