Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update plugin-getter version matching check #12953

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 127 additions & 128 deletions packer/plugin-getter/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package plugingetter

import (
"archive/zip"
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
Expand All @@ -24,7 +26,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"
Expand Down Expand Up @@ -94,6 +95,32 @@ func (rlerr *RateLimitError) Error() string {
return s
}

// PrereleaseInstallError is returned when a getter encounters the install of a pre-release version.
type PrereleaseInstallError struct {
PluginSrc string
Err error
}

func (e *PrereleaseInstallError) Error() string {
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")
s.WriteString("\npacker plugins install --path '<plugin_binary>' " + e.PluginSrc)
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-"
Expand Down Expand Up @@ -741,11 +768,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
Expand All @@ -758,7 +782,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)
Expand All @@ -767,22 +791,24 @@ 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)
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{
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
}
Expand All @@ -791,56 +817,28 @@ 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)
errs = multierror.Append(errs, err)
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
}

Expand All @@ -851,8 +849,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
Expand All @@ -863,126 +860,89 @@ 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)
if err != nil {
log.Printf("[ERROR] failed to create temp plugin executable: %s", err)
return nil, multierror.Append(errs, err)
}
defer os.Remove(tempPluginPath)

_, err = io.Copy(tempOutput, copyFrom)
if err != nil {
log.Printf("[ERROR] failed to copy uncompressed binary to %q: %s", tempPluginPath, err)
return nil, multierror.Append(errs, err)
}

// 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()

desc, err := GetPluginDescription(tempPluginPath)
if err != nil {
err := fmt.Errorf("failed to describe plugin binary %q: %s", tempPluginPath, err)
var outputFileData bytes.Buffer
if _, err := io.Copy(&outputFileData, copyFrom); err != nil {
err := fmt.Errorf("extract file: %w", err)
errs = multierror.Append(errs, err)
continue
return nil, errs
}

descVersion, err := goversion.NewSemver(desc.Version)
tmpBinFileName := filepath.Join(os.TempDir(), expectedBinaryFilename)
tmpOutputFile, err := os.OpenFile(tmpBinFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
nywilken marked this conversation as resolved.
Show resolved Hide resolved
err := fmt.Errorf("invalid self-reported version %q: %s", desc.Version, err)
err = fmt.Errorf("could not create temporary file to download plugin: %w", 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())
continue
return nil, errs
}
defer func() {
nywilken marked this conversation as resolved.
Show resolved Hide resolved
os.Remove(tmpBinFileName)
}()

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.
//
// 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() != "" {
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 '<plugin_binary>' %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
if _, err := tmpOutputFile.Write(outputFileData.Bytes()); err != nil {
err := fmt.Errorf("extract file: %w", err)
errs = multierror.Append(errs, err)
return nil, errs
}
tmpOutputFile.Close()

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)
if err := checkVersion(tmpBinFileName, pr.Identifier.String(), version); err != nil {
errs = multierror.Append(errs, err)
var continuableError *ContinuableInstallError
if errors.As(err, &continuableError) {
continue
}
return nil, errs
}

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)
// 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
}
defer outputFile.Close()

if _, err := io.Copy(outputFile, copyFrom); err != nil {
err := fmt.Errorf("extract file: %w", err)
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 := outputFile.Seek(0, 0); err != nil {
err := fmt.Errorf("Error seeking begining of binary file for checksumming: %w", err)
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)
log.Printf("[WARNING] %v, ignoring", err)
return nil, errs
}
outputFile.Close()

cs, err := checksum.Checksummer.Sum(outputFile)
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(localOutputFileName+checksum.Checksummer.FileExt(), []byte(hex.EncodeToString(cs)), 0644); err != nil {
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)
os.Remove(outputFileName)
continue
}

// Success !!
return &Installation{
BinaryPath: strings.ReplaceAll(localOutputFileName, "\\", "/"),
BinaryPath: strings.ReplaceAll(outputFileName, "\\", "/"),
Version: "v" + version.String(),
}, nil
}

}
}

}
}

if errs == nil || errs.Len() == 0 {
if errs.ErrorOrNil() == nil {
nywilken marked this conversation as resolved.
Show resolved Hide resolved
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
}

Expand All @@ -998,6 +958,45 @@ 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}
}
if version.Prerelease() != "" {
Copy link
Contributor Author

@nywilken nywilken May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In cases where a plugin is tagged and released as a prerelease but the describe version is actually a final version (e.g github.com/hashicorp/hashicup@v1.2.0-dev) Packer should prevent the installation because of a mismatch version that will not load properly.

Copy link
Contributor

@lbajolet-hashicorp lbajolet-hashicorp May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugin "github.com/hashicorp/hashicups" release v1.0.2-dev binary reports version "1.0.2-dev". This is likely an upstream issue.

Yeah I get this on my latest harden branch, so my original hunch that we got the candidates through a Constraint is not right it seems. IMHO, we should reject remotely installing non-final versions of a plugin, even before we even try to contact Github, so we should check the version requested, and only accept final versions for commands like plugins install or packer init.

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.
//
// 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{
PluginSrc: identifier,
Err: errors.New("binary reported a pre-release version of " + descVersion.String()),
}
}
return nil
}

func init() {
var err error
// Should never error if both components are set
Expand Down
Loading