aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/plugin/discovery/get.go
blob: c1d9e3bed667cc2c804992715966d21353d6d77a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
package discovery

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/hashicorp/errwrap"
	getter "github.com/hashicorp/go-getter"
	multierror "github.com/hashicorp/go-multierror"
	"github.com/hashicorp/terraform/httpclient"
	"github.com/hashicorp/terraform/registry"
	"github.com/hashicorp/terraform/registry/regsrc"
	"github.com/hashicorp/terraform/registry/response"
	"github.com/hashicorp/terraform/svchost/disco"
	"github.com/hashicorp/terraform/tfdiags"
	tfversion "github.com/hashicorp/terraform/version"
	"github.com/mitchellh/cli"
)

// Releases are located by querying the terraform registry.

const protocolVersionHeader = "x-terraform-protocol-version"

var httpClient *http.Client

var errVersionNotFound = errors.New("version not found")

func init() {
	httpClient = httpclient.New()

	httpGetter := &getter.HttpGetter{
		Client: httpClient,
		Netrc:  true,
	}

	getter.Getters["http"] = httpGetter
	getter.Getters["https"] = httpGetter
}

// An Installer maintains a local cache of plugins by downloading plugins
// from an online repository.
type Installer interface {
	Get(name string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error)
	PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
}

// ProviderInstaller is an Installer implementation that knows how to
// download Terraform providers from the official HashiCorp releases service
// into a local directory. The files downloaded are compliant with the
// naming scheme expected by FindPlugins, so the target directory of a
// provider installer can be used as one of several plugin discovery sources.
type ProviderInstaller struct {
	Dir string

	// Cache is used to access and update a local cache of plugins if non-nil.
	// Can be nil to disable caching.
	Cache PluginCache

	PluginProtocolVersion uint

	// OS and Arch specify the OS and architecture that should be used when
	// installing plugins. These use the same labels as the runtime.GOOS and
	// runtime.GOARCH variables respectively, and indeed the values of these
	// are used as defaults if either of these is the empty string.
	OS   string
	Arch string

	// Skip checksum and signature verification
	SkipVerify bool

	Ui cli.Ui // Ui for output

	// Services is a required *disco.Disco, which may have services and
	// credentials pre-loaded.
	Services *disco.Disco

	// registry client
	registry *registry.Client
}

