// Copyright IBM Corp. 2013, 2025 // SPDX-License-Identifier: BUSL-1.1 package command import ( "bytes" "context" "crypto/sha256" "encoding/json" "flag" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/hashicorp/packer/packer/plugin-getter/release" "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" ) type PluginsInstallCommand struct { Meta } func (c *PluginsInstallCommand) Synopsis() string { return "Install latest Packer plugin [matching version constraint]" } func (c *PluginsInstallCommand) Help() string { helpText := ` Usage: packer plugins install [OPTIONS...] [] 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" 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 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) } func (c *PluginsInstallCommand) Run(args []string) int { ctx, cleanup := handleTermInterrupt(c.Ui) defer cleanup() 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 (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 { 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 != "" { 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{ PluginDirectory: c.Meta.CoreConfig.Components.PluginConfig.PluginDirectory, BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{ OS: runtime.GOOS, ARCH: runtime.GOARCH, APIVersionMajor: pluginsdk.APIVersionMajor, APIVersionMinor: pluginsdk.APIVersionMinor, Checksummers: []plugingetter.Checksummer{ {Type: "sha256", Hash: sha256.New()}, }, ReleasesOnly: true, }, } if runtime.GOOS == "windows" { opts.BinaryInstallationOptions.Ext = ".exe" } plugin, err := addrs.ParsePluginSourceString(args.PluginIdentifier) if err != nil { c.Ui.Errorf("Invalid source string %q: %s", args.PluginIdentifier, err) 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) } // a plugin requirement that matches them all pluginRequirement := plugingetter.Requirement{ Identifier: plugin, } if args.Version != "" { constraints, err := version.NewConstraint(args.Version) if err != nil { c.Ui.Error(err.Error()) return 1 } hasPrerelease := false for _, con := range constraints { if con.Prerelease() { hasPrerelease = true } } if hasPrerelease { c.Ui.Errorf("Unsupported prerelease for constraint %q", args.Version) return 1 } pluginRequirement.VersionConstraints = constraints } getters := []plugingetter.Getter{ &release.Getter{ Name: "releases.hashicorp.com", }, &github.Getter{ // In the past some terraform plugins downloads were blocked from a // specific aws region by s3. Changing the user agent unblocked the // downloads so having one user agent per version will help mitigate // that a little more. Especially in the case someone forks this // code to make it more aggressive or something. // TODO: allow to set this from the config file or an environment // variable. UserAgent: "packer-getter-github-" + pkrversion.String(), Name: "github.com", }, } newInstall, err := pluginRequirement.InstallLatest(plugingetter.InstallOptions{ PluginDirectory: opts.PluginDirectory, BinaryInstallationOptions: opts.BinaryInstallationOptions, Getters: getters, Force: args.Force, }) if err != nil { c.Ui.Error(err.Error()) return 1 } if newInstall != nil { msg := fmt.Sprintf("Installed plugin %s %s in %q", pluginRequirement.Identifier, newInstall.Version, newInstall.BinaryPath) ui := &packer.ColoredUi{ Color: packer.UiColorCyan, Ui: c.Ui, } ui.Say(msg) return 0 } return 0 } func (c *PluginsInstallCommand) InstallFromBinary(opts plugingetter.ListInstallationsOptions, pluginIdentifier *addrs.Plugin, args *PluginsInstallArgs) int { pluginDir := opts.PluginDirectory var err error args.PluginPath, err = filepath.Abs(args.PluginPath) 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) 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() != "" && semver.Prerelease() != "dev" { 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) or development pre-releases (ex: 1.0.0-dev), 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{ 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() // 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), }}) } // Remove metadata from plugin path noMetaVersion := semver.Core().String() if semver.Prerelease() != "" { noMetaVersion = fmt.Sprintf("%s-%s", noMetaVersion, semver.Prerelease()) } outputPrefix := fmt.Sprintf( "packer-plugin-%s_v%s_%s", pluginIdentifier.Name(), noMetaVersion, 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) 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{})) c.Ui.Say(fmt.Sprintf("Successfully installed plugin %s from %s to %s", args.PluginIdentifier, args.PluginPath, binaryPath)) return 0 }