mirror of
https://github.com/hashicorp/packer.git
synced 2026-02-20 08:20:06 -05:00
When remotely installing a plugin, constraints are used by Packer to determine which version of a plugin to install. These constraints can be arbitrarily complex, including operators and ranges in which to look for valid versions. However, the versions specified in those constraints should always be final releases, and not a pre-release since we don't explicitly support remotely installing pre-releases. This commit therefore addds checks to make sure these are reported ASAP, even before the source is contacted to list releases and picking one to install.
372 lines
12 KiB
Go
372 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// 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/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...] <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"
|
|
|
|
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)
|
|
}
|
|
|
|
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{
|
|
&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{
|
|
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
|
|
}
|