// Get is part of an implementation of type Installer, and attempts to download
// and install a Terraform provider matching the given constraints.
//
// This method may return one of a number of sentinel errors from this
// package to indicate issues that are likely to be resolvable via user action:
//
//     ErrorNoSuchProvider: no provider with the given name exists in the repository.
//     ErrorNoSuitableVersion: the provider exists but no available version matches constraints.
//     ErrorNoVersionCompatible: a plugin was found within the constraints but it is
//                               incompatible with the current Terraform version.
//
// These errors should be recognized and handled as special cases by the caller
// to present a suitable user-oriented error message.
//
// All other errors indicate an internal problem that is likely _not_ solvable
// through user action, or at least not within Terraform's scope. Error messages
// are produced under the assumption that if presented to the user they will
// be presented alongside context about what is being installed, and thus the
// error messages do not redundantly include such information.
func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error) {
	var diags tfdiags.Diagnostics

	// a little bit of initialization.
	if i.OS == "" {
		i.OS = runtime.GOOS
	}
	if i.Arch == "" {
		i.Arch = runtime.GOARCH
	}
	if i.registry == nil {
		i.registry = registry.NewClient(i.Services, nil)
	}

	// get a full listing of versions for the requested provider
	allVersions, err := i.listProviderVersions(provider)

	// TODO: return multiple errors
	if err != nil {
		log.Printf("[DEBUG] %s", err)
		if registry.IsServiceUnreachable(err) {
			registryHost, err := i.hostname()
			if err == nil && registryHost == regsrc.PublicRegistryHost.Raw {
				return PluginMeta{}, diags, ErrorPublicRegistryUnreachable
			}
			return PluginMeta{}, diags, ErrorServiceUnreachable
		}
		if registry.IsServiceNotProvided(err) {
			return PluginMeta{}, diags, err
		}
		return PluginMeta{}, diags, ErrorNoSuchProvider
	}

	// Add any warnings from the response to diags
	for _, warning := range allVersions.Warnings {
		hostname, err := i.hostname()
		if err != nil {
			return PluginMeta{}, diags, err
		}
		diag := tfdiags.SimpleWarning(fmt.Sprintf("%s: %s", hostname, warning))
		diags = diags.Append(diag)
	}

	if len(allVersions.Versions) == 0 {
		return PluginMeta{}, diags, ErrorNoSuitableVersion
	}
	providerSource := allVersions.ID

	// Filter the list of plugin versions to those which meet the version constraints
	versions := allowedVersions(allVersions, req)
	if len(versions) == 0 {
		return PluginMeta{}, diags, ErrorNoSuitableVersion
	}

	// sort them newest to oldest. The newest version wins!
	response.ProviderVersionCollection(versions).Sort()

	// if the chosen provider version does not support the requested platform,
	// filter the list of acceptable versions to those that support that platform
	if err := i.checkPlatformCompatibility(versions[0]); err != nil {
		versions = i.platformCompatibleVersions(versions)
		if len(versions) == 0 {
			return PluginMeta{}, diags, ErrorNoVersionCompatibleWithPlatform
		}
	}

	// we now have a winning platform-compatible version
	versionMeta := versions[0]
	v := VersionStr(versionMeta.Version).MustParse()

	// check protocol compatibility
	if err := i.checkPluginProtocol(versionMeta); err != nil {
		closestMatch, err := i.findClosestProtocolCompatibleVersion(allVersions.Versions)
		if err != nil {
			// No operation here if we can't find a version with compatible protocol
			return PluginMeta{}, diags, err
		}

		// Prompt version suggestion to UI based on closest protocol match
		var errMsg string
		closestVersion := VersionStr(closestMatch.Version).MustParse()
		if v.NewerThan(closestVersion) {
			errMsg = providerProtocolTooNew
		} else {
			errMsg = providerProtocolTooOld
		}

		constraintStr := req.String()
		if constraintStr == "" {
			constraintStr = "(any version)"
		}

		return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
			errMsg, provider, v.String(), tfversion.String(),
			closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
	}

	downloadURLs, err := i.listProviderDownloadURLs(providerSource, versionMeta.Version)
	if err != nil {
		return PluginMeta{}, diags, err
	}
	providerURL := downloadURLs.DownloadURL

	if !i.SkipVerify {
		// Terraform verifies the integrity of a provider release before downloading
		// the plugin binary. The digital signature (SHA256SUMS.sig) on the
		// release distribution (SHA256SUMS) is verified with the public key of the
		// publisher provided in the Terraform Registry response, ensuring that
		// everything is as intended by the publisher. The checksum of the provider
		// plugin is expected in the SHA256SUMS file and is double checked to match
		// the checksum of the original published release to the Registry. This
		// enforces immutability of releases between the Registry and the plugin's
		// host location. Lastly, the integrity of the binary is verified upon
		// download matches the Registry and signed checksum.
		sha256, err := i.getProviderChecksum(downloadURLs)
		if err != nil {
			return PluginMeta{}, diags, err
		}

		// add the checksum parameter for go-getter to verify the download for us.
		if sha256 != "" {
			providerURL = providerURL + "?checksum=sha256:" + sha256
		}
	}

	printedProviderName := fmt.Sprintf("%q (%s)", provider, providerSource)
	i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %s %s...", printedProviderName, versionMeta.Version))
	log.Printf("[DEBUG] getting provider %s version %q", printedProviderName, versionMeta.Version)
	err = i.install(provider, v, providerURL)
	if err != nil {
		return PluginMeta{}, diags, err
	}

	// Find what we just installed
	// (This is weird, because go-getter doesn't directly return
	//  information about what was extracted, and we just extracted
	//  the archive directly into a shared dir here.)
	log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, versionMeta.Version)
	metas := FindPlugins("provider", []string{i.Dir})
	log.Printf("[DEBUG] all plugins found %#v", metas)
	metas, _ = metas.ValidateVersions()
	metas = metas.WithName(provider).WithVersion(v)
	log.Printf("[DEBUG] filtered plugins %#v", metas)
	if metas.Count() == 0 {
		// This should never happen. Suggests that the release archive
		// contains an executable file whose name doesn't match the
		// expected convention.
		return PluginMeta{}, diags, fmt.Errorf(
			"failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
			versionMeta.Version,
		)
	}

	if metas.Count() > 1 {
		// This should also never happen, and suggests that a
		// particular version was re-released with a different
		// executable filename. We consider releases as immutable, so
		// this is an error.
		return PluginMeta{}, diags, fmt.Errorf(
			"multiple plugins installed for version %s; this is a bug in Terraform and should be reported",
			versionMeta.Version,
		)
	}

	// By now we know we have exactly one meta, and so "Newest" will
	// return that one.
	return metas.Newest(), diags, nil
}

