diff --git a/hcl2template/plugin.go b/hcl2template/plugin.go index 323263596f8..48c67f66414 100644 --- a/hcl2template/plugin.go +++ b/hcl2template/plugin.go @@ -41,6 +41,10 @@ func (cfg *PackerConfig) PluginRequirements() (plugingetter.Requirements, hcl.Di continue } + if block.Path != "" { + continue + } + reqs = append(reqs, &plugingetter.Requirement{ Accessor: name, Identifier: block.Type, @@ -54,6 +58,19 @@ func (cfg *PackerConfig) PluginRequirements() (plugingetter.Requirements, hcl.Di return reqs, diags } +func (cfg *PackerConfig) getLocalPlugins() []*RequiredPlugin { + reqs := []*RequiredPlugin{} + for _, block := range cfg.Packer.RequiredPlugins { + for _, reqPlugin := range block.RequiredPlugins { + if reqPlugin.Path != "" { + reqs = append(reqs, reqPlugin) + } + } + } + + return reqs +} + func (cfg *PackerConfig) DetectPluginBinaries() hcl.Diagnostics { opts := plugingetter.ListInstallationsOptions{ FromFolders: cfg.parser.PluginConfig.KnownPluginFolders, @@ -106,6 +123,21 @@ func (cfg *PackerConfig) DetectPluginBinaries() hcl.Diagnostics { } } + localPlugins := cfg.getLocalPlugins() + for _, local := range localPlugins { + err := cfg.parser.PluginConfig.DiscoverMultiPlugin(local.Name, local.Path) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to load required plugin", + Detail: fmt.Sprintf("The required plugin %q, failed to be loaded from path %q: %s", + local.Name, + local.Path, + err), + }) + } + } + if len(uninstalledPlugins) > 0 { detailMessage := &strings.Builder{} detailMessage.WriteString("The following plugins are required, but not installed:\n\n") diff --git a/hcl2template/types.required_plugins.go b/hcl2template/types.required_plugins.go index d5e010345ea..596a9ea0e66 100644 --- a/hcl2template/types.required_plugins.go +++ b/hcl2template/types.required_plugins.go @@ -5,6 +5,7 @@ package hcl2template import ( "fmt" + "os" "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" @@ -51,7 +52,10 @@ type RequiredPlugin struct { // for example, "awesomecloud" instead of github.com/awesome/awesomecloud. // This one is left here in case we want to go back to allowing inexplicit // source url definitions. - Source string + Source string + // Path supersedes every other attribute if specified, and will load the + // plugin from the local path specified. + Path string Type *addrs.Plugin Requirement VersionConstraint DeclRange hcl.Range @@ -103,6 +107,42 @@ func decodeRequiredPluginsBlock(block *hcl.Block) (*RequiredPlugins, hcl.Diagnos continue case expr.Type().IsObjectType(): + if expr.Type().HasAttribute("path") { + path := expr.GetAttr("path") + + if !path.Type().Equals(cty.String) || path.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "path" value`, + Detail: `Path must be specified as a string. For example: path = "./packer-plugin-example"`, + Subject: attr.Expr.Range().Ptr(), + }) + break + } + + rp.Path = path.AsString() + + _, err := os.Stat(rp.Path) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Plugin not found", + Detail: fmt.Sprintf("The plugin %q could not be found at path %q: %s", attr.Name, rp.Path, err), + Subject: attr.Expr.Range().Ptr(), + }) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Loading plugin from local path", + Detail: fmt.Sprintf("The plugin %q is loaded from a local path %q. "+ + "This reduces the portability of the template, and thus should"+ + " only be used for local testing.", attr.Name, rp.Path), + }) + + break + } + if !expr.Type().HasAttribute("version") { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -182,18 +222,17 @@ func decodeRequiredPluginsBlock(block *hcl.Block) (*RequiredPlugins, hcl.Diagnos attrTypes := expr.Type().AttributeTypes() for name := range attrTypes { - if name == "version" || name == "source" { + if name == "version" || name == "source" || name == "path" { continue } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid required_plugins object", - Detail: `required_plugins objects can only contain "version" and "source" attributes.`, + Detail: `required_plugins objects can only contain "version", "source" or "path" attributes.`, Subject: attr.Expr.Range().Ptr(), }) break } - default: // should not happen diags = append(diags, &hcl.Diagnostic{ diff --git a/hcl2template/types.required_plugins_test.go b/hcl2template/types.required_plugins_test.go index 0329d1505ff..97ae138c91d 100644 --- a/hcl2template/types.required_plugins_test.go +++ b/hcl2template/types.required_plugins_test.go @@ -130,6 +130,47 @@ func TestPackerConfig_required_plugin_parse(t *testing.T) { RequiredPlugins: nil, }, }}, + { + name: "test-path-nonexistent-plugin", + cfg: PackerConfig{ + parser: getBasicParser(func(p *Parser) {}), + }, + requirePlugins: ` + packer { + required_plugins { + unknown = { + path = "./invalid" + } + } + } + `, + restOfTemplate: ` + source "null" "test" { + communicator = "none" + } + build { + sources = ["null.test"] + } + `, + wantDiags: true, + wantConfig: PackerConfig{ + Packer: struct { + VersionConstraints []VersionConstraint + RequiredPlugins []*RequiredPlugins + }{ + RequiredPlugins: []*RequiredPlugins{ + { + RequiredPlugins: map[string]*RequiredPlugin{ + "unknown": { + Name: "unknown", + Path: "./invalid", + }, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -138,7 +179,9 @@ func TestPackerConfig_required_plugin_parse(t *testing.T) { if len(diags) > 0 { t.Fatal(diags) } - if diags := cfg.decodeRequiredPluginsBlock(file); len(diags) > 0 { + + diags = cfg.decodeRequiredPluginsBlock(file) + if !tt.wantDiags && len(diags) > 0 { t.Fatal(diags) }