Implicit required_plugin blocks (#10732)

* used components that don't have a required_plugin block will make Packer 'implicitly' require those. These components are manually selected and commented for now.
* add tests
 * docs
This commit is contained in:
Adrien Delorme 2021-03-24 11:31:39 +01:00 committed by GitHub
parent 70ceed1110
commit 0e3fcb589b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 652 additions and 18 deletions

View File

@ -121,12 +121,54 @@ func (c *InitCommand) RunContext(buildCtx context.Context, cla *InitArgs) int {
Getters: getters,
})
if err != nil {
c.Ui.Error(err.Error())
ret = 1
if pluginRequirement.Implicit {
msg := fmt.Sprintf(`
Warning! At least one component used in your config file(s) has moved out of
Packer into the %q plugin.
For that reason, Packer init tried to install the latest version of the %s
plugin. Unfortunately, this failed :
%s`,
pluginRequirement.Identifier,
pluginRequirement.Identifier.Type,
err)
c.Ui.Say(msg)
} else {
c.Ui.Error(err.Error())
ret = 1
}
}
if newInstall != nil {
if pluginRequirement.Implicit {
msg := fmt.Sprintf("Installed implicitly required plugin %s %s in %q", pluginRequirement.Identifier, newInstall.Version, newInstall.BinaryPath)
ui.Say(msg)
warn := fmt.Sprintf(`
Warning, at least one component used in your config file(s) has moved out of
Packer into the %[2]q plugin and is now being implicitly required.
For more details on implicitly required plugins see https://packer.io/docs/commands/init#implicit-required-plugin
To avoid any backward incompatible changes with your
config file you may want to lock the plugin version by pasting the following to your config:
packer {
required_plugins {
%[1]s = {
source = "%[2]s"
version = "~> %[3]s"
}
}
}
`,
pluginRequirement.Identifier.Type,
pluginRequirement.Identifier,
newInstall.Version,
)
ui.Error(warn)
continue
}
msg := fmt.Sprintf("Installed plugin %s %s in %q", pluginRequirement.Identifier, newInstall.Version, newInstall.BinaryPath)
ui.Say(msg)
}
}
return ret

View File

