// 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 }