378 lines
12 KiB
Go
378 lines
12 KiB
Go
package hcl2template
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"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"
|
|
)
|
|
|
|
func (cfg *PackerConfig) decodeRequiredPluginsBlock(f *hcl.File) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, moreDiags := f.Body.Content(configSchema)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
for _, block := range content.Blocks {
|
|
switch block.Type {
|
|
case packerLabel:
|
|
content, contentDiags := block.Body.Content(packerBlockSchema)
|
|
diags = append(diags, contentDiags...)
|
|
|
|
// We ignore "packer_version"" here because
|
|
// sniffCoreVersionRequirements already dealt with that
|
|
|
|
for _, innerBlock := range content.Blocks {
|
|
switch innerBlock.Type {
|
|
case "required_plugins":
|
|
reqs, reqsDiags := decodeRequiredPluginsBlock(innerBlock)
|
|
diags = append(diags, reqsDiags...)
|
|
cfg.Packer.RequiredPlugins = append(cfg.Packer.RequiredPlugins, reqs)
|
|
default:
|
|
continue
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
return diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeImplicitRequiredPluginsBlocks(f *hcl.File) hcl.Diagnostics {
|
|
// 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
|
|
|
|
content, moreDiags := f.Body.Content(configSchema)
|
|
diags = append(diags, moreDiags...)
|
|
|
|
for _, block := range content.Blocks {
|
|
|
|
switch block.Type {
|
|
case sourceLabel:
|
|
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 {
|
|
Name string
|
|
// Source used to be able to tell how the template referenced this source,
|
|
// 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
|
|
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
|
|
}
|
|
|
|
func decodeRequiredPluginsBlock(block *hcl.Block) (*RequiredPlugins, hcl.Diagnostics) {
|
|
attrs, diags := block.Body.JustAttributes()
|
|
ret := &RequiredPlugins{
|
|
RequiredPlugins: nil,
|
|
DeclRange: block.DefRange,
|
|
}
|
|
for name, attr := range attrs {
|
|
expr, err := attr.Expr.Value(nil)
|
|
if err != nil {
|
|
diags = append(diags, err...)
|
|
}
|
|
|
|
nameDiags := checkPluginNameNormalized(name, attr.Expr.Range())
|
|
diags = append(diags, nameDiags...)
|
|
|
|
rp := &RequiredPlugin{
|
|
Name: name,
|
|
DeclRange: attr.Expr.Range(),
|
|
}
|
|
|
|
switch {
|
|
case expr.Type().IsPrimitiveType():
|
|
c := "version"
|
|
if cs, _ := decodeVersionConstraint(attr); len(cs.Required) > 0 {
|
|
c = cs.Required.String()
|
|
}
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid plugin requirement",
|
|
Detail: fmt.Sprintf(`'%s = "%s"' plugin requirement calls are not possible.`+
|
|
` You must define a whole block. For example:`+"\n"+
|
|
`%[1]s = {`+"\n"+
|
|
` source = "github.com/hashicorp/%[1]s"`+"\n"+
|
|
` version = "%[2]s"`+"\n"+`}`,
|
|
name, c),
|
|
Subject: attr.Range.Ptr(),
|
|
})
|
|
continue
|
|
|
|
case expr.Type().IsObjectType():
|
|
if !expr.Type().HasAttribute("version") {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "No version constraint was set",
|
|
Detail: "The version field must be specified as a string. Ex: `version = \">= 1.2.0, < 2.0.0\". See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#version-constraints for docs",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
vc := VersionConstraint{
|
|
DeclRange: attr.Range,
|
|
}
|
|
constraint := expr.GetAttr("version")
|
|
if !constraint.Type().Equals(cty.String) || constraint.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid version constraint",
|
|
Detail: "Version must be specified as a string. See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#version-constraint-syntax for docs.",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
constraintStr := constraint.AsString()
|
|
constraints, err := version.NewConstraint(constraintStr)
|
|
if err != nil {
|
|
// NewConstraint doesn't return user-friendly errors, so we'll just
|
|
// ignore the provided error and produce our own generic one.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid version constraint",
|
|
Detail: "This string does not use correct version constraint syntax. " +
|
|
"See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#version-constraint-syntax for docs.\n" +
|
|
err.Error(),
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
vc.Required = constraints
|
|
rp.Requirement = vc
|
|
|
|
if !expr.Type().HasAttribute("source") {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "No source was set",
|
|
Detail: "The source field must be specified as a string. Ex: `source = \"coolcloud\". See https://www.packer.io/docs/templates/hcl_templates/blocks/packer#specifying-plugin-requirements for docs",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
source := expr.GetAttr("source")
|
|
|
|
if !source.Type().Equals(cty.String) || source.IsNull() {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid source",
|
|
Detail: "Source must be specified as a string. For example: " + `source = "coolcloud"`,
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
rp.Source = source.AsString()
|
|
p, sourceDiags := addrs.ParsePluginSourceString(rp.Source)
|
|
|
|
if sourceDiags.HasErrors() {
|
|
for _, diag := range sourceDiags {
|
|
if diag.Subject == nil {
|
|
diag.Subject = attr.Expr.Range().Ptr()
|
|
}
|
|
}
|
|
diags = append(diags, sourceDiags...)
|
|
continue
|
|
} else {
|
|
rp.Type = p
|
|
}
|
|
|
|
attrTypes := expr.Type().AttributeTypes()
|
|
for name := range attrTypes {
|
|
if name == "version" || name == "source" {
|
|
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.`,
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
break
|
|
}
|
|
|
|
default:
|
|
// should not happen
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid required_plugins syntax",
|
|
Detail: "required_plugins entries must be objects.",
|
|
Subject: attr.Expr.Range().Ptr(),
|
|
})
|
|
}
|
|
|
|
if ret.RequiredPlugins == nil {
|
|
ret.RequiredPlugins = make(map[string]*RequiredPlugin)
|
|
}
|
|
ret.RequiredPlugins[rp.Name] = rp
|
|
}
|
|
|
|
return ret, diags
|
|
}
|
|
|
|
// checkPluginNameNormalized verifies that the given string is already
|
|
// normalized and returns an error if not.
|
|
func checkPluginNameNormalized(name string, declrange hcl.Range) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
// verify that the plugin local name is normalized
|
|
normalized, err := addrs.IsPluginPartNormalized(name)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid plugin local name",
|
|
Detail: fmt.Sprintf("%s is an invalid plugin local name: %s", name, err),
|
|
Subject: &declrange,
|
|
})
|
|
return diags
|
|
}
|
|
if !normalized {
|
|
// we would have returned this error already
|
|
normalizedPlugin, _ := addrs.ParsePluginPart(name)
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid plugin local name",
|
|
Detail: fmt.Sprintf("Plugin names must be normalized. Replace %q with %q to fix this error.", name, normalizedPlugin),
|
|
Subject: &declrange,
|
|
})
|
|
}
|
|
return diags
|
|
}
|