@ -20,8 +20,8 @@ import (
const lockedVersion = "v1.5.0"
func getBasicParser() *Parser {
return &Parser{
func getBasicParser(opts ...getParserOption) *Parser {
parser := &Parser{
CorePackerVersion: version.Must(version.NewSemver(lockedVersion)),
CorePackerVersionString: lockedVersion,
Parser: hclparse.NewParser(),
@ -44,8 +44,14 @@ func getBasicParser() *Parser {
},
},
}
for _, configure := range opts {
configure(parser)
}
return parser
}
type getParserOption func(*Parser)
type parseTestArgs struct {
filename string
vars map[string]string
@ -338,7 +344,7 @@ var cmpOpts = []cmp.Option{
PackerConfig{},
Variable{},
SourceBlock{},
Datasource{},
DatasourceBlock{},
ProvisionerBlock{},
PostProcessorBlock{},
packer.CoreBuild{},

View File

@ -0,0 +1,11 @@
package hcl2template
// ComponentKind helps enumerate what kind of components exist in this Package.
type ComponentKind int
const (
Builder ComponentKind = iota
Provisioner
PostProcessor
Datasource
)

View File

@ -41,6 +41,7 @@ func (cfg *PackerConfig) PluginRequirements() (plugingetter.Requirements, hcl.Di
Accessor: name,
Identifier: block.Type,
VersionConstraints: block.Requirement.Required,
Implicit: block.PluginDependencyReason == PluginDependencyImplicit,
})
uniq[name] = block
}

View File

@ -11,8 +11,8 @@ import (
"github.com/zclconf/go-cty/cty"
)
// DataBlock references an HCL 'data' block.
type Datasource struct {
// DatasourceBlock references an HCL 'data' block.
type DatasourceBlock struct {
Type string
Name string
@ -25,9 +25,9 @@ type DatasourceRef struct {
Name string
}
type Datasources map[DatasourceRef]Datasource
type Datasources map[DatasourceRef]DatasourceBlock
func (data *Datasource) Ref() DatasourceRef {
func (data *DatasourceBlock) Ref() DatasourceRef {
return DatasourceRef{
Type: data.Type,
Name: data.Name,
@ -124,9 +124,9 @@ func (cfg *PackerConfig) startDatasource(dataSourceStore packer.DatasourceStore,
return datasource, diags
}
func (p *Parser) decodeDataBlock(block *hcl.Block) (*Datasource, hcl.Diagnostics) {
func (p *Parser) decodeDataBlock(block *hcl.Block) (*DatasourceBlock, hcl.Diagnostics) {
var diags hcl.Diagnostics
r := &Datasource{
r := &DatasourceBlock{
Type: block.Labels[0],
Name: block.Labels[1],
block: block,

View File

@ -131,7 +131,7 @@ func TestParser_complete(t *testing.T) {
},
},
Datasources: Datasources{
DatasourceRef{Type: "amazon-ami", Name: "test"}: Datasource{
DatasourceRef{Type: "amazon-ami", Name: "test"}: DatasourceBlock{
Type: "amazon-ami",
Name: "test",
value: cty.StringVal("foo"),

View File

@ -6,6 +6,7 @@ import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/hcl2template/addrs"
"github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty"
)
@ -41,9 +42,13 @@ func (cfg *PackerConfig) decodeRequiredPluginsBlock(f *hcl.File) hcl.Diagnostics
}
func (cfg *PackerConfig) decodeImplicitRequiredPluginsBlocks(f *hcl.File) hcl.Diagnostics {
// when a plugin is used but not defined in the required plugin blocks, it
// is 'implicitly used'. Here we read common configuration blocks to try to
// guess plugins.
// when a plugin is used but not available it should be 'implicitly
// required'. Here we read common configuration blocks to try to guess
// plugin usages.
// decodeRequiredPluginsBlock needs to be called before
// decodeImplicitRequiredPluginsBlocks; otherwise all required plugins will
// be implicitly required too.
var diags hcl.Diagnostics
@ -51,14 +56,112 @@ func (cfg *PackerConfig) decodeImplicitRequiredPluginsBlocks(f *hcl.File) hcl.Di
diags = append(diags, moreDiags...)
for _, block := range content.Blocks {
switch block.Type {
case sourceLabel:
// TODO
diags = append(diags, cfg.decodeImplicitRequiredPluginsBlock(Builder, block)...)
case dataSourceLabel:
diags = append(diags, cfg.decodeImplicitRequiredPluginsBlock(Datasource, block)...)
case buildLabel:
content, _, moreDiags := block.Body.PartialContent(buildSchema)
diags = append(diags, moreDiags...)
for _, block := range content.Blocks {
switch block.Type {
case buildProvisionerLabel:
diags = append(diags, cfg.decodeImplicitRequiredPluginsBlock(Provisioner, block)...)
case buildPostProcessorLabel:
diags = append(diags, cfg.decodeImplicitRequiredPluginsBlock(PostProcessor, block)...)
case buildPostProcessorsLabel:
content, _, moreDiags := block.Body.PartialContent(postProcessorsSchema)
diags = append(diags, moreDiags...)
for _, block := range content.Blocks {
switch block.Type {
case buildPostProcessorLabel:
diags = append(diags, cfg.decodeImplicitRequiredPluginsBlock(PostProcessor, block)...)
}
}
}
}
}
}
return diags
}
func (cfg *PackerConfig) decodeImplicitRequiredPluginsBlock(k ComponentKind, block *hcl.Block) hcl.Diagnostics {
if len(block.Labels) == 0 {
// malformed block ? Let's not panic :)
return nil
}
// Currently all block types are `type "component-kind" ["name"] {`
// this makes this simple.
componentName := block.Labels[0]
store := map[ComponentKind]packer.BasicStore{
Builder: cfg.parser.PluginConfig.Builders,
PostProcessor: cfg.parser.PluginConfig.PostProcessors,
Provisioner: cfg.parser.PluginConfig.Provisioners,
Datasource: cfg.parser.PluginConfig.DataSources,
}[k]
if store.Has(componentName) {
// If any core or pre-loaded plugin defines the `happycloud-uploader`
// pp, skip. This happens for core and manually installed plugins, as
// they will be listed in the PluginConfig before parsing any HCL.
return nil
}
redirect := map[ComponentKind]map[string]string{
Builder: cfg.parser.PluginConfig.BuilderRedirects,
PostProcessor: cfg.parser.PluginConfig.PostProcessorRedirects,
Provisioner: cfg.parser.PluginConfig.ProvisionerRedirects,
Datasource: cfg.parser.PluginConfig.DatasourceRedirects,
}[k][componentName]
if redirect == "" {
// no known redirect for this component
return nil
}
redirectAddr, diags := addrs.ParsePluginSourceString(redirect)
if diags.HasErrors() {
// This should never happen, since the map is manually filled.
return diags
}
for _, req := range cfg.Packer.RequiredPlugins {
if _, found := req.RequiredPlugins[redirectAddr.Type]; found {
// This could happen if a plugin was forked. For example, I forked
// the github.com/hashicorp/happycloud plugin into
// github.com/azr/happycloud that is required in my config file; and
// am using the `happycloud-uploader` pp component from it. In that
// case - and to avoid miss-requires - we won't implicitly import
// any other `happycloud` plugin.
return nil
}
}
cfg.implicitlyRequirePlugin(redirectAddr)
return nil
}
func (cfg *PackerConfig) implicitlyRequirePlugin(plugin *addrs.Plugin) {
cfg.Packer.RequiredPlugins = append(cfg.Packer.RequiredPlugins, &RequiredPlugins{
RequiredPlugins: map[string]*RequiredPlugin{
plugin.Type: {
Name: plugin.Type,
Source: plugin.String(),
Type: plugin,
Requirement: VersionConstraint{
Required: nil, // means latest
},
PluginDependencyReason: PluginDependencyImplicit,
},
},
})
}
// RequiredPlugin represents a declaration of a dependency on a particular
// Plugin version or source.
type RequiredPlugin struct {
@ -71,8 +174,24 @@ type RequiredPlugin struct {
Type *addrs.Plugin
Requirement VersionConstraint
DeclRange hcl.Range
PluginDependencyReason
}
// PluginDependencyReason is an enumeration of reasons why a dependency might be
// present.
type PluginDependencyReason int
const (
// PluginDependencyExplicit means that there is an explicit
// "required_plugin" block in the configuration.
PluginDependencyExplicit PluginDependencyReason = iota
// PluginDependencyImplicit means that there is no explicit
// "required_plugin" block but there is at least one resource that uses this
// plugin.
PluginDependencyImplicit
)
type RequiredPlugins struct {
RequiredPlugins map[string]*RequiredPlugin
DeclRange hcl.Range

View File

@ -0,0 +1,363 @@
package hcl2template
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-version"
"github.com/hashicorp/packer/hcl2template/addrs"
)
func TestPackerConfig_required_plugin_parse(t *testing.T) {
tests := []struct {
name string
cfg PackerConfig
requirePlugins string
restOfTemplate string
wantDiags bool
wantConfig PackerConfig
}{
{"required_plugin", PackerConfig{parser: getBasicParser()}, `
packer {
required_plugins {
amazon = {
source = "github.com/hashicorp/amazon"
version = "~> v1.2.3"
}
}
} `, `
source "amazon-ebs" "example" {
}
`, false, PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"amazon": {
Name: "amazon",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.2.3")),
},
PluginDependencyReason: PluginDependencyExplicit,
},
}},
},
},
}},
{"required_plugin_forked_no_redirect", PackerConfig{parser: getBasicParser()}, `
packer {
required_plugins {
amazon = {
source = "github.com/azr/amazon"
version = "~> v1.2.3"
}
}
} `, `
source "amazon-chroot" "example" {
}
`, false, PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"amazon": {
Name: "amazon",
Source: "github.com/azr/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "azr", Type: "amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.2.3")),
},
PluginDependencyReason: PluginDependencyExplicit,
},
}},
},
},
}},
{"required_plugin_forked", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.BuilderRedirects = map[string]string{
"amazon-chroot": "github.com/hashicorp/amazon",
}
},
)}, `
packer {
required_plugins {
amazon = {
source = "github.com/azr/amazon"
version = "~> v1.2.3"
}
}
} `, `
source "amazon-chroot" "example" {
}
`, false, PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"amazon": {
Name: "amazon",
Source: "github.com/azr/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "azr", Type: "amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.2.3")),
},
PluginDependencyReason: PluginDependencyExplicit,
},
}},
},
},
}},
{"missing-required-plugin-for-pre-defined-builder", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.BuilderRedirects = map[string]string{
"amazon-ebs": "github.com/hashicorp/amazon",
}
},
)},
`
packer {
}`, `
# amazon-ebs is mocked in getBasicParser()
source "amazon-ebs" "example" {
}
`,
false,
PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: nil,
},
}},
{"missing-required-plugin-for-builder", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.BuilderRedirects = map[string]string{
"amazon-chroot": "github.com/hashicorp/amazon",
}
},
)},
`
packer {
}`, `
source "amazon-chroot" "example" {
}
`,
false,
PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"amazon": {
Name: "amazon",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "amazon"},
Requirement: VersionConstraint{
Required: nil,
},
PluginDependencyReason: PluginDependencyImplicit,
},
}},
},
},
}},
{"missing-required-plugin-for-provisioner", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.ProvisionerRedirects = map[string]string{
"ansible-local": "github.com/ansible/ansible",
}
},
)},
`
packer {
}`, `
build {
provisioner "ansible-local" {}
}
`,
false,
PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"ansible": {
Name: "ansible",
Source: "github.com/ansible/ansible",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "ansible", Type: "ansible"},
Requirement: VersionConstraint{
Required: nil,
},
PluginDependencyReason: PluginDependencyImplicit,
},
}},
},
},
}},
{"missing-required-plugin-for-post-processor", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.PostProcessorRedirects = map[string]string{
"docker-push": "github.com/hashicorp/docker",
}
},
)},
`
packer {
}`, `
build {
post-processor "docker-push" {}
}
`,
false,
PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"docker": {
Name: "docker",
Source: "github.com/hashicorp/docker",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "docker"},
Requirement: VersionConstraint{
Required: nil,
},
PluginDependencyReason: PluginDependencyImplicit,
},
}},
},
},
}},
{"missing-required-plugin-for-nested-post-processor", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.PostProcessorRedirects = map[string]string{
"docker-push": "github.com/hashicorp/docker",
}
},
)},
`
packer {
}`, `
build {
post-processors {
post-processor "docker-push" {
}
}
}
`,
false,
PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"docker": {
Name: "docker",
Source: "github.com/hashicorp/docker",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "docker"},
Requirement: VersionConstraint{
Required: nil,
},
PluginDependencyReason: PluginDependencyImplicit,
},
}},
},
},
}},
{"required-plugin-renamed", PackerConfig{
parser: getBasicParser(func(p *Parser) {
p.PluginConfig.BuilderRedirects = map[string]string{
"amazon-chroot": "github.com/hashicorp/amazon",
}
},
)},
`
packer {
required_plugins {
amazon-v1 = {
source = "github.com/hashicorp/amazon"
version = "~> v1.0"
}
}
}`, `
source "amazon-v1-chroot" "example" {
}
source "amazon-chroot" "example" {
}
`,
false,
PackerConfig{
Packer: struct {
VersionConstraints []VersionConstraint
RequiredPlugins []*RequiredPlugins
}{
RequiredPlugins: []*RequiredPlugins{
{RequiredPlugins: map[string]*RequiredPlugin{
"amazon-v1": {
Name: "amazon-v1",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "amazon"},
Requirement: VersionConstraint{
Required: mustVersionConstraints(version.NewConstraint("~> v1.0")),
},
PluginDependencyReason: PluginDependencyExplicit,
},
}},
{RequiredPlugins: map[string]*RequiredPlugin{
"amazon": {
Name: "amazon",
Source: "github.com/hashicorp/amazon",
Type: &addrs.Plugin{Hostname: "github.com", Namespace: "hashicorp", Type: "amazon"},
Requirement: VersionConstraint{
Required: nil,
},
PluginDependencyReason: PluginDependencyImplicit,
},
}},
},
},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := tt.cfg
file, diags := cfg.parser.ParseHCL([]byte(tt.requirePlugins), "required_plugins.pkr.hcl")
if len(diags) > 0 {
t.Fatal(diags)
}
if diags := cfg.decodeRequiredPluginsBlock(file); len(diags) > 0 {
t.Fatal(diags)
}
rest, diags := cfg.parser.ParseHCL([]byte(tt.restOfTemplate), "rest.pkr.hcl")
if len(diags) > 0 {
t.Fatal(diags)
}
if gotDiags := cfg.decodeImplicitRequiredPluginsBlocks(rest); (len(gotDiags) > 0) != tt.wantDiags {
t.Fatal(gotDiags)
}
if diff := cmp.Diff(tt.wantConfig, cfg, cmpOpts...); diff != "" {
t.Errorf("PackerConfig.inferImplicitRequiredPluginFromBlocks() unexpected PackerConfig: %v", diff)
}
})
}
}

