2023-03-02 15:37:05 -05:00
// Copyright (c) HashiCorp, Inc.
2023-08-10 18:53:29 -04:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-02 15:37:05 -05:00
2022-02-10 16:53:50 -05:00
package command
import (
2023-12-01 13:14:08 -05:00
"bytes"
2022-02-10 16:53:50 -05:00
"context"
"crypto/sha256"
2023-10-03 11:52:48 -04:00
"encoding/json"
"flag"
2022-02-10 16:53:50 -05: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 16:53:50 -05:00
"runtime"
"strings"
"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 16:53:50 -05: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 16:53:50 -05:00
2022-03-15 16:32:49 -04:00
This command will install the most recent compatible Packer plugin matching
2022-02-10 16:53:50 -05: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 16:53:50 -05: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 16:53:50 -05: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 16:53:50 -05: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 16:53:50 -05: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 16:53:50 -05:00
opts := plugingetter . ListInstallationsOptions {
2024-01-15 14:41:25 -05:00
PluginDirectory : c . Meta . CoreConfig . Components . PluginConfig . PluginDirectory ,
2022-02-10 16:53:50 -05:00
BinaryInstallationOptions : plugingetter . BinaryInstallationOptions {
OS : runtime . GOOS ,
ARCH : runtime . GOARCH ,
APIVersionMajor : pluginsdk . APIVersionMajor ,
APIVersionMinor : pluginsdk . APIVersionMinor ,
Checksummers : [ ] plugingetter . Checksummer {
{ Type : "sha256" , Hash : sha256 . New ( ) } ,
} ,
} ,
}
2023-12-01 13:14:08 -05:00
if runtime . GOOS == "windows" {
opts . BinaryInstallationOptions . Ext = ".exe"
}
2022-02-10 16:53:50 -05:00
2023-12-01 13:14:08 -05:00
plugin , diags := addrs . ParsePluginSourceString ( args . PluginIdentifier )
2022-02-10 16:53:50 -05:00
if diags . HasErrors ( ) {
c . Ui . Error ( diags . Error ( ) )
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 16:53:50 -05: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 16:53:50 -05:00
if err != nil {
c . Ui . Error ( err . Error ( ) )
return 1
}
pluginRequirement . VersionConstraints = constraints
}
getters := [ ] plugingetter . Getter {
& 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 ( ) ,
} ,
}
newInstall , err := pluginRequirement . InstallLatest ( plugingetter . InstallOptions {
2024-01-15 14:41:25 -05:00
PluginDirectory : opts . PluginDirectory ,
2022-02-10 16:53:50 -05:00
BinaryInstallationOptions : opts . BinaryInstallationOptions ,
Getters : getters ,
2023-10-25 11:56:30 -04:00
Force : args . Force ,
2022-02-10 16:53:50 -05: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" ,
pluginIdentifier . Type ,
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
}