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 modulesServiceID = "modules.v1"
27 providersServiceID = "providers.v1"
30 var tfVersion = version.String()
32 // Client provides methods to query Terraform Registries.
34 // this is the client to be used for all requests.
37 // services is a required *disco.Disco, which may have services and
38 // credentials pre-loaded.
42 // NewClient returns a new initialized registry client.
43 func NewClient(services *disco.Disco, client *http.Client) *Client {
45 services = disco.New()
49 client = httpclient.New()
50 client.Timeout = requestTimeout
53 services.Transport = client.Transport
61 // Discover queries the host, and returns the url for the registry.
62 func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) {
63 service, err := c.services.DiscoverServiceURL(host, serviceID)
65 return nil, &ServiceUnreachableError{err}
67 if !strings.HasSuffix(service.Path, "/") {
73 // ModuleVersions queries the registry for a module, and returns the available versions.
74 func (c *Client) ModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) {
75 host, err := module.SvcHost()
80 service, err := c.Discover(host, modulesServiceID)
85 p, err := url.Parse(path.Join(module.Module(), "versions"))
90 service = service.ResolveReference(p)
92 log.Printf("[DEBUG] fetching module versions from %q", service)
94 req, err := http.NewRequest("GET", service.String(), nil)
99 c.addRequestCreds(host, req)
100 req.Header.Set(xTerraformVersion, tfVersion)
102 resp, err := c.client.Do(req)
106 defer resp.Body.Close()
108 switch resp.StatusCode {
111 case http.StatusNotFound:
112 return nil, &errModuleNotFound{addr: module}
114 return nil, fmt.Errorf("error looking up module versions: %s", resp.Status)
117 var versions response.ModuleVersions
119 dec := json.NewDecoder(resp.Body)
120 if err := dec.Decode(&versions); err != nil {
124 for _, mod := range versions.Modules {
125 for _, v := range mod.Versions {
126 log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source)
130 return &versions, nil
133 func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) {
134 creds, err := c.services.CredentialsForHost(host)
136 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
141 creds.PrepareRequest(req)
145 // ModuleLocation find the download location for a specific version module.
146 // This returns a string, because the final location may contain special go-getter syntax.
147 func (c *Client) ModuleLocation(module *regsrc.Module, version string) (string, error) {
148 host, err := module.SvcHost()
153 service, err := c.Discover(host, modulesServiceID)
160 p, err = url.Parse(path.Join(module.Module(), "download"))
162 p, err = url.Parse(path.Join(module.Module(), version, "download"))
167 download := service.ResolveReference(p)
169 log.Printf("[DEBUG] looking up module location from %q", download)
171 req, err := http.NewRequest("GET", download.String(), nil)
176 c.addRequestCreds(host, req)
177 req.Header.Set(xTerraformVersion, tfVersion)
179 resp, err := c.client.Do(req)
183 defer resp.Body.Close()
185 // there should be no body, but save it for logging
186 body, err := ioutil.ReadAll(resp.Body)
188 return "", fmt.Errorf("error reading response body from registry: %s", err)
191 switch resp.StatusCode {
192 case http.StatusOK, http.StatusNoContent:
194 case http.StatusNotFound:
195 return "", fmt.Errorf("module %q version %q not found", module, version)
197 // anything else is an error:
198 return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body)
201 // the download location is in the X-Terraform-Get header
202 location := resp.Header.Get(xTerraformGet)
204 return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body)
207 // If location looks like it's trying to be a relative URL, treat it as
210 // We don't do this for just _any_ location, since the X-Terraform-Get
211 // header is a go-getter location rather than a URL, and so not all
212 // possible values will parse reasonably as URLs.)
214 // When used in conjunction with go-getter we normally require this header
215 // to be an absolute URL, but we are more liberal here because third-party
216 // registry implementations may not "know" their own absolute URLs if
217 // e.g. they are running behind a reverse proxy frontend, or such.
218 if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") {
219 locationURL, err := url.Parse(location)
221 return "", fmt.Errorf("invalid relative URL for %q: %s", module, err)
223 locationURL = download.ResolveReference(locationURL)
224 location = locationURL.String()
230 // TerraformProviderVersions queries the registry for a provider, and returns the available versions.
231 func (c *Client) TerraformProviderVersions(provider *regsrc.TerraformProvider) (*response.TerraformProviderVersions, error) {
232 host, err := provider.SvcHost()
237 service, err := c.Discover(host, providersServiceID)
242 p, err := url.Parse(path.Join(provider.TerraformProvider(), "versions"))
247 service = service.ResolveReference(p)
249 log.Printf("[DEBUG] fetching provider versions from %q", service)
251 req, err := http.NewRequest("GET", service.String(), nil)
256 c.addRequestCreds(host, req)
257 req.Header.Set(xTerraformVersion, tfVersion)
259 resp, err := c.client.Do(req)
263 defer resp.Body.Close()
265 switch resp.StatusCode {
268 case http.StatusNotFound:
269 return nil, &errProviderNotFound{addr: provider}
271 return nil, fmt.Errorf("error looking up provider versions: %s", resp.Status)
274 var versions response.TerraformProviderVersions
276 dec := json.NewDecoder(resp.Body)
277 if err := dec.Decode(&versions); err != nil {
281 return &versions, nil
284 // TerraformProviderLocation queries the registry for a provider download metadata
285 func (c *Client) TerraformProviderLocation(provider *regsrc.TerraformProvider, version string) (*response.TerraformProviderPlatformLocation, error) {
286 host, err := provider.SvcHost()
291 service, err := c.Discover(host, providersServiceID)
296 p, err := url.Parse(path.Join(
297 provider.TerraformProvider(),
307 service = service.ResolveReference(p)
309 log.Printf("[DEBUG] fetching provider location from %q", service)
311 req, err := http.NewRequest("GET", service.String(), nil)
316 c.addRequestCreds(host, req)
317 req.Header.Set(xTerraformVersion, tfVersion)
319 resp, err := c.client.Do(req)
323 defer resp.Body.Close()
325 var loc response.TerraformProviderPlatformLocation
327 dec := json.NewDecoder(resp.Body)
328 if err := dec.Decode(&loc); err != nil {
332 switch resp.StatusCode {
333 case http.StatusOK, http.StatusNoContent:
335 case http.StatusNotFound:
336 return nil, fmt.Errorf("provider %q version %q not found", provider.TerraformProvider(), version)
338 // anything else is an error:
339 return nil, fmt.Errorf("error getting download location for %q: %s", provider.TerraformProvider(), resp.Status)