github.com/trawler/terraform@v0.10.8-0.20171106022149-4b1c7a1d9b48/config/module/registry.go (about)

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