Implement enforced provisioner parsing and handling

- Introduced a new package `enforcedparser` to handle parsing of enforced provisioner blocks from HCL and JSON formats.

- Refactored existing code to utilize the new `ParseProvisionerBlocks` function from the `enforcedparser` package.

- Updated `GetCoreBuildProvisionerFromEnforcedBlock` method to convert enforced provisioner blocks into core build provisioners.

- Enhanced error handling and logging during the parsing process.

- Added tests for the new parsing functionality and ensured existing tests were updated to reflect changes.

- Modified `InjectEnforcedProvisioners` method in JSON registry to utilize the new parsing logic.
feature/enforcedProvisioner
Hari Om 2 weeks ago
parent 7122c28125
commit 802199dde1

@ -4,140 +4,39 @@
package hcl2template
import (
"encoding/json"
"fmt"
"log"
"strconv"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/hashicorp/packer/internal/enforcedparser"
"github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty"
)
var enforcedProvisionerSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: buildProvisionerLabel, LabelNames: []string{"type"}},
},
}
// ParseProvisionerBlocks parses a string containing one or more top-level provisioner blocks
// in either HCL or JSON syntax, and returns a slice of parsed ProvisionerBlock objects along
// with any diagnostics encountered during parsing.
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
parser := &Parser{Parser: hclparse.NewParser()}
log.Printf("[DEBUG] parsing enforced provisioner block content as HCL")
file, diags := parser.ParseHCL([]byte(blockContent), "enforced_provisioner.pkr.hcl")
if !diags.HasErrors() {
log.Printf("[DEBUG] parsed enforced provisioner block content as HCL")
return parseProvisionerBlocksFromFile(parser, file, diags)
}
log.Printf("[DEBUG] failed to parse enforced provisioner block content as HCL, trying JSON fallback")
// Fallback to HCL-JSON for enforced block content authored in JSON syntax.
jsonFile, jsonDiags := parser.ParseJSON([]byte(blockContent), "enforced_provisioner.pkr.json")
if jsonDiags.HasErrors() {
log.Printf("[DEBUG] failed to parse enforced provisioner block content as JSON")
return nil, append(diags, jsonDiags...)
}
provisioners, provisionerDiags := parseProvisionerBlocksFromFile(parser, jsonFile, jsonDiags)
if !provisionerDiags.HasErrors() && len(provisioners) > 0 {
log.Printf("[DEBUG] parsed enforced provisioner block content as JSON")
return provisioners, provisionerDiags
}
// Backward compatibility fallback for legacy JSON shape:
// {"provisioners":[{"type":"shell", ...}]}
legacyJSON, ok, err := normalizeLegacyEnforcedProvisionersJSON(blockContent)
if err == nil && ok {
legacyFile, legacyDiags := parser.ParseJSON([]byte(legacyJSON), "enforced_provisioner_legacy.pkr.json")
if !legacyDiags.HasErrors() {
legacyProvisioners, legacyProvisionerDiags := parseProvisionerBlocksFromFile(parser, legacyFile, legacyDiags)
if !legacyProvisionerDiags.HasErrors() && len(legacyProvisioners) > 0 {
log.Printf("[DEBUG] parsed enforced provisioner block content as legacy JSON")
return legacyProvisioners, legacyProvisionerDiags
}
}
}
if provisionerDiags.HasErrors() {
return nil, provisionerDiags
}
log.Printf("[DEBUG] parsed enforced provisioner block content as JSON but found no valid provisioner blocks")
return provisioners, provisionerDiags
}
func normalizeLegacyEnforcedProvisionersJSON(blockContent string) (string, bool, error) {
type legacyPayload struct {
Provisioners []map[string]interface{} `json:"provisioners"`
}
var payload legacyPayload
if err := json.Unmarshal([]byte(blockContent), &payload); err != nil {
return "", false, err
}
if len(payload.Provisioners) == 0 {
return "", false, nil
}
normalized := make([]map[string]interface{}, 0, len(payload.Provisioners))
for _, p := range payload.Provisioners {
typeName, ok := p["type"].(string)
if !ok || typeName == "" {
continue
}
cfg := make(map[string]interface{})
for k, v := range p {
if k == "type" {
continue
}
cfg[k] = v
}
normalized = append(normalized, map[string]interface{}{typeName: cfg})
}
if len(normalized) == 0 {
return "", false, nil
}
out := map[string]interface{}{
"provisioner": normalized,
}
b, err := json.Marshal(out)
if err != nil {
return "", false, err
func provisionerBlockFromEnforced(pb *enforcedparser.ProvisionerBlock) *ProvisionerBlock {
return &ProvisionerBlock{
PType: pb.PType,
PName: pb.PName,
PauseBefore: pb.PauseBefore,
MaxRetries: pb.MaxRetries,
Timeout: pb.Timeout,
Override: pb.Override,
OnlyExcept: OnlyExcept{
Only: pb.OnlyExcept.Only,
Except: pb.OnlyExcept.Except,
},
HCL2Ref: HCL2Ref{
DefRange: pb.DefRange,
TypeRange: pb.TypeRange,
LabelsRanges: pb.LabelsRange,
Rest: pb.Rest,
},
}
return string(b), true, nil
}
func parseProvisionerBlocksFromFile(parser *Parser, file *hcl.File, diags hcl.Diagnostics) ([]*ProvisionerBlock, hcl.Diagnostics) {
content, moreDiags := file.Body.Content(enforcedProvisionerSchema)
diags = append(diags, moreDiags...)
if diags.HasErrors() {
return nil, diags
}
ectx := &hcl.EvalContext{Variables: map[string]cty.Value{}}
provisioners := make([]*ProvisionerBlock, 0, len(content.Blocks))
for _, block := range content.Blocks {
prov, moreDiags := parser.decodeProvisioner(block, ectx)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
continue
}
provisioners = append(provisioners, prov)
}
return provisioners, diags
// GetCoreBuildProvisionerFromEnforcedBlock converts a shared enforced provisioner block
// into a CoreBuildProvisioner using HCL runtime semantics.
func (cfg *PackerConfig) GetCoreBuildProvisionerFromEnforcedBlock(pb *enforcedparser.ProvisionerBlock, buildName string) (packer.CoreBuildProvisioner, hcl.Diagnostics) {
return cfg.GetCoreBuildProvisionerFromBlock(provisionerBlockFromEnforced(pb), buildName)
}
// GetCoreBuildProvisionerFromBlock converts a ProvisionerBlock to a CoreBuildProvisioner.

@ -5,6 +5,8 @@ package hcl2template
import (
"testing"
"github.com/hashicorp/packer/internal/enforcedparser"
)
func TestGetCoreBuildProvisionerFromBlock_AppliesOverrideForBuild(t *testing.T) {
@ -14,7 +16,7 @@ func TestGetCoreBuildProvisionerFromBlock_AppliesOverrideForBuild(t *testing.T)
CorePackerVersionString: lockedVersion,
}
blocks, diags := ParseProvisionerBlocks(`
blocks, diags := enforcedparser.ParseProvisionerBlocks(`
provisioner "shell" {
override = {
"amazon-ebs.ubuntu" = {
@ -31,7 +33,7 @@ provisioner "shell" {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
coreProv, diags := cfg.GetCoreBuildProvisionerFromEnforcedBlock(blocks[0], "amazon-ebs.ubuntu")
if diags.HasErrors() {
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
}
@ -57,7 +59,7 @@ func TestGetCoreBuildProvisionerFromBlock_OverrideNotAppliedForOtherBuild(t *tes
CorePackerVersionString: lockedVersion,
}
blocks, diags := ParseProvisionerBlocks(`
blocks, diags := enforcedparser.ParseProvisionerBlocks(`
provisioner "shell" {
override = {
"amazon-ebs.ubuntu" = {
@ -74,7 +76,7 @@ provisioner "shell" {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "virtualbox-iso.base")
coreProv, diags := cfg.GetCoreBuildProvisionerFromEnforcedBlock(blocks[0], "virtualbox-iso.base")
if diags.HasErrors() {
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
}
@ -247,7 +249,7 @@ provisioner "shell" {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blocks, diags := ParseProvisionerBlocks(tt.blockContent)
blocks, diags := enforcedparser.ParseProvisionerBlocks(tt.blockContent)
if tt.wantErr {
if !diags.HasErrors() {
@ -282,7 +284,7 @@ provisioner "shell" {
inline = ["echo 'test'"]
}
`
blocks, diags := ParseProvisionerBlocks(blockContent)
blocks, diags := enforcedparser.ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
@ -304,7 +306,7 @@ provisioner "shell" {
inline = ["echo 'test'"]
}
`
blocks, diags := ParseProvisionerBlocks(blockContent)
blocks, diags := enforcedparser.ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
@ -325,7 +327,7 @@ provisioner "shell" {
inline = ["echo 'test'"]
}
`
blocks, diags := ParseProvisionerBlocks(blockContent)
blocks, diags := enforcedparser.ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
@ -364,7 +366,7 @@ func TestParseProvisionerBlocksJSONWithOptions(t *testing.T) {
]
}`
blocks, diags := ParseProvisionerBlocks(blockContent)
blocks, diags := enforcedparser.ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}

@ -0,0 +1,286 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package enforcedparser
import (
"encoding/json"
"fmt"
"log"
"time"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclparse"
hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
"github.com/zclconf/go-cty/cty"
)
const provisionerBlockLabel = "provisioner"
var enforcedProvisionerSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: provisionerBlockLabel, LabelNames: []string{"type"}},
},
}
type OnlyExcept struct {
Only []string `json:"only,omitempty"`
Except []string `json:"except,omitempty"`
}
func (o *OnlyExcept) Skip(n string) bool {
if len(o.Only) > 0 {
for _, v := range o.Only {
if v == n {
return false
}
}
return true
}
if len(o.Except) > 0 {
for _, v := range o.Except {
if v == n {
return true
}
}
return false
}
return false
}
func (o *OnlyExcept) Validate() hcl.Diagnostics {
var diags hcl.Diagnostics
if len(o.Only) > 0 && len(o.Except) > 0 {
diags = diags.Append(&hcl.Diagnostic{
Summary: "only one of 'only' or 'except' may be specified",
Severity: hcl.DiagError,
})
}
return diags
}
type ProvisionerBlock struct {
PType string
PName string
PauseBefore time.Duration
MaxRetries int
Timeout time.Duration
Override map[string]interface{}
OnlyExcept OnlyExcept
DefRange hcl.Range
TypeRange hcl.Range
LabelsRange []hcl.Range
Rest hcl.Body
}
// ParseProvisionerBlocks parses raw enforced block content into a neutral provisioner model.
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
parser := hclparse.NewParser()
log.Printf("[DEBUG] parsing enforced provisioner block content as HCL")
file, diags := parser.ParseHCL([]byte(blockContent), "enforced_provisioner.pkr.hcl")
if !diags.HasErrors() {
log.Printf("[DEBUG] parsed enforced provisioner block content as HCL")
return parseProvisionerBlocksFromFile(file, diags)
}
log.Printf("[DEBUG] failed to parse enforced provisioner block content as HCL, trying JSON fallback")
jsonFile, jsonDiags := parser.ParseJSON([]byte(blockContent), "enforced_provisioner.pkr.json")
if jsonDiags.HasErrors() {
log.Printf("[DEBUG] failed to parse enforced provisioner block content as JSON")
return nil, append(diags, jsonDiags...)
}
provisioners, provisionerDiags := parseProvisionerBlocksFromFile(jsonFile, jsonDiags)
if !provisionerDiags.HasErrors() && len(provisioners) > 0 {
log.Printf("[DEBUG] parsed enforced provisioner block content as JSON")
return provisioners, provisionerDiags
}
legacyJSON, ok, err := normalizeLegacyEnforcedProvisionersJSON(blockContent)
if err == nil && ok {
legacyFile, legacyDiags := parser.ParseJSON([]byte(legacyJSON), "enforced_provisioner_legacy.pkr.json")
if !legacyDiags.HasErrors() {
legacyProvisioners, legacyProvisionerDiags := parseProvisionerBlocksFromFile(legacyFile, legacyDiags)
if !legacyProvisionerDiags.HasErrors() && len(legacyProvisioners) > 0 {
log.Printf("[DEBUG] parsed enforced provisioner block content as legacy JSON")
return legacyProvisioners, legacyProvisionerDiags
}
}
}
if provisionerDiags.HasErrors() {
return nil, provisionerDiags
}
log.Printf("[DEBUG] parsed enforced provisioner block content as JSON but found no valid provisioner blocks")
return provisioners, provisionerDiags
}
func normalizeLegacyEnforcedProvisionersJSON(blockContent string) (string, bool, error) {
type legacyPayload struct {
Provisioners []map[string]interface{} `json:"provisioners"`
}
var payload legacyPayload
if err := json.Unmarshal([]byte(blockContent), &payload); err != nil {
return "", false, err
}
if len(payload.Provisioners) == 0 {
return "", false, nil
}
normalized := make([]map[string]interface{}, 0, len(payload.Provisioners))
for _, p := range payload.Provisioners {
typeName, ok := p["type"].(string)
if !ok || typeName == "" {
continue
}
cfg := make(map[string]interface{})
for k, v := range p {
if k == "type" {
continue
}
cfg[k] = v
}
normalized = append(normalized, map[string]interface{}{typeName: cfg})
}
if len(normalized) == 0 {
return "", false, nil
}
out := map[string]interface{}{
"provisioner": normalized,
}
b, err := json.Marshal(out)
if err != nil {
return "", false, err
}
return string(b), true, nil
}
func parseProvisionerBlocksFromFile(file *hcl.File, diags hcl.Diagnostics) ([]*ProvisionerBlock, hcl.Diagnostics) {
content, moreDiags := file.Body.Content(enforcedProvisionerSchema)
diags = append(diags, moreDiags...)
if diags.HasErrors() {
return nil, diags
}
ectx := &hcl.EvalContext{Variables: map[string]cty.Value{}}
provisioners := make([]*ProvisionerBlock, 0, len(content.Blocks))
for _, block := range content.Blocks {
prov, moreDiags := decodeProvisioner(block, ectx)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
continue
}
provisioners = append(provisioners, prov)
}
return provisioners, diags
}
func decodeProvisioner(block *hcl.Block, ectx *hcl.EvalContext) (*ProvisionerBlock, hcl.Diagnostics) {
var b struct {
Name string `hcl:"name,optional"`
PauseBefore string `hcl:"pause_before,optional"`
MaxRetries int `hcl:"max_retries,optional"`
Timeout string `hcl:"timeout,optional"`
Only []string `hcl:"only,optional"`
Except []string `hcl:"except,optional"`
Override cty.Value `hcl:"override,optional"`
Rest hcl.Body `hcl:",remain"`
}
diags := gohcl.DecodeBody(block.Body, ectx, &b)
if diags.HasErrors() {
return nil, diags
}
provisioner := &ProvisionerBlock{
PType: block.Labels[0],
PName: b.Name,
MaxRetries: b.MaxRetries,
OnlyExcept: OnlyExcept{Only: b.Only, Except: b.Except},
DefRange: block.DefRange,
TypeRange: block.TypeRange,
LabelsRange: block.LabelRanges,
Rest: b.Rest,
}
diags = diags.Extend(provisioner.OnlyExcept.Validate())
if diags.HasErrors() {
return nil, diags
}
if !b.Override.IsNull() {
if !b.Override.Type().IsObjectType() {
return nil, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "provisioner's override block must be an HCL object",
Subject: block.DefRange.Ptr(),
})
}
override := make(map[string]interface{})
for buildName, overrides := range b.Override.AsValueMap() {
buildOverrides := make(map[string]interface{})
if !overrides.Type().IsObjectType() {
return nil, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf(
"provisioner's override.'%s' block must be an HCL object",
buildName),
Subject: block.DefRange.Ptr(),
})
}
for option, value := range overrides.AsValueMap() {
buildOverrides[option] = hcl2shim.ConfigValueFromHCL2(value)
}
override[buildName] = buildOverrides
}
provisioner.Override = override
}
if b.PauseBefore != "" {
pauseBefore, err := time.ParseDuration(b.PauseBefore)
if err != nil {
return nil, append(diags, &hcl.Diagnostic{
Summary: "Failed to parse pause_before duration",
Severity: hcl.DiagError,
Detail: err.Error(),
Subject: &block.DefRange,
})
}
provisioner.PauseBefore = pauseBefore
}
if b.Timeout != "" {
timeout, err := time.ParseDuration(b.Timeout)
if err != nil {
return nil, append(diags, &hcl.Diagnostic{
Summary: "Failed to parse timeout duration",
Severity: hcl.DiagError,
Detail: err.Error(),
Subject: &block.DefRange,
})
}
provisioner.Timeout = timeout
}
return provisioner, diags
}

@ -0,0 +1,114 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package enforcedparser
import "testing"
func TestParseProvisionerBlocks_BasicFormats(t *testing.T) {
tests := []struct {
name string
blockContent string
wantCount int
wantType string
}{
{
name: "hcl",
blockContent: `
provisioner "shell" {
inline = ["echo hello"]
}
`,
wantCount: 1,
wantType: "shell",
},
{
name: "hcl json",
blockContent: `{
"provisioner": [
{
"shell": {
"inline": ["echo hello"]
}
}
]
}`,
wantCount: 1,
wantType: "shell",
},
{
name: "legacy json fallback",
blockContent: `{
"provisioners": [
{
"type": "shell",
"inline": ["echo hello"]
}
]
}`,
wantCount: 1,
wantType: "shell",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blocks, diags := ParseProvisionerBlocks(tt.blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
if len(blocks) != tt.wantCount {
t.Fatalf("ParseProvisionerBlocks() got %d blocks, want %d", len(blocks), tt.wantCount)
}
if blocks[0].PType != tt.wantType {
t.Fatalf("first block type = %q, want %q", blocks[0].PType, tt.wantType)
}
})
}
}
func TestParseProvisionerBlocks_OverrideAndOnlyExcept(t *testing.T) {
blocks, diags := ParseProvisionerBlocks(`
provisioner "shell" {
only = ["amazon-ebs.ubuntu"]
override = {
"amazon-ebs.ubuntu" = {
bool = false
}
}
}
`)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
if len(blocks) != 1 {
t.Fatalf("expected 1 block, got %d", len(blocks))
}
pb := blocks[0]
if pb.OnlyExcept.Skip("amazon-ebs.ubuntu") {
t.Fatal("Skip() should return false for source in only list")
}
if !pb.OnlyExcept.Skip("null.test") {
t.Fatal("Skip() should return true for source not in only list")
}
rawOverride, ok := pb.Override["amazon-ebs.ubuntu"]
if !ok {
t.Fatal("expected override for amazon-ebs.ubuntu")
}
override, ok := rawOverride.(map[string]interface{})
if !ok {
t.Fatalf("override type = %T, want map[string]interface{}", rawOverride)
}
if got, ok := override["bool"]; !ok || got != false {
t.Fatalf("override bool = %#v, want false", override["bool"])
}
}
func TestParseProvisionerBlocks_InvalidContent(t *testing.T) {
_, diags := ParseProvisionerBlocks("this is not valid { hcl }}}")
if !diags.HasErrors() {
t.Fatal("expected parse error, got none")
}
}

@ -12,6 +12,7 @@ import (
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/hcl2template"
"github.com/hashicorp/packer/internal/enforcedparser"
"github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty"
)
@ -111,7 +112,7 @@ func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl
continue
}
provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent)
provBlocks, diags := enforcedparser.ParseProvisionerBlocks(eb.BlockContent)
if diags.HasErrors() {
allDiags = append(allDiags, &hcl.Diagnostic{
Severity: hcl.DiagError,
@ -135,7 +136,7 @@ func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl
continue
}
coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromBlock(pb, build.Type)
coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromEnforcedBlock(pb, build.Type)
if moreDiags.HasErrors() {
allDiags = append(allDiags, moreDiags...)
continue

@ -12,6 +12,7 @@ import (
"github.com/hashicorp/hcl/v2"
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/internal/enforcedparser"
"github.com/hashicorp/packer/packer"
)
@ -120,16 +121,78 @@ func (h *JSONRegistry) FetchEnforcedBlocks(ctx context.Context) error {
}
// InjectEnforcedProvisioners injects enforced provisioners into the builds
// Note: JSON templates don't support enforced provisioners as they are a legacy format
func (h *JSONRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
if len(h.bucket.EnforcedBlocks) > 0 {
return hcl.Diagnostics{
&hcl.Diagnostic{
enforcedBlocks := h.bucket.EnforcedBlocks
if len(enforcedBlocks) == 0 {
return nil
}
var allDiags hcl.Diagnostics
for _, eb := range enforcedBlocks {
if eb.BlockContent == "" {
continue
}
provBlocks, diags := enforcedparser.ParseProvisionerBlocks(eb.BlockContent)
if diags.HasErrors() {
allDiags = append(allDiags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Enforced provisioners are not supported for legacy JSON templates",
Detail: "Linked enforced blocks were found for this bucket, but the current build is a legacy JSON template.",
},
Summary: fmt.Sprintf("Failed to parse enforced block %q", eb.Name),
Detail: diags.Error(),
})
continue
}
if len(provBlocks) > 0 {
h.ui.Say(fmt.Sprintf("Loaded %d enforced provisioner(s) from HCP block %q and template type %q", len(provBlocks), eb.Name, eb.TemplateType))
}
for _, build := range builds {
buildName := build.Type
injected := make([]packer.CoreBuildProvisioner, 0, len(provBlocks))
for _, pb := range provBlocks {
if pb.OnlyExcept.Skip(buildName) {
log.Printf("[DEBUG] skipping enforced provisioner %q for legacy JSON build %q due to only/except rules",
pb.PType, build.Name())
continue
}
coreProv, moreDiags := h.configuration.GenerateCoreBuildProvisionerFromHCLBody(
pb.PType,
pb.Rest,
pb.Override,
pb.PauseBefore,
pb.MaxRetries,
pb.Timeout,
buildName,
)
if moreDiags.HasErrors() {
allDiags = append(allDiags, moreDiags...)
continue
}
build.Provisioners = append(build.Provisioners, coreProv)
injected = append(injected, coreProv)
log.Printf("[INFO] injected enforced provisioner %q from block %q into legacy JSON build %q",
pb.PType, eb.Name, build.Name())
}
if len(injected) == 0 {
continue
}
if err := build.PrepareProvisioners(injected...); err != nil {
allDiags = append(allDiags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to prepare enforced provisioners for legacy JSON build %q", build.Name()),
Detail: err.Error(),
})
}
}
}
return nil
return allDiags
}

@ -0,0 +1,125 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package registry
import (
"os"
"testing"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
packertemplate "github.com/hashicorp/packer-plugin-sdk/template"
"github.com/hashicorp/packer/packer"
)
func testJSONRegistryWithBuilds(t *testing.T, builderNames ...string) (*JSONRegistry, []*packer.CoreBuild, *packersdk.MockProvisioner) {
t.Helper()
if err := os.Setenv("HCP_PACKER_BUCKET_NAME", "test-bucket"); err != nil {
t.Fatalf("Setenv() unexpected error: %v", err)
}
t.Cleanup(func() {
_ = os.Unsetenv("HCP_PACKER_BUCKET_NAME")
})
coreConfig := packer.TestCoreConfig(t)
packer.TestBuilder(t, coreConfig, "test")
provisioner := packer.TestProvisioner(t, coreConfig, "test")
builders := make(map[string]*packertemplate.Builder, len(builderNames))
for _, name := range builderNames {
builders[name] = &packertemplate.Builder{
Name: name,
Type: "test",
Config: map[string]interface{}{},
}
}
coreConfig.Template = &packertemplate.Template{
Path: "test.json",
Builders: builders,
}
core := packer.TestCore(t, coreConfig)
registry, diags := NewJSONRegistry(core, packer.TestUi(t))
if diags.HasErrors() {
t.Fatalf("NewJSONRegistry() unexpected error: %v", diags)
}
builds, diags := core.GetBuilds(packer.GetBuildsOptions{})
if diags.HasErrors() {
t.Fatalf("GetBuilds() unexpected error: %v", diags)
}
return registry, builds, provisioner
}
func TestJSONRegistry_InjectEnforcedProvisioners_AppliesOverride(t *testing.T) {
registry, builds, provisioner := testJSONRegistryWithBuilds(t, "app")
registry.bucket.EnforcedBlocks = []*EnforcedBlock{{
Name: "enforced",
BlockContent: `provisioner "test" {
override = {
app = {
foo = "bar"
}
}
}`,
}}
diags := registry.InjectEnforcedProvisioners(builds)
if diags.HasErrors() {
t.Fatalf("InjectEnforcedProvisioners() unexpected error: %v", diags)
}
if got := len(builds[0].Provisioners); got != 1 {
t.Fatalf("build provisioner count = %d, want 1", got)
}
if !provisioner.PrepCalled {
t.Fatal("expected injected legacy JSON provisioner to be prepared")
}
foundOverride := false
for _, raw := range provisioner.PrepConfigs {
config, ok := raw.(map[string]interface{})
if !ok {
continue
}
if value, ok := config["foo"]; ok && value == "bar" {
foundOverride = true
break
}
}
if !foundOverride {
t.Fatal("expected override config to be passed to injected provisioner")
}
}
func TestJSONRegistry_InjectEnforcedProvisioners_RespectsOnlyExcept(t *testing.T) {
registry, builds, _ := testJSONRegistryWithBuilds(t, "app", "other")
registry.bucket.EnforcedBlocks = []*EnforcedBlock{{
Name: "enforced",
BlockContent: `provisioner "test" {
only = ["app"]
}`,
}}
diags := registry.InjectEnforcedProvisioners(builds)
if diags.HasErrors() {
t.Fatalf("InjectEnforcedProvisioners() unexpected error: %v", diags)
}
provisionerCounts := make(map[string]int, len(builds))
for _, build := range builds {
provisionerCounts[build.Type] = len(build.Provisioners)
}
if provisionerCounts["app"] != 1 {
t.Fatalf("app build provisioner count = %d, want 1", provisionerCounts["app"])
}
if provisionerCounts["other"] != 0 {
t.Fatalf("other build provisioner count = %d, want 0", provisionerCounts["other"])
}
}

@ -145,6 +145,58 @@ func (b *CoreBuild) Name() string {
return b.Type
}
func (b *CoreBuild) packerConfig() map[string]interface{} {
return map[string]interface{}{
common.BuildNameConfigKey: b.Type,
common.BuilderTypeConfigKey: b.BuilderType,
common.CoreVersionConfigKey: version.FormattedVersion(),
common.DebugConfigKey: b.debug,
common.ForceConfigKey: b.force,
common.OnErrorConfigKey: b.onError,
common.TemplatePathKey: b.TemplatePath,
common.UserVariablesConfigKey: b.Variables,
common.SensitiveVarsConfigKey: b.SensitiveVars,
}
}
func (b *CoreBuild) prepareProvisioners(provisioners []CoreBuildProvisioner, packerConfig map[string]interface{}, generatedVars []string) error {
generatedPlaceholderMap := placeholderDataFromGeneratedVars(generatedVars)
for _, coreProv := range provisioners {
configs := make([]interface{}, len(coreProv.config), len(coreProv.config)+1)
copy(configs, coreProv.config)
configs = append(configs, packerConfig)
configs = append(configs, generatedPlaceholderMap)
if err := coreProv.Provisioner.Prepare(configs...); err != nil {
return err
}
}
return nil
}
func placeholderDataFromGeneratedVars(generatedVars []string) map[string]string {
generatedPlaceholderMap := BasicPlaceholderData()
for _, k := range generatedVars {
generatedPlaceholderMap[k] = fmt.Sprintf("Build_%s. "+
packerbuilderdata.PlaceholderMsg, k)
}
return generatedPlaceholderMap
}
// PrepareProvisioners prepares provisioners injected after the build itself has already been prepared.
func (b *CoreBuild) PrepareProvisioners(provisioners ...CoreBuildProvisioner) error {
packerConfig := b.packerConfig()
generatedVars, _, err := b.Builder.Prepare(b.BuilderConfig, packerConfig)
if err != nil {
return err
}
return b.prepareProvisioners(provisioners, packerConfig, generatedVars)
}
// Prepare prepares the build by doing some initialization for the builder
// and any hooks. This _must_ be called prior to Run. The parameter is the
// overrides for the variables within the template (if any).
@ -167,17 +219,7 @@ func (b *CoreBuild) Prepare() (warn []string, err error) {
// a custom json area instead of just aborting early for HCL.
b.prepareCalled = true
packerConfig := map[string]interface{}{
common.BuildNameConfigKey: b.Type,
common.BuilderTypeConfigKey: b.BuilderType,
common.CoreVersionConfigKey: version.FormattedVersion(),
common.DebugConfigKey: b.debug,
common.ForceConfigKey: b.force,
common.OnErrorConfigKey: b.onError,
common.TemplatePathKey: b.TemplatePath,
common.UserVariablesConfigKey: b.Variables,
common.SensitiveVarsConfigKey: b.SensitiveVars,
}
packerConfig := b.packerConfig()
// Prepare the builder
generatedVars, warn, err := b.Builder.Prepare(b.BuilderConfig, packerConfig)
@ -186,27 +228,11 @@ func (b *CoreBuild) Prepare() (warn []string, err error) {
return
}
// If the builder has provided a list of to-be-generated variables that
// should be made accessible to provisioners, pass that list into
// the provisioner prepare() so that the provisioner can appropriately
// validate user input against what will become available.
generatedPlaceholderMap := BasicPlaceholderData()
for _, k := range generatedVars {
generatedPlaceholderMap[k] = fmt.Sprintf("Build_%s. "+
packerbuilderdata.PlaceholderMsg, k)
if err = b.prepareProvisioners(b.Provisioners, packerConfig, generatedVars); err != nil {
return
}
// Prepare the provisioners
for _, coreProv := range b.Provisioners {
configs := make([]interface{}, len(coreProv.config), len(coreProv.config)+1)
copy(configs, coreProv.config)
configs = append(configs, packerConfig)
configs = append(configs, generatedPlaceholderMap)
if err = coreProv.Provisioner.Prepare(configs...); err != nil {
return
}
}
generatedPlaceholderMap := placeholderDataFromGeneratedVars(generatedVars)
// Prepare the on-error-cleanup provisioner
if b.CleanupProvisioner.PType != "" {

@ -11,6 +11,7 @@ import (
"sort"
"strconv"
"strings"
"time"
ttmp "text/template"
@ -18,12 +19,15 @@ import (
multierror "github.com/hashicorp/go-multierror"
version "github.com/hashicorp/go-version"
hcl "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/didyoumean"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
packerversion "github.com/hashicorp/packer/version"
"github.com/zclconf/go-cty/cty"
)
// Core is the main executor of Packer. If Packer is being used as a
@ -259,6 +263,12 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName
"provisioner failed to be started and did not error: %s", rawP.Type)
}
return c.generateCoreBuildProvisionerWithProvisioner(rawP, rawName, provisioner)
}
func (c *Core) generateCoreBuildProvisionerWithProvisioner(rawP *template.Provisioner, rawName string, provisioner packersdk.Provisioner) (CoreBuildProvisioner, error) {
cbp := CoreBuildProvisioner{}
// Get the configuration
config := make([]interface{}, 1, 2)
config[0] = rawP.Config
@ -312,6 +322,79 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName
return cbp, nil
}
// GenerateCoreBuildProvisionerFromHCLBody converts a parsed enforced provisioner body into
// a legacy JSON core build provisioner, using the same runtime behavior as normal JSON templates.
func (c *Core) GenerateCoreBuildProvisionerFromHCLBody(
provisionerType string,
configBody hcl.Body,
override map[string]interface{},
pauseBefore time.Duration,
maxRetries int,
timeout time.Duration,
rawName string,
) (CoreBuildProvisioner, hcl.Diagnostics) {
var diags hcl.Diagnostics
if !c.components.PluginConfig.Provisioners.Has(provisionerType) {
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", provisionerType),
Detail: fmt.Sprintf("The provisioner plugin %q could not be loaded.", provisionerType),
}}
}
provisioner, err := c.components.PluginConfig.Provisioners.Start(provisionerType)
if err != nil {
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", provisionerType),
Detail: fmt.Sprintf("The provisioner plugin could not be loaded: %s", err.Error()),
}}
}
if provisioner == nil {
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", provisionerType),
Detail: "The provisioner failed to start and returned no instance.",
}}
}
flatProvisionerCfg, moreDiags := hcldec.Decode(configBody, provisioner.ConfigSpec(), &hcl.EvalContext{Variables: map[string]cty.Value{}})
diags = append(diags, moreDiags...)
if diags.HasErrors() {
return CoreBuildProvisioner{}, diags
}
flatProvisionerCfg = hcl2shim.WriteUnknownPlaceholderValues(flatProvisionerCfg)
decodedConfig := hcl2shim.ConfigValueFromHCL2(flatProvisionerCfg)
configMap, _ := decodedConfig.(map[string]interface{})
if configMap == nil {
configMap = make(map[string]interface{})
}
rawProvisioner := &template.Provisioner{
Type: provisionerType,
Config: configMap,
Override: override,
PauseBefore: pauseBefore,
Timeout: timeout,
}
if maxRetries > 0 {
rawProvisioner.MaxRetries = strconv.Itoa(maxRetries)
}
coreProvisioner, err := c.generateCoreBuildProvisionerWithProvisioner(rawProvisioner, rawName, provisioner)
if err != nil {
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to prepare enforced provisioner %q", provisionerType),
Detail: err.Error(),
}}
}
return coreProvisioner, diags
}
// This is used for json templates to launch the build plugins.
// They will be prepared via b.Prepare() later.
func (c *Core) GetBuilds(opts GetBuildsOptions) ([]*CoreBuild, hcl.Diagnostics) {

@ -1,56 +0,0 @@
packer {
required_plugins {
docker = {
version = ">= 1.1.0"
source = "github.com/hashicorp/docker"
}
}
}
# HCP Packer registry provisioner blocks below will be
# automatically published as enforced blocks to this bucket.
hcp_packer_registry {
bucket_name = "ubuntu-test"
description = "Test Ubuntu image with enforced provisioners"
bucket_labels = {
"team" = "platform"
"os" = "ubuntu"
"purpose" = "testing"
}
}
source "docker" "ubuntu" {
image = "ubuntu:22.04"
commit = true
}
build {
name = "ubuntu-test"
sources = ["source.docker.ubuntu"]
provisioner "shell" {
inline = [
"apt-get update -y",
"apt-get install -y curl wget jq"
]
}
provisioner "shell" {
inline = [
"echo 'Creating app user...'",
"useradd -m -s /bin/bash appuser",
"mkdir -p /opt/app",
"chown appuser:appuser /opt/app"
]
}
provisioner "shell" {
inline = [
"echo 'Applying security hardening...'",
"echo 'net.ipv4.ip_forward = 0' >> /etc/sysctl.conf",
"echo 'Build complete!'"
]
}
}
Loading…
Cancel
Save