mirror of
https://github.com/hashicorp/packer.git
synced 2026-09-19 22:41:40 -04:00
In previous versions of Packer, the parser would load a HCL file (in either HCL or JSON format), and process it in phases. This process meant that if the file was not valid in the first place, we'd have some duplicate errors first. Another thing is that sources and build blocks were not being parsed at the beginning of the parsing phase at all, but were discovered later in the process, when the data sources have been executed and variables have been evaluated. This commit changes the parsing to happen all at once, so we parse the files with the parser, which will fail should one file be malformatted, then we visit the body once using the top-level schema for a Packer build template, handling misplaced blocks once in the process. Then, since the builds and sources may contain some dynamic blocks that need to be expanded once their context is ready (i.e. the variables/datasources they depend on), we can expand the build block for them, and populate their respective objects. This last step should also be adapted for datasources later, as they may need to support dynamic blocks, and be expanded lazily.
586 lines
16 KiB
Go
586 lines
16 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package hcl2template
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/hashicorp/go-version"
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/ext/dynblock"
|
|
"github.com/hashicorp/hcl/v2/gohcl"
|
|
"github.com/hashicorp/hcl/v2/hclparse"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
|
pkrfunction "github.com/hashicorp/packer/hcl2template/function"
|
|
"github.com/hashicorp/packer/packer"
|
|
"github.com/zclconf/go-cty/cty"
|
|
"github.com/zclconf/go-cty/cty/function"
|
|
)
|
|
|
|
const (
|
|
packerLabel = "packer"
|
|
sourceLabel = "source"
|
|
variablesLabel = "variables"
|
|
variableLabel = "variable"
|
|
localsLabel = "locals"
|
|
localLabel = "local"
|
|
dataSourceLabel = "data"
|
|
buildLabel = "build"
|
|
communicatorLabel = "communicator"
|
|
)
|
|
|
|
var configSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: packerLabel},
|
|
{Type: sourceLabel, LabelNames: []string{"type", "name"}},
|
|
{Type: variablesLabel},
|
|
{Type: variableLabel, LabelNames: []string{"name"}},
|
|
{Type: localsLabel},
|
|
{Type: localLabel, LabelNames: []string{"name"}},
|
|
{Type: dataSourceLabel, LabelNames: []string{"type", "name"}},
|
|
{Type: buildLabel},
|
|
{Type: communicatorLabel, LabelNames: []string{"type", "name"}},
|
|
},
|
|
}
|
|
|
|
// packerBlockSchema is the schema for a top-level "packer" block in
|
|
// a configuration file.
|
|
var packerBlockSchema = &hcl.BodySchema{
|
|
Attributes: []hcl.AttributeSchema{
|
|
{Name: "required_version"},
|
|
},
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{Type: "required_plugins"},
|
|
},
|
|
}
|
|
|
|
// Parser helps you parse HCL folders. It will parse an hcl file or directory
|
|
// and start builders, provisioners and post-processors to configure them with
|
|
// the parsed HCL and then return a []packersdk.Build. Packer will use that list
|
|
// of Builds to run everything in order.
|
|
type Parser struct {
|
|
CorePackerVersion *version.Version
|
|
|
|
CorePackerVersionString string
|
|
|
|
PluginConfig *packer.PluginConfig
|
|
|
|
ValidationOptions
|
|
|
|
*hclparse.Parser
|
|
}
|
|
|
|
const (
|
|
hcl2FileExt = ".pkr.hcl"
|
|
hcl2JsonFileExt = ".pkr.json"
|
|
hcl2VarFileExt = ".pkrvars.hcl"
|
|
hcl2VarJsonFileExt = ".pkrvars.json"
|
|
hcl2AutoVarFileExt = ".auto.pkrvars.hcl"
|
|
hcl2AutoVarJsonFileExt = ".auto.pkrvars.json"
|
|
)
|
|
|
|
// Parse will Parse all HCL files in filename. Path can be a folder or a file.
|
|
//
|
|
// Parse will first Parse packer and variables blocks, omitting the rest, which
|
|
// can be expanded with dynamic blocks. We need to evaluate all variables for
|
|
// that, so that data sources can expand dynamic blocks too.
|
|
//
|
|
// Parse returns a PackerConfig that contains configuration layout of a packer
|
|
// build; sources(builders)/provisioners/posts-processors will not be started
|
|
// and their contents wont be verified; Most syntax errors will cause an error,
|
|
// init should be called next to expand dynamic blocks and verify that used
|
|
// things do exist.
|
|
func (p *Parser) Parse(filename string, varFiles []string, argVars map[string]string) (*PackerConfig, hcl.Diagnostics) {
|
|
var files []*hcl.File
|
|
var diags hcl.Diagnostics
|
|
|
|
// parse config files
|
|
if filename != "" {
|
|
hclFiles, jsonFiles, moreDiags := GetHCL2Files(filename, hcl2FileExt, hcl2JsonFileExt)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
// here this probably means that the file was not found, let's
|
|
// simply leave early.
|
|
return nil, diags
|
|
}
|
|
if len(hclFiles)+len(jsonFiles) == 0 {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Could not find any config file in " + filename,
|
|
Detail: "A config file must be suffixed with `.pkr.hcl` or " +
|
|
"`.pkr.json`. A folder can be referenced.",
|
|
})
|
|
}
|
|
for _, filename := range hclFiles {
|
|
f, moreDiags := p.ParseHCLFile(filename)
|
|
diags = append(diags, moreDiags...)
|
|
files = append(files, f)
|
|
}
|
|
for _, filename := range jsonFiles {
|
|
f, moreDiags := p.ParseJSONFile(filename)
|
|
diags = append(diags, moreDiags...)
|
|
files = append(files, f)
|
|
}
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
}
|
|
|
|
basedir := filename
|
|
if isDir, err := isDir(basedir); err == nil && !isDir {
|
|
basedir = filepath.Dir(basedir)
|
|
}
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Could not find current working directory",
|
|
Detail: err.Error(),
|
|
})
|
|
}
|
|
cfg := &PackerConfig{
|
|
Basedir: basedir,
|
|
Cwd: wd,
|
|
CorePackerVersionString: p.CorePackerVersionString,
|
|
HCPVars: map[string]cty.Value{},
|
|
ValidationOptions: p.ValidationOptions,
|
|
parser: p,
|
|
files: files,
|
|
}
|
|
|
|
for _, file := range files {
|
|
coreVersionConstraints, moreDiags := sniffCoreVersionRequirements(file.Body)
|
|
cfg.Packer.VersionConstraints = append(cfg.Packer.VersionConstraints, coreVersionConstraints...)
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
|
|
// Before we go further, we'll check to make sure this version can read
|
|
// all files, so we can produce a version-related error message rather than
|
|
// potentially-confusing downstream errors.
|
|
versionDiags := cfg.CheckCoreVersionRequirements(p.CorePackerVersion)
|
|
diags = append(diags, versionDiags...)
|
|
if versionDiags.HasErrors() {
|
|
return cfg, diags
|
|
}
|
|
|
|
for _, file := range files {
|
|
diags = append(diags, cfg.decodeFile(file)...)
|
|
}
|
|
|
|
// parse var files
|
|
{
|
|
hclVarFiles, jsonVarFiles, moreDiags := GetHCL2Files(filename, hcl2AutoVarFileExt, hcl2AutoVarJsonFileExt)
|
|
diags = append(diags, moreDiags...)
|
|
for _, file := range varFiles {
|
|
switch filepath.Ext(file) {
|
|
case ".hcl":
|
|
hclVarFiles = append(hclVarFiles, file)
|
|
case ".json":
|
|
jsonVarFiles = append(jsonVarFiles, file)
|
|
default:
|
|
diags = append(moreDiags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Could not guess format of " + file,
|
|
Detail: "A var file must be suffixed with `.hcl` or `.json`.",
|
|
})
|
|
}
|
|
}
|
|
var varFiles []*hcl.File
|
|
for _, filename := range hclVarFiles {
|
|
f, moreDiags := p.ParseHCLFile(filename)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
continue
|
|
}
|
|
varFiles = append(varFiles, f)
|
|
}
|
|
for _, filename := range jsonVarFiles {
|
|
f, moreDiags := p.ParseJSONFile(filename)
|
|
diags = append(diags, moreDiags...)
|
|
if moreDiags.HasErrors() {
|
|
continue
|
|
}
|
|
varFiles = append(varFiles, f)
|
|
}
|
|
|
|
diags = append(diags, cfg.collectInputVariableValues(os.Environ(), varFiles, argVars)...)
|
|
}
|
|
|
|
return cfg, diags
|
|
}
|
|
|
|
// sniffCoreVersionRequirements does minimal parsing of the given body for
|
|
// "packer" blocks with "required_version" attributes, returning the
|
|
// requirements found.
|
|
//
|
|
// This is intended to maximize the chance that we'll be able to read the
|
|
// requirements (syntax errors notwithstanding) even if the config file contains
|
|
// constructs that might've been added in future versions
|
|
//
|
|
// This is a "best effort" sort of method which will return constraints it is
|
|
// able to find, but may return no constraints at all if the given body is
|
|
// so invalid that it cannot be decoded at all.
|
|
func sniffCoreVersionRequirements(body hcl.Body) ([]VersionConstraint, hcl.Diagnostics) {
|
|
|
|
var sniffRootSchema = &hcl.BodySchema{
|
|
Blocks: []hcl.BlockHeaderSchema{
|
|
{
|
|
Type: packerLabel,
|
|
},
|
|
},
|
|
}
|
|
|
|
rootContent, _, diags := body.PartialContent(sniffRootSchema)
|
|
|
|
var constraints []VersionConstraint
|
|
|
|
for _, block := range rootContent.Blocks {
|
|
content, blockDiags := block.Body.Content(packerBlockSchema)
|
|
diags = append(diags, blockDiags...)
|
|
|
|
attr, exists := content.Attributes["required_version"]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
constraint, constraintDiags := decodeVersionConstraint(attr)
|
|
diags = append(diags, constraintDiags...)
|
|
if !constraintDiags.HasErrors() {
|
|
constraints = append(constraints, constraint)
|
|
}
|
|
}
|
|
|
|
return constraints, diags
|
|
}
|
|
|
|
func filterVarsFromLogs(inputOrLocal Variables) {
|
|
for _, variable := range inputOrLocal {
|
|
if !variable.Sensitive {
|
|
continue
|
|
}
|
|
value := variable.Value()
|
|
_ = cty.Walk(value, func(_ cty.Path, nested cty.Value) (bool, error) {
|
|
if nested.IsWhollyKnown() && !nested.IsNull() && nested.Type().Equals(cty.String) {
|
|
packersdk.LogSecretFilter.Set(nested.AsString())
|
|
}
|
|
return true, nil
|
|
})
|
|
}
|
|
}
|
|
|
|
// decodeFile attempts to decode the configuration from a HCL file, and starts
|
|
// populating the config from it.
|
|
func (cfg *PackerConfig) decodeFile(file *hcl.File) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
content, moreDiags := file.Body.Content(configSchema)
|
|
diags = append(diags, moreDiags...)
|
|
// If basic parsing failed, we should not continue
|
|
if diags.HasErrors() {
|
|
return diags
|
|
}
|
|
|
|
for _, block := range content.Blocks {
|
|
diags = append(diags, cfg.decodeBlock(block)...)
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeBlock(block *hcl.Block) hcl.Diagnostics {
|
|
switch block.Type {
|
|
case packerLabel:
|
|
return cfg.decodePackerBlock(block)
|
|
case dataSourceLabel:
|
|
return cfg.decodeDatasource(block)
|
|
case variableLabel:
|
|
return cfg.decodeVariableBlock(block)
|
|
case variablesLabel:
|
|
return cfg.decodeVariablesBlock(block)
|
|
case localLabel:
|
|
return cfg.decodeLocalBlock(block)
|
|
case localsLabel:
|
|
return cfg.decodeLocalsBlock(block)
|
|
// NOTE: Both build and source blocks can be dynamically expanded.
|
|
//
|
|
// Because of that, we only populate the config's top-level build and
|
|
// source lists, but we don't attempt to dynamically expand them now,
|
|
// since dynamic blocks can depend on other resources that are executed
|
|
// later in the process.
|
|
//
|
|
// The source and builds are only ready to use when we have called the
|
|
// finalizeDecode/finalizeDecodeSource respectively on the blocks.
|
|
case buildLabel:
|
|
return cfg.decodeBuildBlock(block)
|
|
case sourceLabel:
|
|
return cfg.decodeSourceBlock(block)
|
|
}
|
|
|
|
return hcl.Diagnostics{
|
|
&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid block type",
|
|
Detail: fmt.Sprintf("The block %q is not a valid top-level block", block.Type),
|
|
Subject: &block.DefRange,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodePackerBlock(block *hcl.Block) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
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) decodeDatasource(block *hcl.Block) hcl.Diagnostics {
|
|
datasource, diags := cfg.decodeDataBlock(block)
|
|
if diags.HasErrors() {
|
|
return diags
|
|
}
|
|
ref := datasource.Ref()
|
|
if existing, found := cfg.Datasources[ref]; found {
|
|
return append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate " + dataSourceLabel + " block",
|
|
Detail: fmt.Sprintf("This "+dataSourceLabel+" block has the "+
|
|
"same data type and name as a previous block declared "+
|
|
"at %s. Each "+dataSourceLabel+" must have a unique name per builder type.",
|
|
existing.block.DefRange.Ptr()),
|
|
Subject: datasource.block.DefRange.Ptr(),
|
|
})
|
|
}
|
|
if cfg.Datasources == nil {
|
|
cfg.Datasources = Datasources{}
|
|
}
|
|
cfg.Datasources[ref] = *datasource
|
|
|
|
return diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeDataBlock(block *hcl.Block) (*DatasourceBlock, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
r := &DatasourceBlock{
|
|
Type: block.Labels[0],
|
|
Name: block.Labels[1],
|
|
block: block,
|
|
}
|
|
|
|
if !hclsyntax.ValidIdentifier(r.Type) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid data source name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
if !hclsyntax.ValidIdentifier(r.Name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid data resource name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[1],
|
|
})
|
|
}
|
|
|
|
return r, diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeLocalBlock(block *hcl.Block) hcl.Diagnostics {
|
|
name := block.Labels[0]
|
|
|
|
content, diags := block.Body.Content(localBlockSchema)
|
|
if !hclsyntax.ValidIdentifier(name) {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Invalid local name",
|
|
Detail: badIdentifierDetail,
|
|
Subject: &block.LabelRanges[0],
|
|
})
|
|
}
|
|
|
|
l := &LocalBlock{
|
|
Name: name,
|
|
}
|
|
|
|
if attr, exists := content.Attributes["sensitive"]; exists {
|
|
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &l.Sensitive)
|
|
diags = append(diags, valDiags...)
|
|
}
|
|
|
|
if def, ok := content.Attributes["expression"]; ok {
|
|
l.Expr = def.Expr
|
|
}
|
|
|
|
cfg.LocalBlocks = append(cfg.LocalBlocks, l)
|
|
|
|
return diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeLocalsBlock(block *hcl.Block) hcl.Diagnostics {
|
|
attrs, diags := block.Body.JustAttributes()
|
|
|
|
for name, attr := range attrs {
|
|
cfg.LocalBlocks = append(cfg.LocalBlocks, &LocalBlock{
|
|
Name: name,
|
|
Expr: attr.Expr,
|
|
})
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeVariableBlock(block *hcl.Block) hcl.Diagnostics {
|
|
// for input variables we allow to use env in the default value section.
|
|
ectx := &hcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"env": pkrfunction.EnvFunc,
|
|
},
|
|
}
|
|
|
|
return cfg.InputVariables.decodeVariableBlock(block, ectx)
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeVariablesBlock(block *hcl.Block) hcl.Diagnostics {
|
|
// for input variables we allow to use env in the default value section.
|
|
ectx := &hcl.EvalContext{
|
|
Functions: map[string]function.Function{
|
|
"env": pkrfunction.EnvFunc,
|
|
},
|
|
}
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
attrs, moreDiags := block.Body.JustAttributes()
|
|
diags = append(diags, moreDiags...)
|
|
for key, attr := range attrs {
|
|
moreDiags = cfg.InputVariables.decodeVariable(key, attr, ectx)
|
|
diags = append(diags, moreDiags...)
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// decodeBuildBlock shallowly decodes a build block from the config.
|
|
//
|
|
// The final decoding step (which requires an up-to-date context) will be done
|
|
// when we need it.
|
|
func (cfg *PackerConfig) decodeBuildBlock(block *hcl.Block) hcl.Diagnostics {
|
|
build := &BuildBlock{
|
|
block: block,
|
|
}
|
|
|
|
cfg.Builds = append(cfg.Builds, build)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeSourceBlock(block *hcl.Block) hcl.Diagnostics {
|
|
source, diags := cfg.decodeSource(block)
|
|
if diags.HasErrors() {
|
|
return diags
|
|
}
|
|
|
|
if cfg.Sources == nil {
|
|
cfg.Sources = map[SourceRef]SourceBlock{}
|
|
}
|
|
|
|
ref := source.Ref()
|
|
if existing, found := cfg.Sources[ref]; found {
|
|
return append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Duplicate " + sourceLabel + " block",
|
|
Detail: fmt.Sprintf("This "+sourceLabel+" block has the "+
|
|
"same builder type and name as a previous block declared "+
|
|
"at %s. Each "+sourceLabel+" must have a unique name per builder type.",
|
|
existing.block.DefRange.Ptr()),
|
|
Subject: source.block.DefRange.Ptr(),
|
|
})
|
|
}
|
|
|
|
cfg.Sources[ref] = source
|
|
|
|
return diags
|
|
}
|
|
|
|
// decodeBuildSource reads a used source block from a build:
|
|
//
|
|
// build {
|
|
// source "type.example" {
|
|
// name = "local_name"
|
|
// }
|
|
// }
|
|
func (cfg *PackerConfig) decodeBuildSource(block *hcl.Block) (SourceUseBlock, hcl.Diagnostics) {
|
|
ref := sourceRefFromString(block.Labels[0])
|
|
out := SourceUseBlock{SourceRef: ref}
|
|
var b struct {
|
|
Name string `hcl:"name,optional"`
|
|
Rest hcl.Body `hcl:",remain"`
|
|
}
|
|
diags := gohcl.DecodeBody(block.Body, nil, &b)
|
|
if diags.HasErrors() {
|
|
return out, diags
|
|
}
|
|
out.LocalName = b.Name
|
|
out.Body = b.Rest
|
|
return out, nil
|
|
}
|
|
|
|
func (source *SourceBlock) finalizeDecode(cfg *PackerConfig) hcl.Diagnostics {
|
|
if source.Ready {
|
|
return nil
|
|
}
|
|
|
|
dyn := dynblock.Expand(source.block.Body, cfg.EvalContext(DatasourceContext, nil))
|
|
// Expand without a base schema since nothing is known in advance for a
|
|
// source, but we still want to expand dynamic blocks if any
|
|
_, rem, diags := dyn.PartialContent(&hcl.BodySchema{})
|
|
|
|
// Only try to expand once, regardless of whether the source succeeded
|
|
// to expand dynamic data or not.
|
|
source.Ready = true
|
|
if diags.HasErrors() {
|
|
return diags
|
|
}
|
|
|
|
source.block = &hcl.Block{
|
|
Labels: []string{
|
|
source.Type,
|
|
source.Name,
|
|
},
|
|
Body: rem,
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func (cfg *PackerConfig) decodeSource(block *hcl.Block) (SourceBlock, hcl.Diagnostics) {
|
|
source := SourceBlock{
|
|
Type: block.Labels[0],
|
|
Name: block.Labels[1],
|
|
block: block,
|
|
}
|
|
var diags hcl.Diagnostics
|
|
|
|
return source, diags
|
|
}
|