Add `packer plugins` command and subcommands to interact with plugins (#11553)

* add basic docs for plugins command

* refactor docs

Co-Authored-By: Wilken Rivera <1749304+nywilken@users.noreply.github.com>

* add plugins command

* add plugins subcommands

they do nothing for now

* add plugins installed command + tests

* add plugins install command

* add remove plugin command

* better docs for the plugins install command

* remove duplicate content

* better output for installed plugins

* add plugins required command

* Update plugins_install.go

* add newline after `Usage:`

* Update plugins_remove.go

* Update plugins_required.go

* Update plugins_remove.go

* Update plugins_installed.go

* Update plugins_install.go

* add docs

* Update plugins_install.go

* fix typos

* Update plugins_test.go

* fix typos

Co-Authored-By: Wilken Rivera <1749304+nywilken@users.noreply.github.com>

* Update core_wrapper.go

Co-Authored-By: Wilken Rivera <1749304+nywilken@users.noreply.github.com>

* Update website/content/docs/commands/plugins/remove.mdx

Co-authored-by: Wilken Rivera <wilken@hashicorp.com>

* Update website/content/docs/commands/plugins/required.mdx

Co-authored-by: Wilken Rivera <wilken@hashicorp.com>

* Update website/content/docs/commands/plugins/required.mdx

Co-authored-by: Wilken Rivera <wilken@hashicorp.com>

* Update plugins_required.go

* Update install.mdx

* Update required.mdx

* plugins requirement, warn when no plugin was found

* Update website/content/docs/commands/plugins/required.mdx

Co-authored-by: Wilken Rivera <wilken@hashicorp.com>

Co-authored-by: Wilken Rivera <1749304+nywilken@users.noreply.github.com>
Co-authored-by: Wilken Rivera <wilken@hashicorp.com>
pull/11561/head
Adrien Delorme 4 years ago committed by GitHub
parent 759cadaf0d
commit 9f4a1281dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -99,12 +99,17 @@ func (ia *InitArgs) AddFlagSets(flags *flag.FlagSet) {
ia.MetaArgs.AddFlagSets(flags)
}
// InitArgs represents a parsed cli line for a `packer build`
// InitArgs represents a parsed cli line for a `packer init <path>`
type InitArgs struct {
MetaArgs
Upgrade bool
}
// PluginsRequiredArgs represents a parsed cli line for a `packer plugins required <path>`
type PluginsRequiredArgs struct {
MetaArgs
}
// ConsoleArgs represents a parsed cli line for a `packer console`
type ConsoleArgs struct {
MetaArgs

@ -29,8 +29,8 @@ func (c *CoreWrapper) Initialize(_ packer.InitializeOptions) hcl.Diagnostics {
func (c *CoreWrapper) PluginRequirements() (plugingetter.Requirements, hcl.Diagnostics) {
return nil, hcl.Diagnostics{
&hcl.Diagnostic{
Summary: "Packer init is supported for HCL2 configuration templates only",
Detail: "Please manually install plugins or use a HCL2 configuration that will do that for you.",
Summary: "Packer plugins currently only works with HCL2 configuration templates",
Detail: "Please manually install plugins with the plugins command or use a HCL2 configuration that will do that for you.",
Severity: hcl.DiagError,
},
}

@ -0,0 +1,32 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
type PluginsCommand struct {
Meta
}
func (c *PluginsCommand) Synopsis() string {
return "Interact with Packer plugins and catalog"
}
func (c *PluginsCommand) Help() string {
helpText := `
Usage: packer plugins <subcommand> [options] [args]
This command groups subcommands for interacting with Packer plugins.
Related but not under the "plugins" command :
- "packer init <path>" will install all plugins required by a config.
`
return strings.TrimSpace(helpText)
}
func (c *PluginsCommand) Run(args []string) int {
return cli.RunResultHelp
}

@ -0,0 +1,124 @@
package command
import (
"context"
"crypto/sha256"
"fmt"
"runtime"
"strings"
"github.com/hashicorp/go-version"
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"
"github.com/mitchellh/cli"
)
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 <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
`
return strings.TrimSpace(helpText)
}
func (c *PluginsInstallCommand) Run(args []string) int {
ctx, cleanup := handleTermInterrupt(c.Ui)
defer cleanup()
return c.RunContext(ctx, args)
}
func (c *PluginsInstallCommand) RunContext(buildCtx context.Context, args []string) int {
if len(args) < 1 || len(args) > 2 {
return cli.RunResultHelp
}
opts := plugingetter.ListInstallationsOptions{
FromFolders: c.Meta.CoreConfig.Components.PluginConfig.KnownPluginFolders,
BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{
OS: runtime.GOOS,
ARCH: runtime.GOARCH,
APIVersionMajor: pluginsdk.APIVersionMajor,
APIVersionMinor: pluginsdk.APIVersionMinor,
Checksummers: []plugingetter.Checksummer{
{Type: "sha256", Hash: sha256.New()},
},
},
}
plugin, diags := addrs.ParsePluginSourceString(args[0])
if diags.HasErrors() {
c.Ui.Error(diags.Error())
return 1
}
// a plugin requirement that matches them all
pluginRequirement := plugingetter.Requirement{
Identifier: plugin,
Implicit: false,
}
if len(args) > 1 {
constraints, err := version.NewConstraint(args[1])
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{
InFolders: opts.FromFolders,
BinaryInstallationOptions: opts.BinaryInstallationOptions,
Getters: getters,
})
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
}

@ -0,0 +1,78 @@
package command
import (
"context"
"crypto/sha256"
"log"
"runtime"
"strings"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
)
type PluginsInstalledCommand struct {
Meta
}
func (c *PluginsInstalledCommand) Synopsis() string {
return "List all installed Packer plugin binaries"
}
func (c *PluginsInstalledCommand) Help() string {
helpText := `
Usage: packer plugins installed
This command lists all installed plugin binaries that match with the current
OS and architecture. Packer's API version will be ignored.
`
return strings.TrimSpace(helpText)
}
func (c *PluginsInstalledCommand) Run(args []string) int {
ctx, cleanup := handleTermInterrupt(c.Ui)
defer cleanup()
return c.RunContext(ctx)
}
func (c *PluginsInstalledCommand) RunContext(buildCtx context.Context) int {
opts := plugingetter.ListInstallationsOptions{
FromFolders: c.Meta.CoreConfig.Components.PluginConfig.KnownPluginFolders,
BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{
OS: runtime.GOOS,
ARCH: runtime.GOARCH,
Checksummers: []plugingetter.Checksummer{
{Type: "sha256", Hash: sha256.New()},
},
},
}
if runtime.GOOS == "windows" && opts.Ext == "" {
opts.BinaryInstallationOptions.Ext = ".exe"
}
log.Printf("[TRACE] init: %#v", opts)
// a plugin requirement that matches them all
allPlugins := plugingetter.Requirement{
Accessor: "",
VersionConstraints: nil,
Identifier: nil,
Implicit: false,
}
installations, err := allPlugins.ListInstallations(opts)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
for _, installation := range installations {
c.Ui.Message(installation.BinaryPath)
}
return 0
}

@ -0,0 +1,96 @@
package command
import (
"context"
"crypto/sha256"
"os"
"runtime"
"strings"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer/hcl2template/addrs"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
"github.com/mitchellh/cli"
)
type PluginsRemoveCommand struct {
Meta
}
func (c *PluginsRemoveCommand) Synopsis() string {
return "Remove Packer plugins [matching a version]"
}
func (c *PluginsRemoveCommand) Help() string {
helpText := `
Usage: packer plugins remove <plugin> [<version constraint>]
This command will remove all Packer plugins matching the version constraint
for the current OS and architecture.
When the version is omitted all installed versions will be removed.
Ex: packer plugins remove github.com/hashicorp/happycloud v1.2.3
`
return strings.TrimSpace(helpText)
}
func (c *PluginsRemoveCommand) Run(args []string) int {
ctx, cleanup := handleTermInterrupt(c.Ui)
defer cleanup()
return c.RunContext(ctx, args)
}
func (c *PluginsRemoveCommand) RunContext(buildCtx context.Context, args []string) int {
if len(args) < 1 || len(args) > 2 {
return cli.RunResultHelp
}
opts := plugingetter.ListInstallationsOptions{
FromFolders: c.Meta.CoreConfig.Components.PluginConfig.KnownPluginFolders,
BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{
OS: runtime.GOOS,
ARCH: runtime.GOARCH,
Checksummers: []plugingetter.Checksummer{
{Type: "sha256", Hash: sha256.New()},
},
},
}
plugin, diags := addrs.ParsePluginSourceString(args[0])
if diags.HasErrors() {
c.Ui.Error(diags.Error())
return 1
}
// a plugin requirement that matches them all
pluginRequirement := plugingetter.Requirement{
Identifier: plugin,
Implicit: false,
}
if len(args) > 1 {
constraints, err := version.NewConstraint(args[1])
if err != nil {
c.Ui.Error(err.Error())
return 1
}
pluginRequirement.VersionConstraints = constraints
}
installations, err := pluginRequirement.ListInstallations(opts)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
for _, installation := range installations {
if err := os.Remove(installation.BinaryPath); err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Message(installation.BinaryPath)
}
return 0
}

@ -0,0 +1,118 @@
package command
import (
"context"
"crypto/sha256"
"fmt"
"runtime"
"strings"
pluginsdk "github.com/hashicorp/packer-plugin-sdk/plugin"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
"github.com/mitchellh/cli"
)
type PluginsRequiredCommand struct {
Meta
}
func (c *PluginsRequiredCommand) Synopsis() string {
return "List plugins required by a config"
}
func (c *PluginsRequiredCommand) Help() string {
helpText := `
Usage: packer plugins required <path>
This command will list every Packer plugin required by a Packer config, in
packer.required_plugins blocks. All binaries matching the required version
constrain and the current OS and Architecture will be listed. The most recent
version (and the first of the list) will be the one picked by Packer during a
build.
Ex: packer plugins required require.pkr.hcl
Ex: packer plugins required path/to/folder/
`
return strings.TrimSpace(helpText)
}
func (c *PluginsRequiredCommand) Run(args []string) int {
ctx, cleanup := handleTermInterrupt(c.Ui)
defer cleanup()
cfg, ret := c.ParseArgs(args)
if ret != 0 {
return ret
}
return c.RunContext(ctx, cfg)
}
func (c *PluginsRequiredCommand) ParseArgs(args []string) (*PluginsRequiredArgs, int) {
var cfg PluginsRequiredArgs
flags := c.Meta.FlagSet("plugins required", 0)
flags.Usage = func() { c.Ui.Say(c.Help()) }
cfg.AddFlagSets(flags)
if err := flags.Parse(args); err != nil {
return &cfg, 1
}
args = flags.Args()
if len(args) != 1 {
return &cfg, cli.RunResultHelp
}
cfg.Path = args[0]
return &cfg, 0
}
func (c *PluginsRequiredCommand) RunContext(buildCtx context.Context, cla *PluginsRequiredArgs) int {
packerStarter, ret := c.GetConfig(&cla.MetaArgs)
if ret != 0 {
return ret
}
// Get plugins requirements
reqs, diags := packerStarter.PluginRequirements()
ret = writeDiags(c.Ui, nil, diags)
if ret != 0 {
return ret
}
opts := plugingetter.ListInstallationsOptions{
FromFolders: c.Meta.CoreConfig.Components.PluginConfig.KnownPluginFolders,
BinaryInstallationOptions: plugingetter.BinaryInstallationOptions{
OS: runtime.GOOS,
ARCH: runtime.GOARCH,
APIVersionMajor: pluginsdk.APIVersionMajor,
APIVersionMinor: pluginsdk.APIVersionMinor,
Checksummers: []plugingetter.Checksummer{
{Type: "sha256", Hash: sha256.New()},
},
},
}
for _, pluginRequirement := range reqs {
s := fmt.Sprintf("%s %s %q", pluginRequirement.Accessor, pluginRequirement.Identifier.String(), pluginRequirement.VersionConstraints.String())
installs, err := pluginRequirement.ListInstallations(opts)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
for _, install := range installs {
s += fmt.Sprintf(" %s", install.BinaryPath)
}
c.Ui.Message(s)
}
if len(reqs) == 0 {
c.Ui.Message(`
No plugins requirement found, make sure you reference a Packer config
containing a packer.required_plugins block. See
https://www.packer.io/docs/templates/hcl_templates/blocks/packer
for more info.`)
}
return 0
}

@ -62,6 +62,36 @@ func init() {
}, nil
},
"plugins": func() (cli.Command, error) {
return &command.PluginsCommand{
Meta: *CommandMeta,
}, nil
},
"plugins installed": func() (cli.Command, error) {
return &command.PluginsInstalledCommand{
Meta: *CommandMeta,
}, nil
},
"plugins install": func() (cli.Command, error) {
return &command.PluginsInstallCommand{
Meta: *CommandMeta,
}, nil
},
"plugins remove": func() (cli.Command, error) {
return &command.PluginsRemoveCommand{
Meta: *CommandMeta,
}, nil
},
"plugins required": func() (cli.Command, error) {
return &command.PluginsRequiredCommand{
Meta: *CommandMeta,
}, nil
},
"validate": func() (cli.Command, error) {
return &command.ValidateCommand{
Meta: *CommandMeta,

@ -83,6 +83,9 @@ func (rlerr *RateLimitError) Error() string {
}
func (pr Requirement) FilenamePrefix() string {
if pr.Identifier == nil {
return "packer-plugin-"
}
return "packer-plugin-" + pr.Identifier.Type + "_"
}
@ -104,7 +107,12 @@ func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallL
filenameSuffix := opts.filenameSuffix()
log.Printf("[TRACE] listing potential installations for %q that match %q. %#v", pr.Identifier, pr.VersionConstraints, opts)
for _, knownFolder := range opts.FromFolders {
glob := filepath.Join(knownFolder, pr.Identifier.Hostname, pr.Identifier.Namespace, pr.Identifier.Type, FilenamePrefix+"*"+filenameSuffix)
glob := ""
if pr.Identifier == nil {
glob = filepath.Join(knownFolder, "*", "*", "*", FilenamePrefix+"*"+filenameSuffix)
} else {
glob = filepath.Join(knownFolder, pr.Identifier.Hostname, pr.Identifier.Namespace, pr.Identifier.Type, FilenamePrefix+"*"+filenameSuffix)
}
matches, err := filepath.Glob(glob)
if err != nil {
@ -120,7 +128,13 @@ func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallL
versionsStr := strings.TrimPrefix(fname, FilenamePrefix)
versionsStr = strings.TrimSuffix(versionsStr, filenameSuffix)
// versionsStr now looks like v1.2.3_x5.1
if pr.Identifier == nil {
if idx := strings.Index(versionsStr, "_"); idx > 0 {
versionsStr = versionsStr[idx+1:]
}
}
// versionsStr now looks like v1.2.3_x5.1 or amazon_v1.2.3_x5.1
parts := strings.SplitN(versionsStr, "_", 2)
pluginVersionStr, protocolVerionStr := parts[0], parts[1]
pv, err := version.NewVersion(pluginVersionStr)
@ -131,7 +145,7 @@ func (pr Requirement) ListInstallations(opts ListInstallationsOptions) (InstallL
}
// no constraint means always pass, this will happen for implicit
// plugin requirements
// plugin requirements and when we list all plugins.
if !pr.VersionConstraints.Check(pv) {
log.Printf("[TRACE] version %q of file %q does not match constraint %q", pluginVersionStr, path, pr.VersionConstraints.String())
continue
@ -258,6 +272,11 @@ func (binOpts *BinaryInstallationOptions) CheckProtocolVersion(remoteProt string
}
vMajor, vMinor := parts[0], parts[1]
// no protocol version check
if binOpts.APIVersionMajor == "" && binOpts.APIVersionMinor == "" {
return nil
}
if vMajor != binOpts.APIVersionMajor {
return fmt.Errorf("Unsupported remote protocol MAJOR version %q. The current MAJOR protocol version is %q."+
" This version of Packer can only communicate with plugins using that version.", vMajor, binOpts.APIVersionMajor)

@ -16,6 +16,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/hcl2template/addrs"
)
@ -66,6 +67,61 @@ func TestPlugin_ListInstallations(t *testing.T) {
wantErr bool
want InstallList
}{
{
"windows_all_plugins",
fields{
// empty
},
ListInstallationsOptions{
[]string{
pluginFolderOne,
pluginFolderTwo,
},
BinaryInstallationOptions{
OS: "windows", ARCH: "amd64",
Ext: ".exe",
Checksummers: []Checksummer{
{
Type: "sha256",
Hash: sha256.New(),
},
},
},
},
false,
[]*Installation{
{
Version: "v1.2.3",
BinaryPath: filepath.Join(pluginFolderOne, "github.com", "hashicorp", "amazon", "packer-plugin-amazon_v1.2.3_x5.0_windows_amd64.exe"),
},
{
Version: "v1.2.4",
BinaryPath: filepath.Join(pluginFolderOne, "github.com", "hashicorp", "amazon", "packer-plugin-amazon_v1.2.4_x5.0_windows_amd64.exe"),
},
{
Version: "v1.2.5",
BinaryPath: filepath.Join(pluginFolderOne, "github.com", "hashicorp", "amazon", "packer-plugin-amazon_v1.2.5_x5.0_windows_amd64.exe"),
},
{
BinaryPath: filepath.Join(pluginFolderOne, "github.com", "hashicorp", "google", "packer-plugin-google_v4.5.6_x5.0_windows_amd64.exe"),
Version: "v4.5.6",
},
{
Version: "v4.5.7",
BinaryPath: filepath.Join(pluginFolderOne, "github.com", "hashicorp", "google", "packer-plugin-google_v4.5.7_x5.0_windows_amd64.exe"),
},
{
Version: "v4.5.8",
BinaryPath: filepath.Join(pluginFolderOne, "github.com", "hashicorp", "google", "packer-plugin-google_v4.5.8_x5.0_windows_amd64.exe"),
},
{
Version: "v4.5.9",
BinaryPath: filepath.Join(pluginFolderTwo, "github.com", "hashicorp", "google", "packer-plugin-google_v4.5.9_x5.0_windows_amd64.exe"),
},
},
},
{
"darwin_amazon_prot_5.0",
fields{
@ -227,9 +283,13 @@ func TestPlugin_ListInstallations(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
identifier, diags := addrs.ParsePluginSourceString(tt.fields.Identifier)
if diags.HasErrors() {
t.Fatalf("%v", diags)
var identifier *addrs.Plugin
if tt.fields.Identifier != "" {
var diags hcl.Diagnostics
identifier, diags = addrs.ParsePluginSourceString(tt.fields.Identifier)
if diags.HasErrors() {
t.Fatalf("%v", diags)
}
}
p := Requirement{
Identifier: identifier,

@ -0,0 +1,30 @@
---
description: |
The "plugin" command groups subcommands for interacting with
Packer's plugin and the plugin catalog.
page_title: plugins Command
---
# `plugins`
The `plugins` command groups subcommands for interacting with Packers' plugins.
```shell-session
$ packer plugins -h
Usage: packer plugins <subcommand> [options] [args]
This command groups subcommands for interacting with Packer plugins.
Related but not under the "plugins" command :
- "packer init <path>" will install all plugins required by a config.
Subcommands:
install Install latest Packer plugin [matching version constraint]
installed List all installed Packer plugin binaries
remove Remove Packer plugins [matching a version]
required List plugins required by a config
```
## Related
- [`packer init`](/docs/commands/init) will install all required plugins.

@ -0,0 +1,24 @@
---
description: |
The "plugins install" command can install a plugin at a version constraint.
page_title: plugins Command
---
# `plugins install`
The `plugins install` subcommand installs a Packer plugin at a version constraint
```shell-session
$ packer plugins install -h
Usage: packer plugins install <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
```
## Related
- [`packer init`](/docs/commands/init) will install all required plugins.

@ -0,0 +1,21 @@
---
description: |
The "plugins installed" command will list installed plugins.
page_title: plugins Command
---
# `plugins installed`
The `plugins installed` subcommand lists installed Packer plugins
```shell-session
$ packer plugins installed -h
Usage: packer plugins installed
This command lists all installed plugin binaries that match with the current
OS and architecture. Packer's API version will be ignored.
```
## Related
- [`packer init`](/docs/commands/init) will install all required plugins.

@ -0,0 +1,24 @@
---
description: |
The "plugins remove" command can remove a plugin at a version constraint.
page_title: plugins Command
---
# `plugins remove`
The `plugins remove` subcommand removes a Packer plugin at a version constraint
```shell-session
$ packer plugins remove -h
Usage: packer plugins remove <plugin> [<version constraint>]
This command will remove all Packer plugins matching the version constraint
for the current OS and architecture.
When the version is omitted all installed versions will be removed.
Ex: packer plugins remove github.com/hashicorp/happycloud v1.2.3
```
## Related
- [`packer init`](/docs/commands/init) will install all required plugins.

@ -0,0 +1,30 @@
---
description: |
The "plugins required" command lists all plugins required in a Packer configuration.
page_title: plugins Command
---
# `plugins required`
The `plugins required` command lists all plugins required by a Packer config and
all the installed binaries that match the constraint. The first binary
is the most up-to-date installed version and will be the one picked by Packer in a build.
```shell-session
$ packer plugins required -h
Usage: packer plugins required <path>
This command will list every Packer plugin required by a Packer config, in
packer.required_plugins blocks. All binaries matching the required version
constrain and the current OS and Architecture will be listed. The most recent
version (and the first of the list) will be the one picked by Packer during a
build.
Ex: packer plugins required require.pkr.hcl
Ex: packer plugins required path/to/folder/
```
## Related
- [`packer init`](/docs/commands/init) will install all required plugins.

@ -17,6 +17,31 @@
"title": "<code>init</code>",
"path": "commands/init"
},
{
"title": "<code>plugins</code>",
"routes": [
{
"title": "Overview",
"path": "commands/plugins"
},
{
"title": "<code>install</code>",
"path": "commands/plugins/install"
},
{
"title": "<code>installed</code>",
"path": "commands/plugins/installed"
},
{
"title": "<code>remove</code>",
"path": "commands/plugins/remove"
},
{
"title": "<code>required</code>",
"path": "commands/plugins/required"
}
]
},
{
"title": "<code>build</code>",
"path": "commands/build"

Loading…
Cancel
Save