github.com/hashicorp/packer@v1.14.3/command/plugins_install.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package command
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/sha256"
    10  	"encoding/json"
    11  	"flag"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"runtime"
    18  	"strings"
    19  
    20  	"github.com/hashicorp/packer/packer/plugin-getter/release"
    21  
    22  	"github.com/hashicorp/go-version"
    23  	"github.com/hashicorp/hcl/v2"
    24  	"github.com/hashicorp/packer-plugin-sdk/plugin"
    25  	pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
    26  	"github.com/hashicorp/packer/hcl2template/addrs"
    27  	"github.com/hashicorp/packer/packer"
    28  	plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
    29  	"github.com/hashicorp/packer/packer/plugin-getter/github"
    30  	pkrversion "github.com/hashicorp/packer/version"
    31  )
    32  
    33  type PluginsInstallCommand struct {
    34  	Meta
    35  }
    36  
    37  func (c *PluginsInstallCommand) Synopsis() string {
    38  	return "Install latest Packer plugin [matching version constraint]"
    39  }
    40  
    41  func (c *PluginsInstallCommand) Help() string {
    42  	helpText := `
    43  Usage: packer plugins install [OPTIONS...] <plugin> [<version constraint>]
    44  
    45    This command will install the most recent compatible Packer plugin matching
    46    version constraint.
    47    When the version constraint is omitted, the most recent version will be
    48    installed.
    49  
    50    Ex: packer plugins install github.com/hashicorp/happycloud v1.2.3
    51        packer plugins install --path ./packer-plugin-happycloud "github.com/hashicorp/happycloud"
    52  
    53  Options:
    54    -path <path>                  Install the plugin from a locally-sourced plugin binary.
    55                                  This installs the plugin where a normal invocation would, but will
    56                                  not try to download it from a remote location, and instead
    57                                  install the binary in the Packer plugins path. This option cannot
    58                                  be specified with a version constraint.
    59    -force                        Forces reinstallation of plugins, even if already installed.
    60  `
    61  
    62  	return strings.TrimSpace(helpText)
    63  }
    64  
    65  func (c *PluginsInstallCommand) Run(args []string) int {
    66  	ctx, cleanup := handleTermInterrupt(c.Ui)
    67  	defer cleanup()
    68  
    69  	cmdArgs, ret := c.ParseArgs(args)
    70  	if ret != 0 {
    71  		return ret
    72  	}
    73  
    74  	return c.RunContext(ctx, cmdArgs)
    75  }
    76  
    77  type PluginsInstallArgs struct {
    78  	MetaArgs
    79  	PluginIdentifier string
    80  	PluginPath       string
    81  	Version          string
    82  	Force            bool
    83  }
    84  
    85  func (pa *PluginsInstallArgs) AddFlagSets(flags *flag.FlagSet) {
    86  	flags.StringVar(&pa.PluginPath, "path", "", "install the binary specified by path as a Packer plugin.")
    87  	flags.BoolVar(&pa.Force, "force", false, "force installation of the specified plugin, even if already installed.")
    88  	pa.MetaArgs.AddFlagSets(flags)
    89  }
    90  
    91  func (c *PluginsInstallCommand) ParseArgs(args []string) (*PluginsInstallArgs, int) {
    92  	pa := &PluginsInstallArgs{}
    93  
    94  	flags := c.Meta.FlagSet("plugins install")
    95  	flags.Usage = func() { c.Ui.Say(c.Help()) }
    96  	pa.AddFlagSets(flags)
    97  	err := flags.Parse(args)
    98  	if err != nil {
    99  		c.Ui.Error(fmt.Sprintf("Failed to parse options: %s", err))
   100  		return pa, 1
   101  	}
   102  
   103  	args = flags.Args()
   104  	if len(args) < 1 || len(args) > 2 {
   105  		c.Ui.Error(fmt.Sprintf("Invalid arguments, expected either 1 or 2 positional arguments, got %d", len(args)))
   106  		flags.Usage()
   107  		return pa, 1
   108  	}
   109  
   110  	if len(args) == 2 {
   111  		pa.Version = args[1]
   112  	}
   113  
   114  	if pa.Path != "" && pa.Version != "" {
   115  		c.Ui.Error("Invalid arguments: a version cannot be specified when using --path to install a local plugin binary")
   116  		flags.Usage()
   117  		return pa, 1
   118  	}
   119  
   120  	pa.PluginIdentifier = args[0]
   121  	return pa, 0
   122  }
   123  
   124  func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args *PluginsInstallArgs) int {
   125  	opts := plugingetter.ListInstallationsOptions{
   126  		PluginDirectory: c.Meta.CoreConfig.Components.PluginConfig.PluginDirectory,
   127  		BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{
   128  			OS:              runtime.GOOS,
   129  			ARCH:            runtime.GOARCH,
   130  			APIVersionMajor: pluginsdk.APIVersionMajor,
   131  			APIVersionMinor: pluginsdk.APIVersionMinor,
   132  			Checksummers: []plugingetter.Checksummer{
   133  				{Type: "sha256", Hash: sha256.New()},
   134  			},
   135  			ReleasesOnly: true,
   136  		},
   137  	}
   138  	if runtime.GOOS == "windows" {
   139  		opts.BinaryInstallationOptions.Ext = ".exe"
   140  	}
   141  
   142  	plugin, err := addrs.ParsePluginSourceString(args.PluginIdentifier)
   143  	if err != nil {
   144  		c.Ui.Errorf("Invalid source string %q: %s", args.PluginIdentifier, err)
   145  		return 1
   146  	}
   147  
   148  	// If we did specify a binary to install the plugin from, we ignore
   149  	// the Github-based getter in favour of installing it directly.
   150  	if args.PluginPath != "" {
   151  		return c.InstallFromBinary(opts, plugin, args)
   152  	}
   153  
   154  	// a plugin requirement that matches them all
   155  	pluginRequirement := plugingetter.Requirement{
   156  		Identifier: plugin,
   157  	}
   158  
   159  	if args.Version != "" {
   160  		constraints, err := version.NewConstraint(args.Version)
   161  		if err != nil {
   162  			c.Ui.Error(err.Error())
   163  			return 1
   164  		}
   165  
   166  		hasPrerelease := false
   167  		for _, con := range constraints {
   168  			if con.Prerelease() {
   169  				hasPrerelease = true
   170  			}
   171  		}
   172  		if hasPrerelease {
   173  			c.Ui.Errorf("Unsupported prerelease for constraint %q", args.Version)
   174  			return 1
   175  		}
   176  
   177  		pluginRequirement.VersionConstraints = constraints
   178  	}
   179  
   180  	getters := []plugingetter.Getter{
   181  		&release.Getter{
   182  			Name: "releases.hashicorp.com",
   183  		},
   184  		&github.Getter{
   185  			// In the past some terraform plugins downloads were blocked from a
   186  			// specific aws region by s3. Changing the user agent unblocked the
   187  			// downloads so having one user agent per version will help mitigate
   188  			// that a little more. Especially in the case someone forks this
   189  			// code to make it more aggressive or something.
   190  			// TODO: allow to set this from the config file or an environment
   191  			// variable.
   192  			UserAgent: "packer-getter-github-" + pkrversion.String(),
   193  			Name:      "github.com",
   194  		},
   195  	}
   196  
   197  	newInstall, err := pluginRequirement.InstallLatest(plugingetter.InstallOptions{
   198  		PluginDirectory:           opts.PluginDirectory,
   199  		BinaryInstallationOptions: opts.BinaryInstallationOptions,
   200  		Getters:                   getters,
   201  		Force:                     args.Force,
   202  	})
   203  
   204  	if err != nil {
   205  		c.Ui.Error(err.Error())
   206  		return 1
   207  	}
   208  
   209  	if newInstall != nil {
   210  		msg := fmt.Sprintf("Installed plugin %s %s in %q", pluginRequirement.Identifier, newInstall.Version, newInstall.BinaryPath)
   211  		ui := &packer.ColoredUi{
   212  			Color: packer.UiColorCyan,
   213  			Ui:    c.Ui,
   214  		}
   215  		ui.Say(msg)
   216  		return 0
   217  	}
   218  
   219  	return 0
   220  }
   221  
   222  func (c *PluginsInstallCommand) InstallFromBinary(opts plugingetter.ListInstallationsOptions, pluginIdentifier *addrs.Plugin, args *PluginsInstallArgs) int {
   223  	pluginDir := opts.PluginDirectory
   224  
   225  	var err error
   226  
   227  	args.PluginPath, err = filepath.Abs(args.PluginPath)
   228  	if err != nil {
   229  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   230  			Severity: hcl.DiagError,
   231  			Summary:  "Failed to transform path",
   232  			Detail:   fmt.Sprintf("Failed to transform the given path to an absolute one: %s", err),
   233  		}})
   234  	}
   235  
   236  	s, err := os.Stat(args.PluginPath)
   237  	if err != nil {
   238  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   239  			Severity: hcl.DiagError,
   240  			Summary:  "Unable to find plugin to promote",
   241  			Detail:   fmt.Sprintf("The plugin %q failed to be opened because of an error: %s", args.PluginIdentifier, err),
   242  		}})
   243  	}
   244  
   245  	if s.IsDir() {
   246  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   247  			Severity: hcl.DiagError,
   248  			Summary:  "Plugin to promote cannot be a directory",
   249  			Detail:   "The packer plugin promote command can only install binaries, not directories",
   250  		}})
   251  	}
   252  
   253  	describeCmd, err := exec.Command(args.PluginPath, "describe").Output()
   254  	if err != nil {
   255  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   256  			Severity: hcl.DiagError,
   257  			Summary:  "Failed to describe the plugin",
   258  			Detail:   fmt.Sprintf("Packer failed to run %s describe: %s", args.PluginPath, err),
   259  		}})
   260  	}
   261  
   262  	var desc plugin.SetDescription
   263  	if err := json.Unmarshal(describeCmd, &desc); err != nil {
   264  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   265  			Severity: hcl.DiagError,
   266  			Summary:  "Failed to decode plugin describe info",
   267  			Detail:   fmt.Sprintf("'%s describe' produced information that Packer couldn't decode: %s", args.PluginPath, err),
   268  		}})
   269  	}
   270  
   271  	semver, err := version.NewSemver(desc.Version)
   272  	if err != nil {
   273  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   274  			Severity: hcl.DiagError,
   275  			Summary:  "Invalid version",
   276  			Detail:   fmt.Sprintf("Plugin's reported version (%q) is not semver-compatible: %s", desc.Version, err),
   277  		}})
   278  	}
   279  	if semver.Prerelease() != "" && semver.Prerelease() != "dev" {
   280  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   281  			Severity: hcl.DiagError,
   282  			Summary:  "Invalid version",
   283  			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),
   284  		}})
   285  	}
   286  
   287  	pluginBinary, err := os.Open(args.PluginPath)
   288  	if err != nil {
   289  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   290  			Severity: hcl.DiagError,
   291  			Summary:  "Failed to open plugin binary",
   292  			Detail:   fmt.Sprintf("Failed to open plugin binary from %q: %s", args.PluginPath, err),
   293  		}})
   294  	}
   295  
   296  	pluginContents := bytes.Buffer{}
   297  	_, err = io.Copy(&pluginContents, pluginBinary)
   298  	if err != nil {
   299  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   300  			Severity: hcl.DiagError,
   301  			Summary:  "Failed to read plugin binary's contents",
   302  			Detail:   fmt.Sprintf("Failed to read plugin binary from %q: %s", args.PluginPath, err),
   303  		}})
   304  	}
   305  	_ = pluginBinary.Close()
   306  
   307  	// At this point, we know the provided binary behaves correctly with
   308  	// describe, so it's very likely to be a plugin, let's install it.
   309  	installDir := filepath.Join(
   310  		pluginDir,
   311  		filepath.Join(pluginIdentifier.Parts()...),
   312  	)
   313  	err = os.MkdirAll(installDir, 0755)
   314  	if err != nil {
   315  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   316  			Severity: hcl.DiagError,
   317  			Summary:  "Failed to create output directory",
   318  			Detail:   fmt.Sprintf("The installation directory %q failed to be created because of an error: %s", installDir, err),
   319  		}})
   320  	}
   321  
   322  	// Remove metadata from plugin path
   323  	noMetaVersion := semver.Core().String()
   324  	if semver.Prerelease() != "" {
   325  		noMetaVersion = fmt.Sprintf("%s-%s", noMetaVersion, semver.Prerelease())
   326  	}
   327  
   328  	outputPrefix := fmt.Sprintf(
   329  		"packer-plugin-%s_v%s_%s",
   330  		pluginIdentifier.Name(),
   331  		noMetaVersion,
   332  		desc.APIVersion,
   333  	)
   334  	binaryPath := filepath.Join(
   335  		installDir,
   336  		outputPrefix+opts.BinaryInstallationOptions.FilenameSuffix(),
   337  	)
   338  
   339  	outputPlugin, err := os.OpenFile(binaryPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0755)
   340  	if err != nil {
   341  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   342  			Severity: hcl.DiagError,
   343  			Summary:  "Failed to create plugin binary",
   344  			Detail:   fmt.Sprintf("Failed to create plugin binary at %q: %s", binaryPath, err),
   345  		}})
   346  	}
   347  	defer outputPlugin.Close()
   348  
   349  	_, err = outputPlugin.Write(pluginContents.Bytes())
   350  	if err != nil {
   351  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   352  			Severity: hcl.DiagError,
   353  			Summary:  "Failed to copy plugin binary's contents",
   354  			Detail:   fmt.Sprintf("Failed to copy plugin binary from %q to %q: %s", args.PluginPath, binaryPath, err),
   355  		}})
   356  	}
   357  
   358  	// We'll install the SHA256SUM file alongside the plugin, based on the
   359  	// contents of the plugin being passed.
   360  	shasum := sha256.New()
   361  	_, _ = shasum.Write(pluginContents.Bytes())
   362  
   363  	shasumPath := fmt.Sprintf("%s_SHA256SUM", binaryPath)
   364  	shaFile, err := os.OpenFile(shasumPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
   365  	if err != nil {
   366  		return writeDiags(c.Ui, nil, hcl.Diagnostics{&hcl.Diagnostic{
   367  			Severity: hcl.DiagError,
   368  			Summary:  "Failed to create plugin SHA256SUM file",
   369  			Detail:   fmt.Sprintf("Failed to create SHA256SUM file at %q: %s", shasumPath, err),
   370  		}})
   371  	}
   372  	defer shaFile.Close()
   373  
   374  	fmt.Fprintf(shaFile, "%x", shasum.Sum([]byte{}))
   375  	c.Ui.Say(fmt.Sprintf("Successfully installed plugin %s from %s to %s", args.PluginIdentifier, args.PluginPath, binaryPath))
   376  
   377  	return 0
   378  }