]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
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 ( | |
107c1cdb ND |
23 | xTerraformGet = "X-Terraform-Get" |
24 | xTerraformVersion = "X-Terraform-Version" | |
25 | requestTimeout = 10 * time.Second | |
26 | modulesServiceID = "modules.v1" | |
27 | providersServiceID = "providers.v1" | |
15c0b25d AP |
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 | ||
42 | // NewClient returns a new initialized registry client. | |
43 | func NewClient(services *disco.Disco, client *http.Client) *Client { | |
44 | if services == nil { | |
45 | services = disco.New() | |
46 | } | |
47 | ||
48 | if client == nil { | |
49 | client = httpclient.New() | |
50 | client.Timeout = requestTimeout | |
51 | } | |
52 | ||
53 | services.Transport = client.Transport | |
54 | ||
55 | return &Client{ | |
56 | client: client, | |
57 | services: services, | |
58 | } | |
59 | } | |
60 | ||
61 | // Discover queries the host, and returns the url for the registry. | |
107c1cdb | 62 | func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) { |
15c0b25d AP |
63 | service, err := c.services.DiscoverServiceURL(host, serviceID) |
64 | if err != nil { | |
107c1cdb | 65 | return nil, &ServiceUnreachableError{err} |
15c0b25d AP |
66 | } |
67 | if !strings.HasSuffix(service.Path, "/") { | |
68 | service.Path += "/" | |
69 | } | |
70 | return service, nil | |
71 | } | |
72 | ||
107c1cdb ND |
73 | // ModuleVersions queries the registry for a module, and returns the available versions. |
74 | func (c *Client) ModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) { | |
15c0b25d AP |
75 | host, err := module.SvcHost() |
76 | if err != nil { | |
77 | return nil, err | |
78 | } | |
79 | ||
107c1cdb | 80 | service, err := c.Discover(host, modulesServiceID) |
15c0b25d AP |
81 | if err != nil { |
82 | return nil, err | |
83 | } | |
84 | ||
85 | p, err := url.Parse(path.Join(module.Module(), "versions")) | |
86 | if err != nil { | |
87 | return nil, err | |
88 | } | |
89 | ||
90 | service = service.ResolveReference(p) | |
91 | ||
92 | log.Printf("[DEBUG] fetching module versions from %q", service) | |
93 | ||
94 | req, err := http.NewRequest("GET", service.String(), nil) | |
95 | if err != nil { | |
96 | return nil, err | |
97 | } | |
98 | ||
99 | c.addRequestCreds(host, req) | |
100 | req.Header.Set(xTerraformVersion, tfVersion) | |
101 | ||
102 | resp, err := c.client.Do(req) | |
103 | if err != nil { | |
104 | return nil, err | |
105 | } | |
106 | defer resp.Body.Close() | |
107 | ||
108 | switch resp.StatusCode { | |
109 | case http.StatusOK: | |
110 | // OK | |
111 | case http.StatusNotFound: | |
112 | return nil, &errModuleNotFound{addr: module} | |
113 | default: | |
114 | return nil, fmt.Errorf("error looking up module versions: %s", resp.Status) | |
115 | } | |
116 | ||
117 | var versions response.ModuleVersions | |
118 | ||
119 | dec := json.NewDecoder(resp.Body) | |
120 | if err := dec.Decode(&versions); err != nil { | |
121 | return nil, err | |
122 | } | |
123 | ||
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) | |
127 | } | |
128 | } | |
129 | ||
130 | return &versions, nil | |
131 | } | |
132 | ||
133 | func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { | |
134 | creds, err := c.services.CredentialsForHost(host) | |
135 | if err != nil { | |
136 | log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) | |
137 | return | |
138 | } | |
139 | ||
140 | if creds != nil { | |
141 | creds.PrepareRequest(req) | |
142 | } | |
143 | } | |
144 | ||
107c1cdb | 145 | // ModuleLocation find the download location for a specific version module. |
15c0b25d | 146 | // This returns a string, because the final location may contain special go-getter syntax. |
107c1cdb | 147 | func (c *Client) ModuleLocation(module *regsrc.Module, version string) (string, error) { |
15c0b25d AP |
148 | host, err := module.SvcHost() |
149 | if err != nil { | |
150 | return "", err | |
151 | } | |
152 | ||
107c1cdb | 153 | service, err := c.Discover(host, modulesServiceID) |
15c0b25d AP |
154 | if err != nil { |
155 | return "", err | |
156 | } | |
157 | ||
158 | var p *url.URL | |
159 | if version == "" { | |
160 | p, err = url.Parse(path.Join(module.Module(), "download")) | |
161 | } else { | |
162 | p, err = url.Parse(path.Join(module.Module(), version, "download")) | |
163 | } | |
164 | if err != nil { | |
165 | return "", err | |
166 | } | |
167 | download := service.ResolveReference(p) | |
168 | ||
169 | log.Printf("[DEBUG] looking up module location from %q", download) | |
170 | ||
171 | req, err := http.NewRequest("GET", download.String(), nil) | |
172 | if err != nil { | |
173 | return "", err | |
174 | } | |
175 | ||
176 | c.addRequestCreds(host, req) | |
177 | req.Header.Set(xTerraformVersion, tfVersion) | |
178 | ||
179 | resp, err := c.client.Do(req) | |
180 | if err != nil { | |
181 | return "", err | |
182 | } | |
183 | defer resp.Body.Close() | |
184 | ||
185 | // there should be no body, but save it for logging | |
186 | body, err := ioutil.ReadAll(resp.Body) | |
187 | if err != nil { | |
188 | return "", fmt.Errorf("error reading response body from registry: %s", err) | |
189 | } | |
190 | ||
191 | switch resp.StatusCode { | |
192 | case http.StatusOK, http.StatusNoContent: | |
193 | // OK | |
194 | case http.StatusNotFound: | |
195 | return "", fmt.Errorf("module %q version %q not found", module, version) | |
196 | default: | |
197 | // anything else is an error: | |
198 | return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body) | |
199 | } | |
200 | ||
201 | // the download location is in the X-Terraform-Get header | |
202 | location := resp.Header.Get(xTerraformGet) | |
203 | if location == "" { | |
204 | return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body) | |
205 | } | |
206 | ||
207 | // If location looks like it's trying to be a relative URL, treat it as | |
208 | // one. | |
209 | // | |
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.) | |
213 | // | |
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) | |
220 | if err != nil { | |
221 | return "", fmt.Errorf("invalid relative URL for %q: %s", module, err) | |
222 | } | |
223 | locationURL = download.ResolveReference(locationURL) | |
224 | location = locationURL.String() | |
225 | } | |
226 | ||
227 | return location, nil | |
228 | } | |
107c1cdb ND |
229 | |
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() | |
233 | if err != nil { | |
234 | return nil, err | |
235 | } | |
236 | ||
237 | service, err := c.Discover(host, providersServiceID) | |
238 | if err != nil { | |
239 | return nil, err | |
240 | } | |
241 | ||
242 | p, err := url.Parse(path.Join(provider.TerraformProvider(), "versions")) | |
243 | if err != nil { | |
244 | return nil, err | |
245 | } | |
246 | ||
247 | service = service.ResolveReference(p) | |
248 | ||
249 | log.Printf("[DEBUG] fetching provider versions from %q", service) | |
250 | ||
251 | req, err := http.NewRequest("GET", service.String(), nil) | |
252 | if err != nil { | |
253 | return nil, err | |
254 | } | |
255 | ||
256 | c.addRequestCreds(host, req) | |
257 | req.Header.Set(xTerraformVersion, tfVersion) | |
258 | ||
259 | resp, err := c.client.Do(req) | |
260 | if err != nil { | |
261 | return nil, err | |
262 | } | |
263 | defer resp.Body.Close() | |
264 | ||
265 | switch resp.StatusCode { | |
266 | case http.StatusOK: | |
267 | // OK | |
268 | case http.StatusNotFound: | |
269 | return nil, &errProviderNotFound{addr: provider} | |
270 | default: | |
271 | return nil, fmt.Errorf("error looking up provider versions: %s", resp.Status) | |
272 | } | |
273 | ||
274 | var versions response.TerraformProviderVersions | |
275 | ||
276 | dec := json.NewDecoder(resp.Body) | |
277 | if err := dec.Decode(&versions); err != nil { | |
278 | return nil, err | |
279 | } | |
280 | ||
281 | return &versions, nil | |
282 | } | |
283 | ||
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() | |
287 | if err != nil { | |
288 | return nil, err | |
289 | } | |
290 | ||
291 | service, err := c.Discover(host, providersServiceID) | |
292 | if err != nil { | |
293 | return nil, err | |
294 | } | |
295 | ||
296 | p, err := url.Parse(path.Join( | |
297 | provider.TerraformProvider(), | |
298 | version, | |
299 | "download", | |
300 | provider.OS, | |
301 | provider.Arch, | |
302 | )) | |
303 | if err != nil { | |
304 | return nil, err | |
305 | } | |
306 | ||
307 | service = service.ResolveReference(p) | |
308 | ||
309 | log.Printf("[DEBUG] fetching provider location from %q", service) | |
310 | ||
311 | req, err := http.NewRequest("GET", service.String(), nil) | |
312 | if err != nil { | |
313 | return nil, err | |
314 | } | |
315 | ||
316 | c.addRequestCreds(host, req) | |
317 | req.Header.Set(xTerraformVersion, tfVersion) | |
318 | ||
319 | resp, err := c.client.Do(req) | |
320 | if err != nil { | |
321 | return nil, err | |
322 | } | |
323 | defer resp.Body.Close() | |
324 | ||
325 | var loc response.TerraformProviderPlatformLocation | |
326 | ||
327 | dec := json.NewDecoder(resp.Body) | |
328 | if err := dec.Decode(&loc); err != nil { | |
329 | return nil, err | |
330 | } | |
331 | ||
332 | switch resp.StatusCode { | |
333 | case http.StatusOK, http.StatusNoContent: | |
334 | // OK | |
335 | case http.StatusNotFound: | |
336 | return nil, fmt.Errorf("provider %q version %q not found", provider.TerraformProvider(), version) | |
337 | default: | |
338 | // anything else is an error: | |
339 | return nil, fmt.Errorf("error getting download location for %q: %s", provider.TerraformProvider(), resp.Status) | |
340 | } | |
341 | ||
342 | return &loc, nil | |
343 | } |