func (i *ProviderInstaller) install(provider string, version Version, url string) error {
	if i.Cache != nil {
		log.Printf("[DEBUG] looking for provider %s %s in plugin cache", provider, version)
		cached := i.Cache.CachedPluginPath("provider", provider, version)
		if cached == "" {
			log.Printf("[DEBUG] %s %s not yet in cache, so downloading %s", provider, version, url)
			err := getter.Get(i.Cache.InstallDir(), url)
			if err != nil {
				return err
			}
			// should now be in cache
			cached = i.Cache.CachedPluginPath("provider", provider, version)
			if cached == "" {
				// should never happen if the getter is behaving properly
				// and the plugins are packaged properly.
				return fmt.Errorf("failed to find downloaded plugin in cache %s", i.Cache.InstallDir())
			}
		}

		// Link or copy the cached binary into our install dir so the
		// normal resolution machinery can find it.
		filename := filepath.Base(cached)
		targetPath := filepath.Join(i.Dir, filename)
		// check if the target dir exists, and create it if not
		var err error
		if _, StatErr := os.Stat(i.Dir); os.IsNotExist(StatErr) {
			err = os.MkdirAll(i.Dir, 0700)
		}
		if err != nil {
			return err
		}

		log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached)

		// Delete if we can. If there's nothing there already then no harm done.
		// This is important because we can't create a link if there's
		// already a file of the same name present.
		// (any other error here we'll catch below when we try to write here)
		os.Remove(targetPath)

		// We don't attempt linking on Windows because links are not
		// comprehensively supported by all tools/apps in Windows and
		// so we choose to be conservative to avoid creating any
		// weird issues for Windows users.
		linkErr := errors.New("link not supported for Windows") // placeholder error, never actually returned
		if runtime.GOOS != "windows" {
			// Try hard linking first. Hard links are preferable because this
			// creates a self-contained directory that doesn't depend on the
			// cache after install.
			linkErr = os.Link(cached, targetPath)

			// If that failed, try a symlink. This _does_ depend on the cache
			// after install, so the user must manage the cache more carefully
			// in this case, but avoids creating redundant copies of the
			// plugins on disk.
			if linkErr != nil {
				linkErr = os.Symlink(cached, targetPath)
			}
		}

		// If we still have an error then we'll try a copy as a fallback.
		// In this case either the OS is Windows or the target filesystem
		// can't support symlinks.
		if linkErr != nil {
			srcFile, err := os.Open(cached)
			if err != nil {
				return fmt.Errorf("failed to open cached plugin %s: %s", cached, err)
			}
			defer srcFile.Close()

			destFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
			if err != nil {
				return fmt.Errorf("failed to create %s: %s", targetPath, err)
			}

			_, err = io.Copy(destFile, srcFile)
			if err != nil {
				destFile.Close()
				return fmt.Errorf("failed to copy cached plugin from %s to %s: %s", cached, targetPath, err)
			}

			err = destFile.Close()
			if err != nil {
				return fmt.Errorf("error creating %s: %s", targetPath, err)
			}
		}

		// One way or another, by the time we get here we should have either
		// a link or a copy of the cached plugin within i.Dir, as expected.
	} else {
		log.Printf("[DEBUG] plugin cache is disabled, so downloading %s %s from %s", provider, version, url)
		err := getter.Get(i.Dir, url)
		if err != nil {
			return err
		}
	}
	return nil
}

func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) {
	purge := make(PluginMetaSet)

	present := FindPlugins("provider", []string{i.Dir})
	for meta := range present {
		chosen, ok := used[meta.Name]
		if !ok {
			purge.Add(meta)
		}
		if chosen.Path != meta.Path {
			purge.Add(meta)
		}
	}

	removed := make(PluginMetaSet)
	var errs error
	for meta := range purge {
		path := meta.Path
		err := os.Remove(path)
		if err != nil {
			errs = multierror.Append(errs, fmt.Errorf(
				"failed to remove unused provider plugin %s: %s",
				path, err,
			))
		} else {
			removed.Add(meta)
		}
	}

	return removed, errs
}

