2025-11-30 18:32:15 +00:00
// Copyright IBM Corp. 2013, 2025
2023-08-10 15:53:29 -07:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-02 15:37:05 -05:00
2022-02-10 22:53:50 +01:00
package command
import (
2023-12-01 13:14:08 -05:00
"bytes"
2022-02-10 22:53:50 +01:00
"context"
"crypto/sha256"
2023-10-03 11:52:48 -04:00
"encoding/json"
"flag"
2022-02-10 22:53:50 +01:00
"fmt"
2023-10-03 11:52:48 -04:00
"io"
"os"
"os/exec"
2023-12-01 13:14:08 -05:00
"path/filepath"
2022-02-10 22:53:50 +01:00
"runtime"
"strings"
2025-07-29 08:50:23 +05:30
"github.com/hashicorp/packer/packer/plugin-getter/release"
2022-02-10 22:53:50 +01:00
"github.com/hashicorp/go-version"
2023-10-03 11:52:48 -04:00
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer-plugin-sdk/plugin"
2022-02-10 22:53:50 +01:00
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 := `
2023-10-03 11:52:48 -04:00
Usage : packer plugins install [ OPTIONS ... ] < plugin > [ < version constraint > ]
2022-02-10 22:53:50 +01:00
2022-03-15 16:32:49 -04:00
This command will install the most recent compatible Packer plugin matching
2022-02-10 22:53:50 +01:00
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
2023-12-01 13:14:08 -05:00
packer plugins install -- path . / packer - plugin - happycloud "github.com/hashicorp/happycloud"
2023-10-03 11:52:48 -04:00
Options :
2024-02-02 11:06:59 -05:00
- path < path > Install the plugin from a locally - sourced plugin binary .
This installs the plugin where a normal invocation would , but will
2023-12-05 14:15:57 -05:00
not try to download it from a remote location , and instead
2024-02-02 11:06:59 -05:00
install the binary in the Packer plugins path . This option cannot
2023-12-05 14:15:57 -05:00
be specified with a version constraint .
- force Forces reinstallation of plugins , even if already installed .
2022-02-10 22:53:50 +01:00
`
return strings . TrimSpace ( helpText )
}
func ( c * PluginsInstallCommand ) Run ( args [ ] string ) int {
ctx , cleanup := handleTermInterrupt ( c . Ui )
defer cleanup ( )
2023-10-03 11:52:48 -04:00
cmdArgs , ret := c . ParseArgs ( args )
if ret != 0 {
return ret
}
return c . RunContext ( ctx , cmdArgs )
}
type PluginsInstallArgs struct {
MetaArgs
2023-12-01 13:14:08 -05:00
PluginIdentifier string
PluginPath string
Version string
Force bool
2023-10-03 11:52:48 -04:00
}
func ( pa * PluginsInstallArgs ) AddFlagSets ( flags * flag . FlagSet ) {
2023-12-01 13:14:08 -05:00
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." )
2023-10-03 11:52:48 -04:00
pa . MetaArgs . AddFlagSets ( flags )
2022-02-10 22:53:50 +01:00
}
2023-10-03 11:52:48 -04:00
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 ( )
2022-02-10 22:53:50 +01:00
if len ( args ) < 1 || len ( args ) > 2 {
2023-10-03 11:52:48 -04:00
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 ]
2022-02-10 22:53:50 +01:00
}
2023-11-24 10:56:01 -05:00
if pa . Path != "" && pa . Version != "" {
2023-12-01 13:14:08 -05:00
c . Ui . Error ( "Invalid arguments: a version cannot be specified when using --path to install a local plugin binary" )
2023-11-24 10:56:01 -05:00
flags . Usage ( )
return pa , 1
}
2023-12-01 13:14:08 -05:00
pa . PluginIdentifier = args [ 0 ]
2023-10-03 11:52:48 -04:00
return pa , 0
}
func ( c * PluginsInstallCommand ) RunContext ( buildCtx context . Context , args * PluginsInstallArgs ) int {
2022-02-10 22:53:50 +01:00
opts := plugingetter . ListInstallationsOptions {
2024-01-15 14:41:25 -05:00
PluginDirectory : c . Meta . CoreConfig . Components . PluginConfig . PluginDirectory ,
2022-02-10 22:53:50 +01:00
BinaryInstallationOptions : plugingetter . BinaryInstallationOptions {
OS : runtime . GOOS ,
ARCH : runtime . GOARCH ,
APIVersionMajor : pluginsdk . APIVersionMajor ,
APIVersionMinor : pluginsdk . APIVersionMinor ,
Checksummers : [ ] plugingetter . Checksummer {
{ Type : "sha256" , Hash : sha256 . New ( ) } ,
} ,
command: list releases only for remote installs
When installing a plugin from a remote source, we list the installed
plugins that match the constraints specified, and if the constraint is
already satisfied, we don't do anything.
However, since remote installation is only relevant for releases of a
plugin, we should only look at the installed releases of a plugin, and
not consider pre-releases for that step.
This wasn't the case before this commit, as if a prerelease version of a
commit (ex: 10.8.1-dev), and we try to invoke `packer init` with a
constraint on this version specifically, Packer would locate that
pre-release and assume it was already installed, so would silently
succeed the command and do nothing.
This isn't the expected behaviour as we should install the final release
of that plugin, regardless of any prerelease installation of the plugin.
So this commit fixes that by only listing releases, so we don't report
the plugin being already installed if a prerelease is what's installed.
2024-05-02 09:52:51 -04:00
ReleasesOnly : true ,
2022-02-10 22:53:50 +01:00
} ,
}
2023-12-01 13:14:08 -05:00
if runtime . GOOS == "windows" {
opts . BinaryInstallationOptions . Ext = ".exe"
}
2022-02-10 22:53:50 +01:00
2024-05-09 17:11:15 -04:00
plugin , err := addrs . ParsePluginSourceString ( args . PluginIdentifier )
if err != nil {
c . Ui . Errorf ( "Invalid source string %q: %s" , args . PluginIdentifier , err )
2022-02-10 22:53:50 +01:00
return 1
}
2023-10-03 11:52:48 -04:00
// 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 != "" {
2023-12-01 13:14:08 -05:00
return c . InstallFromBinary ( opts , plugin , args )
2023-10-03 11:52:48 -04:00
}
2022-02-10 22:53:50 +01:00
// a plugin requirement that matches them all
pluginRequirement := plugingetter . Requirement {
Identifier : plugin ,
}
2023-10-03 11:52:48 -04:00
if args . Version != "" {
constraints , err := version . NewConstraint ( args . Version )
2022-02-10 22:53:50 +01:00
if err != nil {
c . Ui . Error ( err . Error ( ) )
return 1
}
2024-05-30 08:25:21 -04:00
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
}
2022-02-10 22:53:50 +01:00
pluginRequirement . VersionConstraints = constraints
}
getters := [ ] plugingetter . Getter {
2025-07-29 08:50:23 +05:30
& release . Getter {
Name : "releases.hashicorp.com" ,
} ,
2022-02-10 22:53:50 +01:00
& 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 ( ) ,
2025-07-29 08:50:23 +05:30
Name : "github.com" ,
2022-02-10 22:53:50 +01:00
} ,
}
newInstall , err := pluginRequirement . InstallLatest ( plugingetter . InstallOptions {
2024-01-15 14:41:25 -05:00
PluginDirectory : opts . PluginDirectory ,
2022-02-10 22:53:50 +01:00
BinaryInstallationOptions : opts . BinaryInstallationOptions ,
Getters : getters ,
2023-10-25 11:56:30 -04:00
Force : args . Force ,
2022-02-10 22:53:50 +01:00
} )
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
}
2023-10-03 11:52:48 -04:00
2023-12-01 13:14:08 -05:00
func ( c * PluginsInstallCommand ) InstallFromBinary ( opts plugingetter . ListInstallationsOptions , pluginIdentifier * addrs . Plugin , args * PluginsInstallArgs ) int {
2024-01-15 14:41:25 -05:00
pluginDir := opts . PluginDirectory
2023-10-03 11:52:48 -04:00
2023-12-01 13:14:08 -05:00
var err error
2023-10-03 11:52:48 -04:00
2023-12-01 13:14:08 -05:00
args . PluginPath , err = filepath . Abs ( args . PluginPath )
if err != nil {
2023-10-03 11:52:48 -04:00
return writeDiags ( c . Ui , nil , hcl . Diagnostics { & hcl . Diagnostic {
Severity : hcl . DiagError ,
2023-12-01 13:14:08 -05:00
Summary : "Failed to transform path" ,
Detail : fmt . Sprintf ( "Failed to transform the given path to an absolute one: %s" , err ) ,
2023-10-03 11:52:48 -04:00
} } )
}
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" ,
2023-12-01 13:14:08 -05:00
Detail : fmt . Sprintf ( "The plugin %q failed to be opened because of an error: %s" , args . PluginIdentifier , err ) ,
2023-10-03 11:52:48 -04:00
} } )
}
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 ) ,
} } )
}
2023-12-01 13:14:08 -05:00
2023-10-03 11:52:48 -04:00
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 ) ,
} } )
}
2023-12-01 13:14:08 -05:00
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 ) ,
} } )
}
2024-02-02 11:04:44 -05:00
if semver . Prerelease ( ) != "" && semver . Prerelease ( ) != "dev" {
2023-11-24 11:20:54 -05:00
return writeDiags ( c . Ui , nil , hcl . Diagnostics { & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid version" ,
2024-02-02 11:04:44 -05:00
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 ) ,
2023-11-24 11:20:54 -05:00
} } )
}
2023-10-03 11:52:48 -04:00
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 ) ,
} } )
}
2023-12-01 13:14:08 -05:00
pluginContents := bytes . Buffer { }
_ , err = io . Copy ( & pluginContents , pluginBinary )
2023-10-03 11:52:48 -04:00
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 ) ,
} } )
}
2023-12-01 13:14:08 -05:00
_ = pluginBinary . Close ( )
2023-10-03 11:52:48 -04:00
// 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.
2023-12-01 13:14:08 -05:00
installDir := filepath . Join (
pluginDir ,
filepath . Join ( pluginIdentifier . Parts ( ) ... ) ,
)
2023-10-03 11:52:48 -04:00
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 ) ,
} } )
}
2024-03-19 10:11:07 -04:00
// Remove metadata from plugin path
noMetaVersion := semver . Core ( ) . String ( )
if semver . Prerelease ( ) != "" {
noMetaVersion = fmt . Sprintf ( "%s-%s" , noMetaVersion , semver . Prerelease ( ) )
}
2023-12-01 13:14:08 -05:00
outputPrefix := fmt . Sprintf (
"packer-plugin-%s_v%s_%s" ,
2024-04-04 16:23:28 -04:00
pluginIdentifier . Name ( ) ,
2024-03-19 10:11:07 -04:00
noMetaVersion ,
2023-10-03 11:52:48 -04:00
desc . APIVersion ,
)
2023-12-01 13:14:08 -05:00
binaryPath := filepath . Join (
installDir ,
outputPrefix + opts . BinaryInstallationOptions . FilenameSuffix ( ) ,
)
2023-10-03 11:52:48 -04:00
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 ( )
2023-12-01 13:14:08 -05:00
_ , err = outputPlugin . Write ( pluginContents . Bytes ( ) )
2023-10-03 11:52:48 -04:00
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 ) ,
} } )
}
2023-12-01 13:14:08 -05:00
// 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 ( ) )
2023-10-03 11:52:48 -04:00
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 { } ) )
2023-12-01 13:14:08 -05:00
c . Ui . Say ( fmt . Sprintf ( "Successfully installed plugin %s from %s to %s" , args . PluginIdentifier , args . PluginPath , binaryPath ) )
2023-10-03 11:52:48 -04:00
return 0
}