github.com/paybyphone/terraform@v0.9.5-0.20170613192930-9706042ddd51/plugin/discovery/get.go (about)

     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  // Plugins are referred to by the short name, but all URLs and files will use
    36  // the full name prefixed with terraform-<plugin_type>-
    37  func providerName(name string) string {
    38  	return "terraform-provider-" + name
    39  }
    40  
    41  // providerVersionsURL returns the path to the released versions directory for the provider:
    42  // https://releases.hashicorp.com/terraform-provider-name/
    43  func providerVersionsURL(name string) string {
    44  	return releaseHost + "/" + providerName(name) + "/"
    45  }
    46  
    47  // providerURL returns the full path to the provider file, using the current OS
    48  // and ARCH:
    49  // .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
    50  func providerURL(name, version string) string {
    51  	fileName := fmt.Sprintf("%s_%s_%s_%s.zip", providerName(name), version, runtime.GOOS, runtime.GOARCH)
    52  	u := fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, fileName)
    53  	return u
    54  }
    55  
    56  // An Installer maintains a local cache of plugins by downloading plugins
    57  // from an online repository.
    58  type Installer interface {
    59  	Get(name string, req Constraints) (PluginMeta, error)
    60  	PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error)
    61  }
    62  
    63  // ProviderInstaller is an Installer implementation that knows how to
    64  // download Terraform providers from the official HashiCorp releases service
    65  // into a local directory. The files downloaded are compliant with the
    66  // naming scheme expected by FindPlugins, so the target directory of a
    67  // provider installer can be used as one of several plugin discovery sources.
    68  type ProviderInstaller struct {
    69  	Dir string
    70  
    71  	PluginProtocolVersion uint
    72  }
    73  
    74  func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, error) {
    75  	versions, err := listProviderVersions(provider)
    76  	// TODO: return multiple errors
    77  	if err != nil {
    78  		return PluginMeta{}, err
    79  	}
    80  
    81  	if len(versions) == 0 {
    82  		return PluginMeta{}, fmt.Errorf("no plugins found for provider %q", provider)
    83  	}
    84  
    85  	versions = allowedVersions(versions, req)
    86  	if len(versions) == 0 {
    87  		return PluginMeta{}, fmt.Errorf("no version of %q available that fulfills constraints %s", provider, req)
    88  	}
    89  
    90  	// sort them newest to oldest
    91  	Versions(versions).Sort()
    92  
    93  	// take the first matching plugin we find
    94  	for _, v := range versions {
    95  		url := providerURL(provider, v.String())
    96  		log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
    97  		if checkPlugin(url, i.PluginProtocolVersion) {
    98  			log.Printf("[DEBUG] getting provider %q version %q at %s", provider, v, url)
    99  			err := getter.Get(i.Dir, url)
   100  			if err != nil {
   101  				return PluginMeta{}, err
   102  			}
   103  
   104  			// Find what we just installed
   105  			// (This is weird, because go-getter doesn't directly return
   106  			//  information about what was extracted, and we just extracted
   107  			//  the archive directly into a shared dir here.)
   108  			log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, v)
   109  			metas := FindPlugins("provider", []string{i.Dir})
   110  			log.Printf("all plugins found %#v", metas)
   111  			metas, _ = metas.ValidateVersions()
   112  			metas = metas.WithName(provider).WithVersion(v)
   113  			log.Printf("filtered plugins %#v", metas)
   114  			if metas.Count() == 0 {
   115  				// This should never happen. Suggests that the release archive
   116  				// contains an executable file whose name doesn't match the
   117  				// expected convention.
   118  				return PluginMeta{}, fmt.Errorf(
   119  					"failed to find installed provider %s %s; this is a bug in Terraform and should be reported",
   120  					provider, v,
   121  				)
   122  			}
   123  
   124  			if metas.Count() > 1 {
   125  				// This should also never happen, and suggests that a
   126  				// particular version was re-released with a different
   127  				// executable filename. We consider releases as immutable, so
   128  				// this is an error.
   129  				return PluginMeta{}, fmt.Errorf(
   130  					"multiple plugins installed for %s %s; this is a bug in Terraform and should be reported",
   131  					provider, v,
   132  				)
   133  			}
   134  
   135  			// By now we know we have exactly one meta, and so "Newest" will
   136  			// return that one.
   137  			return metas.Newest(), nil
   138  		}
   139  
   140  		log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v)
   141  	}
   142  
   143  	return PluginMeta{}, fmt.Errorf("no versions of %q compatible with the plugin ProtocolVersion", provider)
   144  }
   145  
   146  func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) {
   147  	purge := make(PluginMetaSet)
   148  
   149  	present := FindPlugins("provider", []string{i.Dir})
   150  	for meta := range present {
   151  		chosen, ok := used[meta.Name]
   152  		if !ok {
   153  			purge.Add(meta)
   154  		}
   155  		if chosen.Path != meta.Path {
   156  			purge.Add(meta)
   157  		}
   158  	}
   159  
   160  	removed := make(PluginMetaSet)
   161  	var errs error
   162  	for meta := range purge {
   163  		path := meta.Path
   164  		err := os.Remove(path)
   165  		if err != nil {
   166  			errs = multierror.Append(errs, fmt.Errorf(
   167  				"failed to remove unused provider plugin %s: %s",
   168  				path, err,
   169  			))
   170  		} else {
   171  			removed.Add(meta)
   172  		}
   173  	}
   174  
   175  	return removed, errs
   176  }
   177  
   178  // Return the plugin version by making a HEAD request to the provided url
   179  func checkPlugin(url string, pluginProtocolVersion uint) bool {
   180  	resp, err := httpClient.Head(url)
   181  	if err != nil {
   182  		log.Printf("[ERROR] error fetching plugin headers: %s", err)
   183  		return false
   184  	}
   185  
   186  	if resp.StatusCode != http.StatusOK {
   187  		log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status)
   188  		return false
   189  	}
   190  
   191  	proto := resp.Header.Get(protocolVersionHeader)
   192  	if proto == "" {
   193  		log.Printf("[WARNING] missing %s from: %s", protocolVersionHeader, url)
   194  		return false
   195  	}
   196  
   197  	protoVersion, err := strconv.Atoi(proto)
   198  	if err != nil {
   199  		log.Printf("[ERROR] invalid ProtocolVersion: %s", proto)
   200  		return false
   201  	}
   202  
   203  	return protoVersion == int(pluginProtocolVersion)
   204  }
   205  
   206  var errVersionNotFound = errors.New("version not found")
   207  
   208  // take the list of available versions for a plugin, and filter out those that
   209  // don't fit the constraints.
   210  func allowedVersions(available []Version, required Constraints) []Version {
   211  	var allowed []Version
   212  
   213  	for _, v := range available {
   214  		if required.Allows(v) {
   215  			allowed = append(allowed, v)
   216  		}
   217  	}
   218  
   219  	return allowed
   220  }
   221  
   222  // list the version available for the named plugin
   223  func listProviderVersions(name string) ([]Version, error) {
   224  	versions, err := listPluginVersions(providerVersionsURL(name))
   225  	if err != nil {
   226  		return nil, fmt.Errorf("failed to fetch versions for provider %q: %s", name, err)
   227  	}
   228  	return versions, nil
   229  }
   230  
   231  // return a list of the plugin versions at the given URL
   232  func listPluginVersions(url string) ([]Version, error) {
   233  	resp, err := httpClient.Get(url)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	defer resp.Body.Close()
   238  
   239  	if resp.StatusCode != http.StatusOK {
   240  		body, _ := ioutil.ReadAll(resp.Body)
   241  		log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body)
   242  		return nil, errors.New(resp.Status)
   243  	}
   244  
   245  	body, err := html.Parse(resp.Body)
   246  	if err != nil {
   247  		log.Fatal(err)
   248  	}
   249  
   250  	names := []string{}
   251  
   252  	// all we need to do is list links on the directory listing page that look like plugins
   253  	var f func(*html.Node)
   254  	f = func(n *html.Node) {
   255  		if n.Type == html.ElementNode && n.Data == "a" {
   256  			c := n.FirstChild
   257  			if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") {
   258  				names = append(names, c.Data)
   259  				return
   260  			}
   261  		}
   262  		for c := n.FirstChild; c != nil; c = c.NextSibling {
   263  			f(c)
   264  		}
   265  	}
   266  	f(body)
   267  
   268  	return versionsFromNames(names), nil
   269  }
   270  
   271  // parse the list of directory names into a sorted list of available versions
   272  func versionsFromNames(names []string) []Version {
   273  	var versions []Version
   274  	for _, name := range names {
   275  		parts := strings.SplitN(name, "_", 2)
   276  		if len(parts) == 2 && parts[1] != "" {
   277  			v, err := VersionStr(parts[1]).Parse()
   278  			if err != nil {
   279  				// filter invalid versions scraped from the page
   280  				log.Printf("[WARN] invalid version found for %q: %s", name, err)
   281  				continue
   282  			}
   283  
   284  			versions = append(versions, v)
   285  		}
   286  	}
   287  
   288  	return versions
   289  }