]>
Commit | Line | Data |
---|---|---|
c680a8e1 RS |
1 | package discovery |
2 | ||
3 | import ( | |
4 | "errors" | |
5 | "fmt" | |
15c0b25d | 6 | "io" |
c680a8e1 RS |
7 | "io/ioutil" |
8 | "log" | |
9 | "net/http" | |
10 | "os" | |
15c0b25d | 11 | "path/filepath" |
c680a8e1 RS |
12 | "runtime" |
13 | "strconv" | |
14 | "strings" | |
15 | ||
107c1cdb | 16 | "github.com/hashicorp/errwrap" |
c680a8e1 RS |
17 | getter "github.com/hashicorp/go-getter" |
18 | multierror "github.com/hashicorp/go-multierror" | |
15c0b25d | 19 | "github.com/hashicorp/terraform/httpclient" |
107c1cdb ND |
20 | "github.com/hashicorp/terraform/registry" |
21 | "github.com/hashicorp/terraform/registry/regsrc" | |
22 | "github.com/hashicorp/terraform/registry/response" | |
23 | "github.com/hashicorp/terraform/svchost/disco" | |
24 | "github.com/hashicorp/terraform/tfdiags" | |
25 | tfversion "github.com/hashicorp/terraform/version" | |
15c0b25d | 26 | "github.com/mitchellh/cli" |
c680a8e1 RS |
27 | ) |
28 | ||
107c1cdb | 29 | // Releases are located by querying the terraform registry. |
c680a8e1 RS |
30 | |
31 | const protocolVersionHeader = "x-terraform-protocol-version" | |
32 | ||
15c0b25d AP |
33 | var httpClient *http.Client |
34 | ||
107c1cdb ND |
35 | var errVersionNotFound = errors.New("version not found") |
36 | ||
15c0b25d AP |
37 | func init() { |
38 | httpClient = httpclient.New() | |
39 | ||
40 | httpGetter := &getter.HttpGetter{ | |
41 | Client: httpClient, | |
42 | Netrc: true, | |
43 | } | |
44 | ||
45 | getter.Getters["http"] = httpGetter | |
46 | getter.Getters["https"] = httpGetter | |
47 | } | |
c680a8e1 RS |
48 | |
49 | // An Installer maintains a local cache of plugins by downloading plugins | |
50 | // from an online repository. | |
51 | type Installer interface { | |
107c1cdb | 52 | Get(name string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error) |
c680a8e1 RS |
53 | PurgeUnused(used map[string]PluginMeta) (removed PluginMetaSet, err error) |
54 | } | |
55 | ||
56 | // ProviderInstaller is an Installer implementation that knows how to | |
57 | // download Terraform providers from the official HashiCorp releases service | |
58 | // into a local directory. The files downloaded are compliant with the | |
59 | // naming scheme expected by FindPlugins, so the target directory of a | |
60 | // provider installer can be used as one of several plugin discovery sources. | |
61 | type ProviderInstaller struct { | |
62 | Dir string | |
63 | ||
15c0b25d AP |
64 | // Cache is used to access and update a local cache of plugins if non-nil. |
65 | // Can be nil to disable caching. | |
66 | Cache PluginCache | |
67 | ||
c680a8e1 RS |
68 | PluginProtocolVersion uint |
69 | ||
70 | // OS and Arch specify the OS and architecture that should be used when | |
71 | // installing plugins. These use the same labels as the runtime.GOOS and | |
72 | // runtime.GOARCH variables respectively, and indeed the values of these | |
73 | // are used as defaults if either of these is the empty string. | |
74 | OS string | |
75 | Arch string | |
76 | ||
77 | // Skip checksum and signature verification | |
78 | SkipVerify bool | |
15c0b25d AP |
79 | |
80 | Ui cli.Ui // Ui for output | |
107c1cdb ND |
81 | |
82 | // Services is a required *disco.Disco, which may have services and | |
83 | // credentials pre-loaded. | |
84 | Services *disco.Disco | |
85 | ||
86 | // registry client | |
87 | registry *registry.Client | |
c680a8e1 RS |
88 | } |
89 | ||
90 | // Get is part of an implementation of type Installer, and attempts to download | |
91 | // and install a Terraform provider matching the given constraints. | |
92 | // | |
93 | // This method may return one of a number of sentinel errors from this | |
94 | // package to indicate issues that are likely to be resolvable via user action: | |
95 | // | |
96 | // ErrorNoSuchProvider: no provider with the given name exists in the repository. | |
97 | // ErrorNoSuitableVersion: the provider exists but no available version matches constraints. | |
98 | // ErrorNoVersionCompatible: a plugin was found within the constraints but it is | |
99 | // incompatible with the current Terraform version. | |
100 | // | |
101 | // These errors should be recognized and handled as special cases by the caller | |
102 | // to present a suitable user-oriented error message. | |
103 | // | |
104 | // All other errors indicate an internal problem that is likely _not_ solvable | |
105 | // through user action, or at least not within Terraform's scope. Error messages | |
106 | // are produced under the assumption that if presented to the user they will | |
107 | // be presented alongside context about what is being installed, and thus the | |
108 | // error messages do not redundantly include such information. | |
107c1cdb ND |
109 | func (i *ProviderInstaller) Get(provider string, req Constraints) (PluginMeta, tfdiags.Diagnostics, error) { |
110 | var diags tfdiags.Diagnostics | |
111 | ||
112 | // a little bit of initialization. | |
113 | if i.OS == "" { | |
114 | i.OS = runtime.GOOS | |
115 | } | |
116 | if i.Arch == "" { | |
117 | i.Arch = runtime.GOARCH | |
118 | } | |
119 | if i.registry == nil { | |
120 | i.registry = registry.NewClient(i.Services, nil) | |
121 | } | |
122 | ||
123 | // get a full listing of versions for the requested provider | |
124 | allVersions, err := i.listProviderVersions(provider) | |
125 | ||
c680a8e1 RS |
126 | // TODO: return multiple errors |
127 | if err != nil { | |
107c1cdb ND |
128 | log.Printf("[DEBUG] %s", err) |
129 | if registry.IsServiceUnreachable(err) { | |
130 | registryHost, err := i.hostname() | |
131 | if err == nil && registryHost == regsrc.PublicRegistryHost.Raw { | |
132 | return PluginMeta{}, diags, ErrorPublicRegistryUnreachable | |
133 | } | |
134 | return PluginMeta{}, diags, ErrorServiceUnreachable | |
135 | } | |
136 | if registry.IsServiceNotProvided(err) { | |
137 | return PluginMeta{}, diags, err | |
138 | } | |
139 | return PluginMeta{}, diags, ErrorNoSuchProvider | |
c680a8e1 RS |
140 | } |
141 | ||
107c1cdb ND |
142 | // Add any warnings from the response to diags |
143 | for _, warning := range allVersions.Warnings { | |
144 | hostname, err := i.hostname() | |
145 | if err != nil { | |
146 | return PluginMeta{}, diags, err | |
147 | } | |
148 | diag := tfdiags.SimpleWarning(fmt.Sprintf("%s: %s", hostname, warning)) | |
149 | diags = diags.Append(diag) | |
c680a8e1 RS |
150 | } |
151 | ||
107c1cdb ND |
152 | if len(allVersions.Versions) == 0 { |
153 | return PluginMeta{}, diags, ErrorNoSuitableVersion | |
154 | } | |
155 | providerSource := allVersions.ID | |
156 | ||
157 | // Filter the list of plugin versions to those which meet the version constraints | |
158 | versions := allowedVersions(allVersions, req) | |
c680a8e1 | 159 | if len(versions) == 0 { |
107c1cdb | 160 | return PluginMeta{}, diags, ErrorNoSuitableVersion |
c680a8e1 RS |
161 | } |
162 | ||
107c1cdb ND |
163 | // sort them newest to oldest. The newest version wins! |
164 | response.ProviderVersionCollection(versions).Sort() | |
c680a8e1 | 165 | |
107c1cdb ND |
166 | // if the chosen provider version does not support the requested platform, |
167 | // filter the list of acceptable versions to those that support that platform | |
168 | if err := i.checkPlatformCompatibility(versions[0]); err != nil { | |
169 | versions = i.platformCompatibleVersions(versions) | |
170 | if len(versions) == 0 { | |
171 | return PluginMeta{}, diags, ErrorNoVersionCompatibleWithPlatform | |
172 | } | |
15c0b25d AP |
173 | } |
174 | ||
107c1cdb ND |
175 | // we now have a winning platform-compatible version |
176 | versionMeta := versions[0] | |
177 | v := VersionStr(versionMeta.Version).MustParse() | |
c680a8e1 | 178 | |
107c1cdb ND |
179 | // check protocol compatibility |
180 | if err := i.checkPluginProtocol(versionMeta); err != nil { | |
181 | closestMatch, err := i.findClosestProtocolCompatibleVersion(allVersions.Versions) | |
182 | if err != nil { | |
183 | // No operation here if we can't find a version with compatible protocol | |
184 | return PluginMeta{}, diags, err | |
185 | } | |
c680a8e1 | 186 | |
107c1cdb ND |
187 | // Prompt version suggestion to UI based on closest protocol match |
188 | var errMsg string | |
189 | closestVersion := VersionStr(closestMatch.Version).MustParse() | |
190 | if v.NewerThan(closestVersion) { | |
191 | errMsg = providerProtocolTooNew | |
192 | } else { | |
193 | errMsg = providerProtocolTooOld | |
c680a8e1 RS |
194 | } |
195 | ||
107c1cdb ND |
196 | constraintStr := req.String() |
197 | if constraintStr == "" { | |
198 | constraintStr = "(any version)" | |
199 | } | |
c680a8e1 | 200 | |
107c1cdb ND |
201 | return PluginMeta{}, diags, errwrap.Wrap(ErrorVersionIncompatible, fmt.Errorf(fmt.Sprintf( |
202 | errMsg, provider, v.String(), tfversion.String(), | |
203 | closestVersion.String(), closestVersion.MinorUpgradeConstraintStr(), constraintStr))) | |
204 | } | |
c680a8e1 | 205 | |
107c1cdb ND |
206 | downloadURLs, err := i.listProviderDownloadURLs(providerSource, versionMeta.Version) |
207 | providerURL := downloadURLs.DownloadURL | |
208 | ||
209 | if !i.SkipVerify { | |
210 | // Terraform verifies the integrity of a provider release before downloading | |
211 | // the plugin binary. The digital signature (SHA256SUMS.sig) on the | |
212 | // release distribution (SHA256SUMS) is verified with the public key of the | |
213 | // publisher provided in the Terraform Registry response, ensuring that | |
214 | // everything is as intended by the publisher. The checksum of the provider | |
215 | // plugin is expected in the SHA256SUMS file and is double checked to match | |
216 | // the checksum of the original published release to the Registry. This | |
217 | // enforces immutability of releases between the Registry and the plugin's | |
218 | // host location. Lastly, the integrity of the binary is verified upon | |
219 | // download matches the Registry and signed checksum. | |
220 | sha256, err := i.getProviderChecksum(downloadURLs) | |
221 | if err != nil { | |
222 | return PluginMeta{}, diags, err | |
223 | } | |
c680a8e1 | 224 | |
107c1cdb ND |
225 | // add the checksum parameter for go-getter to verify the download for us. |
226 | if sha256 != "" { | |
227 | providerURL = providerURL + "?checksum=sha256:" + sha256 | |
c680a8e1 | 228 | } |
107c1cdb ND |
229 | } |
230 | ||
231 | printedProviderName := fmt.Sprintf("%q (%s)", provider, providerSource) | |
232 | i.Ui.Info(fmt.Sprintf("- Downloading plugin for provider %s %s...", printedProviderName, versionMeta.Version)) | |
233 | log.Printf("[DEBUG] getting provider %s version %q", printedProviderName, versionMeta.Version) | |
234 | err = i.install(provider, v, providerURL) | |
235 | if err != nil { | |
236 | return PluginMeta{}, diags, err | |
237 | } | |
238 | ||
239 | // Find what we just installed | |
240 | // (This is weird, because go-getter doesn't directly return | |
241 | // information about what was extracted, and we just extracted | |
242 | // the archive directly into a shared dir here.) | |
243 | log.Printf("[DEBUG] looking for the %s %s plugin we just installed", provider, versionMeta.Version) | |
244 | metas := FindPlugins("provider", []string{i.Dir}) | |
245 | log.Printf("[DEBUG] all plugins found %#v", metas) | |
246 | metas, _ = metas.ValidateVersions() | |
247 | metas = metas.WithName(provider).WithVersion(v) | |
248 | log.Printf("[DEBUG] filtered plugins %#v", metas) | |
249 | if metas.Count() == 0 { | |
250 | // This should never happen. Suggests that the release archive | |
251 | // contains an executable file whose name doesn't match the | |
252 | // expected convention. | |
253 | return PluginMeta{}, diags, fmt.Errorf( | |
254 | "failed to find installed plugin version %s; this is a bug in Terraform and should be reported", | |
255 | versionMeta.Version, | |
256 | ) | |
257 | } | |
c680a8e1 | 258 | |
107c1cdb ND |
259 | if metas.Count() > 1 { |
260 | // This should also never happen, and suggests that a | |
261 | // particular version was re-released with a different | |
262 | // executable filename. We consider releases as immutable, so | |
263 | // this is an error. | |
264 | return PluginMeta{}, diags, fmt.Errorf( | |
265 | "multiple plugins installed for version %s; this is a bug in Terraform and should be reported", | |
266 | versionMeta.Version, | |
267 | ) | |
c680a8e1 RS |
268 | } |
269 | ||
107c1cdb ND |
270 | // By now we know we have exactly one meta, and so "Newest" will |
271 | // return that one. | |
272 | return metas.Newest(), diags, nil | |
c680a8e1 RS |
273 | } |
274 | ||
15c0b25d AP |
275 | func (i *ProviderInstaller) install(provider string, version Version, url string) error { |
276 | if i.Cache != nil { | |
277 | log.Printf("[DEBUG] looking for provider %s %s in plugin cache", provider, version) | |
278 | cached := i.Cache.CachedPluginPath("provider", provider, version) | |
279 | if cached == "" { | |
280 | log.Printf("[DEBUG] %s %s not yet in cache, so downloading %s", provider, version, url) | |
281 | err := getter.Get(i.Cache.InstallDir(), url) | |
282 | if err != nil { | |
283 | return err | |
284 | } | |
285 | // should now be in cache | |
286 | cached = i.Cache.CachedPluginPath("provider", provider, version) | |
287 | if cached == "" { | |
288 | // should never happen if the getter is behaving properly | |
289 | // and the plugins are packaged properly. | |
290 | return fmt.Errorf("failed to find downloaded plugin in cache %s", i.Cache.InstallDir()) | |
291 | } | |
292 | } | |
293 | ||
294 | // Link or copy the cached binary into our install dir so the | |
295 | // normal resolution machinery can find it. | |
296 | filename := filepath.Base(cached) | |
297 | targetPath := filepath.Join(i.Dir, filename) | |
107c1cdb ND |
298 | // check if the target dir exists, and create it if not |
299 | var err error | |
300 | if _, StatErr := os.Stat(i.Dir); os.IsNotExist(StatErr) { | |
301 | err = os.MkdirAll(i.Dir, 0700) | |
302 | } | |
303 | if err != nil { | |
304 | return err | |
305 | } | |
15c0b25d AP |
306 | |
307 | log.Printf("[DEBUG] installing %s %s to %s from local cache %s", provider, version, targetPath, cached) | |
308 | ||
309 | // Delete if we can. If there's nothing there already then no harm done. | |
310 | // This is important because we can't create a link if there's | |
311 | // already a file of the same name present. | |
312 | // (any other error here we'll catch below when we try to write here) | |
313 | os.Remove(targetPath) | |
314 | ||
315 | // We don't attempt linking on Windows because links are not | |
316 | // comprehensively supported by all tools/apps in Windows and | |
317 | // so we choose to be conservative to avoid creating any | |
318 | // weird issues for Windows users. | |
319 | linkErr := errors.New("link not supported for Windows") // placeholder error, never actually returned | |
320 | if runtime.GOOS != "windows" { | |
321 | // Try hard linking first. Hard links are preferable because this | |
322 | // creates a self-contained directory that doesn't depend on the | |
323 | // cache after install. | |
324 | linkErr = os.Link(cached, targetPath) | |
325 | ||
326 | // If that failed, try a symlink. This _does_ depend on the cache | |
327 | // after install, so the user must manage the cache more carefully | |
328 | // in this case, but avoids creating redundant copies of the | |
329 | // plugins on disk. | |
330 | if linkErr != nil { | |
331 | linkErr = os.Symlink(cached, targetPath) | |
332 | } | |
333 | } | |
334 | ||
335 | // If we still have an error then we'll try a copy as a fallback. | |
336 | // In this case either the OS is Windows or the target filesystem | |
337 | // can't support symlinks. | |
338 | if linkErr != nil { | |
339 | srcFile, err := os.Open(cached) | |
340 | if err != nil { | |
341 | return fmt.Errorf("failed to open cached plugin %s: %s", cached, err) | |
342 | } | |
343 | defer srcFile.Close() | |
344 | ||
345 | destFile, err := os.OpenFile(targetPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm) | |
346 | if err != nil { | |
347 | return fmt.Errorf("failed to create %s: %s", targetPath, err) | |
348 | } | |
349 | ||
350 | _, err = io.Copy(destFile, srcFile) | |
351 | if err != nil { | |
352 | destFile.Close() | |
353 | return fmt.Errorf("failed to copy cached plugin from %s to %s: %s", cached, targetPath, err) | |
354 | } | |
355 | ||
356 | err = destFile.Close() | |
357 | if err != nil { | |
358 | return fmt.Errorf("error creating %s: %s", targetPath, err) | |
359 | } | |
360 | } | |
361 | ||
362 | // One way or another, by the time we get here we should have either | |
363 | // a link or a copy of the cached plugin within i.Dir, as expected. | |
364 | } else { | |
365 | log.Printf("[DEBUG] plugin cache is disabled, so downloading %s %s from %s", provider, version, url) | |
366 | err := getter.Get(i.Dir, url) | |
367 | if err != nil { | |
368 | return err | |
369 | } | |
370 | } | |
15c0b25d AP |
371 | return nil |
372 | } | |
373 | ||
c680a8e1 RS |
374 | func (i *ProviderInstaller) PurgeUnused(used map[string]PluginMeta) (PluginMetaSet, error) { |
375 | purge := make(PluginMetaSet) | |
376 | ||
377 | present := FindPlugins("provider", []string{i.Dir}) | |
378 | for meta := range present { | |
379 | chosen, ok := used[meta.Name] | |
380 | if !ok { | |
381 | purge.Add(meta) | |
382 | } | |
383 | if chosen.Path != meta.Path { | |
384 | purge.Add(meta) | |
385 | } | |
386 | } | |
387 | ||
388 | removed := make(PluginMetaSet) | |
389 | var errs error | |
390 | for meta := range purge { | |
391 | path := meta.Path | |
392 | err := os.Remove(path) | |
393 | if err != nil { | |
394 | errs = multierror.Append(errs, fmt.Errorf( | |
395 | "failed to remove unused provider plugin %s: %s", | |
396 | path, err, | |
397 | )) | |
398 | } else { | |
399 | removed.Add(meta) | |
400 | } | |
401 | } | |
402 | ||
403 | return removed, errs | |
404 | } | |
405 | ||
107c1cdb ND |
406 | func (i *ProviderInstaller) getProviderChecksum(resp *response.TerraformProviderPlatformLocation) (string, error) { |
407 | // Get SHA256SUMS file. | |
408 | shasums, err := getFile(resp.ShasumsURL) | |
409 | if err != nil { | |
410 | log.Printf("[ERROR] error fetching checksums from %q: %s", resp.ShasumsURL, err) | |
411 | return "", ErrorMissingChecksumVerification | |
412 | } | |
c680a8e1 | 413 | |
107c1cdb ND |
414 | // Get SHA256SUMS.sig file. |
415 | signature, err := getFile(resp.ShasumsSignatureURL) | |
416 | if err != nil { | |
417 | log.Printf("[ERROR] error fetching checksums signature from %q: %s", resp.ShasumsSignatureURL, err) | |
418 | return "", ErrorSignatureVerification | |
c680a8e1 | 419 | } |
107c1cdb ND |
420 | |
421 | // Verify the GPG signature returned from the Registry. | |
422 | asciiArmor := resp.SigningKeys.GPGASCIIArmor() | |
423 | signer, err := verifySig(shasums, signature, asciiArmor) | |
424 | if err != nil { | |
425 | log.Printf("[ERROR] error verifying signature: %s", err) | |
426 | return "", ErrorSignatureVerification | |
c680a8e1 | 427 | } |
c680a8e1 | 428 | |
107c1cdb ND |
429 | // Also verify the GPG signature against the HashiCorp public key. This is |
430 | // a temporary additional check until a more robust key verification | |
431 | // process is added in a future release. | |
432 | _, err = verifySig(shasums, signature, HashicorpPublicKey) | |
433 | if err != nil { | |
434 | log.Printf("[ERROR] error verifying signature against HashiCorp public key: %s", err) | |
435 | return "", ErrorSignatureVerification | |
436 | } | |
c680a8e1 | 437 | |
107c1cdb ND |
438 | // Display identity for GPG key which succeeded verifying the signature. |
439 | // This could also be used to display to the user with i.Ui.Info(). | |
440 | identities := []string{} | |
441 | for k := range signer.Identities { | |
442 | identities = append(identities, k) | |
443 | } | |
444 | identity := strings.Join(identities, ", ") | |
445 | log.Printf("[DEBUG] verified GPG signature with key from %s", identity) | |
446 | ||
447 | // Extract checksum for this os/arch platform binary and verify against Registry | |
448 | checksum := checksumForFile(shasums, resp.Filename) | |
449 | if checksum == "" { | |
450 | log.Printf("[ERROR] missing checksum for %s from source %s", resp.Filename, resp.ShasumsURL) | |
451 | return "", ErrorMissingChecksumVerification | |
452 | } else if checksum != resp.Shasum { | |
453 | log.Printf("[ERROR] unexpected checksum for %s from source %q", resp.Filename, resp.ShasumsURL) | |
454 | return "", ErrorChecksumVerification | |
455 | } | |
c680a8e1 | 456 | |
107c1cdb | 457 | return checksum, nil |
c680a8e1 RS |
458 | } |
459 | ||
107c1cdb ND |
460 | func (i *ProviderInstaller) hostname() (string, error) { |
461 | provider := regsrc.NewTerraformProvider("", i.OS, i.Arch) | |
462 | svchost, err := provider.SvcHost() | |
c680a8e1 RS |
463 | if err != nil { |
464 | return "", err | |
465 | } | |
466 | ||
107c1cdb | 467 | return svchost.ForDisplay(), nil |
c680a8e1 RS |
468 | } |
469 | ||
107c1cdb ND |
470 | // list all versions available for the named provider |
471 | func (i *ProviderInstaller) listProviderVersions(name string) (*response.TerraformProviderVersions, error) { | |
472 | provider := regsrc.NewTerraformProvider(name, i.OS, i.Arch) | |
473 | versions, err := i.registry.TerraformProviderVersions(provider) | |
474 | return versions, err | |
475 | } | |
c680a8e1 | 476 | |
107c1cdb ND |
477 | func (i *ProviderInstaller) listProviderDownloadURLs(name, version string) (*response.TerraformProviderPlatformLocation, error) { |
478 | urls, err := i.registry.TerraformProviderLocation(regsrc.NewTerraformProvider(name, i.OS, i.Arch), version) | |
479 | if urls == nil { | |
480 | return nil, fmt.Errorf("No download urls found for provider %s", name) | |
c680a8e1 | 481 | } |
107c1cdb ND |
482 | return urls, err |
483 | } | |
484 | ||
485 | // findClosestProtocolCompatibleVersion searches for the provider version with the closest protocol match. | |
486 | // Prerelease versions are filtered. | |
487 | func (i *ProviderInstaller) findClosestProtocolCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) { | |
488 | // Loop through all the provider versions to find the earliest and latest | |
489 | // versions that match the installer protocol to then select the closest of the two | |
490 | var latest, earliest *response.TerraformProviderVersion | |
491 | for _, version := range versions { | |
492 | // Prereleases are filtered and will not be suggested | |
493 | v, err := VersionStr(version.Version).Parse() | |
494 | if err != nil || v.IsPrerelease() { | |
495 | continue | |
496 | } | |
c680a8e1 | 497 | |
107c1cdb ND |
498 | if err := i.checkPluginProtocol(version); err == nil { |
499 | if earliest == nil { | |
500 | // Found the first provider version with compatible protocol | |
501 | earliest = version | |
502 | } | |
503 | // Update the latest protocol compatible version | |
504 | latest = version | |
505 | } | |
506 | } | |
507 | if earliest == nil { | |
508 | // No compatible protocol was found for any version | |
509 | return nil, ErrorNoVersionCompatible | |
c680a8e1 RS |
510 | } |
511 | ||
107c1cdb ND |
512 | // Convert protocols to comparable types |
513 | protoString := strconv.Itoa(int(i.PluginProtocolVersion)) | |
514 | protocolVersion, err := VersionStr(protoString).Parse() | |
c680a8e1 | 515 | if err != nil { |
107c1cdb | 516 | return nil, fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion) |
c680a8e1 RS |
517 | } |
518 | ||
107c1cdb | 519 | earliestVersionProtocol, err := VersionStr(earliest.Protocols[0]).Parse() |
c680a8e1 | 520 | if err != nil { |
c680a8e1 RS |
521 | return nil, err |
522 | } | |
c680a8e1 | 523 | |
107c1cdb ND |
524 | // Compare installer protocol version with the first protocol listed of the earliest match |
525 | // [A, B] where A is assumed the earliest compatible major version of the protocol pair | |
526 | if protocolVersion.NewerThan(earliestVersionProtocol) { | |
527 | // Provider protocols are too old, the closest version is the earliest compatible version | |
528 | return earliest, nil | |
c680a8e1 RS |
529 | } |
530 | ||
107c1cdb ND |
531 | // Provider protocols are too new, the closest version is the latest compatible version |
532 | return latest, nil | |
c680a8e1 RS |
533 | } |
534 | ||
107c1cdb ND |
535 | func (i *ProviderInstaller) checkPluginProtocol(versionMeta *response.TerraformProviderVersion) error { |
536 | // TODO: should this be a different error? We should probably differentiate between | |
537 | // no compatible versions and no protocol versions listed at all | |
538 | if len(versionMeta.Protocols) == 0 { | |
539 | return fmt.Errorf("no plugin protocol versions listed") | |
540 | } | |
541 | ||
542 | protoString := strconv.Itoa(int(i.PluginProtocolVersion)) | |
543 | protocolVersion, err := VersionStr(protoString).Parse() | |
c680a8e1 | 544 | if err != nil { |
107c1cdb ND |
545 | return fmt.Errorf("invalid plugin protocol version: %q", i.PluginProtocolVersion) |
546 | } | |
547 | protocolConstraint, err := protocolVersion.MinorUpgradeConstraintStr().Parse() | |
548 | if err != nil { | |
549 | // This should not fail if the preceding function succeeded. | |
550 | return fmt.Errorf("invalid plugin protocol version: %q", protocolVersion.String()) | |
c680a8e1 | 551 | } |
c680a8e1 | 552 | |
107c1cdb ND |
553 | for _, p := range versionMeta.Protocols { |
554 | proPro, err := VersionStr(p).Parse() | |
555 | if err != nil { | |
556 | // invalid protocol reported by the registry. Move along. | |
557 | log.Printf("[WARN] invalid provider protocol version %q found in the registry", versionMeta.Version) | |
558 | continue | |
559 | } | |
560 | // success! | |
561 | if protocolConstraint.Allows(proPro) { | |
562 | return nil | |
c680a8e1 | 563 | } |
c680a8e1 RS |
564 | } |
565 | ||
107c1cdb ND |
566 | return ErrorNoVersionCompatible |
567 | } | |
568 | ||
569 | // REVIEWER QUESTION (again): this ends up swallowing a bunch of errors from | |
570 | // checkPluginProtocol. Do they need to be percolated up better, or would | |
571 | // debug messages would suffice in these situations? | |
572 | func (i *ProviderInstaller) findPlatformCompatibleVersion(versions []*response.TerraformProviderVersion) (*response.TerraformProviderVersion, error) { | |
573 | for _, version := range versions { | |
574 | if err := i.checkPlatformCompatibility(version); err == nil { | |
575 | return version, nil | |
576 | } | |
c680a8e1 RS |
577 | } |
578 | ||
107c1cdb ND |
579 | return nil, ErrorNoVersionCompatibleWithPlatform |
580 | } | |
c680a8e1 | 581 | |
107c1cdb ND |
582 | // platformCompatibleVersions returns a list of provider versions that are |
583 | // compatible with the requested platform. | |
584 | func (i *ProviderInstaller) platformCompatibleVersions(versions []*response.TerraformProviderVersion) []*response.TerraformProviderVersion { | |
585 | var v []*response.TerraformProviderVersion | |
586 | for _, version := range versions { | |
587 | if err := i.checkPlatformCompatibility(version); err == nil { | |
588 | v = append(v, version) | |
c680a8e1 RS |
589 | } |
590 | } | |
107c1cdb ND |
591 | return v |
592 | } | |
c680a8e1 | 593 | |
107c1cdb ND |
594 | func (i *ProviderInstaller) checkPlatformCompatibility(versionMeta *response.TerraformProviderVersion) error { |
595 | if len(versionMeta.Platforms) == 0 { | |
596 | return fmt.Errorf("no supported provider platforms listed") | |
597 | } | |
598 | for _, p := range versionMeta.Platforms { | |
599 | if p.Arch == i.Arch && p.OS == i.OS { | |
600 | return nil | |
601 | } | |
602 | } | |
603 | return fmt.Errorf("version %s does not support the requested platform %s_%s", versionMeta.Version, i.OS, i.Arch) | |
c680a8e1 RS |
604 | } |
605 | ||
107c1cdb ND |
606 | // take the list of available versions for a plugin, and filter out those that |
607 | // don't fit the constraints. | |
608 | func allowedVersions(available *response.TerraformProviderVersions, required Constraints) []*response.TerraformProviderVersion { | |
609 | var allowed []*response.TerraformProviderVersion | |
c680a8e1 | 610 | |
107c1cdb ND |
611 | for _, v := range available.Versions { |
612 | version, err := VersionStr(v.Version).Parse() | |
613 | if err != nil { | |
614 | log.Printf("[WARN] invalid version found for %q: %s", available.ID, err) | |
615 | continue | |
616 | } | |
617 | if required.Allows(version) { | |
618 | allowed = append(allowed, v) | |
c680a8e1 RS |
619 | } |
620 | } | |
107c1cdb | 621 | return allowed |
c680a8e1 RS |
622 | } |
623 | ||
624 | func checksumForFile(sums []byte, name string) string { | |
625 | for _, line := range strings.Split(string(sums), "\n") { | |
626 | parts := strings.Fields(line) | |
627 | if len(parts) > 1 && parts[1] == name { | |
628 | return parts[0] | |
629 | } | |
630 | } | |
631 | return "" | |
632 | } | |
633 | ||
c680a8e1 RS |
634 | func getFile(url string) ([]byte, error) { |
635 | resp, err := httpClient.Get(url) | |
636 | if err != nil { | |
637 | return nil, err | |
638 | } | |
639 | defer resp.Body.Close() | |
640 | ||
641 | if resp.StatusCode != http.StatusOK { | |
642 | return nil, fmt.Errorf("%s", resp.Status) | |
643 | } | |
644 | ||
645 | data, err := ioutil.ReadAll(resp.Body) | |
646 | if err != nil { | |
647 | return data, err | |
648 | } | |
649 | return data, nil | |
650 | } | |
15c0b25d | 651 | |
107c1cdb ND |
652 | // providerProtocolTooOld is a message sent to the CLI UI if the provider's |
653 | // supported protocol versions are too old for the user's version of terraform, | |
654 | // but an older version of the provider is compatible. | |
655 | const providerProtocolTooOld = ` | |
656 | [reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red] | |
657 | ||
658 | Provider version %s is the earliest compatible version. Select it with | |
659 | the following version constraint: | |
660 | ||
661 | version = %q | |
662 | ||
663 | Terraform checked all of the plugin versions matching the given constraint: | |
664 | %s | |
665 | ||
666 | Consult the documentation for this provider for more information on | |
667 | compatibility between provider and Terraform versions. | |
668 | ` | |
669 | ||
670 | // providerProtocolTooNew is a message sent to the CLI UI if the provider's | |
671 | // supported protocol versions are too new for the user's version of terraform, | |
672 | // and the user could either upgrade terraform or choose an older version of the | |
673 | // provider | |
674 | const providerProtocolTooNew = ` | |
675 | [reset][bold][red]Provider %q v%s is not compatible with Terraform %s.[reset][red] | |
676 | ||
677 | Provider version %s is the latest compatible version. Select it with | |
678 | the following constraint: | |
679 | ||
680 | version = %q | |
681 | ||
682 | Terraform checked all of the plugin versions matching the given constraint: | |
683 | %s | |
684 | ||
685 | Consult the documentation for this provider for more information on | |
686 | compatibility between provider and Terraform versions. | |
687 | ||
688 | Alternatively, upgrade to the latest version of Terraform for compatibility with newer provider releases. | |
689 | ` |