github.com/hashicorp/packer@v1.14.3/packer/plugin-getter/plugins.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package plugingetter 5 6 import ( 7 "archive/zip" 8 "bytes" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "io/fs" 15 "log" 16 "os" 17 "os/exec" 18 "path" 19 "path/filepath" 20 "regexp" 21 "sort" 22 "strconv" 23 "strings" 24 "time" 25 26 "github.com/hashicorp/go-multierror" 27 goversion "github.com/hashicorp/go-version" 28 pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" 29 "github.com/hashicorp/packer-plugin-sdk/tmp" 30 "github.com/hashicorp/packer/hcl2template/addrs" 31 "golang.org/x/mod/semver" 32 ) 33 34 const JSONExtension = ".json" 35 36 var HTTPFailure = errors.New("http call failed to releases.hashicorp.com failed") 37 38 type ManifestMeta struct { 39 Metadata Metadata `json:"metadata"` 40 } 41 type Metadata struct { 42 ProtocolVersion string `json:"protocol_version"` 43 } 44 45 type Requirements []*Requirement 46 47 // Requirement describes a required plugin and how it is installed. Usually a list 48 // of required plugins is generated from a config file. From it we check what 49 // is actually installed and what needs to happen to get in the desired state. 50 type Requirement struct { 51 // Plugin accessor as defined in the config file. 52 // For Packer, using : 53 // required_plugins { amazon = {...} } 54 // Will set Accessor to `amazon`. 55 Accessor string 56 57 // Something like github.com/hashicorp/packer-plugin-amazon, from the 58 // previous example. 59 Identifier *addrs.Plugin 60 61 // VersionConstraints as defined by user. Empty ( to be avoided ) means 62 // highest found version. 63 VersionConstraints goversion.Constraints 64 } 65 66 type BinaryInstallationOptions struct { 67 // The API version with which to check remote compatibility 68 // 69 // They're generally extracted from the SDK since it's what Packer Core 70 // supports as far as the protocol goes 71 APIVersionMajor, APIVersionMinor string 72 // OS and ARCH usually should be runtime.GOOS and runtime.ARCH, they allow 73 // to pick the correct binary. 74 OS, ARCH string 75 76 // Ext is ".exe" on windows 77 Ext string 78 79 Checksummers []Checksummer 80 81 // ReleasesOnly may be set by commands like validate or build, and 82 // forces Packer to not consider plugin pre-releases. 83 ReleasesOnly bool 84 } 85 86 type ListInstallationsOptions struct { 87 // The directory in which to look for when installing plugins 88 PluginDirectory string 89 90 BinaryInstallationOptions 91 } 92 93 // RateLimitError is returned when a getter is being rate limited. 94 type RateLimitError struct { 95 SetableEnvVar string 96 ResetTime time.Time 97 Err error 98 } 99 100 func (rlerr *RateLimitError) Error() string { 101 s := fmt.Sprintf("Plugin host rate limited the plugin getter. Try again in %s.\n", time.Until(rlerr.ResetTime)) 102 if rlerr.SetableEnvVar != "" { 103 s += fmt.Sprintf("HINT: Set the %s env var with a token to get more requests.\n", rlerr.SetableEnvVar) 104 } 105 s += rlerr.Err.Error() 106 return s 107 } 108 109 // PrereleaseInstallError is returned when a getter encounters the install of a pre-release version. 110 type PrereleaseInstallError struct { 111 PluginSrc string 112 Err error 113 } 114 115 func (e *PrereleaseInstallError) Error() string { 116 var s strings.Builder 117 s.WriteString(e.Err.Error() + "\n") 118 s.WriteString("Remote installation of pre-release plugin versions is unsupported.\n") 119 s.WriteString("This is likely an upstream issue, which should be reported.\n") 120 s.WriteString("If you require this specific version of the plugin, download the binary and install it manually.\n") 121 s.WriteString("\npacker plugins install --path '<plugin_binary>' " + e.PluginSrc) 122 return s.String() 123 } 124 125 // ContinuableInstallError describe a failed getter install that is 126 // capable of falling back to next available version. 127 type ContinuableInstallError struct { 128 Err error 129 } 130 131 func (e *ContinuableInstallError) Error() string { 132 return fmt.Sprintf("Continuing to next available version: %s", e.Err) 133 } 134 135 func (pr Requirement) FilenamePrefix() string { 136 if pr.Identifier == nil { 137 return "packer-plugin-" 138 } 139 140 return "packer-plugin-" + pr.Identifier.Name() + "_" 141 } 142 143 func (opts BinaryInstallationOptions) FilenameSuffix() string { 144 return "_" + opts.OS + "_" + opts.ARCH + opts.Ext 145 } 146 147 // getPluginBinaries lists the plugin binaries installed locally. 148 // 149 // Each plugin binary must be in the right hierarchy (not root) and has to be 150 // conforming to the packer-plugin-<name>_<version>_<API>_<os>_<arch> convention. 151 func (pr Requirement) getPluginBinaries(opts ListInstallationsOptions) ([]string, error) { 152 var matches []string 153 154 rootdir := opts.PluginDirectory 155 if pr.Identifier != nil { 156 rootdir = filepath.Join(rootdir, path.Dir(pr.Identifier.Source)) 157 } 158 159 if _, err := os.Lstat(rootdir); err != nil { 160 log.Printf("Directory %q does not exist, the plugin likely isn't installed locally yet.", rootdir) 161 return matches, nil 162 } 163 164 err := filepath.WalkDir(rootdir, func(path string, d fs.DirEntry, err error) error { 165 if err != nil { 166 return err 167 } 168 169 // No need to inspect directory entries, we can continue walking 170 if d.IsDir() { 171 return nil 172 } 173 174 // Skip plugins installed at root, only those in a hierarchy should be considered valid 175 if filepath.Dir(path) == opts.PluginDirectory { 176 return nil 177 } 178 179 // If the binary's name doesn't start with packer-plugin-, we skip it. 180 if !strings.HasPrefix(filepath.Base(path), pr.FilenamePrefix()) { 181 return nil 182 } 183 // If the binary's name doesn't match the expected convention, we skip it 184 if !strings.HasSuffix(filepath.Base(path), opts.FilenameSuffix()) { 185 return nil 186 } 187 188 matches = append(matches, path) 189 190 return nil 191 }) 192 if err != nil { 193 return nil, err 194 } 195 196 retMatches := make([]string, 0, len(matches)) 197 // Don't keep plugins that are nested too deep in the hierarchy 198 for _, match := range matches { 199 dir := strings.Replace(filepath.Dir(match), opts.PluginDirectory, "", 1) 200 parts := strings.FieldsFunc(dir, func(r rune) bool { 201 return r == '/' 202 }) 203 if len(parts) > 16 { 204 log.Printf("[WARN] plugin %q ignored, too many levels of depth: %d (max 16)", match, len(parts)) 205 continue 206 } 207 208 retMatches = append(retMatches, match) 209 } 210 211 return retMatches, err 212 } 213 214 // ListInstallations lists unique installed versions of plugin Requirement pr 215 // with opts as a filter. 216 // 217 // Installations are sorted by version and one binary per version is returned. 218 // Last binary detected takes precedence: in the order 'FromFolders' option. 219 // 220 // At least one opts.Checksumers must be given for a binary to be even 221 // considered. 222 func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallList, error) { 223 res := InstallList{} 224 log.Printf("[TRACE] listing potential installations for %q that match %q. %#v", pr.Identifier, pr.VersionConstraints, opts) 225 226 matches, err := pr.getPluginBinaries(opts) 227 if err != nil { 228 return nil, fmt.Errorf("ListInstallations: failed to list installed plugins: %s", err) 229 } 230 231 for _, path := range matches { 232 fname := filepath.Base(path) 233 if fname == "." { 234 continue 235 } 236 237 checksumOk := false 238 for _, checksummer := range opts.Checksummers { 239 240 cs, err := checksummer.GetCacheChecksumOfFile(path) 241 if err != nil { 242 log.Printf("[TRACE] GetChecksumOfFile(%q) failed: %v", path, err) 243 continue 244 } 245 246 if err := checksummer.ChecksumFile(cs, path); err != nil { 247 log.Printf("[TRACE] ChecksumFile(%q) failed: %v", path, err) 248 continue 249 } 250 checksumOk = true 251 break 252 } 253 if !checksumOk { 254 log.Printf("[TRACE] No checksum found for %q ignoring possibly unsafe binary", path) 255 continue 256 } 257 258 // base name could look like packer-plugin-amazon_v1.2.3_x5.1_darwin_amd64.exe 259 versionsStr := strings.TrimPrefix(fname, pr.FilenamePrefix()) 260 versionsStr = strings.TrimSuffix(versionsStr, opts.FilenameSuffix()) 261 262 if pr.Identifier == nil { 263 if idx := strings.Index(versionsStr, "_"); idx > 0 { 264 versionsStr = versionsStr[idx+1:] 265 } 266 } 267 268 describeInfo, err := GetPluginDescription(path) 269 if err != nil { 270 log.Printf("failed to call describe on %q: %s", path, err) 271 continue 272 } 273 274 // versionsStr now looks like v1.2.3_x5.1 or amazon_v1.2.3_x5.1 275 parts := strings.SplitN(versionsStr, "_", 2) 276 pluginVersionStr, protocolVersionStr := parts[0], parts[1] 277 ver, err := goversion.NewVersion(pluginVersionStr) 278 if err != nil { 279 // could not be parsed, ignoring the file 280 log.Printf("found %q with an incorrect %q version, ignoring it. %v", path, pluginVersionStr, err) 281 continue 282 } 283 284 if fmt.Sprintf("v%s", ver.String()) != pluginVersionStr { 285 log.Printf("version %q in path is non canonical, this could introduce ambiguity and is not supported, ignoring it.", pluginVersionStr) 286 continue 287 } 288 289 if ver.Prerelease() != "" && opts.ReleasesOnly { 290 log.Printf("ignoring pre-release plugin %q", path) 291 continue 292 } 293 294 if ver.Metadata() != "" { 295 log.Printf("found version %q with metadata in the name, this could introduce ambiguity and is not supported, ignoring it.", pluginVersionStr) 296 continue 297 } 298 299 descVersion, err := goversion.NewVersion(describeInfo.Version) 300 if err != nil { 301 log.Printf("malformed reported version string %q: %s, ignoring", describeInfo.Version, err) 302 continue 303 } 304 305 if ver.Compare(descVersion) != 0 { 306 log.Printf("plugin %q reported version %q while its name implies version %q, ignoring", path, describeInfo.Version, pluginVersionStr) 307 continue 308 } 309 310 preRel := descVersion.Prerelease() 311 if preRel != "" && preRel != "dev" { 312 log.Printf("invalid plugin pre-release version %q, only development or release binaries are accepted", pluginVersionStr) 313 } 314 315 // Check the API version matches between path and describe 316 if describeInfo.APIVersion != protocolVersionStr { 317 log.Printf("plugin %q reported API version %q while its name implies version %q, ignoring", path, describeInfo.APIVersion, protocolVersionStr) 318 continue 319 } 320 321 // no constraint means always pass, this will happen for implicit 322 // plugin requirements and when we list all plugins. 323 // 324 // Note: we use the raw version name here, without the pre-release 325 // suffix, as otherwise constraints reject them, which is not 326 // what we want by default. 327 if !pr.VersionConstraints.Check(ver.Core()) { 328 log.Printf("[TRACE] version %q of file %q does not match constraint %q", pluginVersionStr, path, pr.VersionConstraints.String()) 329 continue 330 } 331 332 if err := opts.CheckProtocolVersion(protocolVersionStr); err != nil { 333 log.Printf("[NOTICE] binary %s requires protocol version %s that is incompatible "+ 334 "with this version of Packer. %s", path, protocolVersionStr, err) 335 continue 336 } 337 338 res = append(res, &Installation{ 339 BinaryPath: path, 340 Version: pluginVersionStr, 341 APIVersion: describeInfo.APIVersion, 342 }) 343 } 344 345 sort.Sort(res) 346 347 return res, nil 348 } 349 350 // InstallList is a list of installed plugins (binaries) with their versions, 351 // ListInstallations should be used to get an InstallList. 352 // 353 // ListInstallations sorts binaries by version and one binary per version is 354 // returned. 355 type InstallList []*Installation 356 357 func (l InstallList) String() string { 358 v := &strings.Builder{} 359 v.Write([]byte("[")) 360 for i, inst := range l { 361 if i > 0 { 362 v.Write([]byte(",")) 363 } 364 fmt.Fprintf(v, "%v", *inst) 365 } 366 v.Write([]byte("]")) 367 return v.String() 368 } 369 370 // Len is the number of elements in the collection. 371 func (l InstallList) Len() int { 372 return len(l) 373 } 374 375 var rawPluginName = regexp.MustCompile("packer-plugin-[^_]+") 376 377 // Less reports whether the element with index i 378 // must sort before the element with index j. 379 // 380 // If both Less(i, j) and Less(j, i) are false, 381 // then the elements at index i and j are considered equal. 382 // Sort may place equal elements in any order in the final result, 383 // while Stable preserves the original input order of equal elements. 384 // 385 // Less must describe a transitive ordering: 386 // - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well. 387 // - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well. 388 // 389 // Note that floating-point comparison (the < operator on float32 or float64 values) 390 // is not a transitive ordering when not-a-number (NaN) values are involved. 391 // See Float64Slice.Less for a correct implementation for floating-point values. 392 func (l InstallList) Less(i, j int) bool { 393 lowPluginPath := l[i] 394 hiPluginPath := l[j] 395 396 lowRawPluginName := rawPluginName.FindString(path.Base(lowPluginPath.BinaryPath)) 397 hiRawPluginName := rawPluginName.FindString(path.Base(hiPluginPath.BinaryPath)) 398 399 // We group by path, then by descending order for the versions 400 // 401 // i.e. if the path are not the same, we can return the plain 402 // lexicographic order, otherwise, we'll do a semver-conscious 403 // version comparison for sorting. 404 if lowRawPluginName != hiRawPluginName { 405 return lowRawPluginName < hiRawPluginName 406 } 407 408 verCmp := semver.Compare(lowPluginPath.Version, hiPluginPath.Version) 409 if verCmp != 0 { 410 return verCmp < 0 411 } 412 413 // Ignore errors here, they are already validated when populating the InstallList 414 loAPIVer, _ := NewAPIVersion(lowPluginPath.APIVersion) 415 hiAPIVer, _ := NewAPIVersion(hiPluginPath.APIVersion) 416 417 if loAPIVer.Major != hiAPIVer.Major { 418 return loAPIVer.Major < hiAPIVer.Major 419 } 420 421 return loAPIVer.Minor < hiAPIVer.Minor 422 } 423 424 // Swap swaps the elements with indexes i and j. 425 func (l InstallList) Swap(i, j int) { 426 tmp := l[i] 427 l[i] = l[j] 428 l[j] = tmp 429 } 430 431 // Installation describes a plugin installation 432 type Installation struct { 433 // Path to where binary is installed. 434 // Ex: /usr/azr/.packer.d/plugins/github.com/hashicorp/amazon/packer-plugin-amazon_v1.2.3_darwin_amd64 435 BinaryPath string 436 437 // Version of this plugin. Ex: 438 // * v1.2.3 for packer-plugin-amazon_v1.2.3_darwin_x5 439 Version string 440 441 // API version for the plugin. Ex: 442 // * 5.0 for packer-plugin-amazon_v1.2.3_darwin_x5.0 443 // * 5.1 for packer-plugin-amazon_v1.2.3_darwin_x5.1 444 APIVersion string 445 } 446 447 // InstallOptions describes the possible options for installing the plugin that 448 // fits the plugin Requirement. 449 type InstallOptions struct { 450 // Different means to get releases, sha256 and binary files. 451 Getters []Getter 452 453 // The directory in which the plugins should be installed 454 PluginDirectory string 455 456 // Forces installation of the plugin, even if already installed. 457 Force bool 458 459 BinaryInstallationOptions 460 } 461 462 type GetOptions struct { 463 PluginRequirement *Requirement 464 465 BinaryInstallationOptions 466 467 version *goversion.Version 468 469 expectedZipFilename string 470 } 471 472 // ExpectedZipFilename is the filename of the zip we expect to find, the 473 // value is known only after parsing the checksum file file. 474 func (gp *GetOptions) ExpectedZipFilename() string { 475 return gp.expectedZipFilename 476 } 477 478 type APIVersion struct { 479 Major int 480 Minor int 481 } 482 483 func NewAPIVersion(apiVersion string) (APIVersion, error) { 484 ver := APIVersion{} 485 486 apiVersion = strings.TrimPrefix(strings.TrimSpace(apiVersion), "x") 487 parts := strings.Split(apiVersion, ".") 488 if len(parts) < 2 { 489 return ver, fmt.Errorf( 490 "Invalid remote protocol: %q, expected something like '%s.%s'", 491 apiVersion, pluginsdk.APIVersionMajor, pluginsdk.APIVersionMinor, 492 ) 493 } 494 495 vMajor, err := strconv.Atoi(parts[0]) 496 if err != nil { 497 return ver, err 498 } 499 ver.Major = vMajor 500 501 vMinor, err := strconv.Atoi(parts[1]) 502 if err != nil { 503 return ver, err 504 } 505 ver.Minor = vMinor 506 507 return ver, nil 508 } 509 510 var localAPIVersion APIVersion 511 512 func (binOpts *BinaryInstallationOptions) CheckProtocolVersion(remoteProt string) error { 513 // no protocol version check 514 if binOpts.APIVersionMajor == "" && binOpts.APIVersionMinor == "" { 515 return nil 516 } 517 518 localVersion := localAPIVersion 519 if binOpts.APIVersionMajor != pluginsdk.APIVersionMajor || 520 binOpts.APIVersionMinor != pluginsdk.APIVersionMinor { 521 var err error 522 523 localVersion, err = NewAPIVersion(fmt.Sprintf("x%s.%s", binOpts.APIVersionMajor, binOpts.APIVersionMinor)) 524 if err != nil { 525 return fmt.Errorf("Failed to parse API Version from constraints: %s", err) 526 } 527 } 528 529 remoteVersion, err := NewAPIVersion(remoteProt) 530 if err != nil { 531 return err 532 } 533 534 if localVersion.Major != remoteVersion.Major { 535 return fmt.Errorf("unsupported remote protocol MAJOR version %d. The current MAJOR protocol version is %d."+ 536 " This version of Packer can only communicate with plugins using that version", remoteVersion.Major, localVersion.Major) 537 } 538 539 if remoteVersion.Minor > localVersion.Minor { 540 return fmt.Errorf("unsupported remote protocol MINOR version %d. The supported MINOR protocol versions are version %d and below. "+ 541 "Please upgrade Packer or use an older version of the plugin if possible", remoteVersion.Minor, localVersion.Minor) 542 } 543 544 return nil 545 } 546 547 func (gp *GetOptions) Version() string { 548 return "v" + gp.version.String() 549 } 550 551 func (gp *GetOptions) VersionString() string { 552 return gp.version.String() 553 } 554 555 // A Getter helps get the appropriate files to download a binary. 556 type Getter interface { 557 // Get allows Packer to know more information about releases of a plugin in 558 // order to decide which version to install. Get behaves similarly to an 559 // HTTP server. Packer will stream responses from get in order to do what's 560 // needed. In order to minimize the amount of requests done, Packer is 561 // strict on filenames and we highly recommend on automating releases. 562 // In the future, Packer will make it possible to ship plugin getters as 563 // binaries this is why Packer streams from the output of get, which will 564 // then be a command. 565 // 566 // * 'releases', get 'releases' should return the complete list of Releases 567 // in JSON format following the format of the Release struct. It is also 568 // possible to read GetOptions to filter for a smaller response. Some 569 // getters don't. Packer will then decide the highest compatible 570 // version of the plugin to install by using the sha256 function. 571 // 572 // * 'sha256', get 'sha256' should return a SHA256SUMS txt file. It will be 573 // called with the highest possible & user allowed version from get 574 // 'releases'. Packer will check if the release has a binary matching what 575 // Packer can install and use. If so, get 'binary' will be called; 576 // otherwise, lower versions will be checked. 577 // For version 1.0.0 of the 'hashicorp/amazon' builder, the GitHub getter 578 // will fetch the following URL: 579 // https://github.com/hashicorp/packer-plugin-amazon/releases/download/v1.0.0/packer-plugin-amazon_v1.0.0_SHA256SUMS 580 // This URL can be parameterized to the following one: 581 // https://github.com/{plugin.path}/releases/download/{plugin.version}/packer-plugin-{plugin.name}_{plugin.version}_SHA256SUMS 582 // If Packer is running on Linux AMD 64, then Packer will check for the 583 // existence of a packer-plugin-amazon_v1.0.0_x5.0_linux_amd64 checksum in 584 // that file. This filename can be parameterized to the following one: 585 // packer-plugin-{plugin.name}_{plugin.version}_x{proto_ver.major}.{proto_ver._minor}_{os}_{arch} 586 // 587 // See 588 // https://github.com/hashicorp/packer-plugin-scaffolding/blob/main/.goreleaser.yml 589 // and 590 // https://www.packer.io/docs/plugins/creation#plugin-development-basics 591 // to learn how to create and automate your releases and for docs on 592 // plugin development basics. 593 // 594 // * get 'zip' is called once we know what version we want and that it is 595 // compatible with the OS and Packer. Zip expects an io stream of a zip 596 // file containing a binary. For version 1.0.0 of the 'hashicorp/amazon' 597 // builder and on darwin_amd64, the GitHub getter will fetch the 598 // following ZIP: 599 // https://github.com/hashicorp/packer-plugin-amazon/releases/download/v1.0.0/packer-plugin-amazon_v1.0.0_x5.0_darwin_amd64.zip 600 // this zip is expected to contain a 601 // packer-plugin-amazon_v1.0.0_x5.0_linux_amd64 file that will be checksum 602 // verified then copied to the correct plugin location. 603 Get(what string, opts GetOptions) (io.ReadCloser, error) 604 605 // Init this method parses the checksum file and initializes the entry 606 Init(req *Requirement, entry *ChecksumFileEntry) error 607 608 // Validate checks if OS, ARCH, and protocol version matches with the system and local version 609 Validate(opt GetOptions, expectedVersion string, installOpts BinaryInstallationOptions, entry *ChecksumFileEntry) error 610 611 // ExpectedFileName returns the expected file name for the binary, which needs to be installed 612 ExpectedFileName(pr *Requirement, version string, entry *ChecksumFileEntry, zipFileName string) string 613 } 614 615 type Release struct { 616 Version string `json:"version"` 617 } 618 619 func ParseReleases(f io.ReadCloser) ([]Release, error) { 620 var releases []Release 621 defer f.Close() 622 return releases, json.NewDecoder(f).Decode(&releases) 623 } 624 625 type ChecksumFileEntry struct { 626 Filename string `json:"filename"` 627 Checksum string `json:"checksum"` 628 Ext, BinVersion, Os, Arch string 629 ProtVersion string 630 } 631 632 func ParseChecksumFileEntries(f io.Reader) ([]ChecksumFileEntry, error) { 633 var entries []ChecksumFileEntry 634 return entries, json.NewDecoder(f).Decode(&entries) 635 } 636 637 func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) { 638 639 getters := opts.Getters 640 641 versions := goversion.Collection{} 642 var errs *multierror.Error 643 for _, getter := range getters { 644 645 releasesFile, err := getter.Get("releases", GetOptions{ 646 PluginRequirement: pr, 647 BinaryInstallationOptions: opts.BinaryInstallationOptions, 648 }) 649 if err != nil { 650 if errors.Is(err, HTTPFailure) { 651 continue 652 } 653 errs = multierror.Append(errs, err) 654 log.Printf("[TRACE] %s", err.Error()) 655 return nil, errs 656 } 657 658 releases, err := ParseReleases(releasesFile) 659 if err != nil { 660 err := fmt.Errorf("could not parse release: %w", err) 661 errs = multierror.Append(errs, err) 662 log.Printf("[TRACE] %s", err.Error()) 663 continue 664 } 665 if len(releases) == 0 { 666 err := fmt.Errorf("no release found") 667 errs = multierror.Append(errs, err) 668 log.Printf("[TRACE] %s", err.Error()) 669 continue 670 } 671 for _, release := range releases { 672 v, err := goversion.NewVersion(release.Version) 673 if err != nil { 674 err := fmt.Errorf("could not parse release version %s. %w", release.Version, err) 675 errs = multierror.Append(errs, err) 676 log.Printf("[TRACE] %s, ignoring it", err.Error()) 677 continue 678 } 679 if pr.VersionConstraints.Check(v) { 680 versions = append(versions, v) 681 } 682 } 683 if len(versions) == 0 { 684 err := fmt.Errorf("no matching version found in releases. In %v", releases) 685 errs = multierror.Append(errs, err) 686 log.Printf("[TRACE] %s", err.Error()) 687 continue 688 } 689 690 // Here we want to try every release in order, starting from the highest one 691 // that matches the requirements. The system and protocol version need to 692 // match too. 693 sort.Sort(sort.Reverse(versions)) 694 log.Printf("[DEBUG] will try to install: %s", versions) 695 696 for _, version := range versions { 697 //TODO(azr): split in its own InstallVersion(version, opts) function 698 699 outputFolder := filepath.Join( 700 // Pick last folder as it's the one with the highest priority 701 opts.PluginDirectory, 702 // add expected full path 703 filepath.Join(pr.Identifier.Parts()...), 704 ) 705 706 log.Printf("[TRACE] fetching checksums file for the %q version of the %s plugin in %q...", version, pr.Identifier, outputFolder) 707 708 var checksum *FileChecksum 709 for _, checksummer := range opts.Checksummers { 710 if checksum != nil { 711 break 712 } 713 checksumFile, err := getter.Get(checksummer.Type, GetOptions{ 714 PluginRequirement: pr, 715 BinaryInstallationOptions: opts.BinaryInstallationOptions, 716 version: version, 717 }) 718 if err != nil { 719 if errors.Is(err, HTTPFailure) { 720 return nil, err 721 } 722 err := fmt.Errorf("could not get %s checksum file for %s version %s. Is the file present on the release and correctly named ? %w", checksummer.Type, pr.Identifier, version, err) 723 errs = multierror.Append(errs, err) 724 log.Printf("[TRACE] %s", err) 725 continue 726 } 727 728 entries, err := ParseChecksumFileEntries(checksumFile) 729 _ = checksumFile.Close() 730 if err != nil { 731 err := fmt.Errorf("could not parse %s checksumfile: %v. Make sure the checksum file contains a checksum and a binary filename per line", checksummer.Type, err) 732 errs = multierror.Append(errs, err) 733 log.Printf("[TRACE] %s", err) 734 continue 735 } 736 737 for _, entry := range entries { 738 if filepath.Ext(entry.Filename) == JSONExtension { 739 continue 740 } 741 if err := getter.Init(pr, &entry); err != nil { 742 err := fmt.Errorf("could not parse checksum filename %s. Is it correctly formatted ? %s", entry.Filename, err) 743 errs = multierror.Append(errs, err) 744 log.Printf("[TRACE] %s", err) 745 continue 746 } 747 748 metaOpts := GetOptions{ 749 PluginRequirement: pr, 750 BinaryInstallationOptions: opts.BinaryInstallationOptions, 751 version: version, 752 } 753 if err := getter.Validate(metaOpts, version.String(), opts.BinaryInstallationOptions, &entry); err != nil { 754 continue 755 } 756 757 log.Printf("[TRACE] About to get: %s", entry.Filename) 758 759 cs, err := checksummer.ParseChecksum(strings.NewReader(entry.Checksum)) 760 if err != nil { 761 err := fmt.Errorf("could not parse %s checksum: %s. Make sure the checksum file contains the checksum and only the checksum", checksummer.Type, err) 762 errs = multierror.Append(errs, err) 763 log.Printf("[TRACE] %s", err) 764 continue 765 } 766 767 checksum = &FileChecksum{ 768 Filename: entry.Filename, 769 Expected: cs, 770 Checksummer: checksummer, 771 } 772 773 expectedZipFilename := checksum.Filename 774 expectedBinFilename := getter.ExpectedFileName(pr, version.String(), &entry, expectedZipFilename) 775 expectedBinaryFilename := strings.TrimSuffix(expectedBinFilename, filepath.Ext(expectedBinFilename)) + opts.BinaryInstallationOptions.Ext 776 outputFileName := filepath.Join(outputFolder, expectedBinaryFilename) 777 778 for _, potentialChecksumer := range opts.Checksummers { 779 // First check if a local checksum file is already here in the expected 780 // download folder. Here we want to download a binary so we only check 781 // for an existing checksum file from the folder we want to download 782 // into. 783 cs, err := potentialChecksumer.GetCacheChecksumOfFile(outputFileName) 784 if err == nil && len(cs) > 0 { 785 localChecksum := &FileChecksum{ 786 Expected: cs, 787 Checksummer: potentialChecksumer, 788 } 789 790 log.Printf("[TRACE] found a pre-existing %q checksum file", potentialChecksumer.Type) 791 // if outputFile is there and matches the checksum: do nothing more. 792 if err := localChecksum.ChecksumFile(localChecksum.Expected, outputFileName); err == nil && !opts.Force { 793 log.Printf("[INFO] %s v%s plugin is already correctly installed in %q", pr.Identifier, version, outputFileName) 794 return nil, nil // success 795 } 796 } 797 } 798 799 // start fetching binary 800 remoteZipFile, err := getter.Get("zip", GetOptions{ 801 PluginRequirement: pr, 802 BinaryInstallationOptions: opts.BinaryInstallationOptions, 803 version: version, 804 expectedZipFilename: expectedZipFilename, 805 }) 806 if err != nil { 807 if errors.Is(err, HTTPFailure) { 808 return nil, err 809 } 810 errs = multierror.Append(errs, 811 fmt.Errorf("could not get binary for %s version %s. Is the file present on the release and correctly named ? %s", 812 pr.Identifier, version, err)) 813 continue 814 } 815 // create temporary file that will receive a temporary binary.zip 816 tmpFile, err := tmp.File("packer-plugin-*.zip") 817 if err != nil { 818 err = fmt.Errorf("could not create temporary file to download plugin: %w", err) 819 errs = multierror.Append(errs, err) 820 return nil, errs 821 } 822 defer func() { 823 tmpFilePath := tmpFile.Name() 824 tmpFile.Close() 825 os.Remove(tmpFilePath) 826 }() 827 828 // write binary to tmp file 829 _, err = io.Copy(tmpFile, remoteZipFile) 830 _ = remoteZipFile.Close() 831 if err != nil { 832 err := fmt.Errorf("Error getting plugin, trying another getter: %w", err) 833 errs = multierror.Append(errs, err) 834 continue 835 } 836 if _, err := tmpFile.Seek(0, 0); err != nil { 837 err := fmt.Errorf("Error seeking beginning of temporary file for checksumming, continuing: %w", err) 838 errs = multierror.Append(errs, err) 839 continue 840 } 841 842 // verify that the checksum for the zip is what we expect. 843 if err := checksum.Checksummer.Checksum(checksum.Expected, tmpFile); err != nil { 844 err := fmt.Errorf("%w. Is the checksum file correct ? Is the binary file correct ?", err) 845 errs = multierror.Append(errs, err) 846 continue 847 } 848 zr, err := zip.OpenReader(tmpFile.Name()) 849 if err != nil { 850 errs = multierror.Append(errs, fmt.Errorf("zip : %v", err)) 851 return nil, errs 852 } 853 854 var copyFrom io.ReadCloser 855 for _, f := range zr.File { 856 if f.Name != expectedBinaryFilename { 857 continue 858 } 859 copyFrom, err = f.Open() 860 if err != nil { 861 errs = multierror.Append(errs, fmt.Errorf("failed to open temp file: %w", err)) 862 return nil, errs 863 } 864 break 865 } 866 if copyFrom == nil { 867 err := fmt.Errorf("could not find a %q file in zipfile", expectedBinaryFilename) 868 errs = multierror.Append(errs, err) 869 return nil, errs 870 } 871 872 var outputFileData bytes.Buffer 873 if _, err := io.Copy(&outputFileData, copyFrom); err != nil { 874 err := fmt.Errorf("extract file: %w", err) 875 errs = multierror.Append(errs, err) 876 return nil, errs 877 } 878 tmpBinFileName := filepath.Join(os.TempDir(), expectedBinaryFilename) 879 tmpOutputFile, err := os.OpenFile(tmpBinFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 880 if err != nil { 881 err = fmt.Errorf("could not create temporary file to download plugin: %w", err) 882 errs = multierror.Append(errs, err) 883 return nil, errs 884 } 885 defer func() { 886 os.Remove(tmpBinFileName) 887 }() 888 889 if _, err := tmpOutputFile.Write(outputFileData.Bytes()); err != nil { 890 err := fmt.Errorf("extract file: %w", err) 891 errs = multierror.Append(errs, err) 892 return nil, errs 893 } 894 tmpOutputFile.Close() 895 896 if err := checkVersion(tmpBinFileName, pr.Identifier.String(), version); err != nil { 897 errs = multierror.Append(errs, err) 898 var continuableError *ContinuableInstallError 899 if errors.As(err, &continuableError) { 900 continue 901 } 902 return nil, errs 903 } 904 905 // create directories if need be 906 if err := os.MkdirAll(outputFolder, 0755); err != nil { 907 err := fmt.Errorf("could not create plugin folder %q: %w", outputFolder, err) 908 errs = multierror.Append(errs, err) 909 log.Printf("[TRACE] %s", err.Error()) 910 return nil, errs 911 } 912 outputFile, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 913 if err != nil { 914 err = fmt.Errorf("could not create final plugin binary file: %w", err) 915 errs = multierror.Append(errs, err) 916 return nil, errs 917 } 918 if _, err := outputFile.Write(outputFileData.Bytes()); err != nil { 919 err = fmt.Errorf("could not write final plugin binary file: %w", err) 920 errs = multierror.Append(errs, err) 921 return nil, errs 922 } 923 outputFile.Close() 924 925 cs, err = checksum.Checksummer.Sum(&outputFileData) 926 if err != nil { 927 err := fmt.Errorf("failed to checksum binary file: %s", err) 928 errs = multierror.Append(errs, err) 929 log.Printf("[WARNING] %v, ignoring", err) 930 } 931 if err := os.WriteFile(outputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(cs)), 0644); err != nil { 932 err := fmt.Errorf("failed to write local binary checksum file: %s", err) 933 errs = multierror.Append(errs, err) 934 log.Printf("[WARNING] %v, ignoring", err) 935 os.Remove(outputFileName) 936 continue 937 } 938 939 // Success !! 940 return &Installation{ 941 BinaryPath: strings.ReplaceAll(outputFileName, "\\", "/"), 942 Version: "v" + version.String(), 943 }, nil 944 } 945 946 } 947 } 948 } 949 950 if len(versions) == 0 { 951 if errs.Len() == 0 { 952 err := fmt.Errorf("no release version found for constraints: %q", pr.VersionConstraints.String()) 953 errs = multierror.Append(errs, err) 954 } 955 return nil, errs 956 } 957 958 if errs.ErrorOrNil() == nil { 959 err := fmt.Errorf("could not find a local nor a remote checksum for plugin %q %q", pr.Identifier, pr.VersionConstraints) 960 errs = multierror.Append(errs, err) 961 } 962 errs = multierror.Append(errs, fmt.Errorf("could not install any compatible version of plugin %q", pr.Identifier)) 963 return nil, errs 964 } 965 966 func GetPluginDescription(pluginPath string) (pluginsdk.SetDescription, error) { 967 out, err := exec.Command(pluginPath, "describe").Output() 968 if err != nil { 969 return pluginsdk.SetDescription{}, err 970 } 971 972 desc := pluginsdk.SetDescription{} 973 err = json.Unmarshal(out, &desc) 974 975 return desc, err 976 } 977 978 // checkVersion checks the described version of a plugin binary against the requested version constriant. 979 // A ContinuableInstallError is returned upon a version mismatch to indicate that the caller should try the next 980 // available version. A PrereleaseInstallError is returned to indicate an unsupported version install. 981 func checkVersion(binPath string, identifier string, version *goversion.Version) error { 982 desc, err := GetPluginDescription(binPath) 983 if err != nil { 984 err := fmt.Errorf("failed to describe plugin binary %q: %s", binPath, err) 985 return &ContinuableInstallError{Err: err} 986 } 987 descVersion, err := goversion.NewSemver(desc.Version) 988 if err != nil { 989 err := fmt.Errorf("invalid self-reported version %q: %s", desc.Version, err) 990 return &ContinuableInstallError{Err: err} 991 } 992 if descVersion.Core().Compare(version.Core()) != 0 { 993 err := fmt.Errorf("binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String()) 994 return &ContinuableInstallError{Err: err} 995 } 996 if version.Prerelease() != "" { 997 return &PrereleaseInstallError{ 998 PluginSrc: identifier, 999 Err: errors.New("binary reported a pre-release version of " + version.String()), 1000 } 1001 } 1002 // Since only final releases can be installed remotely, a non-empty prerelease version 1003 // means something's not right on the release, as it should report a final version. 1004 // 1005 // Therefore to avoid surprises (and avoid being able to install a version that 1006 // cannot be loaded), we error here, and advise users to manually install the plugin if they 1007 // need it. 1008 if descVersion.Prerelease() != "" { 1009 return &PrereleaseInstallError{ 1010 PluginSrc: identifier, 1011 Err: errors.New("binary reported a pre-release version of " + descVersion.String()), 1012 } 1013 } 1014 return nil 1015 } 1016 1017 func init() { 1018 var err error 1019 // Should never error if both components are set 1020 localAPIVersion, err = NewAPIVersion(fmt.Sprintf("x%s.%s", pluginsdk.APIVersionMajor, pluginsdk.APIVersionMinor)) 1021 if err != nil { 1022 panic("malformed API version in Packer. This is a programming error, please open an error to report it.") 1023 } 1024 }