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"
23 xTerraformGet = "X-Terraform-Get"
24 xTerraformVersion = "X-Terraform-Version"
25 requestTimeout = 10 * time.Second
26 serviceID = "modules.v1"
29 var tfVersion = version.String()
31 // Client provides methods to query Terraform Registries.
33 // this is the client to be used for all requests.
36 // services is a required *disco.Disco, which may have services and
37 // credentials pre-loaded.
41 // NewClient returns a new initialized registry client.
42 func NewClient(services *disco.Disco, client *http.Client) *Client {
44 services = disco.New()
48 client = httpclient.New()
49 client.Timeout = requestTimeout
52 services.Transport = client.Transport
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)
66 if !strings.HasSuffix(service.Path, "/") {
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()
79 service, err := c.Discover(host)
84 p, err := url.Parse(path.Join(module.Module(), "versions"))
89 service = service.ResolveReference(p)
91 log.Printf("[DEBUG] fetching module versions from %q", service)
93 req, err := http.NewRequest("GET", service.String(), nil)
98 c.addRequestCreds(host, req)
99 req.Header.Set(xTerraformVersion, tfVersion)
101 resp, err := c.client.Do(req)
105 defer resp.Body.Close()
107 switch resp.StatusCode {
110 case http.StatusNotFound:
111 return nil, &errModuleNotFound{addr: module}
113 return nil, fmt.Errorf("error looking up module versions: %s", resp.Status)
116 var versions response.ModuleVersions
118 dec := json.NewDecoder(resp.Body)
119 if err := dec.Decode(&versions); err != nil {
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)
129 return &versions, nil
132 func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) {
133 creds, err := c.services.CredentialsForHost(host)
135 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
140 creds.PrepareRequest(req)
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()
152 service, err := c.Discover(host)
159 p, err = url.Parse(path.Join(module.Module(), "download"))
161 p, err = url.Parse(path.Join(module.Module(), version, "download"))
166 download := service.ResolveReference(p)
168 log.Printf("[DEBUG] looking up module location from %q", download)
170 req, err := http.NewRequest("GET", download.String(), nil)
175 c.addRequestCreds(host, req)
176 req.Header.Set(xTerraformVersion, tfVersion)
178 resp, err := c.client.Do(req)
182 defer resp.Body.Close()
184 // there should be no body, but save it for logging
185 body, err := ioutil.ReadAll(resp.Body)
187 return "", fmt.Errorf("error reading response body from registry: %s", err)
190 switch resp.StatusCode {
191 case http.StatusOK, http.StatusNoContent:
193 case http.StatusNotFound:
194 return "", fmt.Errorf("module %q version %q not found", module, version)
196 // anything else is an error:
197 return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
200 // the download location is in the X-Terraform-Get header
201 location := resp.Header.Get(xTerraformGet)
203 return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body)
206 // If location looks like it's trying to be a relative URL, treat it as
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.)
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)
220 return "", fmt.Errorf("invalid relative URL for %q: %s", module, err)
222 locationURL = download.ResolveReference(locationURL)
223 location = locationURL.String()