Skip to content

Commit

Permalink
packer: ensure versions match for remote installs
Browse files Browse the repository at this point in the history
Since we're hardening what Packer is able to load locally when it comes
to plugins, we need also to harden the installation process a bit.

While testing we noticed some remotes had published their plugins with
version mismatches between the tag and the binary.
This was not a problem in the past, as Packer did not care for this,
only the binary name was important, and the plugin could be installed
without problem.

Nowadays however, since Packer enforces the plugin version reported in
the name to be the same as the plugin self-reported version, this makes
it impossible for the installed plugin to load anymore in such an
instance.

Therefore in order to limit confusion, and so users are able to
understand the problem and report it to the plugins with that mismatch,
we reject the installations that expose this mismatch, and report it to
the user if they cannot install anything else.
  • Loading branch information
lbajolet-hashicorp committed Apr 23, 2024
1 parent eab8d8c commit 1729ed9
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 6 deletions.
56 changes: 56 additions & 0 deletions packer/plugin-getter/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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 @@ -817,6 +818,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)
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)
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.Prerelease() != "" {
err := fmt.Errorf("release v%s binary reports version %q, which is unsupported. "+
"Try opening an issue on the plugin repository asking them to update the plugin's version information.",
version, desc.Version)
errs = multierror.Append(errs, err)
continue
}

if descVersion.Compare(version) != 0 {
log.Printf("[ERROR] binary reported version (%q) is different from the expected %q, skipping", desc.Version, version.String())
continue
}

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)
}

outputFile, err := os.OpenFile(outputFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
err := fmt.Errorf("failed to create %s: %w", outputFileName, err)
Expand Down
38 changes: 32 additions & 6 deletions packer/plugin-getter/plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -201,12 +202,17 @@ func TestRequirement_InstallLatest(t *testing.T) {
ChecksumFileEntries: map[string][]ChecksumFileEntry{
"2.10.0": {{
Filename: "packer-plugin-amazon_v2.10.0_x6.0_darwin_amd64.zip",
Checksum: "43156b1900dc09b026b54610c4a152edd277366a7f71ff3812583e4a35dd0d4a",
Checksum: "5763f8b5b5ed248894e8511a089cf399b96c7ef92d784fb30ee6242a7cb35bce",
}},
},
Zips: map[string]io.ReadCloser{
"github.com/hashicorp/packer-plugin-amazon/packer-plugin-amazon_v2.10.0_x6.0_darwin_amd64.zip": zipFile(map[string]string{
"packer-plugin-amazon_v2.10.0_x6.0_darwin_amd64": "v2.10.0_x6.0_darwin_amd64",
// Make the false plugin echo an output that matches a subset of `describe` for install to work
//
// Note: this won't work on Windows as they don't have bin/sh, but this will
// eventually be replaced by acceptance tests.
"packer-plugin-amazon_v2.10.0_x6.0_darwin_amd64": `#!/bin/sh
echo '{"version":"v2.10.0","api_version":"x6.0"}'`,
}),
},
},
Expand Down Expand Up @@ -248,12 +254,17 @@ func TestRequirement_InstallLatest(t *testing.T) {
ChecksumFileEntries: map[string][]ChecksumFileEntry{
"2.10.1": {{
Filename: "packer-plugin-amazon_v2.10.1_x6.1_darwin_amd64.zip",
Checksum: "90ca5b0f13a90238b62581bbf30bacd7e2c9af6592c7f4849627bddbcb039dec",
Checksum: "51451da5cd7f1ecd8699668d806bafe58a9222430842afbefdc62a6698dab260",
}},
},
Zips: map[string]io.ReadCloser{
"github.com/hashicorp/packer-plugin-amazon/packer-plugin-amazon_v2.10.1_x6.1_darwin_amd64.zip": zipFile(map[string]string{
"packer-plugin-amazon_v2.10.1_x6.1_darwin_amd64": "v2.10.1_x6.1_darwin_amd64",
// Make the false plugin echo an output that matches a subset of `describe` for install to work
//
// Note: this won't work on Windows as they don't have bin/sh, but this will
// eventually be replaced by acceptance tests.
"packer-plugin-amazon_v2.10.1_x6.1_darwin_amd64": `#!/bin/sh
echo '{"version":"v2.10.1","api_version":"x6.1"}'`,
}),
},
},
Expand Down Expand Up @@ -295,12 +306,17 @@ func TestRequirement_InstallLatest(t *testing.T) {
ChecksumFileEntries: map[string][]ChecksumFileEntry{
"2.10.0": {{
Filename: "packer-plugin-amazon_v2.10.0_x6.1_linux_amd64.zip",
Checksum: "825fc931ae0cb151df0c56be41a17a9136c4d1f1ee73ddb8ed6baa17cef31afa",
Checksum: "5196f57f37e18bfeac10168db6915caae0341bfc4168ebc3d2b959d746cebd0a",
}},
},
Zips: map[string]io.ReadCloser{
"github.com/hashicorp/packer-plugin-amazon/packer-plugin-amazon_v2.10.0_x6.1_linux_amd64.zip": zipFile(map[string]string{
"packer-plugin-amazon_v2.10.0_x6.1_linux_amd64": "v2.10.0_x6.1_linux_amd64",
// Make the false plugin echo an output that matches a subset of `describe` for install to work
//
// Note: this won't work on Windows as they don't have bin/sh, but this will
// eventually be replaced by acceptance tests.
"packer-plugin-amazon_v2.10.0_x6.1_linux_amd64": `#!/bin/sh
echo '{"version":"v2.10.0","api_version":"x6.1"}'`,
}),
},
},
Expand Down Expand Up @@ -404,6 +420,16 @@ func TestRequirement_InstallLatest(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
switch tt.name {
case "upgrade-with-diff-protocol-version",
"upgrade-with-same-protocol-version",
"upgrade-with-one-missing-checksum-file":
if runtime.GOOS != "windows" {
break
}
t.Skipf("Test %q cannot run on Windows because of a shell script being invoked, skipping.", tt.name)
}

log.Printf("starting %s test", tt.name)

identifier, diags := addrs.ParsePluginSourceString("github.com/hashicorp/" + tt.fields.Identifier)
Expand Down

0 comments on commit 1729ed9

Please sign in to comment.