16 "github.com/hashicorp/errwrap"
17 getter "github.com/hashicorp/go-getter"
18 multierror "github.com/hashicorp/go-multierror"
19 "github.com/hashicorp/terraform/httpclient"
20 "github.com/hashicorp/terraform/registry"
21 "github.com/hashicorp/terraform/registry/regsrc"
22 "github.com/hashicorp/terraform/registry/response"
23 "github.com/hashicorp/terraform/svchost/disco"
24 "github.com/hashicorp/terraform/tfdiags"
25 tfversion "github.com/hashicorp/terraform/version"
26 "github.com/mitchellh/cli"
29 // Releases are located by querying the terraform registry.
31 const protocolVersionHeader = "x-terraform-protocol-version"
33 var httpClient *http.Client
35 var errVersionNotFound = errors.New("version not found")
38 httpClient = httpclient.New()
40 httpGetter := &getter.HttpGetter{
45 getter.Getters["http"] = httpGetter
46 getter.Getters["https"] = httpGetter
49 // An Installer maintains a local cache of plugins by downloading plugins
50 // from an online repository.
51 type Installer interface {
52 Get(name string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error)
53 PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
56 // ProviderInstaller is an Installer implementation that knows how to
57 // download Terraform providers from the official HashiCorp releases service
58 // into a local directory. The files downloaded are compliant with the
59 // naming scheme expected by FindPlugins, so the target directory of a
60 // provider installer can be used as one of several plugin discovery sources.
61 type ProviderInstaller struct {
64 // Cache is used to access and update a local cache of plugins if non-nil.
65 // Can be nil to disable caching.
68 PluginProtocolVersion uint
70 // OS and Arch specify the OS and architecture that should be used when
71 // installing plugins. These use the same labels as the runtime.GOOS and
72 // runtime.GOARCH variables respectively, and indeed the values of these
73 // are used as defaults if either of these is the empty string.
77 // Skip checksum and signature verification
80 Ui cli.Ui // Ui for output
82 // Services is a required *disco.Disco, which may have services and
83 // credentials pre-loaded.
87 registry *registry.Client
90 // Get is part of an implementation of type Installer, and attempts to download
91 // and install a Terraform provider matching the given constraints.
93 // This method may return one of a number of sentinel errors from this
94 // package to indicate issues that are likely to be resolvable via user action:
96 // ErrorNoSuchProvider: no provider with the given name exists in the repository.
97 // ErrorNoSuitableVersion: the provider exists but no available version matches constraints.
98 // ErrorNoVersionCompatible: a plugin was found within the constraints but it is
99 // incompatible with the current Terraform version.
101 // These errors should be recognized and handled as special cases by the caller
102 // to present a suitable user-oriented error message.
104 // All other errors indicate an internal problem that is likely _not_ solvable
105 // through user action, or at least not within Terraform's scope. Error messages
106 // are produced under the assumption that if presented to the user they will
107 // be presented alongside context about what is being installed, and thus the
108 // error messages do not redundantly include such information.
109 func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error) {
110 var diags tfdiags.Diagnostics
112 // a little bit of initialization.
117 i.Arch = runtime.GOARCH
119 if i.registry == nil {
120 i.registry = registry.NewClient(i.Services, nil)
123 // get a full listing of versions for the requested provider
124 allVersions, err := i.listProviderVersions(provider)
126 // TODO: return multiple errors
128 log.Printf("[DEBUG] %s", err)
129 if registry.IsServiceUnreachable(err) {
130 registryHost, err := i.hostname()
131 if err == nil && registryHost == regsrc.PublicRegistryHost.Raw {
132 return PluginMeta{}, diags, ErrorPublicRegistryUnreachable
134 return PluginMeta{}, diags, ErrorServiceUnreachable
136 if registry.IsServiceNotProvided(err) {
137 return PluginMeta{}, diags, err
139 return PluginMeta{}, diags, ErrorNoSuchProvider
142 // Add any warnings from the response to diags
143 for _, warning := range allVersions.Warnings {
144 hostname, err := i.hostname()
146 return PluginMeta{}, diags, err
148 diag := tfdiags.SimpleWarning(fmt.Sprintf("%s: %s", hostname, warning))
149 diags = diags.Append(diag)
152 if len(allVersions.Versions) == 0 {
153 return PluginMeta{}, diags, ErrorNoSuitableVersion
155 providerSource := allVersions.ID
157 // Filter the list of plugin versions to those which meet the version constraints
158 versions := allowedVersions(allVersions, req)
159 if len(versions) == 0 {
160 return PluginMeta{}, diags, ErrorNoSuitableVersion
163 // sort them newest to oldest. The newest version wins!
164 response.ProviderVersionCollection(versions).Sort()
166 // if the chosen provider version does not support the requested platform,
167 // filter the list of acceptable versions to those that support that platform
168 if err := i.checkPlatformCompatibility(versions[0]); err != nil {
169 versions = i.platformCompatibleVersions(versions)
170 if len(versions) == 0 {
171 return PluginMeta{}, diags, ErrorNoVersionCompatibleWithPlatform
175 // we now have a winning platform-compatible version
176 versionMeta := versions[0]
177 v := VersionStr(versionMeta.Version).MustParse()
179 // check protocol compatibility
180 if err := i.checkPluginProtocol(versionMeta); err != nil {
181 closestMatch, err := i.findClosestProtocolCompatibleVersion(allVersions.Versions)
183 // No operation here if we can't find a version with compatible protocol
184 return PluginMeta{}, diags, err
187 // Prompt version suggestion to UI based on closest protocol match
189 closestVersion := VersionStr(closestMatch.Version).MustParse()
190 if v.NewerThan(closestVersion) {
191 errMsg = providerProtocolTooNew
193 errMsg = providerProtocolTooOld
196 constraintStr := req.String()
197 if constraintStr == "" {
198 constraintStr = "(any version)"
201 return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf(
202 errMsg, provider, v.String(), tfversion.String(),
203 closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr)))
206 downloadURLs, err := i.listProviderDownloadURLs(providerSource, versionMeta.Version)
207 providerURL := downloadURLs.DownloadURL
210 // Terraform verifies the integrity of a provider release before downloading
211 // the plugin binary. The digital signature (SHA256SUMS.sig) on the
212 // release distribution (SHA256SUMS) is verified with the public key of the
213 // publisher provided in the Terraform Registry response, ensuring that
214 // everything is as intended by the publisher. The checksum of the provider
215 // plugin is expected in the SHA256SUMS file and is double checked to match
216 // the checksum of the original published release to the Registry. This
217 // enforces immutability of releases between the Registry and the plugin's
218 // host location. Lastly, the integrity of the binary is verified upon
219 // download matches the Registry and signed checksum.
220 sha256, err := i.getProviderChecksum(downloadURLs)
222 return PluginMeta{}, diags, err
225 // add the checksum parameter for go-getter to verify the download for us.
227 providerURL = providerURL + "?checksum=sha256:" + sha256
231 printedProviderName := fmt.Sprintf("%q (%s)", provider, providerSource)
232 i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %s %s...", printedProviderName, versionMeta.Version))
233 log.Printf("[DEBUG] getting provider %s version %q", printedProviderName, versionMeta.Version)
234 err = i.install(provider, v, providerURL)
236 return PluginMeta{}, diags, err
239 // Find what we just installed
240 // (This is weird, because go-getter doesn't directly return
241 // information about what was extracted, and we just extracted
242 // the archive directly into a shared dir here.)
243 log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, versionMeta.Version)
244 metas := FindPlugins("provider", []string{i.Dir})
245 log.Printf("[DEBUG] all plugins found %#v", metas)
246 metas, _ = metas.ValidateVersions()
247 metas = metas.WithName(provider).WithVersion(v)
248 log.Printf("[DEBUG] filtered plugins %#v", metas)
249 if metas.Count() == 0 {
250 // This should never happen. Suggests that the release archive
251 // contains an executable file whose name doesn't match the
252 // expected convention.
253 return PluginMeta{}, diags, fmt.Errorf(
254 "failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
259 if metas.Count() > 1 {
260 // This should also never happen, and suggests that a
261 // particular version was re-released with a different
262 // executable filename. We consider releases as immutable, so
264 return PluginMeta{}, diags, fmt.Errorf(
265 "multiple plugins installed for version %s; this is a bug in Terraform and should be reported",
270 // By now we know we have exactly one meta, and so "Newest" will
272 return metas.Newest(), diags, nil
275 func (i *ProviderInstaller) install(provider string, version Version, url string) error {
277 log.Printf("[DEBUG] looking for provider %s %s in plugin cache", provider, version)
278 cached := i.Cache.CachedPluginPath("provider", provider, version)
280 log.Printf("[DEBUG] %s %s not yet in cache, so downloading %s", provider, version, url)
281 err := getter.Get(i.Cache.InstallDir(), url)
285 // should now be in cache
286 cached = i.Cache.CachedPluginPath("provider", provider, version)
288 // should never happen if the getter is behaving properly
289 // and the plugins are packaged properly.
290 return fmt.Errorf("failed to find downloaded plugin in cache %s", i.Cache.InstallDir())
294 // Link or copy the cached binary into our install dir so the
295 // normal resolution machinery can find it.
296 filename := filepath.Base(cached)
297 targetPath := filepath.Join(i.Dir, filename)
298 // check if the target dir exists, and create it if not
300 if _, StatErr := os.Stat(i.Dir); os.IsNotExist(StatErr) {
301 err = os.MkdirAll(i.Dir, 0700)
307 log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached)
309 // Delete if we can. If there's nothing there already then no harm done.
310 // This is important because we can't create a link if there's
311 // already a file of the same name present.
312 // (any other error here we'll catch below when we try to write here)
313 os.Remove(targetPath)
315 // We don't attempt linking on Windows because links are not
316 // comprehensively supported by all tools/apps in Windows and
317 // so we choose to be conservative to avoid creating any
318 // weird issues for Windows users.
319 linkErr := errors.New("link not supported for Windows") // placeholder error, never actually returned
320 if runtime.GOOS != "windows" {
321 // Try hard linking first. Hard links are preferable because this
322 // creates a self-contained directory that doesn't depend on the
323 // cache after install.
324 linkErr = os.Link(cached, targetPath)
326 // If that failed, try a symlink. This _does_ depend on the cache
327 // after install, so the user must manage the cache more carefully
328 // in this case, but avoids creating redundant copies of the
331 linkErr = os.Symlink(cached, targetPath)
335 // If we still have an error then we'll try a copy as a fallback.
336 // In this case either the OS is Windows or the target filesystem
337 // can't support symlinks.
339 srcFile, err := os.Open(cached)
341 return fmt.Errorf("failed to open cached plugin %s: %s", cached, err)
343 defer srcFile.Close()
345 destFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
347 return fmt.Errorf("failed to create %s: %s", targetPath, err)
350 _, err = io.Copy(destFile, srcFile)
353 return fmt.Errorf("failed to copy cached plugin from %s to %s: %s", cached, targetPath, err)
356 err = destFile.Close()
358 return fmt.Errorf("error creating %s: %s", targetPath, err)
362 // One way or another, by the time we get here we should have either
363 // a link or a copy of the cached plugin within i.Dir, as expected.
365 log.Printf("[DEBUG] plugin cache is disabled, so downloading %s %s from %s", provider, version, url)
366 err := getter.Get(i.Dir, url)
374 func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) {
375 purge := make(PluginMetaSet)
377 present := FindPlugins("provider", []string{i.Dir})
378 for meta := range present {
379 chosen, ok := used[meta.Name]
383 if chosen.Path != meta.Path {
388 removed := make(PluginMetaSet)
390 for meta := range purge {
392 err := os.Remove(path)
394 errs = multierror.Append(errs, fmt.Errorf(
395 "failed to remove unused provider plugin %s: %s",
406 func (i *ProviderInstaller) getProviderChecksum(resp *response.TerraformProviderPlatformLocation) (string, error) {
407 // Get SHA256SUMS file.
408 shasums, err := getFile(resp.ShasumsURL)
410 log.Printf("[ERROR] error fetching checksums from %q: %s", resp.ShasumsURL, err)
411 return "", ErrorMissingChecksumVerification
414 // Get SHA256SUMS.sig file.
415 signature, err := getFile(resp.ShasumsSignatureURL)
417 log.Printf("[ERROR] error fetching checksums signature from %q: %s", resp.ShasumsSignatureURL, err)
418 return "", ErrorSignatureVerification
421 // Verify the GPG signature returned from the Registry.
422 asciiArmor := resp.SigningKeys.GPGASCIIArmor()
423 signer, err := verifySig(shasums, signature, asciiArmor)
425 log.Printf("[ERROR] error verifying signature: %s", err)
426 return "", ErrorSignatureVerification
429 // Also verify the GPG signature against the HashiCorp public key. This is
430 // a temporary additional check until a more robust key verification
431 // process is added in a future release.
432 _, err = verifySig(shasums, signature, HashicorpPublicKey)
434 log.Printf("[ERROR] error verifying signature against HashiCorp public key: %s", err)
435 return "", ErrorSignatureVerification
438 // Display identity for GPG key which succeeded verifying the signature.
439 // This could also be used to display to the user with i.Ui.Info().
440 identities := []string{}
441 for k := range signer.Identities {
442 identities = append(identities, k)
444 identity := strings.Join(identities, ", ")
445 log.Printf("[DEBUG] verified GPG signature with key from %s", identity)
447 // Extract checksum for this os/arch platform binary and verify against Registry
448 checksum := checksumForFile(shasums, resp.Filename)
450 log.Printf("[ERROR] missing checksum for %s from source %s", resp.Filename, resp.ShasumsURL)
451 return "", ErrorMissingChecksumVerification
452 } else if checksum != resp.Shasum {
453 log.Printf("[ERROR] unexpected checksum for %s from source %q", resp.Filename, resp.ShasumsURL)
454 return "", ErrorChecksumVerification
460 func (i *ProviderInstaller) hostname() (string, error) {
461 provider := regsrc.NewTerraformProvider("", i.OS, i.Arch)
462 svchost, err := provider.SvcHost()
467 return svchost.ForDisplay(), nil
470 // list all versions available for the named provider
471 func (i *ProviderInstaller) listProviderVersions(name string) (*response.TerraformProviderVersions, error) {
472 provider := regsrc.NewTerraformProvider(name, i.OS, i.Arch)
473 versions, err := i.registry.TerraformProviderVersions(provider)
477 func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) {
478 urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version)
480 return nil, fmt.Errorf("No download urls found for provider %s", name)
485 // findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match.
486 // Prerelease versions are filtered.
487 func (i *ProviderInstaller) findClosestProtocolCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
488 // Loop through all the provider versions to find the earliest and latest
489 // versions that match the installer protocol to then select the closest of the two
490 var latest, earliest *response.TerraformProviderVersion
491 for _, version := range versions {
492 // Prereleases are filtered and will not be suggested
493 v, err := VersionStr(version.Version).Parse()
494 if err != nil || v.IsPrerelease() {
498 if err := i.checkPluginProtocol(version); err == nil {
500 // Found the first provider version with compatible protocol
503 // Update the latest protocol compatible version
508 // No compatible protocol was found for any version
509 return nil, ErrorNoVersionCompatible
512 // Convert protocols to comparable types
513 protoString := strconv.Itoa(int(i.PluginProtocolVersion))
514 protocolVersion, err := VersionStr(protoString).Parse()
516 return nil, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
519 earliestVersionProtocol, err := VersionStr(earliest.Protocols[0]).Parse()
524 // Compare installer protocol version with the first protocol listed of the earliest match
525 // [A, B] where A is assumed the earliest compatible major version of the protocol pair
526 if protocolVersion.NewerThan(earliestVersionProtocol) {
527 // Provider protocols are too old, the closest version is the earliest compatible version
531 // Provider protocols are too new, the closest version is the latest compatible version
535 func (i *ProviderInstaller) checkPluginProtocol(versionMeta *response.TerraformProviderVersion) error {
536 // TODO: should this be a different error? We should probably differentiate between
537 // no compatible versions and no protocol versions listed at all
538 if len(versionMeta.Protocols) == 0 {
539 return fmt.Errorf("no plugin protocol versions listed")
542 protoString := strconv.Itoa(int(i.PluginProtocolVersion))
543 protocolVersion, err := VersionStr(protoString).Parse()
545 return fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion)
547 protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse()
549 // This should not fail if the preceding function succeeded.
550 return fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String())
553 for _, p := range versionMeta.Protocols {
554 proPro, err := VersionStr(p).Parse()
556 // invalid protocol reported by the registry. Move along.
557 log.Printf("[WARN] invalid provider protocol version %q found in the registry", versionMeta.Version)
561 if protocolConstraint.Allows(proPro) {
566 return ErrorNoVersionCompatible
569 // REVIEWER QUESTION (again): this ends up swallowing a bunch of errors from
570 // checkPluginProtocol. Do they need to be percolated up better, or would
571 // debug messages would suffice in these situations?
572 func (i *ProviderInstaller) findPlatformCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) {
573 for _, version := range versions {
574 if err := i.checkPlatformCompatibility(version); err == nil {
579 return nil, ErrorNoVersionCompatibleWithPlatform
582 // platformCompatibleVersions returns a list of provider versions that are
583 // compatible with the requested platform.
584 func (i *ProviderInstaller) platformCompatibleVersions(versions []*response.TerraformProviderVersion) []*response.TerraformProviderVersion {
585 var v []*response.TerraformProviderVersion
586 for _, version := range versions {
587 if err := i.checkPlatformCompatibility(version); err == nil {
588 v = append(v, version)
594 func (i *ProviderInstaller) checkPlatformCompatibility(versionMeta *response.TerraformProviderVersion) error {
595 if len(versionMeta.Platforms) == 0 {
596 return fmt.Errorf("no supported provider platforms listed")
598 for _, p := range versionMeta.Platforms {
599 if p.Arch == i.Arch && p.OS == i.OS {
603 return fmt.Errorf("version %s does not support the requested platform %s_%s", versionMeta.Version, i.OS, i.Arch)
606 // take the list of available versions for a plugin, and filter out those that
607 // don't fit the constraints.
608 func allowedVersions(available *response.TerraformProviderVersions, required Constraints) []*response.TerraformProviderVersion {
609 var allowed []*response.TerraformProviderVersion
611 for _, v := range available.Versions {
612 version, err := VersionStr(v.Version).Parse()
614 log.Printf("[WARN] invalid version found for %q: %s", available.ID, err)
617 if required.Allows(version) {
618 allowed = append(allowed, v)
624 func checksumForFile(sums []byte, name string) string {
625 for _, line := range strings.Split(string(sums), "\n") {
626 parts := strings.Fields(line)
627 if len(parts) > 1 && parts[1] == name {
634 func getFile(url string) ([]byte, error) {
635 resp, err := httpClient.Get(url)
639 defer resp.Body.Close()
641 if resp.StatusCode != http.StatusOK {
642 return nil, fmt.Errorf("%s", resp.Status)
645 data, err := ioutil.ReadAll(resp.Body)
652 // providerProtocolTooOld is a message sent to the CLI UI if the provider's
653 // supported protocol versions are too old for the user's version of terraform,
654 // but an older version of the provider is compatible.
655 const providerProtocolTooOld = `
656 [reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]
658 Provider version %s is the earliest compatible version. Select it with
659 the following version constraint:
663 Terraform checked all of the plugin versions matching the given constraint:
666 Consult the documentation for this provider for more information on
667 compatibility between provider and Terraform versions.
670 // providerProtocolTooNew is a message sent to the CLI UI if the provider's
671 // supported protocol versions are too new for the user's version of terraform,
672 // and the user could either upgrade terraform or choose an older version of the
674 const providerProtocolTooNew = `
675 [reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red]
677 Provider version %s is the latest compatible version. Select it with
678 the following constraint:
682 Terraform checked all of the plugin versions matching the given constraint:
685 Consult the documentation for this provider for more information on
686 compatibility between provider and Terraform versions.
688 Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases.