mirror of
https://github.com/hashicorp/packer.git
synced 2026-09-25 01:04:00 -04:00
* Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at https://hashi.co/license-faq, and details of the license at www.hashicorp.com/bsl. * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
381 lines
12 KiB
Go
381 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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
|
|
}
|