diff options
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/plugin/discovery/get.go')
-rw-r--r-- | vendor/github.com/hashicorp/terraform/plugin/discovery/get.go | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go b/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go new file mode 100644 index 0000000..241b5cb --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go | |||
@@ -0,0 +1,424 @@ | |||
1 | package discovery | ||
2 | |||
3 | import ( | ||
4 | "errors" | ||
5 | "fmt" | ||
6 | "io/ioutil" | ||
7 | "log" | ||
8 | "net/http" | ||
9 | "os" | ||
10 | "runtime" | ||
11 | "strconv" | ||
12 | "strings" | ||
13 | |||
14 | "golang.org/x/net/html" | ||
15 | |||
16 | cleanhttp "github.com/hashicorp/go-cleanhttp" | ||
17 | getter "github.com/hashicorp/go-getter" | ||
18 | multierror "github.com/hashicorp/go-multierror" | ||
19 | ) | ||
20 | |||
21 | // Releases are located by parsing the html listing from releases.hashicorp.com. | ||
22 | // | ||
23 | // The URL for releases follows the pattern: | ||
24 | // https://releases.hashicorp.com/terraform-provider-name/<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext> | ||
25 | // | ||
26 | // The plugin protocol version will be saved with the release and returned in | ||
27 | // the header X-TERRAFORM_PROTOCOL_VERSION. | ||
28 | |||
29 | const protocolVersionHeader = "x-terraform-protocol-version" | ||
30 | |||
31 | var releaseHost = "https://releases.hashicorp.com" | ||
32 | |||
33 | var httpClient = cleanhttp.DefaultClient() | ||
34 | |||
35 | // An Installer maintains a local cache of plugins by downloading plugins | ||
36 | // from an online repository. | ||
37 | type Installer interface { | ||
38 | Get(name string, req Constraints) (PluginMeta, error) | ||
39 | PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error) | ||
40 | } | ||
41 | |||
42 | // ProviderInstaller is an Installer implementation that knows how to | ||
43 | // download Terraform providers from the official HashiCorp releases service | ||
44 | // into a local directory. The files downloaded are compliant with the | ||
45 | // naming scheme expected by FindPlugins, so the target directory of a | ||
46 | // provider installer can be used as one of several plugin discovery sources. | ||
47 | type ProviderInstaller struct { | ||
48 | Dir string | ||
49 | |||
50 | PluginProtocolVersion uint | ||
51 | |||
52 | // OS and Arch specify the OS and architecture that should be used when | ||
53 | // installing plugins. These use the same labels as the runtime.GOOS and | ||
54 | // runtime.GOARCH variables respectively, and indeed the values of these | ||
55 | // are used as defaults if either of these is the empty string. | ||
56 | OS string | ||
57 | Arch string | ||
58 | |||
59 | // Skip checksum and signature verification | ||
60 | SkipVerify bool | ||
61 | } | ||
62 | |||
63 | // Get is part of an implementation of type Installer, and attempts to download | ||
64 | // and install a Terraform provider matching the given constraints. | ||
65 | // | ||
66 | // This method may return one of a number of sentinel errors from this | ||
67 | // package to indicate issues that are likely to be resolvable via user action: | ||
68 | // | ||
69 | // ErrorNoSuchProvider: no provider with the given name exists in the repository. | ||
70 | // ErrorNoSuitableVersion: the provider exists but no available version matches constraints. | ||
71 | // ErrorNoVersionCompatible: a plugin was found within the constraints but it is | ||
72 | // incompatible with the current Terraform version. | ||
73 | // | ||
74 | // These errors should be recognized and handled as special cases by the caller | ||
75 | // to present a suitable user-oriented error message. | ||
76 | // | ||
77 | // All other errors indicate an internal problem that is likely _not_ solvable | ||
78 | // through user action, or at least not within Terraform's scope. Error messages | ||
79 | // are produced under the assumption that if presented to the user they will | ||
80 | // be presented alongside context about what is being installed, and thus the | ||
81 | // error messages do not redundantly include such information. | ||
82 | func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) { | ||
83 | versions, err := i.listProviderVersions(provider) | ||
84 | // TODO: return multiple errors | ||
85 | if err != nil { | ||
86 | return PluginMeta{}, err | ||
87 | } | ||
88 | |||
89 | if len(versions) == 0 { | ||
90 | return PluginMeta{}, ErrorNoSuitableVersion | ||
91 | } | ||
92 | |||
93 | versions = allowedVersions(versions, req) | ||
94 | if len(versions) == 0 { | ||
95 | return PluginMeta{}, ErrorNoSuitableVersion | ||
96 | } | ||
97 | |||
98 | // sort them newest to oldest | ||
99 | Versions(versions).Sort() | ||
100 | |||
101 | // take the first matching plugin we find | ||
102 | for _, v := range versions { | ||
103 | url := i.providerURL(provider, v.String()) | ||
104 | |||
105 | if !i.SkipVerify { | ||
106 | sha256, err := i.getProviderChecksum(provider, v.String()) | ||
107 | if err != nil { | ||
108 | return PluginMeta{}, err | ||
109 | } | ||
110 | |||
111 | // add the checksum parameter for go-getter to verify the download for us. | ||
112 | if sha256 != "" { | ||
113 | url = url + "?checksum=sha256:" + sha256 | ||
114 | } | ||
115 | } | ||
116 | |||
117 | log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v) | ||
118 | if checkPlugin(url, i.PluginProtocolVersion) { | ||
119 | log.Printf("[DEBUG] getting provider %q version %q at %s", provider, v, url) | ||
120 | err := getter.Get(i.Dir, url) | ||
121 | if err != nil { | ||
122 | return PluginMeta{}, err | ||
123 | } | ||
124 | |||
125 | // Find what we just installed | ||
126 | // (This is weird, because go-getter doesn't directly return | ||
127 | // information about what was extracted, and we just extracted | ||
128 | // the archive directly into a shared dir here.) | ||
129 | log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, v) | ||
130 | metas := FindPlugins("provider", []string{i.Dir}) | ||
131 | log.Printf("[DEBUG] all plugins found %#v", metas) | ||
132 | metas, _ = metas.ValidateVersions() | ||
133 | metas = metas.WithName(provider).WithVersion(v) | ||
134 | log.Printf("[DEBUG] filtered plugins %#v", metas) | ||
135 | if metas.Count() == 0 { | ||
136 | // This should never happen. Suggests that the release archive | ||
137 | // contains an executable file whose name doesn't match the | ||
138 | // expected convention. | ||
139 | return PluginMeta{}, fmt.Errorf( | ||
140 | "failed to find installed plugin version %s; this is a bug in Terraform and should be reported", | ||
141 | v, | ||
142 | ) | ||
143 | } | ||
144 | |||
145 | if metas.Count() > 1 { | ||
146 | // This should also never happen, and suggests that a | ||
147 | // particular version was re-released with a different | ||
148 | // executable filename. We consider releases as immutable, so | ||
149 | // this is an error. | ||
150 | return PluginMeta{}, fmt.Errorf( | ||
151 | "multiple plugins installed for version %s; this is a bug in Terraform and should be reported", | ||
152 | v, | ||
153 | ) | ||
154 | } | ||
155 | |||
156 | // By now we know we have exactly one meta, and so "Newest" will | ||
157 | // return that one. | ||
158 | return metas.Newest(), nil | ||
159 | } | ||
160 | |||
161 | log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v) | ||
162 | } | ||
163 | |||
164 | return PluginMeta{}, ErrorNoVersionCompatible | ||
165 | } | ||
166 | |||
167 | func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) { | ||
168 | purge := make(PluginMetaSet) | ||
169 | |||
170 | present := FindPlugins("provider", []string{i.Dir}) | ||
171 | for meta := range present { | ||
172 | chosen, ok := used[meta.Name] | ||
173 | if !ok { | ||
174 | purge.Add(meta) | ||
175 | } | ||
176 | if chosen.Path != meta.Path { | ||
177 | purge.Add(meta) | ||
178 | } | ||
179 | } | ||
180 | |||
181 | removed := make(PluginMetaSet) | ||
182 | var errs error | ||
183 | for meta := range purge { | ||
184 | path := meta.Path | ||
185 | err := os.Remove(path) | ||
186 | if err != nil { | ||
187 | errs = multierror.Append(errs, fmt.Errorf( | ||
188 | "failed to remove unused provider plugin %s: %s", | ||
189 | path, err, | ||
190 | )) | ||
191 | } else { | ||
192 | removed.Add(meta) | ||
193 | } | ||
194 | } | ||
195 | |||
196 | return removed, errs | ||
197 | } | ||
198 | |||
199 | // Plugins are referred to by the short name, but all URLs and files will use | ||
200 | // the full name prefixed with terraform-<plugin_type>- | ||
201 | func (i *ProviderInstaller) providerName(name string) string { | ||
202 | return "terraform-provider-" + name | ||
203 | } | ||
204 | |||
205 | func (i *ProviderInstaller) providerFileName(name, version string) string { | ||
206 | os := i.OS | ||
207 | arch := i.Arch | ||
208 | if os == "" { | ||
209 | os = runtime.GOOS | ||
210 | } | ||
211 | if arch == "" { | ||
212 | arch = runtime.GOARCH | ||
213 | } | ||
214 | return fmt.Sprintf("%s_%s_%s_%s.zip", i.providerName(name), version, os, arch) | ||
215 | } | ||
216 | |||
217 | // providerVersionsURL returns the path to the released versions directory for the provider: | ||
218 | // https://releases.hashicorp.com/terraform-provider-name/ | ||
219 | func (i *ProviderInstaller) providerVersionsURL(name string) string { | ||
220 | return releaseHost + "/" + i.providerName(name) + "/" | ||
221 | } | ||
222 | |||
223 | // providerURL returns the full path to the provider file, using the current OS | ||
224 | // and ARCH: | ||
225 | // .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext> | ||
226 | func (i *ProviderInstaller) providerURL(name, version string) string { | ||
227 | return fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, i.providerFileName(name, version)) | ||
228 | } | ||
229 | |||
230 | func (i *ProviderInstaller) providerChecksumURL(name, version string) string { | ||
231 | fileName := fmt.Sprintf("%s_%s_SHA256SUMS", i.providerName(name), version) | ||
232 | u := fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, fileName) | ||
233 | return u | ||
234 | } | ||
235 | |||
236 | func (i *ProviderInstaller) getProviderChecksum(name, version string) (string, error) { | ||
237 | checksums, err := getPluginSHA256SUMs(i.providerChecksumURL(name, version)) | ||
238 | if err != nil { | ||
239 | return "", err | ||
240 | } | ||
241 | |||
242 | return checksumForFile(checksums, i.providerFileName(name, version)), nil | ||
243 | } | ||
244 | |||
245 | // Return the plugin version by making a HEAD request to the provided url. | ||
246 | // If the header is not present, we assume the latest version will be | ||
247 | // compatible, and leave the check for discovery or execution. | ||
248 | func checkPlugin(url string, pluginProtocolVersion uint) bool { | ||
249 | resp, err := httpClient.Head(url) | ||
250 | if err != nil { | ||
251 | log.Printf("[ERROR] error fetching plugin headers: %s", err) | ||
252 | return false | ||
253 | } | ||
254 | |||
255 | if resp.StatusCode != http.StatusOK { | ||
256 | log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status) | ||
257 | return false | ||
258 | } | ||
259 | |||
260 | proto := resp.Header.Get(protocolVersionHeader) | ||
261 | if proto == "" { | ||
262 | // The header isn't present, but we don't make this error fatal since | ||
263 | // the latest version will probably work. | ||
264 | log.Printf("[WARNING] missing %s from: %s", protocolVersionHeader, url) | ||
265 | return true | ||
266 | } | ||
267 | |||
268 | protoVersion, err := strconv.Atoi(proto) | ||
269 | if err != nil { | ||
270 | log.Printf("[ERROR] invalid ProtocolVersion: %s", proto) | ||
271 | return false | ||
272 | } | ||
273 | |||
274 | return protoVersion == int(pluginProtocolVersion) | ||
275 | } | ||
276 | |||
277 | // list the version available for the named plugin | ||
278 | func (i *ProviderInstaller) listProviderVersions(name string) ([]Version, error) { | ||
279 | versions, err := listPluginVersions(i.providerVersionsURL(name)) | ||
280 | if err != nil { | ||
281 | // listPluginVersions returns a verbose error message indicating | ||
282 | // what was being accessed and what failed | ||
283 | return nil, err | ||
284 | } | ||
285 | return versions, nil | ||
286 | } | ||
287 | |||
288 | var errVersionNotFound = errors.New("version not found") | ||
289 | |||
290 | // take the list of available versions for a plugin, and filter out those that | ||
291 | // don't fit the constraints. | ||
292 | func allowedVersions(available []Version, required Constraints) []Version { | ||
293 | var allowed []Version | ||
294 | |||
295 | for _, v := range available { | ||
296 | if required.Allows(v) { | ||
297 | allowed = append(allowed, v) | ||
298 | } | ||
299 | } | ||
300 | |||
301 | return allowed | ||
302 | } | ||
303 | |||
304 | // return a list of the plugin versions at the given URL | ||
305 | func listPluginVersions(url string) ([]Version, error) { | ||
306 | resp, err := httpClient.Get(url) | ||
307 | if err != nil { | ||
308 | // http library produces a verbose error message that includes the | ||
309 | // URL being accessed, etc. | ||
310 | return nil, err | ||
311 | } | ||
312 | defer resp.Body.Close() | ||
313 | |||
314 | if resp.StatusCode != http.StatusOK { | ||
315 | body, _ := ioutil.ReadAll(resp.Body) | ||
316 | log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body) | ||
317 | |||
318 | switch resp.StatusCode { | ||
319 | case http.StatusNotFound, http.StatusForbidden: | ||
320 | // These are treated as indicative of the given name not being | ||
321 | // a valid provider name at all. | ||
322 | return nil, ErrorNoSuchProvider | ||
323 | |||
324 | default: | ||
325 | // All other errors are assumed to be operational problems. | ||
326 | return nil, fmt.Errorf("error accessing %s: %s", url, resp.Status) | ||
327 | } | ||
328 | |||
329 | } | ||
330 | |||
331 | body, err := html.Parse(resp.Body) | ||
332 | if err != nil { | ||
333 | log.Fatal(err) | ||
334 | } | ||
335 | |||
336 | names := []string{} | ||
337 | |||
338 | // all we need to do is list links on the directory listing page that look like plugins | ||
339 | var f func(*html.Node) | ||
340 | f = func(n *html.Node) { | ||
341 | if n.Type == html.ElementNode && n.Data == "a" { | ||
342 | c := n.FirstChild | ||
343 | if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") { | ||
344 | names = append(names, c.Data) | ||
345 | return | ||
346 | } | ||
347 | } | ||
348 | for c := n.FirstChild; c != nil; c = c.NextSibling { | ||
349 | f(c) | ||
350 | } | ||
351 | } | ||
352 | f(body) | ||
353 | |||
354 | return versionsFromNames(names), nil | ||
355 | } | ||
356 | |||
357 | // parse the list of directory names into a sorted list of available versions | ||
358 | func versionsFromNames(names []string) []Version { | ||
359 | var versions []Version | ||
360 | for _, name := range names { | ||
361 | parts := strings.SplitN(name, "_", 2) | ||
362 | if len(parts) == 2 && parts[1] != "" { | ||
363 | v, err := VersionStr(parts[1]).Parse() | ||
364 | if err != nil { | ||
365 | // filter invalid versions scraped from the page | ||
366 | log.Printf("[WARN] invalid version found for %q: %s", name, err) | ||
367 | continue | ||
368 | } | ||
369 | |||
370 | versions = append(versions, v) | ||
371 | } | ||
372 | } | ||
373 | |||
374 | return versions | ||
375 | } | ||
376 | |||
377 | func checksumForFile(sums []byte, name string) string { | ||
378 | for _, line := range strings.Split(string(sums), "\n") { | ||
379 | parts := strings.Fields(line) | ||
380 | if len(parts) > 1 && parts[1] == name { | ||
381 | return parts[0] | ||
382 | } | ||
383 | } | ||
384 | return "" | ||
385 | } | ||
386 | |||
387 | // fetch the SHA256SUMS file provided, and verify its signature. | ||
388 | func getPluginSHA256SUMs(sumsURL string) ([]byte, error) { | ||
389 | sigURL := sumsURL + ".sig" | ||
390 | |||
391 | sums, err := getFile(sumsURL) | ||
392 | if err != nil { | ||
393 | return nil, fmt.Errorf("error fetching checksums: %s", err) | ||
394 | } | ||
395 | |||
396 | sig, err := getFile(sigURL) | ||
397 | if err != nil { | ||
398 | return nil, fmt.Errorf("error fetching checksums signature: %s", err) | ||
399 | } | ||
400 | |||
401 | if err := verifySig(sums, sig); err != nil { | ||
402 | return nil, err | ||
403 | } | ||
404 | |||
405 | return sums, nil | ||
406 | } | ||
407 | |||
408 | func getFile(url string) ([]byte, error) { | ||
409 | resp, err := httpClient.Get(url) | ||
410 | if err != nil { | ||
411 | return nil, err | ||
412 | } | ||
413 | defer resp.Body.Close() | ||
414 | |||
415 | if resp.StatusCode != http.StatusOK { | ||
416 | return nil, fmt.Errorf("%s", resp.Status) | ||
417 | } | ||
418 | |||
419 | data, err := ioutil.ReadAll(resp.Body) | ||
420 | if err != nil { | ||
421 | return data, err | ||
422 | } | ||
423 | return data, nil | ||
424 | } | ||