func (i *ProviderInstaller) getProviderChecksum(resp *response.TerraformProviderPlatformLocation) (string, error) {
	// Get SHA256SUMS file.
	shasums, err := getFile(resp.ShasumsURL)
	if err != nil {
		log.Printf("[ERROR] error fetching checksums from %q: %s", resp.ShasumsURL, err)
		return "", ErrorMissingChecksumVerification
	}

	// Get SHA256SUMS.sig file.
	signature, err := getFile(resp.ShasumsSignatureURL)
	if err != nil {
		log.Printf("[ERROR] error fetching checksums signature from %q: %s", resp.ShasumsSignatureURL, err)
		return "", ErrorSignatureVerification
	}

	// Verify the GPG signature returned from the Registry.
	asciiArmor := resp.SigningKeys.GPGASCIIArmor()
	signer, err := verifySig(shasums, signature, asciiArmor)
	if err != nil {
		log.Printf("[ERROR] error verifying signature: %s", err)
		return "", ErrorSignatureVerification
	}

	// Also verify the GPG signature against the HashiCorp public key. This is
	// a temporary additional check until a more robust key verification
	// process is added in a future release.
	_, err = verifySig(shasums, signature, HashicorpPublicKey)
	if err != nil {
		log.Printf("[ERROR] error verifying signature against HashiCorp public key: %s", err)
		return "", ErrorSignatureVerification
	}

	// Display identity for GPG key which succeeded verifying the signature.
	// This could also be used to display to the user with i.Ui.Info().
	identities := []string{}
	for k := range signer.Identities {
		identities = append(identities, k)
	}
	identity := strings.Join(identities, ", ")
	log.Printf("[DEBUG] verified GPG signature with key from %s", identity)

	// Extract checksum for this os/arch platform binary and verify against Registry
	checksum := checksumForFile(shasums, resp.Filename)
	if checksum == "" {
		log.Printf("[ERROR] missing checksum for %s from source %s", resp.Filename, resp.ShasumsURL)
		return "", ErrorMissingChecksumVerification
	} else if checksum != resp.Shasum {
		log.Printf("[ERROR] unexpected checksum for %s from source %q", resp.Filename, resp.ShasumsURL)
		return "", ErrorChecksumVerification
	}

	return checksum, nil
}

func (i *ProviderInstaller) hostname() (string, error) {
	provider := regsrc.NewTerraformProvider("", i.OS, i.Arch)
	svchost, err := provider.SvcHost()
	if err != nil {
		return "", err
	}

	return svchost.ForDisplay(), nil
}

// list all versions available for the named provider
func (i *ProviderInstaller) listProviderVersions(name string) (*response.TerraformProviderVersions, error) {
	provider := regsrc.NewTerraformProvider(name, i.OS, i.Arch)
	versions, err := i.registry.TerraformProviderVersions(provider)
	return versions, err
}

func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) {
	urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version)
	if urls == nil {
		return nil, fmt.Errorf("No download urls found for provider %s", name)
	}
	return urls, err
}

// findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
// Prerelease versions are filtered.
func (i *ProviderInstaller) findClosestProtocolCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
	// Loop through all the provider versions to find the earliest and latest
	// versions that match the installer protocol to then select the closest of the two
	var latest, earliest *response.TerraformProviderVersion
	for _, version := range versions {
		// Prereleases are filtered and will not be suggested
		v, err := VersionStr(version.Version).Parse()
		if err != nil || v.IsPrerelease() {
			continue
		}

		if err := i.checkPluginProtocol(version); err == nil {
			if earliest == nil {
				// Found the first provider version with compatible protocol
				earliest = version
			}
			// Update the latest protocol compatible version
			latest = version
		}
	}
	if earliest == nil {
		// No compatible protocol was found for any version
		return nil, ErrorNoVersionCompatible
	}

	// Convert protocols to comparable types
	protoString := strconv.Itoa(int(i.PluginProtocolVersion))
	protocolVersion, err := VersionStr(protoString).Parse()
	if err != nil {
		return nil, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
	}

	earliestVersionProtocol, err := VersionStr(earliest.Protocols[0]).Parse()
	if err != nil {
		return nil, err
	}

	// Compare installer protocol version with the first protocol listed of the earliest match
	// [A, B] where A is assumed the earliest compatible major version of the protocol pair
	if protocolVersion.NewerThan(earliestVersionProtocol) {
		// Provider protocols are too old, the closest version is the earliest compatible version
		return earliest, nil
	}

	// Provider protocols are too new, the closest version is the latest compatible version
	return latest, nil
}

