From 1729ed96456f7b3678427aa7d1462de07b1e948a Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Wed, 10 Apr 2024 17:37:36 -0400 Subject: [PATCH] packer: ensure versions match for remote installs 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. --- packer/plugin-getter/plugins.go | 56 ++++++++++++++++++++++++++++ packer/plugin-getter/plugins_test.go | 38 ++++++++++++++++--- 2 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 982832b8363..59552fa332e 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -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" @@ -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) diff --git a/packer/plugin-getter/plugins_test.go b/packer/plugin-getter/plugins_test.go index e92e636b1d2..c404b09db31 100644 --- a/packer/plugin-getter/plugins_test.go +++ b/packer/plugin-getter/plugins_test.go @@ -13,6 +13,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strings" "testing" @@ -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"}'`, }), }, }, @@ -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"}'`, }), }, }, @@ -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"}'`, }), }, }, @@ -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)