Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

command: add --path flag to packer plugins install #12643

Merged
merged 6 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
nywilken marked this conversation as resolved.
Show resolved Hide resolved

ia.MetaArgs.AddFlagSets(flags)
}
Expand All @@ -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 <path>`
Expand Down
14 changes: 11 additions & 3 deletions command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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 reinstallation of plugins, even if already
installed.
`

return strings.TrimSpace(helpText)
Expand Down
250 changes: 238 additions & 12 deletions command/plugins_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@
package command

import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"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 {
Expand All @@ -30,14 +38,23 @@ func (c *PluginsInstallCommand) Synopsis() string {

func (c *PluginsInstallCommand) Help() string {
helpText := `
Usage: packer plugins install <plugin> [<version constraint>]
Usage: packer plugins install [OPTIONS...] <plugin> [<version constraint>]

This command will install the most recent compatible Packer plugin matching
version constraint.
When the version constraint is omitted, the most recent version will be
installed.

Ex: packer plugins install github.com/hashicorp/happycloud v1.2.3
packer plugins install --path ./packer-plugin-happycloud "github.com/hashicorp/happycloud"

lbajolet-hashicorp marked this conversation as resolved.
Show resolved Hide resolved
Options:
- path <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 remote location, and instead
install the binary in the Packer plugins path.
This option cannot be specified with a version constraint.
- force: forces reinstallation of plugins, even if already installed.
`

return strings.TrimSpace(helpText)
Expand All @@ -47,14 +64,62 @@ 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
PluginIdentifier string
PluginPath string
Version string
Force bool
}

func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []string) int {
func (pa *PluginsInstallArgs) AddFlagSets(flags *flag.FlagSet) {
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)
}

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

if pa.Path != "" && pa.Version != "" {
nywilken marked this conversation as resolved.
Show resolved Hide resolved
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.PluginIdentifier = 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{
Expand All @@ -67,31 +132,36 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []stri
},
},
}
if runtime.GOOS == "windows" {
opts.BinaryInstallationOptions.Ext = ".exe"
}

plugin, diags := addrs.ParsePluginSourceString(args[0])
plugin, diags := addrs.ParsePluginSourceString(args.PluginIdentifier)
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(opts, plugin, args)
}
nywilken marked this conversation as resolved.
Show resolved Hide resolved

// 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
}
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
Expand All @@ -109,6 +179,7 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []stri
InFolders: opts.FromFolders,
BinaryInstallationOptions: opts.BinaryInstallationOptions,
Getters: getters,
Force: args.Force,
})

if err != nil {
Expand All @@ -128,3 +199,158 @@ func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []stri

return 0
}

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]

var err error

args.PluginPath, err = filepath.Abs(args.PluginPath)
nywilken marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to transform path",
Detail: fmt.Sprintf("Failed to transform the given path to an absolute one: %s", err),
}})
}

s, err := os.Stat(args.PluginPath)
nywilken marked this conversation as resolved.
Show resolved Hide resolved
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.PluginIdentifier, 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),
}})
}

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",
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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't solve the problem for folks using dev builds right.

Suggested change
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),
Detail: fmt.Sprintf("Installation of prerelease binaries with this command is not allowed, the binary's reported version is %q", desc.Version),
~>  packer plugins install --path ~/.packer.d/plugins/packer-plugin-amazon github.com/hashicorp/happycloud
Error: Invalid version

Packer can only install plugin releases with this command (ex: 1.0.0), the
binary's reported version is "1.2.9-dev"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not indeed for now, it's one of the things we need to address/fix for 1.11.0

}})
}

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

pluginContents := bytes.Buffer{}
_, err = io.Copy(&pluginContents, 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),
}})
}
_ = pluginBinary.Close()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should handle this error in case close fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, the chances of Close failing are excessively thin, and will be closed anyway when the process terminates, so we probably can overlook this one (we would too if we deferred)


// 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 := filepath.Join(
pluginDir,
filepath.Join(pluginIdentifier.Parts()...),
)
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),
}})
}

outputPrefix := fmt.Sprintf(
"packer-plugin-%s_v%s_%s",
pluginIdentifier.Type,
desc.Version,
desc.APIVersion,
)
binaryPath := filepath.Join(
installDir,
outputPrefix+opts.BinaryInstallationOptions.FilenameSuffix(),
)

outputPlugin, err := os.OpenFile(binaryPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0755)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same permissions we use inside of InstallLatest?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so? If not we should probably settle for 755 for both, since read/execute makes sense for eveyone else, and all perms for the owner

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 = outputPlugin.Write(pluginContents.Bytes())
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),
}})
}

// 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 {
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{}))
nywilken marked this conversation as resolved.
Show resolved Hide resolved
c.Ui.Say(fmt.Sprintf("Successfully installed plugin %s from %s to %s", args.PluginIdentifier, args.PluginPath, binaryPath))

return 0
}
Loading
Loading