From 154974fb12ef64e2fe378a04f6521e86603334c0 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Fri, 3 May 2024 14:20:30 -0400 Subject: [PATCH 1/5] Update plugin-getter version matching check This change is an attempt to remove the need for additional temporary files, along with calls to stat the temp files, to reduce the number of files being created, opened, and closed. In addition to this change, the logic for falling back to a previous version if the highest matched version is a pre-release has been removed. Instead we will assume that any prior versions will exhibit the same issue and return immediately. A user can install the version manually if they will or they can modify their version constraint to a properly released version. --- packer/plugin-getter/plugins.go | 171 +++++++++++++------------------- 1 file changed, 67 insertions(+), 104 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 4ad02f60c81..32a31075b00 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -24,7 +24,6 @@ import ( "github.com/hashicorp/go-multierror" goversion "github.com/hashicorp/go-version" pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" - "github.com/hashicorp/packer-plugin-sdk/random" "github.com/hashicorp/packer-plugin-sdk/tmp" "github.com/hashicorp/packer/hcl2template/addrs" "golang.org/x/mod/semver" @@ -741,11 +740,8 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) } expectedZipFilename := checksum.Filename expectedBinaryFilename := strings.TrimSuffix(expectedZipFilename, filepath.Ext(expectedZipFilename)) + opts.BinaryInstallationOptions.Ext + outputFileName := filepath.Join(outputFolder, expectedBinaryFilename) - outputFileName := filepath.Join( - outputFolder, - expectedBinaryFilename, - ) for _, potentialChecksumer := range opts.Checksummers { // First check if a local checksum file is already here in the expected // download folder. Here we want to download a binary so we only check @@ -758,7 +754,7 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) Checksummer: potentialChecksumer, } - log.Printf("[TRACE] found a pre-exising %q checksum file", potentialChecksumer.Type) + log.Printf("[TRACE] found a pre-existing %q checksum file", potentialChecksumer.Type) // if outputFile is there and matches the checksum: do nothing more. if err := localChecksum.ChecksumFile(localChecksum.Expected, outputFileName); err == nil && !opts.Force { log.Printf("[INFO] %s v%s plugin is already correctly installed in %q", pr.Identifier, version, outputFileName) @@ -767,9 +763,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) } } - // The last folder from the installation list is where we will install. - outputFileName = filepath.Join(outputFolder, expectedBinaryFilename) - // create directories if need be if err := os.MkdirAll(outputFolder, 0755); err != nil { err := fmt.Errorf("could not create plugin folder %q: %w", outputFolder, err) @@ -777,12 +770,24 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) log.Printf("[TRACE] %s", err.Error()) return nil, errs } - for _, getter := range getters { + // start fetching binary + remoteZipFile, err := getter.Get("zip", GetOptions{ + PluginRequirement: pr, + BinaryInstallationOptions: opts.BinaryInstallationOptions, + version: version, + expectedZipFilename: expectedZipFilename, + }) + if err != nil { + errs = multierror.Append(errs, + fmt.Errorf("could not get binary for %s version %s. Is the file present on the release and correctly named ? %s", + pr.Identifier, version, err)) + continue + } // create temporary file that will receive a temporary binary.zip tmpFile, err := tmp.File("packer-plugin-*.zip") if err != nil { - err = fmt.Errorf("could not create temporary file to dowload plugin: %w", err) + err = fmt.Errorf("could not create temporary file to download plugin: %w", err) errs = multierror.Append(errs, err) return nil, errs } @@ -791,38 +796,20 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) tmpFile.Close() os.Remove(tmpFilePath) }() - - // start fetching binary - remoteZipFile, err := getter.Get("zip", GetOptions{ - PluginRequirement: pr, - BinaryInstallationOptions: opts.BinaryInstallationOptions, - version: version, - expectedZipFilename: expectedZipFilename, - }) - if err != nil { - err := fmt.Errorf("could not get binary for %s version %s. Is the file present on the release and correctly named ? %s", pr.Identifier, version, err) - errs = multierror.Append(errs, err) - log.Printf("[TRACE] %v", err) - continue - } - // write binary to tmp file _, err = io.Copy(tmpFile, remoteZipFile) _ = remoteZipFile.Close() if err != nil { err := fmt.Errorf("Error getting plugin, trying another getter: %w", err) errs = multierror.Append(errs, err) - log.Printf("[TRACE] %s", err) continue } if _, err := tmpFile.Seek(0, 0); err != nil { - err := fmt.Errorf("Error seeking begining of temporary file for checksumming, continuing: %w", err) + err := fmt.Errorf("Error seeking beginning of temporary file for checksumming, continuing: %w", err) errs = multierror.Append(errs, err) - log.Printf("[TRACE] %s", err) continue } - // verify that the checksum for the zip is what we expect. if err := checksum.Checksummer.Checksum(checksum.Expected, tmpFile); err != nil { err := fmt.Errorf("%w. Is the checksum file correct ? Is the binary file correct ?", err) @@ -830,17 +817,9 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) continue } - tmpFileStat, err := tmpFile.Stat() + zr, err := zip.OpenReader(tmpFile.Name()) if err != nil { - err := fmt.Errorf("failed to stat: %w", err) - errs = multierror.Append(errs, err) - return nil, errs - } - - zr, err := zip.NewReader(tmpFile, tmpFileStat.Size()) - if err != nil { - err := fmt.Errorf("zip : %v", err) - errs = multierror.Append(errs, err) + errs = multierror.Append(errs, fmt.Errorf("zip : %v", err)) return nil, errs } @@ -851,8 +830,7 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) } copyFrom, err = f.Open() if err != nil { - err := fmt.Errorf("failed to open temp file: %w", err) - errs = multierror.Append(errs, err) + multierror.Append(errs, fmt.Errorf("failed to open temp file: %w", err)) return nil, errs } break @@ -863,50 +841,61 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) return nil, errs } - tempPluginPath := filepath.Join(os.TempDir(), fmt.Sprintf( - "packer-plugin-temp-%s%s", - random.Numbers(8), - opts.BinaryInstallationOptions.Ext)) - - // Save binary to temp so we can ensure it is really the version advertised - tempOutput, err := os.OpenFile(tempPluginPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + tmpBinFileName := filepath.Join(os.TempDir(), expectedBinaryFilename) if err != nil { - log.Printf("[ERROR] failed to create temp plugin executable: %s", err) - return nil, multierror.Append(errs, err) + err = fmt.Errorf("could not create temporary file to download plugin: %w", err) + errs = multierror.Append(errs, err) + return nil, errs } - defer os.Remove(tempPluginPath) + defer func() { + tmpBinPath := tmpBinFileName + os.Remove(tmpBinPath) + }() - _, err = io.Copy(tempOutput, copyFrom) + tmpOutputFile, err := os.OpenFile(tmpBinFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { - log.Printf("[ERROR] failed to copy uncompressed binary to %q: %s", tempPluginPath, err) - return nil, multierror.Append(errs, err) + err := fmt.Errorf("failed to create %s: %w", tmpBinFileName, err) + errs = multierror.Append(errs, err) + return nil, errs + } + if _, err := io.Copy(tmpOutputFile, copyFrom); err != nil { + err := fmt.Errorf("extract file: %w", err) + errs = multierror.Append(errs, err) + return nil, errs } - // Not a problem on most platforms, but unsure Windows will let us execute an already - // open file, so we close it temporarily to avoid problems - _ = tempOutput.Close() + if _, err := tmpOutputFile.Seek(0, 0); err != nil { + err := fmt.Errorf("Error seeking beginning of binary file for checksumming: %w", err) + errs = multierror.Append(errs, err) + log.Printf("[WARNING] %v, ignoring", err) + } - desc, err := GetPluginDescription(tempPluginPath) + outputFileCS, err := checksum.Checksummer.Sum(tmpOutputFile) if err != nil { - err := fmt.Errorf("failed to describe plugin binary %q: %s", tempPluginPath, err) + err := fmt.Errorf("failed to checksum binary file: %s", err) errs = multierror.Append(errs, err) - continue + log.Printf("[WARNING] %v, ignoring", err) } + tmpOutputFile.Close() + desc, err := GetPluginDescription(tmpBinFileName) + if err != nil { + err := fmt.Errorf("failed to describe plugin binary %q: %s", tmpBinFileName, err) + errs = multierror.Append(errs, err) + continue + } descVersion, err := goversion.NewSemver(desc.Version) if err != nil { err := fmt.Errorf("invalid self-reported version %q: %s", desc.Version, err) errs = multierror.Append(errs, err) continue } - if descVersion.Core().Compare(version.Core()) != 0 { - log.Printf("[ERROR] binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String()) + err := fmt.Errorf("binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String()) + errs = multierror.Append(errs, err) continue } - localOutputFileName := outputFileName - // Since releases only can be installed remotely, a non-empty prerelease version // means something's not right on the release, as it should report a final version instead. // @@ -914,50 +903,26 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) // cannot be loaded), we error here, and advise users to manually install the plugin if they // need it. if descVersion.Prerelease() != "" { - fmt.Printf("Plugin %q release v%s binary reports version %q. This is likely an upstream issue.\n"+ - "Try opening an issue on the plugin repository asking them to update the plugin's version information.\n", - pr.Identifier.String(), version, desc.Version) - fmt.Printf("If you need this exact version of the plugin, you can download the binary from the source and install "+ - "it locally with `packer plugins install --path '' %q`.\n"+ - "This will install the plugin in version %q (required_plugins constraints may need to be updated).\n", - pr.Identifier.String(), desc.Version) - continue - } + err := fmt.Errorf(`binary for %[1]q release v%[2]s reported version %[3]q. - copyFrom, err = os.OpenFile(tempPluginPath, os.O_RDONLY, 0755) - if err != nil { - log.Printf("[ERROR] failed to re-open temporary plugin file %q: %s", tempPluginPath, err) - return nil, multierror.Append(errs, err) - } +Remote installation of pre-release binaries is unsupported an likely an upstream plugin issue. +We encourage you to open an issue on the plugin repository asking to update the version information. +If you require this specific version of the plugin, you can manually download the binary from the source and install +the versioned binary as %[3]q using the following command: - outputFile, err := os.OpenFile(localOutputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - err := fmt.Errorf("failed to create %s: %w", localOutputFileName, err) - errs = multierror.Append(errs, err) - return nil, errs - } - defer outputFile.Close() +packer plugins install --path '' %[1]q`, + pr.Identifier.String(), version, desc.Version) - if _, err := io.Copy(outputFile, copyFrom); err != nil { - err := fmt.Errorf("extract file: %w", err) errs = multierror.Append(errs, err) return nil, errs } - if _, err := outputFile.Seek(0, 0); err != nil { - err := fmt.Errorf("Error seeking begining of binary file for checksumming: %w", err) + if err := os.Rename(tmpBinFileName, outputFileName); err != nil { + err := fmt.Errorf("failed to write local binary file to plugin path: %s", err) errs = multierror.Append(errs, err) - log.Printf("[WARNING] %v, ignoring", err) - } - - cs, err := checksum.Checksummer.Sum(outputFile) - if err != nil { - err := fmt.Errorf("failed to checksum binary file: %s", err) - errs = multierror.Append(errs, err) - log.Printf("[WARNING] %v, ignoring", err) + return nil, errs } - - if err := os.WriteFile(localOutputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(cs)), 0644); err != nil { + if err := os.WriteFile(outputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(outputFileCS)), 0644); err != nil { err := fmt.Errorf("failed to write local binary checksum file: %s", err) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) @@ -965,7 +930,7 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) // Success !! return &Installation{ - BinaryPath: strings.ReplaceAll(localOutputFileName, "\\", "/"), + BinaryPath: strings.ReplaceAll(outputFileName, "\\", "/"), Version: "v" + version.String(), }, nil } @@ -976,13 +941,11 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) } } - if errs == nil || errs.Len() == 0 { + if errs.ErrorOrNil() == nil { err := fmt.Errorf("could not find a local nor a remote checksum for plugin %q %q", pr.Identifier, pr.VersionConstraints) errs = multierror.Append(errs, err) } - errs = multierror.Append(errs, fmt.Errorf("could not install any compatible version of plugin %q", pr.Identifier)) - return nil, errs } From 879438c1f212bbc7039fb039c92e897cc92543e4 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Fri, 3 May 2024 21:37:39 -0400 Subject: [PATCH 2/5] Add PrereleaseError for handling failed pre-release version installs ``` ~> packer init mondoo_req.pkr.hcl Failed getting the "github.com/mondoohq/cnspec" plugin: error: Remote installation of the plugin version 10.8.1-dev is unsupported. This is likely an upstream issue with the 10.8.1 release, which should be reported. If you require this specific version of the plugin, download the binary and install it manually. packer plugins install --path '' github.com/mondoohq/cnspec ``` --- packer/plugin-getter/plugins.go | 46 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 32a31075b00..5e897ddf6bd 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -93,6 +93,25 @@ func (rlerr *RateLimitError) Error() string { return s } +// PrereleaseInstallError is returned when a getter encounters the install of a pre-release version. +type PrereleaseInstallError struct { + RequestedVersion, ReportedVersion string + Source string +} + +func (pe *PrereleaseInstallError) Error() string { + s := strings.Builder{} + s.WriteString("error:\n") + fmt.Fprintf(&s, "Remote installation of the plugin version %s is unsupported.\n", pe.ReportedVersion) + + if pe.RequestedVersion != pe.ReportedVersion { + fmt.Fprintf(&s, "This is likely an upstream issue with the %s release, which should be reported.\n", pe.RequestedVersion) + } + s.WriteString("If you require this specific version of the plugin, download the binary and install it manually.\n") + fmt.Fprintf(&s, "\npacker plugins install --path '' %s\n", pe.Source) + return s.String() +} + func (pr Requirement) FilenamePrefix() string { if pr.Identifier == nil { return "packer-plugin-" @@ -869,7 +888,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) } - outputFileCS, err := checksum.Checksummer.Sum(tmpOutputFile) if err != nil { err := fmt.Errorf("failed to checksum binary file: %s", err) @@ -896,25 +914,19 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) continue } - // Since releases only can be installed remotely, a non-empty prerelease version - // means something's not right on the release, as it should report a final version instead. + // Since only final releases can be installed remotely, a non-empty prerelease version + // means something's not right on the release, as it should report a final version. // // Therefore to avoid surprises (and avoid being able to install a version that // cannot be loaded), we error here, and advise users to manually install the plugin if they // need it. if descVersion.Prerelease() != "" { - err := fmt.Errorf(`binary for %[1]q release v%[2]s reported version %[3]q. - -Remote installation of pre-release binaries is unsupported an likely an upstream plugin issue. -We encourage you to open an issue on the plugin repository asking to update the version information. -If you require this specific version of the plugin, you can manually download the binary from the source and install -the versioned binary as %[3]q using the following command: - -packer plugins install --path '' %[1]q`, - pr.Identifier.String(), version, desc.Version) - - errs = multierror.Append(errs, err) - return nil, errs + err := PrereleaseInstallError{ + Source: pr.Identifier.String(), + RequestedVersion: version.String(), + ReportedVersion: desc.Version, + } + return nil, &err } if err := os.Rename(tmpBinFileName, outputFileName); err != nil { @@ -926,6 +938,8 @@ packer plugins install --path '' %[1]q`, err := fmt.Errorf("failed to write local binary checksum file: %s", err) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) + os.Remove(outputFileName) + continue } // Success !! @@ -934,10 +948,8 @@ packer plugins install --path '' %[1]q`, Version: "v" + version.String(), }, nil } - } } - } } From ad2d98595b1e63f850dfc8b9659a96388e0bfea3 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Mon, 6 May 2024 22:10:45 -0400 Subject: [PATCH 3/5] Use single buffer for storing binary file before writes * Only create plugin directories if there is potential plugin install --- packer/plugin-getter/plugins.go | 69 +++++++++++++++++---------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 5e897ddf6bd..c36b9ede4e6 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -5,6 +5,7 @@ package plugingetter import ( "archive/zip" + "bytes" "encoding/hex" "encoding/json" "fmt" @@ -782,13 +783,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) } } - // create directories if need be - if err := os.MkdirAll(outputFolder, 0755); err != nil { - err := fmt.Errorf("could not create plugin folder %q: %w", outputFolder, err) - errs = multierror.Append(errs, err) - log.Printf("[TRACE] %s", err.Error()) - return nil, errs - } for _, getter := range getters { // start fetching binary remoteZipFile, err := getter.Get("zip", GetOptions{ @@ -823,7 +817,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) errs = multierror.Append(errs, err) continue } - if _, err := tmpFile.Seek(0, 0); err != nil { err := fmt.Errorf("Error seeking beginning of temporary file for checksumming, continuing: %w", err) errs = multierror.Append(errs, err) @@ -835,7 +828,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) errs = multierror.Append(errs, err) continue } - zr, err := zip.OpenReader(tmpFile.Name()) if err != nil { errs = multierror.Append(errs, fmt.Errorf("zip : %v", err)) @@ -860,40 +852,28 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) return nil, errs } + var outputFileData bytes.Buffer + if _, err := io.Copy(&outputFileData, copyFrom); err != nil { + err := fmt.Errorf("extract file: %w", err) + errs = multierror.Append(errs, err) + return nil, errs + } tmpBinFileName := filepath.Join(os.TempDir(), expectedBinaryFilename) + tmpOutputFile, err := os.OpenFile(tmpBinFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { err = fmt.Errorf("could not create temporary file to download plugin: %w", err) errs = multierror.Append(errs, err) return nil, errs } defer func() { - tmpBinPath := tmpBinFileName - os.Remove(tmpBinPath) + os.Remove(tmpBinFileName) }() - tmpOutputFile, err := os.OpenFile(tmpBinFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - err := fmt.Errorf("failed to create %s: %w", tmpBinFileName, err) - errs = multierror.Append(errs, err) - return nil, errs - } - if _, err := io.Copy(tmpOutputFile, copyFrom); err != nil { + if _, err := tmpOutputFile.Write(outputFileData.Bytes()); err != nil { err := fmt.Errorf("extract file: %w", err) errs = multierror.Append(errs, err) return nil, errs } - - if _, err := tmpOutputFile.Seek(0, 0); err != nil { - err := fmt.Errorf("Error seeking beginning of binary file for checksumming: %w", err) - errs = multierror.Append(errs, err) - log.Printf("[WARNING] %v, ignoring", err) - } - outputFileCS, err := checksum.Checksummer.Sum(tmpOutputFile) - if err != nil { - err := fmt.Errorf("failed to checksum binary file: %s", err) - errs = multierror.Append(errs, err) - log.Printf("[WARNING] %v, ignoring", err) - } tmpOutputFile.Close() desc, err := GetPluginDescription(tmpBinFileName) @@ -913,7 +893,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) errs = multierror.Append(errs, err) continue } - // Since only final releases can be installed remotely, a non-empty prerelease version // means something's not right on the release, as it should report a final version. // @@ -929,12 +908,34 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) return nil, &err } - if err := os.Rename(tmpBinFileName, outputFileName); err != nil { - err := fmt.Errorf("failed to write local binary file to plugin path: %s", err) + // create directories if need be + if err := os.MkdirAll(outputFolder, 0755); err != nil { + err := fmt.Errorf("could not create plugin folder %q: %w", outputFolder, err) + errs = multierror.Append(errs, err) + log.Printf("[TRACE] %s", err.Error()) + return nil, errs + } + + outputFile, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + err = fmt.Errorf("could not create final plugin binary file: %w", err) errs = multierror.Append(errs, err) return nil, errs } - if err := os.WriteFile(outputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(outputFileCS)), 0644); err != nil { + if _, err := outputFile.Write(outputFileData.Bytes()); err != nil { + err = fmt.Errorf("could not write final plugin binary file: %w", err) + errs = multierror.Append(errs, err) + return nil, errs + } + outputFile.Close() + + cs, err := checksum.Checksummer.Sum(&outputFileData) + if err != nil { + err := fmt.Errorf("failed to checksum binary file: %s", err) + errs = multierror.Append(errs, err) + log.Printf("[WARNING] %v, ignoring", err) + } + if err := os.WriteFile(outputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(cs)), 0644); err != nil { err := fmt.Errorf("failed to write local binary checksum file: %s", err) errs = multierror.Append(errs, err) log.Printf("[WARNING] %v, ignoring", err) From b54a7e6c375445383c992cc565d505237f8305ab Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Mon, 6 May 2024 22:24:51 -0400 Subject: [PATCH 4/5] Add ConintuableInstallError for continuing installation on version mismatches --- packer/plugin-getter/plugins.go | 90 ++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index c36b9ede4e6..138bb084d6d 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -8,6 +8,7 @@ import ( "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "io/fs" @@ -100,19 +101,28 @@ type PrereleaseInstallError struct { Source string } -func (pe *PrereleaseInstallError) Error() string { +func (e *PrereleaseInstallError) Error() string { s := strings.Builder{} - s.WriteString("error:\n") - fmt.Fprintf(&s, "Remote installation of the plugin version %s is unsupported.\n", pe.ReportedVersion) + fmt.Fprintf(&s, "Error: Remote installation of the plugin version %s is unsupported.\n", e.ReportedVersion) - if pe.RequestedVersion != pe.ReportedVersion { - fmt.Fprintf(&s, "This is likely an upstream issue with the %s release, which should be reported.\n", pe.RequestedVersion) + if e.RequestedVersion != e.ReportedVersion { + fmt.Fprintf(&s, "This is likely an upstream issue with the %s release, which should be reported.\n", e.RequestedVersion) } s.WriteString("If you require this specific version of the plugin, download the binary and install it manually.\n") - fmt.Fprintf(&s, "\npacker plugins install --path '' %s\n", pe.Source) + fmt.Fprintf(&s, "\npacker plugins install --path '' %s\n", e.Source) return s.String() } +// ContinuableInstallError describe a failed getter install that is +// capable of falling back to next available version. +type ContinuableInstallError struct { + Err error +} + +func (e *ContinuableInstallError) Error() string { + return fmt.Sprintf("Continuing to next available version: %s", e.Err) +} + func (pr Requirement) FilenamePrefix() string { if pr.Identifier == nil { return "packer-plugin-" @@ -876,36 +886,13 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) } tmpOutputFile.Close() - desc, err := GetPluginDescription(tmpBinFileName) - if err != nil { - err := fmt.Errorf("failed to describe plugin binary %q: %s", tmpBinFileName, err) - errs = multierror.Append(errs, err) - continue - } - descVersion, err := goversion.NewSemver(desc.Version) - if err != nil { - err := fmt.Errorf("invalid self-reported version %q: %s", desc.Version, err) + if err := checkVersion(tmpBinFileName, pr.Identifier.String(), version); err != nil { errs = multierror.Append(errs, err) - continue - } - if descVersion.Core().Compare(version.Core()) != 0 { - err := fmt.Errorf("binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String()) - errs = multierror.Append(errs, err) - continue - } - // Since only final releases can be installed remotely, a non-empty prerelease version - // means something's not right on the release, as it should report a final version. - // - // Therefore to avoid surprises (and avoid being able to install a version that - // cannot be loaded), we error here, and advise users to manually install the plugin if they - // need it. - if descVersion.Prerelease() != "" { - err := PrereleaseInstallError{ - Source: pr.Identifier.String(), - RequestedVersion: version.String(), - ReportedVersion: desc.Version, + var continuableError *ContinuableInstallError + if errors.As(err, &continuableError) { + continue } - return nil, &err + return nil, errs } // create directories if need be @@ -915,7 +902,6 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) log.Printf("[TRACE] %s", err.Error()) return nil, errs } - outputFile, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { err = fmt.Errorf("could not create final plugin binary file: %w", err) @@ -974,6 +960,40 @@ func GetPluginDescription(pluginPath string) (pluginsdk.SetDescription, error) { return desc, err } +// checkVersion checks the described version of a plugin binary against the requested version constriant. +// A ContinuableInstallError is returned upon a version mismatch to indicate that the caller should try the next +// available version. A PrereleaseInstallError is returned to indicate an unsupported version install. +func checkVersion(binPath string, identifier string, version *goversion.Version) error { + desc, err := GetPluginDescription(binPath) + if err != nil { + err := fmt.Errorf("failed to describe plugin binary %q: %s", binPath, err) + return &ContinuableInstallError{Err: err} + } + descVersion, err := goversion.NewSemver(desc.Version) + if err != nil { + err := fmt.Errorf("invalid self-reported version %q: %s", desc.Version, err) + return &ContinuableInstallError{Err: err} + } + if descVersion.Core().Compare(version.Core()) != 0 { + err := fmt.Errorf("binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String()) + return &ContinuableInstallError{Err: err} + } + // Since only final releases can be installed remotely, a non-empty prerelease version + // means something's not right on the release, as it should report a final version. + // + // Therefore to avoid surprises (and avoid being able to install a version that + // cannot be loaded), we error here, and advise users to manually install the plugin if they + // need it. + if descVersion.Prerelease() != "" { + return &PrereleaseInstallError{ + Source: identifier, + RequestedVersion: version.String(), + ReportedVersion: desc.Version, + } + } + return nil +} + func init() { var err error // Should never error if both components are set From 66ef5127e16f3248bac0c1c993afba7d886cd2f1 Mon Sep 17 00:00:00 2001 From: Wilken Rivera Date: Tue, 7 May 2024 10:51:50 -0400 Subject: [PATCH 5/5] Add check to prevent the installation of version constraints matching a prerelease * Refactor InstallError string messages --- packer/plugin-getter/plugins.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 138bb084d6d..9813e35afde 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -97,19 +97,17 @@ func (rlerr *RateLimitError) Error() string { // PrereleaseInstallError is returned when a getter encounters the install of a pre-release version. type PrereleaseInstallError struct { - RequestedVersion, ReportedVersion string - Source string + PluginSrc string + Err error } func (e *PrereleaseInstallError) Error() string { - s := strings.Builder{} - fmt.Fprintf(&s, "Error: Remote installation of the plugin version %s is unsupported.\n", e.ReportedVersion) - - if e.RequestedVersion != e.ReportedVersion { - fmt.Fprintf(&s, "This is likely an upstream issue with the %s release, which should be reported.\n", e.RequestedVersion) - } + var s strings.Builder + s.WriteString(e.Err.Error() + "\n") + s.WriteString("Remote installation of pre-release plugin versions is unsupported.\n") + s.WriteString("This is likely an upstream issue, which should be reported.\n") s.WriteString("If you require this specific version of the plugin, download the binary and install it manually.\n") - fmt.Fprintf(&s, "\npacker plugins install --path '' %s\n", e.Source) + s.WriteString("\npacker plugins install --path '' " + e.PluginSrc) return s.String() } @@ -978,6 +976,12 @@ func checkVersion(binPath string, identifier string, version *goversion.Version) err := fmt.Errorf("binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String()) return &ContinuableInstallError{Err: err} } + if version.Prerelease() != "" { + return &PrereleaseInstallError{ + PluginSrc: identifier, + Err: errors.New("binary reported a pre-release version of " + version.String()), + } + } // Since only final releases can be installed remotely, a non-empty prerelease version // means something's not right on the release, as it should report a final version. // @@ -986,9 +990,8 @@ func checkVersion(binPath string, identifier string, version *goversion.Version) // need it. if descVersion.Prerelease() != "" { return &PrereleaseInstallError{ - Source: identifier, - RequestedVersion: version.String(), - ReportedVersion: desc.Version, + PluginSrc: identifier, + Err: errors.New("binary reported a pre-release version of " + descVersion.String()), } } return nil