aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/plugin/discovery/get.go')
-rw-r--r--vendor/github.com/hashicorp/terraform/plugin/discovery/get.go424
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 @@
1package discovery
2
3import (
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
29const protocolVersionHeader = "x-terraform-protocol-version"
30
31var releaseHost = "https://releases.hashicorp.com"
32
33var httpClient = cleanhttp.DefaultClient()
34
35// An Installer maintains a local cache of plugins by downloading plugins
36// from an online repository.
37type 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.
47type 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.
82func (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
167func (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>-
201func (i *ProviderInstaller) providerName(name string) string {
202 return "terraform-provider-" + name
203}
204
205func (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/
219func (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>
226func (i *ProviderInstaller) providerURL(name, version string) string {
227 return fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, i.providerFileName(name, version))
228}
229
230func (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
236func (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.
248func 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
278func (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
288var 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.
292func 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
305func 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
358func 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
377func 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.
388func 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
408func 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}