From 6f360121550a3fcb1a3fe2fef0b86ba4f78238fa Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 1 Dec 2023 13:14:08 -0500 Subject: [PATCH] command: simplify local binary installation Read original binary into memory to fix case when installation destination and source were the same, resulting in an empty binary. --- command/plugins_install.go | 129 +++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/command/plugins_install.go b/command/plugins_install.go index f6d7b11086e..fe64bd9d3ad 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -4,6 +4,7 @@ package command import ( + "bytes" "context" "crypto/sha256" "encoding/json" @@ -12,6 +13,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "runtime" "strings" @@ -48,8 +50,8 @@ Usage: packer plugins install [OPTIONS...] [] Options: - path : install the plugin from a locally-sourced plugin binary. This installs the plugin where a normal invocation would, but will - not try to download it from a web server, but instead directly - install the binary for Packer to be able to load it later on. + not try to download it from a remote location, and instead + install the binary in the Packer plugins path. This option cannot be specified with a version constraint. - force: forces installation of a plugin, even if it is already there. ` @@ -71,15 +73,15 @@ func (c *PluginsInstallCommand) Run(args []string) int { type PluginsInstallArgs struct { MetaArgs - PluginName string - PluginPath string - Version string - Force bool + PluginIdentifier string + PluginPath string + Version string + Force bool } func (pa *PluginsInstallArgs) AddFlagSets(flags *flag.FlagSet) { - flags.StringVar(&pa.PluginPath, "path", "", "install the plugin from a specific path") - flags.BoolVar(&pa.Force, "force", false, "force installation of a plugin, even if already installed") + flags.StringVar(&pa.PluginPath, "path", "", "install the binary specified by path as a Packer plugin.") + flags.BoolVar(&pa.Force, "force", false, "force installation of the specified plugin, even if already installed.") pa.MetaArgs.AddFlagSets(flags) } @@ -107,13 +109,12 @@ func (c *PluginsInstallCommand) ParseArgs(args []string) (*PluginsInstallArgs, i } if pa.Path != "" && pa.Version != "" { - c.Ui.Error("Invalid arguments: a version cannot be specified with --path") + c.Ui.Error("Invalid arguments: a version cannot be specified when using --path to install a local plugin binary") flags.Usage() return pa, 1 } - pa.PluginName = args[0] - + pa.PluginIdentifier = args[0] return pa, 0 } @@ -130,8 +131,11 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *Plugi }, }, } + if runtime.GOOS == "windows" { + opts.BinaryInstallationOptions.Ext = ".exe" + } - plugin, diags := addrs.ParsePluginSourceString(args.PluginName) + plugin, diags := addrs.ParsePluginSourceString(args.PluginIdentifier) if diags.HasErrors() { c.Ui.Error(diags.Error()) return 1 @@ -140,7 +144,7 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *Plugi // If we did specify a binary to install the plugin from, we ignore // the Github-based getter in favour of installing it directly. if args.PluginPath != "" { - return c.InstallFromBinary(args) + return c.InstallFromBinary(opts, plugin, args) } // a plugin requirement that matches them all @@ -157,10 +161,6 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *Plugi pluginRequirement.VersionConstraints = constraints } - if runtime.GOOS == "windows" && opts.Ext == "" { - opts.BinaryInstallationOptions.Ext = ".exe" - } - getters := []plugingetter.Getter{ &github.Getter{ // In the past some terraform plugins downloads were blocked from a @@ -199,42 +199,29 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *Plugi return 0 } -func (c *PluginsInstallCommand) InstallFromBinary(args *PluginsInstallArgs) int { - pluginDirs := c.Meta.CoreConfig.Components.PluginConfig.KnownPluginFolders - - if len(pluginDirs) == 0 { - c.Ui.Say(`Error: cannot find a place to install the plugin to - -In order to install the plugin for later use, Packer needs to know where to -install them. - -This can be specified through the PACKER_CONFIG_DIR environment variable, -but should be automatically inferred by Packer. +func (c *PluginsInstallCommand) InstallFromBinary(opts plugingetter.ListInstallationsOptions, pluginIdentifier *addrs.Plugin, args *PluginsInstallArgs) int { + // As with the other commands, we get the last plugin directory as it + // has precedence over the others, and is where we'll install the + // plugins to. + pluginDir := opts.FromFolders[len(opts.FromFolders)-1] -If you see this message, this is likely a Packer bug, please consider opening -an issue on our Github repo to signal it.`) - } + var err error - pluginSlugParts := strings.Split(args.PluginName, "/") - if len(pluginSlugParts) != 3 { + args.PluginPath, err = filepath.Abs(args.PluginPath) + if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Invalid plugin name specifier", - Detail: fmt.Sprintf("The plugin name specified provided (%q) does not conform to the mandated format of //.", args.PluginName), + Summary: "Failed to transform path", + Detail: fmt.Sprintf("Failed to transform the given path to an absolute one: %s", err), }}) } - // As with the other commands, we get the last plugin directory as it - // has precedence over the others, and is where we'll install the - // plugins to. - pluginDir := pluginDirs[len(pluginDirs)-1] - s, err := os.Stat(args.PluginPath) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unable to find plugin to promote", - Detail: fmt.Sprintf("The plugin %q failed to be opened because of an error: %s", args.PluginName, err), + Detail: fmt.Sprintf("The plugin %q failed to be opened because of an error: %s", args.PluginIdentifier, err), }}) } @@ -254,6 +241,7 @@ an issue on our Github repo to signal it.`) Detail: fmt.Sprintf("Packer failed to run %s describe: %s", args.PluginPath, err), }}) } + var desc plugin.SetDescription if err := json.Unmarshal(describeCmd, &desc); err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ @@ -263,7 +251,15 @@ an issue on our Github repo to signal it.`) }}) } - if strings.Contains(desc.Version, "-") { + semver, err := version.NewSemver(desc.Version) + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version", + Detail: fmt.Sprintf("Plugin's reported version (%q) is not semver-compatible: %s", desc.Version, err), + }}) + } + if semver.Prerelease() != "" { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid version", @@ -279,15 +275,9 @@ an issue on our Github repo to signal it.`) Detail: fmt.Sprintf("Failed to open plugin binary from %q: %s", args.PluginPath, err), }}) } - defer pluginBinary.Close() - // We'll install the SHA256SUM file alongside the plugin, based on the - // contents of the plugin being passed. - // - // This will make our loaders happy as they require a valid checksum - // for loading plugins installed this way. - shasum := sha256.New() - _, err = io.Copy(shasum, pluginBinary) + pluginContents := bytes.Buffer{} + _, err = io.Copy(&pluginContents, pluginBinary) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -295,10 +285,14 @@ an issue on our Github repo to signal it.`) Detail: fmt.Sprintf("Failed to read plugin binary from %q: %s", args.PluginPath, err), }}) } + _ = pluginBinary.Close() // At this point, we know the provided binary behaves correctly with // describe, so it's very likely to be a plugin, let's install it. - installDir := fmt.Sprintf("%s/%s", pluginDir, args.PluginName) + installDir := filepath.Join( + pluginDir, + filepath.Join(pluginIdentifier.Parts()...), + ) err = os.MkdirAll(installDir, 0755) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ @@ -308,15 +302,17 @@ an issue on our Github repo to signal it.`) }}) } - binaryPath := fmt.Sprintf( - "%s/packer-plugin-%s_v%s_%s_%s_%s", - installDir, - pluginSlugParts[2], + outputPrefix := fmt.Sprintf( + "packer-plugin-%s_v%s_%s", + pluginIdentifier.Type, desc.Version, desc.APIVersion, - runtime.GOOS, - runtime.GOARCH, ) + binaryPath := filepath.Join( + installDir, + outputPrefix+opts.BinaryInstallationOptions.FilenameSuffix(), + ) + outputPlugin, err := os.OpenFile(binaryPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0755) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ @@ -327,16 +323,7 @@ an issue on our Github repo to signal it.`) } defer outputPlugin.Close() - _, err = pluginBinary.Seek(0, 0) - if err != nil { - return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Failed to reset plugin's reader", - Detail: fmt.Sprintf("Failed to seek offset 0 while attempting to reset the buffer for the plugin to install: %s", err), - }}) - } - - _, err = io.Copy(outputPlugin, pluginBinary) + _, err = outputPlugin.Write(pluginContents.Bytes()) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -345,6 +332,11 @@ an issue on our Github repo to signal it.`) }}) } + // We'll install the SHA256SUM file alongside the plugin, based on the + // contents of the plugin being passed. + shasum := sha256.New() + _, _ = shasum.Write(pluginContents.Bytes()) + shasumPath := fmt.Sprintf("%s_SHA256SUM", binaryPath) shaFile, err := os.OpenFile(shasumPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644) if err != nil { @@ -357,8 +349,7 @@ an issue on our Github repo to signal it.`) defer shaFile.Close() fmt.Fprintf(shaFile, "%x", shasum.Sum([]byte{})) - - c.Ui.Say(fmt.Sprintf("Successfully installed plugin %s from %s to %s", args.PluginName, args.PluginPath, binaryPath)) + c.Ui.Say(fmt.Sprintf("Successfully installed plugin %s from %s to %s", args.PluginIdentifier, args.PluginPath, binaryPath)) return 0 }