github.com/lymingtonprecision/terraform@v0.9.9-0.20170613092852-62acef9611a9/plugin/discovery/get.go (about)

     1  package discovery
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"net/http"
     9  	"runtime"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"golang.org/x/net/html"
    14  
    15  	cleanhttp "github.com/hashicorp/go-cleanhttp"
    16  	getter "github.com/hashicorp/go-getter"
    17  )
    18  
    19  // Releases are located by parsing the html listing from releases.hashicorp.com.
    20  //
    21  // The URL for releases follows the pattern:
    22  //    https://releases.hashicorp.com/terraform-provider-name/<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
    23  //
    24  // The plugin protocol version will be saved with the release and returned in
    25  // the header X-TERRAFORM_PROTOCOL_VERSION.
    26  
    27  const protocolVersionHeader = "x-terraform-protocol-version"
    28  
    29  var releaseHost = "https://releases.hashicorp.com"
    30  
    31  var httpClient = cleanhttp.DefaultClient()
    32  
    33  // Plugins are referred to by the short name, but all URLs and files will use
    34  // the full name prefixed with terraform-<plugin_type>-
    35  func providerName(name string) string {
    36  	return "terraform-provider-" + name
    37  }
    38  
    39  // providerVersionsURL returns the path to the released versions directory for the provider:
    40  // https://releases.hashicorp.com/terraform-provider-name/
    41  func providerVersionsURL(name string) string {
    42  	return releaseHost + "/" + providerName(name) + "/"
    43  }
    44  
    45  // providerURL returns the full path to the provider file, using the current OS
    46  // and ARCH:
    47  // .../terraform-provider-name_<x.y.z>/terraform-provider-name_<x.y.z>_<os>_<arch>.<ext>
    48  func providerURL(name, version string) string {
    49  	fileName := fmt.Sprintf("%s_%s_%s_%s.zip", providerName(name), version, runtime.GOOS, runtime.GOARCH)
    50  	u := fmt.Sprintf("%s%s/%s", providerVersionsURL(name), version, fileName)
    51  	return u
    52  }
    53  
    54  // GetProvider fetches a provider plugin based on the version constraints, and
    55  // copies it to the dst directory.
    56  //
    57  // TODO: verify checksum and signature
    58  func GetProvider(dst, provider string, req Constraints, pluginProtocolVersion uint) error {
    59  	versions, err := listProviderVersions(provider)
    60  	// TODO: return multiple errors
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	if len(versions) == 0 {
    66  		return fmt.Errorf("no plugins found for provider %q", provider)
    67  	}
    68  
    69  	versions = allowedVersions(versions, req)
    70  	if len(versions) == 0 {
    71  		return fmt.Errorf("no version of %q available that fulfills constraints %s", provider, req)
    72  	}
    73  
    74  	// sort them newest to oldest
    75  	Versions(versions).Sort()
    76  
    77  	// take the first matching plugin we find
    78  	for _, v := range versions {
    79  		url := providerURL(provider, v.String())
    80  		log.Printf("[DEBUG] fetching provider info for %s version %s", provider, v)
    81  		if checkPlugin(url, pluginProtocolVersion) {
    82  			log.Printf("[DEBUG] getting provider %q version %q at %s", provider, v, url)
    83  			return getter.Get(dst, url)
    84  		}
    85  
    86  		log.Printf("[INFO] incompatible ProtocolVersion for %s version %s", provider, v)
    87  	}
    88  
    89  	return fmt.Errorf("no versions of %q compatible with the plugin ProtocolVersion", provider)
    90  }
    91  
    92  // Return the plugin version by making a HEAD request to the provided url
    93  func checkPlugin(url string, pluginProtocolVersion uint) bool {
    94  	resp, err := httpClient.Head(url)
    95  	if err != nil {
    96  		log.Printf("[ERROR] error fetching plugin headers: %s", err)
    97  		return false
    98  	}
    99  
   100  	if resp.StatusCode != http.StatusOK {
   101  		log.Println("[ERROR] non-200 status fetching plugin headers:", resp.Status)
   102  		return false
   103  	}
   104  
   105  	proto := resp.Header.Get(protocolVersionHeader)
   106  	if proto == "" {
   107  		log.Printf("[WARNING] missing %s from: %s", protocolVersionHeader, url)
   108  		return false
   109  	}
   110  
   111  	protoVersion, err := strconv.Atoi(proto)
   112  	if err != nil {
   113  		log.Printf("[ERROR] invalid ProtocolVersion: %s", proto)
   114  		return false
   115  	}
   116  
   117  	return protoVersion == int(pluginProtocolVersion)
   118  }
   119  
   120  var errVersionNotFound = errors.New("version not found")
   121  
   122  // take the list of available versions for a plugin, and filter out those that
   123  // don't fit the constraints.
   124  func allowedVersions(available []Version, required Constraints) []Version {
   125  	var allowed []Version
   126  
   127  	for _, v := range available {
   128  		if required.Allows(v) {
   129  			allowed = append(allowed, v)
   130  		}
   131  	}
   132  
   133  	return allowed
   134  }
   135  
   136  // list the version available for the named plugin
   137  func listProviderVersions(name string) ([]Version, error) {
   138  	versions, err := listPluginVersions(providerVersionsURL(name))
   139  	if err != nil {
   140  		return nil, fmt.Errorf("failed to fetch versions for provider %q: %s", name, err)
   141  	}
   142  	return versions, nil
   143  }
   144  
   145  // return a list of the plugin versions at the given URL
   146  func listPluginVersions(url string) ([]Version, error) {
   147  	resp, err := httpClient.Get(url)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	defer resp.Body.Close()
   152  
   153  	if resp.StatusCode != http.StatusOK {
   154  		body, _ := ioutil.ReadAll(resp.Body)
   155  		log.Printf("[ERROR] failed to fetch plugin versions from %s\n%s\n%s", url, resp.Status, body)
   156  		return nil, errors.New(resp.Status)
   157  	}
   158  
   159  	body, err := html.Parse(resp.Body)
   160  	if err != nil {
   161  		log.Fatal(err)
   162  	}
   163  
   164  	names := []string{}
   165  
   166  	// all we need to do is list links on the directory listing page that look like plugins
   167  	var f func(*html.Node)
   168  	f = func(n *html.Node) {
   169  		if n.Type == html.ElementNode && n.Data == "a" {
   170  			c := n.FirstChild
   171  			if c != nil && c.Type == html.TextNode && strings.HasPrefix(c.Data, "terraform-") {
   172  				names = append(names, c.Data)
   173  				return
   174  			}
   175  		}
   176  		for c := n.FirstChild; c != nil; c = c.NextSibling {
   177  			f(c)
   178  		}
   179  	}
   180  	f(body)
   181  
   182  	return versionsFromNames(names), nil
   183  }
   184  
   185  // parse the list of directory names into a sorted list of available versions
   186  func versionsFromNames(names []string) []Version {
   187  	var versions []Version
   188  	for _, name := range names {
   189  		parts := strings.SplitN(name, "_", 2)
   190  		if len(parts) == 2 && parts[1] != "" {
   191  			v, err := VersionStr(parts[1]).Parse()
   192  			if err != nil {
   193  				// filter invalid versions scraped from the page
   194  				log.Printf("[WARN] invalid version found for %q: %s", name, err)
   195  				continue
   196  			}
   197  
   198  			versions = append(versions, v)
   199  		}
   200  	}
   201  
   202  	return versions
   203  }