Files
Packer-Cn/hcl2template/parser.go
T
Lucas Bajolet 4eae2df2f1 hcl2template: break down datasource execution
Since we want to allow for independently executing datasources when
needed, we break down the logic to do so.

Each datasource will now hold a list of dependencies. For now, these
dependencies are all datasources themselves, but this may change in the
future.

Then, actual execution of the datasource is self-contained within a
method of the structure, so that we can individually execute them.

Finally, we replace the current sequential execution by a method of the
PackerConfig with an approach akin to local variable evaluation, where
we execute all the datasources one-by-one, and if they cannot be
executed, move along.
We retry this loop for as many times as we don't reach either a state
where all the datasources have been executed; or when we reach a
terminal state where datasources cannot be evaluated because of
dependencies remaining, and no changes have occurred after a try.
2023-10-11 14:28:27 -04:00

585 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.
//
// This means that while at this time we can already get some information
// about them, we may not be able to decode their final form at this time
// and we can only do so when their dependencies are evaluated.
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
datasource.getDependencies()
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) finalizeDecodeSource(cfg *PackerConfig) hcl.Diagnostics {
if source.Ready {
return nil
}
source.Ready = true
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
}