github.com/johandry/terraform@v0.11.12-beta1/plugin/discovery/get.go (about)

     1  package discovery
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"golang.org/x/net/html"
    17  
    18  	getter "github.com/hashicorp/go-getter"
    19  	multierror "github.com/hashicorp/go-multierror"
    20  	"github.com/hashicorp/terraform/httpclient"
    21  	"github.com/mitchellh/cli"
    22  )
    23  
    24  // Releases are located by parsing the html listing from releases.hashicorp.com.
    25  //
    26  // The URL for releases follows the pattern:
    27  //    https://releases.hashicorp.com/terraform-provider-name/<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
    28  //
    29  // The plugin protocol version will be saved with the release and returned in
    30  // the header X-TERRAFORM_PROTOCOL_VERSION.
    31  
    32  const protocolVersionHeader = "x-terraform-protocol-version"
    33  
    34  var releaseHost = "https://releases.hashicorp.com"
    35  
    36  var httpClient *http.Client
    37  
    38  func init() {
    39  	httpClient = httpclient.New()
    40  
    41  	httpGetter := &getter.HttpGetter{
    42  		Client: httpClient,
    43  		Netrc:  true,
    44  	}
    45  
    46  	getter.Getters["http"] = httpGetter
    47  	getter.Getters["https"] = httpGetter
    48  }
    49  
    50  // An Installer maintains a local cache of plugins by downloading plugins
    51  // from an online repository.
    52  type Installer interface {
    53  	Get(name string, req Constraints) (PluginMeta, error)
    54  	PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
    55  }
    56  
    57  // ProviderInstaller is an Installer implementation that knows how to
    58  // download Terraform providers from the official HashiCorp releases service
    59  // into a local directory. The files downloaded are compliant with the
    60  // naming scheme expected by FindPlugins, so the target directory of a
    61  // provider installer can be used as one of several plugin discovery sources.
    62  type ProviderInstaller struct {
    63  	Dir string
    64  
    65  	// Cache is used to access and update a local cache of plugins if non-nil.
    66  	// Can be nil to disable caching.
    67  	Cache PluginCache
    68  
    69  	PluginProtocolVersion uint
    70  
    71  	// OS and Arch specify the OS and architecture that should be used when
    72  	// installing plugins. These use the same labels as the runtime.GOOS and
    73  	// runtime.GOARCH variables respectively, and indeed the values of these
    74  	// are used as defaults if either of these is the empty string.
    75  	OS   string
    76  	Arch string
    77  
    78  	// Skip checksum and signature verification
    79  	SkipVerify bool
    80  
    81  	Ui cli.Ui // Ui for output
    82  }
    83  
    84  // Get is part of an implementation of type Installer, and attempts to download
    85  // and install a Terraform provider matching the given constraints.
    86  //
    87  // This method may return one of a number of sentinel errors from this
    88  // package to indicate issues that are likely to be resolvable via user action:
    89  //
    90  //     ErrorNoSuchProvider: no provider with the given name exists in the repository.
    91  //     ErrorNoSuitableVersion: the provider exists but no available version matches constraints.
    92  //     ErrorNoVersionCompatible: a plugin was found within the constraints but it is
    93  //                               incompatible with the current Terraform version.
    94  //
    95  // These errors should be recognized and handled as special cases by the caller
    96  // to present a suitable user-oriented error message.
    97  //
    98  // All other errors indicate an internal problem that is likely _not_ solvable
    99  // through user action, or at least not within Terraform's scope. Error messages
   100  // are produced under the assumption that if presented to the user they will
   101  // be presented alongside context about what is being installed, and thus the
   102  // error messages do not redundantly include such information.
   103  func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) {
   104  	versions, err := i.listProviderVersions(provider)
   105  	// TODO: return multiple errors
   106  	if err != nil {
   107  		return PluginMeta{}, err
   108  	}
   109  
   110  	if len(versions) == 0 {
   111  		return PluginMeta{}, ErrorNoSuitableVersion
   112  	}
   113  
   114  	versions = allowedVersions(versions, req)
   115  	if len(versions) == 0 {
   116  		return PluginMeta{}, ErrorNoSuitableVersion
   117  	}
   118  
   119  	// sort them newest to oldest
   120  	Versions(versions).Sort()
   121  
   122  	// Ensure that our installation directory exists
   123  	err = os.MkdirAll(i.Dir, os.ModePerm)
   124  	if err != nil {
   125  		return PluginMeta{}, fmt.Errorf("failed to create plugin dir %s: %s", i.Dir, err)
   126  	}
   127  
   128  	// take the first matching plugin we find
   129  	for _, v := range versions {
   130  		url := i.providerURL(provider, v.String())
   131  
   132  		if !i.SkipVerify {
   133  			sha256, err := i.getProviderChecksum(provider, v.String())
   134  			if err != nil {
   135  				return PluginMeta{}, err
   136  			}
   137  
   138  			// add the checksum parameter for go-getter to verify the download for us.
   139  			if sha256 != "" {
   140  				url = url + "?checksum=sha256:" + sha256
   141  			}
   142  		}
   143  
   144  		log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
   145  		if checkPlugin(url, i.PluginProtocolVersion) {
   146  			i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %q (%s)...", provider, v.String()))
   147  			log.Printf("[DEBUG] getting provider %q version %q", provider, v)
   148  			err := i.install(provider, v, url)
   149  			if err != nil {
   150  				return PluginMeta{}, err
   151  			}
   152  
   153  			// Find what we just installed
   154  			// (This is weird, because go-getter doesn't directly return
   155  			//  information about what was extracted, and we just extracted
   156  			//  the archive directly into a shared dir here.)
   157  			log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, v)
   158  			metas := FindPlugins("provider", []string{i.Dir})
   159  			log.Printf("[DEBUG] all plugins found %#v", metas)
   160  			metas, _ = metas.ValidateVersions()
   161  			metas = metas.WithName(provider).WithVersion(v)
   162  			log.Printf("[DEBUG] filtered plugins %#v", metas)
   163  			if metas.Count() == 0 {
   164  				// This should never happen. Suggests that the release archive
   165  				// contains an executable file whose name doesn't match the
   166  				// expected convention.
   167  				return PluginMeta{}, fmt.Errorf(
   168  					"failed to find installed plugin version %s; this is a bug in Terraform and should be reported",
   169  					v,
   170  				)
   171  			}
   172  
   173  			if metas.Count() > 1 {
   174  				// This should also never happen, and suggests that a
   175  				// particular version was re-released with a different
   176  				// executable filename. We consider releases as immutable, so
   177  				// this is an error.
   178  				return PluginMeta{}, fmt.Errorf(
   179  					"multiple plugins installed for version %s; this is a bug in Terraform and should be reported",
   180  					v,
   181  				)
   182  			}
   183  
   184  			// By now we know we have exactly one meta, and so "Newest" will
   185  			// return that one.
   186  			return metas.Newest(), nil
   187  		}
   188  
   189  		log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v)
   190  	}
   191  
   192  	return PluginMeta{}, ErrorNoVersionCompatible
   193  }
   194  
   195  func (i *ProviderInstaller) install(provider string, version Version, url string) error {
   196  	if i.Cache != nil {
   197  		log.Printf("[DEBUG] looking for provider %s %s in plugin cache", provider, version)
   198  		cached := i.Cache.CachedPluginPath("provider", provider, version)
   199  		if cached == "" {
   200  			log.Printf("[DEBUG] %s %s not yet in cache, so downloading %s", provider, version, url)
   201  			err := getter.Get(i.Cache.InstallDir(), url)
   202  			if err != nil {
   203  				return err
   204  			}
   205  			// should now be in cache
   206  			cached = i.Cache.CachedPluginPath("provider", provider, version)
   207  			if cached == "" {
   208  				// should never happen if the getter is behaving properly
   209  				// and the plugins are packaged properly.
   210  				return fmt.Errorf("failed to find downloaded plugin in cache %s", i.Cache.InstallDir())
   211  			}
   212  		}
   213  
   214  		// Link or copy the cached binary into our install dir so the
   215  		// normal resolution machinery can find it.
   216  		filename := filepath.Base(cached)
   217  		targetPath := filepath.Join(i.Dir, filename)
   218  
   219  		log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached)
   220  
   221  		// Delete if we can. If there's nothing there already then no harm done.
   222  		// This is important because we can't create a link if there's
   223  		// already a file of the same name present.
   224  		// (any other error here we'll catch below when we try to write here)
   225  		os.Remove(targetPath)
   226  
   227  		// We don't attempt linking on Windows because links are not
   228  		// comprehensively supported by all tools/apps in Windows and
   229  		// so we choose to be conservative to avoid creating any
   230  		// weird issues for Windows users.
   231  		linkErr := errors.New("link not supported for Windows") // placeholder error, never actually returned
   232  		if runtime.GOOS != "windows" {
   233  			// Try hard linking first. Hard links are preferable because this
   234  			// creates a self-contained directory that doesn't depend on the
   235  			// cache after install.
   236  			linkErr = os.Link(cached, targetPath)
   237  
   238  			// If that failed, try a symlink. This _does_ depend on the cache
   239  			// after install, so the user must manage the cache more carefully
   240  			// in this case, but avoids creating redundant copies of the
   241  			// plugins on disk.
   242  			if linkErr != nil {
   243  				linkErr = os.Symlink(cached, targetPath)
   244  			}
   245  		}
   246  
   247  		// If we still have an error then we'll try a copy as a fallback.
   248  		// In this case either the OS is Windows or the target filesystem
   249  		// can't support symlinks.
   250  		if linkErr != nil {
   251  			srcFile, err := os.Open(cached)
   252  			if err != nil {
   253  				return fmt.Errorf("failed to open cached plugin %s: %s", cached, err)
   254  			}
   255  			defer srcFile.Close()
   256  
   257  			destFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
   258  			if err != nil {
   259  				return fmt.Errorf("failed to create %s: %s", targetPath, err)
   260  			}
   261  
   262  			_, err = io.Copy(destFile, srcFile)
   263  			if err != nil {
   264  				destFile.Close()
   265  				return fmt.Errorf("failed to copy cached plugin from %s to %s: %s", cached, targetPath, err)
   266  			}
   267  
   268  			err = destFile.Close()
   269  			if err != nil {
   270  				return fmt.Errorf("error creating %s: %s", targetPath, err)
   271  			}
   272  		}
   273  
   274  		// One way or another, by the time we get here we should have either
   275  		// a link or a copy of the cached plugin within i.Dir, as expected.
   276  	} else {
   277  		log.Printf("[DEBUG] plugin cache is disabled, so downloading %s %s from %s", provider, version, url)
   278  		err := getter.Get(i.Dir, url)
   279  		if err != nil {
   280  			return err
   281  		}
   282  	}
   283  
   284  	return nil
   285  }
   286  
   287  func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) {
   288  	purge := make(PluginMetaSet)
   289  
   290  	present := FindPlugins("provider", []string{i.Dir})
   291  	for meta := range present {
   292  		chosen, ok := used[meta.Name]
   293  		if !ok {
   294  			purge.Add(meta)
   295  		}
   296  		if chosen.Path != meta.Path {
   297  			purge.Add(meta)
   298  		}
   299  	}
   300  
   301  	removed := make(PluginMetaSet)
   302  	var errs error
   303  	for meta := range purge {
   304  		path := meta.Path
   305  		err := os.Remove(path)
   306  		if err != nil {
   307  			errs = multierror.Append(errs, fmt.Errorf(
   308  				"failed to remove unused provider plugin %s: %s",
   309  				path, err,
   310  			))
   311  		} else {
   312  			removed.Add(meta)
   313  		}
   314  	}
   315  
   316  	return removed, errs
   317  }
   318  
   319  // Plugins are referred to by the short name, but all URLs and files will use
   320  // the full name prefixed with terraform-<plugin_type>-
   321  func (i *ProviderInstaller) providerName(name string) string {
   322  	return "terraform-provider-" + name
   323  }
   324  
   325  func (i *ProviderInstaller) providerFileName(name, version string) string {
   326  	os := i.OS
   327  	arch := i.Arch
   328  	if os == "" {
   329  		os = runtime.GOOS
   330  	}
   331  	if arch == "" {
   332  		arch = runtime.GOARCH
   333  	}
   334  	return fmt.Sprintf("%s_%s_%s_%s.zip", i.providerName(name), version, os, arch)
   335  }
   336  
   337  // providerVersionsURL returns the path to the released versions directory for the provider:
   338  // https://releases.hashicorp.com/terraform-provider-name/
   339  func (i *ProviderInstaller) providerVersionsURL(name string) string {
   340  	return releaseHost + "/" + i.providerName(name) + "/"
   341  }
   342  
   343  // providerURL returns the full path to the provider file, using the current OS
   344  // and ARCH:
   345  // .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
   346  func (i *ProviderInstaller) providerURL(name, version string) string {
   347  	return fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, i.providerFileName(name, version))
   348  }
   349  
   350  func (i *ProviderInstaller) providerChecksumURL(name, version string) string {
   351  	fileName := fmt.Sprintf("%s_%s_SHA256SUMS", i.providerName(name), version)
   352  	u := fmt.Sprintf("%s%s/%s", i.providerVersionsURL(name), version, fileName)
   353  	return u
   354  }
   355  
   356  func (i *ProviderInstaller) getProviderChecksum(name, version string) (string, error) {
   357  	checksums, err := getPluginSHA256SUMs(i.providerChecksumURL(name, version))
   358  	if err != nil {
   359  		return "", err
   360  	}
   361  
   362  	return checksumForFile(checksums, i.providerFileName(name, version)), nil
   363  }
   364  
   365  // Return the plugin version by making a HEAD request to the provided url.
   366  // If the header is not present, we assume the latest version will be
   367  // compatible, and leave the check for discovery or execution.
   368  func checkPlugin(url string, pluginProtocolVersion uint) bool {
   369  	resp, err := httpClient.Head(url)
   370  	if err != nil {
   371  		log.Printf("[ERROR] error fetching plugin headers: %s", err)
   372  		return false
   373  	}
   374  
   375  	if resp.StatusCode != http.StatusOK {
   376  		log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status)
   377  		return false
   378  	}
   379  
   380  	proto := resp.Header.Get(protocolVersionHeader)
   381  	if proto == "" {
   382  		// The header isn't present, but we don't make this error fatal since
   383  		// the latest version will probably work.
   384  		log.Printf("[WARN] missing %s from: %s", protocolVersionHeader, url)
   385  		return true
   386  	}
   387  
   388  	protoVersion, err := strconv.Atoi(proto)
   389  	if err != nil {
   390  		log.Printf("[ERROR] invalid ProtocolVersion: %s", proto)
   391  		return false
   392  	}
   393  
   394  	return protoVersion == int(pluginProtocolVersion)
   395  }
   396  
   397  // list the version available for the named plugin
   398  func (i *ProviderInstaller) listProviderVersions(name string) ([]Version, error) {
   399  	versions, err := listPluginVersions(i.providerVersionsURL(name))
   400  	if err != nil {
   401  		// listPluginVersions returns a verbose error message indicating
   402  		// what was being accessed and what failed
   403  		return nil, err
   404  	}
   405  	return versions, nil
   406  }
   407  
   408  var errVersionNotFound = errors.New("version not found")
   409  
   410  // take the list of available versions for a plugin, and filter out those that
   411  // don't fit the constraints.
   412  func allowedVersions(available []Version, required Constraints) []Version {
   413  	var allowed []Version
   414  
   415  	for _, v := range available {
   416  		if required.Allows(v) {
   417  			allowed = append(allowed, v)
   418  		}
   419  	}
   420  
   421  	return allowed
   422  }
   423  
   424  // return a list of the plugin versions at the given URL
   425  func listPluginVersions(url string) ([]Version, error) {
   426  	resp, err := httpClient.Get(url)
   427  	if err != nil {
   428  		// http library produces a verbose error message that includes the
   429  		// URL being accessed, etc.
   430  		return nil, err
   431  	}
   432  	defer resp.Body.Close()
   433  
   434  	if resp.StatusCode != http.StatusOK {
   435  		body, _ := ioutil.ReadAll(resp.Body)
   436  		log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body)
   437  
   438  		switch resp.StatusCode {
   439  		case http.StatusNotFound, http.StatusForbidden:
   440  			// These are treated as indicative of the given name not being
   441  			// a valid provider name at all.
   442  			return nil, ErrorNoSuchProvider
   443  
   444  		default:
   445  			// All other errors are assumed to be operational problems.
   446  			return nil, fmt.Errorf("error accessing %s: %s", url, resp.Status)
   447  		}
   448  
   449  	}
   450  
   451  	body, err := html.Parse(resp.Body)
   452  	if err != nil {
   453  		log.Fatal(err)
   454  	}
   455  
   456  	names := []string{}
   457  
   458  	// all we need to do is list links on the directory listing page that look like plugins
   459  	var f func(*html.Node)
   460  	f = func(n *html.Node) {
   461  		if n.Type == html.ElementNode && n.Data == "a" {
   462  			c := n.FirstChild
   463  			if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") {
   464  				names = append(names, c.Data)
   465  				return
   466  			}
   467  		}
   468  		for c := n.FirstChild; c != nil; c = c.NextSibling {
   469  			f(c)
   470  		}
   471  	}
   472  	f(body)
   473  
   474  	return versionsFromNames(names), nil
   475  }
   476  
   477  // parse the list of directory names into a sorted list of available versions
   478  func versionsFromNames(names []string) []Version {
   479  	var versions []Version
   480  	for _, name := range names {
   481  		parts := strings.SplitN(name, "_", 2)
   482  		if len(parts) == 2 && parts[1] != "" {
   483  			v, err := VersionStr(parts[1]).Parse()
   484  			if err != nil {
   485  				// filter invalid versions scraped from the page
   486  				log.Printf("[WARN] invalid version found for %q: %s", name, err)
   487  				continue
   488  			}
   489  
   490  			versions = append(versions, v)
   491  		}
   492  	}
   493  
   494  	return versions
   495  }
   496  
   497  func checksumForFile(sums []byte, name string) string {
   498  	for _, line := range strings.Split(string(sums), "\n") {
   499  		parts := strings.Fields(line)
   500  		if len(parts) > 1 && parts[1] == name {
   501  			return parts[0]
   502  		}
   503  	}
   504  	return ""
   505  }
   506  
   507  // fetch the SHA256SUMS file provided, and verify its signature.
   508  func getPluginSHA256SUMs(sumsURL string) ([]byte, error) {
   509  	sigURL := sumsURL + ".sig"
   510  
   511  	sums, err := getFile(sumsURL)
   512  	if err != nil {
   513  		return nil, fmt.Errorf("error fetching checksums: %s", err)
   514  	}
   515  
   516  	sig, err := getFile(sigURL)
   517  	if err != nil {
   518  		return nil, fmt.Errorf("error fetching checksums signature: %s", err)
   519  	}
   520  
   521  	if err := verifySig(sums, sig); err != nil {
   522  		return nil, err
   523  	}
   524  
   525  	return sums, nil
   526  }
   527  
   528  func getFile(url string) ([]byte, error) {
   529  	resp, err := httpClient.Get(url)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  	defer resp.Body.Close()
   534  
   535  	if resp.StatusCode != http.StatusOK {
   536  		return nil, fmt.Errorf("%s", resp.Status)
   537  	}
   538  
   539  	data, err := ioutil.ReadAll(resp.Body)
   540  	if err != nil {
   541  		return data, err
   542  	}
   543  	return data, nil
   544  }
   545  
   546  func GetReleaseHost() string {
   547  	return releaseHost
   548  }