61
main.go
View File

@ -297,6 +297,67 @@ func loadConfig() (*config, error) {
PluginMinPort: 10000,
PluginMaxPort: 25000,
KnownPluginFolders: packer.PluginFolders("."),
// BuilderRedirects
BuilderRedirects: map[string]string{
// "amazon-chroot": "github.com/hashicorp/amazon",
// "amazon-ebs": "github.com/hashicorp/amazon",
// "amazon-ebssurrogate": "github.com/hashicorp/amazon",
// "amazon-ebsvolume": "github.com/hashicorp/amazon",
// "amazon-instance": "github.com/hashicorp/amazon",
// "azure-arm": "github.com/hashicorp/azure",
// "azure-chroot": "github.com/hashicorp/azure",
// "dtl": "github.com/hashicorp/azure",
// "docker": "github.com/hashicorp/docker",
// "googlecompute": "github.com/hashicorp/googlecompute",
// "parallels-iso": "github.com/hashicorp/parallels",
// "parallels-pvm": "github.com/hashicorp/parallels",
// "qemu": "github.com/hashicorp/qemu",
// "vagrant": "github.com/hashicorp/vagrant",
// "virtualbox-iso": "github.com/hashicorp/virtualbox",
// "virtualbox-ovf": "github.com/hashicorp/virtualbox",
// "virtualbox-vm": "github.com/hashicorp/virtualbox",
// "vmware-iso": "github.com/hashicorp/vmware",
// "vmware-vmx": "github.com/hashicorp/vmware",
// "vsphere-iso": "github.com/hashicorp/vsphere",
// "vsphere-clone": "github.com/hashicorp/vsphere",
},
DatasourceRedirects: map[string]string{
// "amazon-ami": "github.com/hashicorp/amazon",
},
ProvisionerRedirects: map[string]string{
// "ansible": "github.com/hashicorp/ansible",
// "ansible-local": "github.com/hashicorp/ansible",
// "azure-dtlartifact": "github.com/hashicorp/azure",
},
PostProcessorRedirects: map[string]string{
// "amazon-import": "github.com/hashicorp/amazon",
// "docker-import": "github.com/hashicorp/docker",
// "docker-push": "github.com/hashicorp/docker",
// "docker-save": "github.com/hashicorp/docker",
// "docker-tag": "github.com/hashicorp/docker",
// "googlecompute-export": "github.com/hashicorp/googlecompute",
// "googlecompute-import": "github.com/hashicorp/googlecompute",
// "vagrant": "github.com/hashicorp/vagrant",
// "vagrant-cloud": "github.com/hashicorp/vagrant",
// "vsphere": "github.com/hashicorp/vsphere",
// "vsphere-template": "github.com/hashicorp/vsphere",
},
}
if err := config.Plugins.Discover(); err != nil {
return nil, err

View File

@ -38,6 +38,9 @@ type Requirement struct {
// VersionConstraints as defined by user. Empty ( to be avoided ) means
// highest found version.
VersionConstraints version.Constraints
// was this require implicitly guessed ?
Implicit bool
}
type BinaryInstallationOptions struct {

View File

@ -24,6 +24,23 @@ type PluginConfig struct {
Provisioners ProvisionerSet
PostProcessors PostProcessorSet
DataSources DatasourceSet
// Redirects are only set when a plugin was completely moved out; they allow
// telling where a plugin has moved by checking if a known component of this
// plugin is used. For example implicitly require the
// github.com/hashicorp/amazon plugin if it was moved out and the
// "amazon-ebs" plugin is used, but not found.
//
// Redirects will be bypassed if the redirected components are already found
// in their corresponding sets (Builders, Provisioners, PostProcessors,
// DataSources). That is, for example, if you manually put a single
// component plugin in the plugins folder.
//
// Example BuilderRedirects: "amazon-ebs" => "github.com/hashicorp/amazon"
BuilderRedirects map[string]string
DatasourceRedirects map[string]string
ProvisionerRedirects map[string]string
PostProcessorRedirects map[string]string
}
// PACKERSPACE is used to represent the spaces that separate args for a command

View File

@ -67,10 +67,21 @@ plugins will be installed in the [Plugin
Directory](/docs/configure#packer-s-plugin-directory).
See [Installing Plugins](/docs/plugins#installing-plugins) for more information on how plugin installation works.
### Implicit required plugin
This is part of a set of breaking changes made to decouple Packer releases from
plugin releases. To make the transition easier, we will tag components of these
plugins as "moved out". If one of the components of a moved out plugin is used
in a config file, but there is no mention of that plugin in the
"required_plugin" block, then Packer init will automatically download and
install that plugin. Packer will then display a warning and suggest that you
add the plugin to your required_plugin block. We recommend you use the
required_plugin block even if you are only using official plugins, because it
allows you to set the plugin version to avoid surprises in the future.
## Options
- `-upgrade` - On top of installing missing plugins, update installed plugins to
the latest available version, if there is a new higher one. Note that this
still takes into consideration the version constraint of the config.