From a387e52a90c2d878d9a829a5f7889298e729c4f2 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Tue, 3 Oct 2023 11:52:48 -0400 Subject: [PATCH 1/6] command: add packer plugins install path flag This new flag allows the `packer plugins install' command to install a plugin from a local binary rather than from Github. This command will only call `describe' on the plugin, and won't do any further checks for functionality. The SHA256SUM will be directly computed from the binary, so as with anything manual and potentially sourced by the community, extra care should be applied when invoking this. --- command/plugins_install.go | 237 ++++++++++++++++++++++++++++++-- command/plugins_install_test.go | 5 +- 2 files changed, 231 insertions(+), 11 deletions(-) diff --git a/command/plugins_install.go b/command/plugins_install.go index 3037c0959d0..e584ea778de 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -6,18 +6,24 @@ package command import ( "context" "crypto/sha256" + "encoding/json" + "flag" "fmt" + "io" + "os" + "os/exec" "runtime" "strings" "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/packer-plugin-sdk/plugin" pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer/hcl2template/addrs" "github.com/hashicorp/packer/packer" plugingetter "github.com/hashicorp/packer/packer/plugin-getter" "github.com/hashicorp/packer/packer/plugin-getter/github" pkrversion "github.com/hashicorp/packer/version" - "github.com/mitchellh/cli" ) type PluginsInstallCommand struct { @@ -30,7 +36,7 @@ func (c *PluginsInstallCommand) Synopsis() string { func (c *PluginsInstallCommand) Help() string { helpText := ` -Usage: packer plugins install [] +Usage: packer plugins install [OPTIONS...] [] This command will install the most recent compatible Packer plugin matching version constraint. @@ -38,6 +44,12 @@ Usage: packer plugins install [] installed. Ex: packer plugins install github.com/hashicorp/happycloud v1.2.3 + +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. ` return strings.TrimSpace(helpText) @@ -47,14 +59,55 @@ func (c *PluginsInstallCommand) Run(args []string) int { ctx, cleanup := handleTermInterrupt(c.Ui) defer cleanup() - return c.RunContext(ctx, args) + cmdArgs, ret := c.ParseArgs(args) + if ret != 0 { + return ret + } + + return c.RunContext(ctx, cmdArgs) +} + +type PluginsInstallArgs struct { + MetaArgs + PluginName string + PluginPath string + Version string +} + +func (pa *PluginsInstallArgs) AddFlagSets(flags *flag.FlagSet) { + flags.StringVar(&pa.PluginPath, "path", "", "install the plugin from a specific path") + pa.MetaArgs.AddFlagSets(flags) } -func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []string) int { +func (c *PluginsInstallCommand) ParseArgs(args []string) (*PluginsInstallArgs, int) { + pa := &PluginsInstallArgs{} + + flags := c.Meta.FlagSet("plugins install") + flags.Usage = func() { c.Ui.Say(c.Help()) } + pa.AddFlagSets(flags) + err := flags.Parse(args) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to parse options: %s", err)) + return pa, 1 + } + + args = flags.Args() if len(args) < 1 || len(args) > 2 { - return cli.RunResultHelp + c.Ui.Error(fmt.Sprintf("Invalid arguments, expected either 1 or 2 positional arguments, got %d", len(args))) + flags.Usage() + return pa, 1 + } + + if len(args) == 2 { + pa.Version = args[1] } + pa.PluginName = args[0] + + return pa, 0 +} + +func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *PluginsInstallArgs) int { opts := plugingetter.ListInstallationsOptions{ FromFolders: c.Meta.CoreConfig.Components.PluginConfig.KnownPluginFolders, BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{ @@ -68,19 +121,25 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []stri }, } - plugin, diags := addrs.ParsePluginSourceString(args[0]) + plugin, diags := addrs.ParsePluginSourceString(args.PluginName) if diags.HasErrors() { c.Ui.Error(diags.Error()) return 1 } + // 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) + } + // a plugin requirement that matches them all pluginRequirement := plugingetter.Requirement{ Identifier: plugin, } - if len(args) > 1 { - constraints, err := version.NewConstraint(args[1]) + if args.Version != "" { + constraints, err := version.NewConstraint(args.Version) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -128,3 +187,165 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []stri 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. + +If you see this message, this is likely a Packer bug, please consider opening +an issue on our Github repo to signal it.`) + } + + pluginSlugParts := strings.Split(args.PluginName, "/") + if len(pluginSlugParts) != 3 { + 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), + }}) + } + + // 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), + }}) + } + + if s.IsDir() { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Plugin to promote cannot be a directory", + Detail: "The packer plugin promote command can only install binaries, not directories", + }}) + } + + describeCmd, err := exec.Command(args.PluginPath, "describe").Output() + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to describe the plugin", + 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{ + Severity: hcl.DiagError, + Summary: "Failed to decode plugin describe info", + Detail: fmt.Sprintf("'%s describe' produced information that Packer couldn't decode: %s", args.PluginPath, err), + }}) + } + + // Let's override the plugin's version if we specify it in the options + // of the command + if args.Version != "" { + desc.Version = args.Version + } + + pluginBinary, err := os.Open(args.PluginPath) + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to open plugin binary", + 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) + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to read plugin binary's contents", + Detail: fmt.Sprintf("Failed to read plugin binary from %q: %s", args.PluginPath, err), + }}) + } + + // 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) + err = os.MkdirAll(installDir, 0755) + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to create output directory", + Detail: fmt.Sprintf("The installation directory %q failed to be created because of an error: %s", installDir, err), + }}) + } + + binaryPath := fmt.Sprintf( + "%s/packer-plugin-%s_v%s_%s_%s_%s", + installDir, + pluginSlugParts[2], + desc.Version, + desc.APIVersion, + runtime.GOOS, + runtime.GOARCH, + ) + 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{ + Severity: hcl.DiagError, + Summary: "Failed to create plugin binary", + Detail: fmt.Sprintf("Failed to create plugin binary at %q: %s", binaryPath, err), + }}) + } + 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) + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to copy plugin binary's contents", + Detail: fmt.Sprintf("Failed to copy plugin binary from %q to %q: %s", args.PluginPath, binaryPath, err), + }}) + } + + shasumPath := fmt.Sprintf("%s_SHA256SUM", binaryPath) + shaFile, err := os.OpenFile(shasumPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644) + if err != nil { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to create plugin SHA256SUM file", + Detail: fmt.Sprintf("Failed to create SHA256SUM file at %q: %s", shasumPath, err), + }}) + } + 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)) + + return 0 +} diff --git a/command/plugins_install_test.go b/command/plugins_install_test.go index 105b9b00067..5461360c36e 100644 --- a/command/plugins_install_test.go +++ b/command/plugins_install_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/mitchellh/cli" "golang.org/x/mod/sumdb/dirhash" ) @@ -109,7 +108,7 @@ func TestPluginsInstallCommand_Run(t *testing.T) { expectedPackerConfigDirHashBeforeInstall: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", packerConfigDir: cfg.dir("4_pkr_plugins_config"), pluginSourceArgs: []string{"github.com/sylviamoss/comment", "v0.2.18", "github.com/sylviamoss/comment", "v0.2.19"}, - want: cli.RunResultHelp, + want: 1, dirFiles: nil, expectedPackerConfigDirHashAfterInstall: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", }, @@ -120,7 +119,7 @@ func TestPluginsInstallCommand_Run(t *testing.T) { expectedPackerConfigDirHashBeforeInstall: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", packerConfigDir: cfg.dir("5_pkr_plugins_config"), pluginSourceArgs: []string{}, - want: cli.RunResultHelp, + want: 1, dirFiles: nil, expectedPackerConfigDirHashAfterInstall: "h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", }, From f0abdedc7a09b294be55a45ea14fe68c973ff970 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Wed, 25 Oct 2023 11:56:30 -0400 Subject: [PATCH 2/6] command: add --force option to init/install The --force option for packer init and packer plugins install enforces installation of a plugin, even if it is already locally installed. This will become useful if for some reason a pre-existing plugin binary/version is already installed, and we want to overwrite it. --- command/cli.go | 2 ++ command/init.go | 14 +++++++++++--- command/plugins_install.go | 4 ++++ packer/plugin-getter/plugins.go | 5 ++++- packer/plugin-getter/plugins_test.go | 8 ++++++++ 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/command/cli.go b/command/cli.go index 4ad80440f0e..2dfc57dbd0d 100644 --- a/command/cli.go +++ b/command/cli.go @@ -104,6 +104,7 @@ type BuildArgs struct { func (ia *InitArgs) AddFlagSets(flags *flag.FlagSet) { flags.BoolVar(&ia.Upgrade, "upgrade", false, "upgrade any present plugin to the highest allowed version.") + flags.BoolVar(&ia.Force, "force", false, "force installation of a plugin, even if already installed") ia.MetaArgs.AddFlagSets(flags) } @@ -112,6 +113,7 @@ func (ia *InitArgs) AddFlagSets(flags *flag.FlagSet) { type InitArgs struct { MetaArgs Upgrade bool + Force bool } // PluginsRequiredArgs represents a parsed cli line for a `packer plugins required ` diff --git a/command/init.go b/command/init.go index 71332d57d25..2b8cdd7595a 100644 --- a/command/init.go +++ b/command/init.go @@ -11,6 +11,7 @@ import ( "runtime" "strings" + gversion "github.com/hashicorp/go-version" pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer/packer" plugingetter "github.com/hashicorp/packer/packer/plugin-getter" @@ -120,16 +121,21 @@ for more info.`) return 1 } - log.Printf("[TRACE] for plugin %s found %d matching installation(s)", pluginRequirement.Identifier, len(installs)) + if len(installs) > 0 { + if !cla.Force && !cla.Upgrade { + continue + } - if len(installs) > 0 && cla.Upgrade == false { - continue + if cla.Force && !cla.Upgrade { + pluginRequirement.VersionConstraints, _ = gversion.NewConstraint(fmt.Sprintf("=%s", installs[len(installs)-1].Version)) + } } newInstall, err := pluginRequirement.InstallLatest(plugingetter.InstallOptions{ InFolders: opts.FromFolders, BinaryInstallationOptions: opts.BinaryInstallationOptions, Getters: getters, + Force: cla.Force, }) if err != nil { c.Ui.Error(fmt.Sprintf("Failed getting the %q plugin:", pluginRequirement.Identifier)) @@ -163,6 +169,8 @@ Options: version, if there is a new higher one. Note that this still takes into consideration the version constraint of the config. + -force Forces installation of plugins, even if already + installed. ` return strings.TrimSpace(helpText) diff --git a/command/plugins_install.go b/command/plugins_install.go index e584ea778de..fbb2876cda5 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -50,6 +50,7 @@ Options: 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. + - force: forces installation of a plugin, even if it is already there. ` return strings.TrimSpace(helpText) @@ -72,10 +73,12 @@ type PluginsInstallArgs struct { PluginName 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") pa.MetaArgs.AddFlagSets(flags) } @@ -168,6 +171,7 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *Plugi InFolders: opts.FromFolders, BinaryInstallationOptions: opts.BinaryInstallationOptions, Getters: getters, + Force: args.Force, }) if err != nil { diff --git a/packer/plugin-getter/plugins.go b/packer/plugin-getter/plugins.go index c9fbd0b9d97..29b34da1c04 100644 --- a/packer/plugin-getter/plugins.go +++ b/packer/plugin-getter/plugins.go @@ -244,6 +244,9 @@ type InstallOptions struct { // folder of this list. InFolders []string + // Forces installation of the plugin, even if already installed. + Force bool + BinaryInstallationOptions } @@ -582,7 +585,7 @@ func (pr *Requirement) InstallLatest(opts InstallOptions) (*Installation, error) log.Printf("[TRACE] found a pre-exising %q checksum file", potentialChecksumer.Type) // if outputFile is there and matches the checksum: do nothing more. - if err := localChecksum.ChecksumFile(localChecksum.Expected, potentialOutputFilename); err == nil { + if err := localChecksum.ChecksumFile(localChecksum.Expected, potentialOutputFilename); err == nil && !opts.Force { log.Printf("[INFO] %s v%s plugin is already correctly installed in %q", pr.Identifier, version, potentialOutputFilename) return nil, nil // success } diff --git a/packer/plugin-getter/plugins_test.go b/packer/plugin-getter/plugins_test.go index dfb28767067..513f2c4993b 100644 --- a/packer/plugin-getter/plugins_test.go +++ b/packer/plugin-getter/plugins_test.go @@ -349,6 +349,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "5", APIVersionMinor: "0", OS: "darwin", ARCH: "amd64", @@ -385,6 +386,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "5", APIVersionMinor: "1", OS: "darwin", ARCH: "amd64", @@ -430,6 +432,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "5", APIVersionMinor: "0", OS: "darwin", ARCH: "amd64", @@ -477,6 +480,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "6", APIVersionMinor: "1", OS: "darwin", ARCH: "amd64", @@ -527,6 +531,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "6", APIVersionMinor: "1", OS: "darwin", ARCH: "amd64", @@ -577,6 +582,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "6", APIVersionMinor: "1", OS: "linux", ARCH: "amd64", @@ -621,6 +627,7 @@ func TestRequirement_InstallLatest(t *testing.T) { pluginFolderOne, pluginFolderTwo, }, + false, BinaryInstallationOptions{ APIVersionMajor: "6", APIVersionMinor: "1", OS: "darwin", ARCH: "amd64", @@ -662,6 +669,7 @@ func TestRequirement_InstallLatest(t *testing.T) { []string{ pluginFolderWrongChecksums, }, + false, BinaryInstallationOptions{ APIVersionMajor: "6", APIVersionMinor: "1", OS: "darwin", ARCH: "amd64", From 9b029c126981de2700b3ff3020a6560b5e3bbcf5 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 24 Nov 2023 10:56:01 -0500 Subject: [PATCH 3/6] command: reject version with --path for install To avoid plugins being installed with a specific version when a path is used for installing a plugin from a locally sourced plugin binary, we explicitly reject the combination of both a path and a version for plugins install. --- command/plugins_install.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/command/plugins_install.go b/command/plugins_install.go index fbb2876cda5..81b74cbd601 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -50,6 +50,7 @@ Options: 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. + This option cannot be specified with a version constraint. - force: forces installation of a plugin, even if it is already there. ` @@ -105,6 +106,12 @@ func (c *PluginsInstallCommand) ParseArgs(args []string) (*PluginsInstallArgs, i pa.Version = args[1] } + if pa.Path != "" && pa.Version != "" { + c.Ui.Error("Invalid arguments: a version cannot be specified with --path") + flags.Usage() + return pa, 1 + } + pa.PluginName = args[0] return pa, 0 @@ -256,12 +263,6 @@ an issue on our Github repo to signal it.`) }}) } - // Let's override the plugin's version if we specify it in the options - // of the command - if args.Version != "" { - desc.Version = args.Version - } - pluginBinary, err := os.Open(args.PluginPath) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ From 662b6839130e82f1bd0de90cb4a0590b7211244c Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 24 Nov 2023 11:20:54 -0500 Subject: [PATCH 4/6] command: plugins install reject non-releases When installing a plugin with packer plugins install --path, we only accept release versions of a plugin, as otherwise the loading can be inconsistent if for example a user specifies a required_plugins block in their template, in which case the plugins will be ignored. Until we have a simpler loading scheme then, we will reject non-release versions of plugins to avoid confusion. --- command/plugins_install.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/command/plugins_install.go b/command/plugins_install.go index 81b74cbd601..f6d7b11086e 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -263,6 +263,14 @@ an issue on our Github repo to signal it.`) }}) } + if strings.Contains(desc.Version, "-") { + return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version", + Detail: fmt.Sprintf("Packer can only install plugin releases with this command (ex: 1.0.0), the binary's reported version is %q", desc.Version), + }}) + } + pluginBinary, err := os.Open(args.PluginPath) if err != nil { return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{ From 47e35bd2b4a4ffbcd62f137c4e7de0b05c9dbe31 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 1 Dec 2023 13:14:08 -0500 Subject: [PATCH 5/6] 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 | 130 +++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 69 deletions(-) diff --git a/command/plugins_install.go b/command/plugins_install.go index f6d7b11086e..b467eee5f4d 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" @@ -44,12 +46,13 @@ Usage: packer plugins install [OPTIONS...] [] installed. Ex: packer plugins install github.com/hashicorp/happycloud v1.2.3 + packer plugins install --path ./packer-plugin-happycloud "github.com/hashicorp/happycloud" 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 +74,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 +110,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 +132,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 +145,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 +162,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 +200,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 +242,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 +252,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 +276,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 +286,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 +303,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 +324,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 +333,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 +350,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 } From af2a439d598550d75d69e18b3e9a62f8849e7063 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Mon, 4 Dec 2023 09:50:02 -0500 Subject: [PATCH 6/6] docs: add exerpt on packer plugins install --path --- command/init.go | 2 +- command/plugins_install.go | 2 +- website/content/docs/plugins/install-plugins.mdx | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/command/init.go b/command/init.go index 2b8cdd7595a..9e5b82f04ba 100644 --- a/command/init.go +++ b/command/init.go @@ -169,7 +169,7 @@ Options: version, if there is a new higher one. Note that this still takes into consideration the version constraint of the config. - -force Forces installation of plugins, even if already + -force Forces reinstallation of plugins, even if already installed. ` diff --git a/command/plugins_install.go b/command/plugins_install.go index b467eee5f4d..4d8434a2780 100644 --- a/command/plugins_install.go +++ b/command/plugins_install.go @@ -54,7 +54,7 @@ Options: 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. + - force: forces reinstallation of plugins, even if already installed. ` return strings.TrimSpace(helpText) diff --git a/website/content/docs/plugins/install-plugins.mdx b/website/content/docs/plugins/install-plugins.mdx index 1c2b602823f..b2d97975216 100644 --- a/website/content/docs/plugins/install-plugins.mdx +++ b/website/content/docs/plugins/install-plugins.mdx @@ -227,5 +227,15 @@ If you have a `required_plugins` for the plugin you're manually installing, make it respects the constraints described in the [Plugin loading workflow](#plugin-loading-workflow) section, otherwise Packer will not be able to load it. +Starting with v1.10.0 of Packer, you can also use `packer plugins install` with the +`--path` flag to install a plugin from a binary, following the layout that is required to +work with `required_plugins` block. + +```shell +packer plugins install --path github.com/hashicorp/vagrant +``` + +-> packer plugins install --path only works with release versions of plugins. +