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 }