func (i *ProviderInstaller) checkPluginProtocol(versionMeta *response.TerraformProviderVersion) error {
	// TODO: should this be a different error? We should probably differentiate between
	// no compatible versions and no protocol versions listed at all
	if len(versionMeta.Protocols) == 0 {
		return fmt.Errorf("no plugin protocol versions listed")
	}

	protoString := strconv.Itoa(int(i.PluginProtocolVersion))
	protocolVersion, err := VersionStr(protoString).Parse()
	if err != nil {
		return fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
	}
	protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse()
	if err != nil {
		// This should not fail if the preceding function succeeded.
		return fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String())
	}

	for _, p := range versionMeta.Protocols {
		proPro, err := VersionStr(p).Parse()
		if err != nil {
			// invalid protocol reported by the registry. Move along.
			log.Printf("[WARN] invalid provider protocol version %q found in the registry", versionMeta.Version)
			continue
		}
		// success!
		if protocolConstraint.Allows(proPro) {
			return nil
		}
	}

	return ErrorNoVersionCompatible
}

// REVIEWER QUESTION (again): this ends up swallowing a bunch of errors from
// checkPluginProtocol. Do they need to be percolated up better, or would
// debug messages would suffice in these situations?
func (i *ProviderInstaller) findPlatformCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
	for _, version := range versions {
		if err := i.checkPlatformCompatibility(version); err == nil {
			return version, nil
		}
	}

	return nil, ErrorNoVersionCompatibleWithPlatform
}

// platformCompatibleVersions returns a list of provider versions that are
// compatible with the requested platform.
func (i *ProviderInstaller) platformCompatibleVersions(versions []*response.TerraformProviderVersion) []*response.TerraformProviderVersion {
	var v []*response.TerraformProviderVersion
	for _, version := range versions {
		if err := i.checkPlatformCompatibility(version); err == nil {
			v = append(v, version)
		}
	}
	return v
}

func (i *ProviderInstaller) checkPlatformCompatibility(versionMeta *response.TerraformProviderVersion) error {
	if len(versionMeta.Platforms) == 0 {
		return fmt.Errorf("no supported provider platforms listed")
	}
	for _, p := range versionMeta.Platforms {
		if p.Arch == i.Arch && p.OS == i.OS {
			return nil
		}
	}
	return fmt.Errorf("version %s does not support the requested platform %s_%s", versionMeta.Version, i.OS, i.Arch)
}

// take the list of available versions for a plugin, and filter out those that
// don't fit the constraints.
func allowedVersions(available *response.TerraformProviderVersions, required Constraints) []*response.TerraformProviderVersion {
	var allowed []*response.TerraformProviderVersion

	for _, v := range available.Versions {
		version, err := VersionStr(v.Version).Parse()
		if err != nil {
			log.Printf("[WARN] invalid version found for %q: %s", available.ID, err)
			continue
		}
		if required.Allows(version) {
			allowed = append(allowed, v)
		}
	}
	return allowed
}

func checksumForFile(sums []byte, name string) string {
	for _, line := range strings.Split(string(sums), "\n") {
		parts := strings.Fields(line)
		if len(parts) > 1 && parts[1] == name {
			return parts[0]
		}
	}
	return ""
}

func getFile(url string) ([]byte, error) {
	resp, err := httpClient.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("%s", resp.Status)
	}

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return data, err
	}
	return data, nil
}

// providerProtocolTooOld is a message sent to the CLI UI if the provider's
// supported protocol versions are too old for the user's version of terraform,
// but an older version of the provider is compatible.
const providerProtocolTooOld = `
[reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]

Provider version %s is the earliest compatible version. Select it with 
the following version constraint:

	version = %q

Terraform checked all of the plugin versions matching the given constraint:
    %s

Consult the documentation for this provider for more information on
compatibility between provider and Terraform versions.
`

// providerProtocolTooNew is a message sent to the CLI UI if the provider's
// supported protocol versions are too new for the user's version of terraform,
// and the user could either upgrade terraform or choose an older version of the
// provider
const providerProtocolTooNew = `
[reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]

Provider version %s is the latest compatible version. Select it with 
the following constraint:

    version = %q

Terraform checked all of the plugin versions matching the given constraint:
    %s

Consult the documentation for this provider for more information on
compatibility between provider and Terraform versions.

Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases.
`