From 6f12837e62924cb23b73bfbcbf2afd1eb01eb116 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 | 54 ++++++++++++++++++++++++++++ packer/plugin-getter/plugins_test.go | 38 ++++++++++++++++---- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index 09446ab41de..d6db5d8bb9c 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -823,6 +823,60 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) return nil, errs } + // Save binary to temp so we can ensure it is really the version advertised + tempOutput, err := os.CreateTemp("", fmt.Sprintf("packer-plugin*%s", opts.BinaryInstallationOptions.Ext)) + if err != nil { + log.Printf("[ERROR] failed to create temp plugin executable: %s", err) + return nil, multierror.Append(errs, err) + } + tempPluginPath := tempOutput.Name() + + _, 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) + } + + err = tempOutput.Chmod(0755) + if err != nil { + log.Printf("[ERROR] failed to change permissions of extracted binary: %s", 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. This is likely a problem with the plugin release workflows.", 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..854a12838e3 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.Skip("Test %q cannot run on Windows because of a shell script being invoked, skipping.") + } + log.Printf("starting %s test", tt.name) identifier, diags := addrs.ParsePluginSourceString("github.com/hashicorp/" + tt.fields.Identifier)