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 }