Feature/enforced provisioner (#13591)

* added the parser for the enforced block

* Enhance enforced provisioner parsing to support HCL and JSON formats

- Updated ParseProvisionerBlocks to handle both HCL and JSON syntax, including legacy JSON format.

- Added comprehensive test cases for JSON provisioner parsing.

- Improved ExtractBuildProvisionerHCL to merge inline commands from shell provisioners.

- Enhanced logging for enforced block operations in HCP Packer.

* Remove PublishEnforcedBlocks function from Bucket struct

* Remove ExtractBuildProvisionerHCL function and unused imports

* Reverted the version upgrade

* Added the internal-sdk for the enforcedProvsioner api changes

* Enhance enforced provisioner handling and error reporting

- Update error handling in FetchEnforcedBlocks to return detailed errors instead of warnings.
- Modify GetCoreBuildProvisionerFromBlock to accept build name for overrides.
- Add tests for FetchEnforcedBlocks to ensure correct behavior and error handling.
- Implement diagnostics for unsupported legacy JSON templates.

* 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.

* Add test case for -skip-enforcement flag in BuildArgs

* Refactor sensitive variable handling in provisioners and add related tests

* Refactor enforced provisioner handling: remove internal parser, update tests, and streamline API interactions

* Enhance provisioner block parsing: add error handling for invalid combinations and expand test coverage

* Remove internal SDK replacement for enforced block types in go.mod

* Update dependencies in go.mod and go.sum: bump hcp-sdk-go and packer-plugin-sdk versions, adjust syft version, and update OpenTelemetry packages

* Update hcp-sdk-go dependency to v0.172.0 in go.mod and go.sum

* Fix formatting in TestBuildCommand_ParseArgs and add newline at end of json_enforced_test.go

* Refactor testJSONRegistryWithBuilds: remove environment variable setup and streamline registry initialization

* Rename injected variable for clarity in InjectEnforcedProvisioners function

---------

Co-authored-by: Hari Om <58305594+Madhav008@users.noreply.github.com>
backport_v1.15.2
Hari 1 month ago committed by Hari Om
parent 16292a5251
commit 3b0f5d512a

@ -150,6 +150,26 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int
return ret
}
// Fetch and inject enforced provisioners from HCP Packer (if configured)
if !cla.SkipEnforcement {
if err := hcpRegistry.FetchEnforcedBlocks(buildCtx); err != nil {
return writeDiags(c.Ui, nil, hcl.Diagnostics{
&hcl.Diagnostic{
Summary: "HCP: fetching enforced provisioners failed",
Severity: hcl.DiagError,
Detail: err.Error(),
},
})
}
diags := hcpRegistry.InjectEnforcedProvisioners(builds)
if diags.HasErrors() {
return writeDiags(c.Ui, nil, diags)
}
} else {
c.Ui.Say("Skipping HCP Packer enforced provisioners (--skip-enforcement flag set)")
}
if cla.Debug {
c.Ui.Say("Debug mode enabled. Builds will not be parallelized.")
}
@ -456,6 +476,7 @@ Options:
-warn-on-undeclared-var Display warnings for user variable files containing undeclared variables.
-ignore-prerelease-plugins Disable the loading of prerelease plugin binaries (x.y.z-dev).
-use-sequential-evaluation Fallback to using a sequential approach for local/datasource evaluation.
-skip-enforcement Skip injection of HCP Packer enforced provisioners.
`
return strings.TrimSpace(helpText)

@ -1131,6 +1131,16 @@ func TestBuildCommand_ParseArgs(t *testing.T) {
},
0,
},
{fields{defaultMeta},
args{[]string{"-skip-enforcement", "file.json"}},
&BuildArgs{
MetaArgs: MetaArgs{Path: "file.json"},
ParallelBuilds: math.MaxInt64,
Color: true,
SkipEnforcement: true,
},
0,
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%s", tt.args.args), func(t *testing.T) {

@ -101,6 +101,8 @@ func (ba *BuildArgs) AddFlagSets(flags *flag.FlagSet) {
flags.BoolVar(&ba.ReleaseOnly, "ignore-prerelease-plugins", false, "Disable the loading of prerelease plugin binaries (x.y.z-dev).")
flags.BoolVar(&ba.SkipEnforcement, "skip-enforcement", false, "Skip injection of HCP Packer enforced provisioners. Requires admin privileges.")
ba.MetaArgs.AddFlagSets(flags)
}
@ -136,6 +138,7 @@ type BuildArgs struct {
ParallelBuilds int64
OnError string
ReleaseOnly bool
SkipEnforcement bool
}
func (ia *InitArgs) AddFlagSets(flags *flag.FlagSet) {

@ -22,7 +22,7 @@ require (
github.com/hashicorp/go-uuid v1.0.3
github.com/hashicorp/go-version v1.8.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/hashicorp/hcp-sdk-go v0.167.0
github.com/hashicorp/hcp-sdk-go v0.172.0
github.com/hashicorp/packer-plugin-sdk v0.6.7
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869
github.com/klauspost/compress v1.18.4

@ -732,8 +732,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hashicorp/hcp-sdk-go v0.167.0 h1:t2v+mm3gN1z4qvdJ7g9RuDdXDvIExMtjV1Fvzn2LuVc=
github.com/hashicorp/hcp-sdk-go v0.167.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
github.com/hashicorp/hcp-sdk-go v0.172.0 h1:j4VrSN2yd8prFb8Y0gQWQbTpsV5uVPgYEUozOGfPOOc=
github.com/hashicorp/hcp-sdk-go v0.172.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=

@ -0,0 +1,81 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package hcl2template
import (
"fmt"
"strconv"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/packer/packer"
)
// GetCoreBuildProvisionerFromBlock converts a ProvisionerBlock to a CoreBuildProvisioner.
// This is used for enforced provisioners that need to be injected into builds.
func (cfg *PackerConfig) GetCoreBuildProvisionerFromBlock(pb *ProvisionerBlock, buildName string) (packer.CoreBuildProvisioner, hcl.Diagnostics) {
var diags hcl.Diagnostics
// Get the provisioner plugin
provisioner, err := cfg.parser.PluginConfig.Provisioners.Start(pb.PType)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", pb.PType),
Detail: fmt.Sprintf("The provisioner plugin could not be loaded: %s", err.Error()),
})
return packer.CoreBuildProvisioner{}, diags
}
// Create basic builder variables
builderVars := map[string]interface{}{
"packer_core_version": cfg.CorePackerVersionString,
"packer_debug": strconv.FormatBool(cfg.debug),
"packer_force": strconv.FormatBool(cfg.force),
"packer_on_error": cfg.onError,
"packer_sensitive_variables": cfg.sensitiveInputVariableKeys(),
}
// Create evaluation context
ectx := cfg.EvalContext(BuildContext, nil)
// Create the HCL2Provisioner wrapper
hclProvisioner := &HCL2Provisioner{
Provisioner: provisioner,
provisionerBlock: pb,
evalContext: ectx,
builderVariables: builderVars,
}
if pb.Override != nil {
if override, ok := pb.Override[buildName]; ok {
if typedOverride, ok := override.(map[string]interface{}); ok {
hclProvisioner.override = typedOverride
}
}
}
// Prepare the provisioner
err = hclProvisioner.HCL2Prepare(nil)
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Failed to prepare enforced provisioner %q", pb.PType),
Detail: err.Error(),
})
return packer.CoreBuildProvisioner{}, diags
}
// Wrap provisioner with any special behavior (pause, timeout, retry)
wrappedProvisioner := packer.WrapProvisionerWithOptions(hclProvisioner, packer.ProvisionerWrapOptions{
PauseBefore: pb.PauseBefore,
Timeout: pb.Timeout,
MaxRetries: pb.MaxRetries,
})
return packer.CoreBuildProvisioner{
PType: pb.PType,
PName: pb.PName,
Provisioner: wrappedProvisioner,
}, diags
}

@ -0,0 +1,146 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package hcl2template
import (
"encoding/json"
"log"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/zclconf/go-cty/cty"
)
var standaloneProvisionerSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{Type: buildProvisionerLabel, LabelNames: []string{"type"}},
},
}
// ParseProvisionerBlocks parses raw provisioner block content into ProvisionerBlocks.
// It accepts HCL, HCL JSON, and the legacy JSON payload used for enforced provisioners.
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
parser := &Parser{Parser: hclparse.NewParser()}
return parser.parseProvisionerBlocks(blockContent)
}
func (p *Parser) parseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
hclParser := p.Parser
if hclParser == nil {
hclParser = hclparse.NewParser()
}
log.Printf("[DEBUG] parsing provisioner block content as HCL")
file, diags := hclParser.ParseHCL([]byte(blockContent), "provisioner.pkr.hcl")
if !diags.HasErrors() {
provisioners, provisionerDiags := p.parseProvisionerBlocksFromFile(file, diags)
if provisionerDiags.HasErrors() {
return nil, provisionerDiags
}
log.Printf("[DEBUG] parsed provisioner block content as HCL")
return provisioners, provisionerDiags
}
log.Printf("[DEBUG] failed to parse provisioner block content as HCL, trying JSON fallback")
jsonFile, jsonDiags := hclParser.ParseJSON([]byte(blockContent), "provisioner.pkr.json")
if jsonDiags.HasErrors() {
log.Printf("[DEBUG] failed to parse provisioner block content as JSON")
return nil, append(diags, jsonDiags...)
}
provisioners, provisionerDiags := p.parseProvisionerBlocksFromFile(jsonFile, jsonDiags)
if !provisionerDiags.HasErrors() && len(provisioners) > 0 {
log.Printf("[DEBUG] parsed provisioner block content as JSON")
return provisioners, provisionerDiags
}
legacyJSON, ok, err := normalizeLegacyProvisionersJSON(blockContent)
if err == nil && ok {
legacyFile, legacyDiags := hclParser.ParseJSON([]byte(legacyJSON), "provisioner_legacy.pkr.json")
if !legacyDiags.HasErrors() {
legacyProvisioners, legacyProvisionerDiags := p.parseProvisionerBlocksFromFile(legacyFile, legacyDiags)
if !legacyProvisionerDiags.HasErrors() && len(legacyProvisioners) > 0 {
log.Printf("[DEBUG] parsed provisioner block content as legacy JSON")
return legacyProvisioners, legacyProvisionerDiags
}
}
}
if provisionerDiags.HasErrors() {
return nil, provisionerDiags
}
log.Printf("[DEBUG] parsed provisioner block content as JSON but found no valid provisioner blocks")
return provisioners, provisionerDiags
}
func normalizeLegacyProvisionersJSON(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 _, provisioner := range payload.Provisioners {
typeName, ok := provisioner["type"].(string)
if !ok || typeName == "" {
continue
}
cfg := make(map[string]interface{})
for key, value := range provisioner {
if key == "type" {
continue
}
cfg[key] = value
}
normalized = append(normalized, map[string]interface{}{typeName: cfg})
}
if len(normalized) == 0 {
return "", false, nil
}
out := map[string]interface{}{
buildProvisionerLabel: normalized,
}
b, err := json.Marshal(out)
if err != nil {
return "", false, err
}
return string(b), true, nil
}
func (p *Parser) parseProvisionerBlocksFromFile(file *hcl.File, diags hcl.Diagnostics) ([]*ProvisionerBlock, hcl.Diagnostics) {
content, moreDiags := file.Body.Content(standaloneProvisionerSchema)
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 {
provisioner, moreDiags := p.decodeProvisioner(block, ectx)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
continue
}
provisioners = append(provisioners, provisioner)
}
return provisioners, diags
}

@ -0,0 +1,496 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package hcl2template
import (
"testing"
)
func TestGetCoreBuildProvisionerFromBlock_AppliesOverrideForBuild(t *testing.T) {
parser := getBasicParser()
cfg := &PackerConfig{
parser: parser,
CorePackerVersionString: lockedVersion,
}
blocks, diags := ParseProvisionerBlocks(`
provisioner "shell" {
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))
}
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
if diags.HasErrors() {
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
}
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
if !ok {
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
}
if hclProv.override == nil {
t.Fatal("expected override to be applied, got nil")
}
if got, ok := hclProv.override["bool"]; !ok || got != false {
t.Fatalf("expected override bool=false, got %#v", hclProv.override["bool"])
}
}
func TestGetCoreBuildProvisionerFromBlock_OverrideNotAppliedForOtherBuild(t *testing.T) {
parser := getBasicParser()
cfg := &PackerConfig{
parser: parser,
CorePackerVersionString: lockedVersion,
}
blocks, diags := ParseProvisionerBlocks(`
provisioner "shell" {
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))
}
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "virtualbox-iso.base")
if diags.HasErrors() {
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
}
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
if !ok {
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
}
if hclProv.override != nil {
t.Fatalf("expected no override to be applied, got %#v", hclProv.override)
}
}
func TestGetCoreBuildProvisionerFromBlock_IncludesSensitiveVariables(t *testing.T) {
parser := getBasicParser()
cfg := &PackerConfig{
parser: parser,
CorePackerVersionString: lockedVersion,
InputVariables: Variables{
"visible": &Variable{Name: "visible"},
"secret": &Variable{Name: "secret", Sensitive: true},
},
}
blocks, diags := ParseProvisionerBlocks(`
provisioner "shell" {
override = {
"amazon-ebs.ubuntu" = {
bool = false
}
}
}
`)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
if diags.HasErrors() {
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
}
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
if !ok {
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
}
sensitiveVars, ok := hclProv.builderVariables["packer_sensitive_variables"].([]string)
if !ok {
t.Fatalf("expected []string packer_sensitive_variables, got %T", hclProv.builderVariables["packer_sensitive_variables"])
}
if len(sensitiveVars) != 1 || sensitiveVars[0] != "secret" {
t.Fatalf("expected sensitive vars [secret], got %#v", sensitiveVars)
}
}
func TestParseProvisionerBlocks(t *testing.T) {
tests := []struct {
name string
blockContent string
wantCount int
wantTypes []string
wantErr bool
}{
{
name: "single shell provisioner",
blockContent: `
provisioner "shell" {
inline = ["echo 'Hello from enforced provisioner'"]
}
`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "multiple provisioners",
blockContent: `
provisioner "shell" {
inline = ["echo 'First enforced provisioner'"]
}
provisioner "shell" {
name = "security-scan"
inline = ["echo 'Security scan running...'"]
}
`,
wantCount: 2,
wantTypes: []string{"shell", "shell"},
wantErr: false,
},
{
name: "provisioner with pause_before",
blockContent: `
provisioner "shell" {
pause_before = "10s"
inline = ["echo 'Waiting before execution'"]
}
`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "provisioner with max_retries",
blockContent: `
provisioner "shell" {
max_retries = 3
inline = ["echo 'Retry test'"]
}
`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "provisioner with only filter",
blockContent: `
provisioner "shell" {
only = ["amazon-ebs.ubuntu"]
inline = ["echo 'Only for amazon-ebs.ubuntu'"]
}
`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "provisioner with except filter",
blockContent: `
provisioner "shell" {
except = ["null.test"]
inline = ["echo 'Except for null.test'"]
}
`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "provisioner with both only and except",
blockContent: `
provisioner "shell" {
only = ["amazon-ebs.ubuntu"]
except = ["null.test"]
inline = ["echo 'invalid filter combination'"]
}
`,
wantCount: 0,
wantTypes: nil,
wantErr: true,
},
{
name: "empty block content",
blockContent: "",
wantCount: 0,
wantTypes: nil,
wantErr: false,
},
{
name: "whitespace only block content",
blockContent: "\n\n\t \n",
wantCount: 0,
wantTypes: nil,
wantErr: false,
},
{
name: "invalid HCL syntax",
blockContent: "this is not valid { hcl }}}",
wantCount: 0,
wantTypes: nil,
wantErr: true,
},
{
name: "json single shell provisioner",
blockContent: `{
"provisioner": [
{
"shell": {
"inline": ["echo 'Hello from enforced provisioner JSON'"]
}
}
]
}`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "json provisioner with escaped newline in string value",
blockContent: `{
"provisioner": [
{
"shell": {
"inline": ["echo first line\necho second line"]
}
}
]
}`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "json multiple provisioners",
blockContent: `{
"provisioner": [
{
"shell": {
"inline": ["echo 'first'"]
}
},
{
"shell": {
"name": "security-scan",
"inline": ["echo 'second'"]
}
}
]
}`,
wantCount: 2,
wantTypes: []string{"shell", "shell"},
wantErr: false,
},
{
name: "invalid json syntax",
blockContent: `{"provisioner": [ { "shell": { "inline": ["test"] } ] }`,
wantCount: 0,
wantTypes: nil,
wantErr: true,
},
{
name: "legacy json provisioners format",
blockContent: `{
"provisioners": [
{
"type": "shell",
"inline": ["echo legacy json format"]
}
]
}`,
wantCount: 1,
wantTypes: []string{"shell"},
wantErr: false,
},
{
name: "legacy json provisioners with escaped newline and multiple types",
blockContent: `{
"provisioners": [
{
"type": "shell",
"inline": ["echo legacy line 1\necho legacy line 2"]
},
{
"type": "file",
"source": "source.txt",
"destination": "destination.txt"
}
]
}`,
wantCount: 2,
wantTypes: []string{"shell", "file"},
wantErr: false,
},
{
name: "hcl provisioners with windows newlines",
blockContent: "provisioner \"shell\" {\r\n inline = [\"echo first\"]\r\n}\r\n\r\nprovisioner \"file\" {\r\n source = \"source.txt\"\r\n destination = \"destination.txt\"\r\n}\r\n",
wantCount: 2,
wantTypes: []string{"shell", "file"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
blocks, diags := ParseProvisionerBlocks(tt.blockContent)
if tt.wantErr {
if !diags.HasErrors() {
t.Errorf("ParseProvisionerBlocks() expected error but got none")
}
return
}
if diags.HasErrors() {
t.Errorf("ParseProvisionerBlocks() unexpected error: %v", diags)
return
}
if len(blocks) != tt.wantCount {
t.Errorf("ParseProvisionerBlocks() got %d blocks, want %d", len(blocks), tt.wantCount)
return
}
for i, wantType := range tt.wantTypes {
if blocks[i].PType != wantType {
t.Errorf("ParseProvisionerBlocks() block[%d].PType = %q, want %q", i, blocks[i].PType, wantType)
}
}
})
}
}
func TestParseProvisionerBlocksWithPauseBefore(t *testing.T) {
blockContent := `
provisioner "shell" {
pause_before = "30s"
inline = ["echo 'test'"]
}
`
blocks, diags := ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
if len(blocks) != 1 {
t.Fatalf("Expected 1 block, got %d", len(blocks))
}
// pause_before should be parsed as 30 seconds
if blocks[0].PauseBefore.Seconds() != 30 {
t.Errorf("Expected PauseBefore=30s, got %v", blocks[0].PauseBefore)
}
}
func TestParseProvisionerBlocksWithMaxRetries(t *testing.T) {
blockContent := `
provisioner "shell" {
max_retries = 5
inline = ["echo 'test'"]
}
`
blocks, diags := ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
if len(blocks) != 1 {
t.Fatalf("Expected 1 block, got %d", len(blocks))
}
if blocks[0].MaxRetries != 5 {
t.Errorf("Expected MaxRetries=5, got %d", blocks[0].MaxRetries)
}
}
func TestParseProvisionerBlocksWithOnlyExcept(t *testing.T) {
blockContent := `
provisioner "shell" {
only = ["amazon-ebs.ubuntu", "azure-arm.windows"]
inline = ["echo 'test'"]
}
`
blocks, diags := ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
if len(blocks) != 1 {
t.Fatalf("Expected 1 block, got %d", len(blocks))
}
// Check only filter
if len(blocks[0].OnlyExcept.Only) != 2 {
t.Errorf("Expected 2 only values, got %d", len(blocks[0].OnlyExcept.Only))
}
// Skip should return true for sources not in the only list
if !blocks[0].OnlyExcept.Skip("null.test") {
t.Error("Skip() should return true for source not in only list")
}
// Skip should return false for sources in the only list
if blocks[0].OnlyExcept.Skip("amazon-ebs.ubuntu") {
t.Error("Skip() should return false for source in only list")
}
}
func TestParseProvisionerBlocksJSONWithOptions(t *testing.T) {
blockContent := `{
"provisioner": [
{
"shell": {
"pause_before": "15s",
"max_retries": 2,
"only": ["docker.ubuntu"],
"inline": ["echo 'json test'"]
}
}
]
}`
blocks, diags := ParseProvisionerBlocks(blockContent)
if diags.HasErrors() {
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
}
if len(blocks) != 1 {
t.Fatalf("Expected 1 block, got %d", len(blocks))
}
if blocks[0].PauseBefore.Seconds() != 15 {
t.Errorf("Expected PauseBefore=15s, got %v", blocks[0].PauseBefore)
}
if blocks[0].MaxRetries != 2 {
t.Errorf("Expected MaxRetries=2, got %d", blocks[0].MaxRetries)
}
if blocks[0].OnlyExcept.Skip("docker.ubuntu") {
t.Error("Skip() should return false for source in only list")
}
if !blocks[0].OnlyExcept.Skip("null.test") {
t.Error("Skip() should return true for source not in only list")
}
}

@ -185,16 +185,7 @@ func (cfg *PackerConfig) startProvisioner(source SourceUseBlock, pb *Provisioner
builderVars["packer_debug"] = strconv.FormatBool(cfg.debug)
builderVars["packer_force"] = strconv.FormatBool(cfg.force)
builderVars["packer_on_error"] = cfg.onError
sensitiveVars := make([]string, 0, len(cfg.InputVariables))
for key, variable := range cfg.InputVariables {
if variable.Sensitive {
sensitiveVars = append(sensitiveVars, key)
}
}
builderVars["packer_sensitive_variables"] = sensitiveVars
builderVars["packer_sensitive_variables"] = cfg.sensitiveInputVariableKeys()
hclProvisioner := &HCL2Provisioner{
Provisioner: provisioner,

@ -36,7 +36,9 @@ func (p *HCL2Provisioner) HCL2Prepare(buildVars map[string]interface{}) error {
ectx = p.evalContext.NewChild()
buildValues := map[string]cty.Value{}
if !p.evalContext.Variables[buildAccessor].IsNull() {
buildValues = p.evalContext.Variables[buildAccessor].AsValueMap()
for k, v := range p.evalContext.Variables[buildAccessor].AsValueMap() {
buildValues[k] = v
}
}
for k, v := range buildVars {
val, err := ConvertPluginConfigValueToHCLValue(v)

@ -827,14 +827,8 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]*packer.Core
pcb.Provisioners = provisioners
pcb.PostProcessors = pps
pcb.Prepared = true
pcb.SensitiveVars = make([]string, 0, len(cfg.InputVariables))
for key, variable := range cfg.InputVariables {
if variable.Sensitive {
pcb.SensitiveVars = append(pcb.SensitiveVars, key)
}
}
pcb.SetGeneratedVars(generatedVars)
pcb.SensitiveVars = cfg.sensitiveInputVariableKeys()
// Prepare just sets the "prepareCalled" flag on CoreBuild, since
// we did all the prep here.
@ -926,6 +920,18 @@ func (p *PackerConfig) printVariables() string {
return out.String()
}
func (cfg *PackerConfig) sensitiveInputVariableKeys() []string {
sensitiveVars := make([]string, 0, len(cfg.InputVariables))
for key, variable := range cfg.InputVariables {
if variable.Sensitive {
sensitiveVars = append(sensitiveVars, key)
}
}
return sensitiveVars
}
func (p *PackerConfig) printBuilds() string {
out := &strings.Builder{}
out.WriteString("> builds:\n")

@ -25,6 +25,9 @@ type MockPackerClientService struct {
UpdateChannelCalled bool
TrackCalledServiceMethods bool
// Enforced block tracking
GetEnforcedBlocksByBucketCalled bool
// Mock Creates
CreateBucketResp *hcpPackerModels.HashicorpCloudPacker20230101CreateBucketResponse
CreateVersionResp *hcpPackerModels.HashicorpCloudPacker20230101CreateVersionResponse
@ -33,6 +36,10 @@ type MockPackerClientService struct {
// Mock Gets
GetVersionResp *hcpPackerModels.HashicorpCloudPacker20230101GetVersionResponse
// Mock enforced blocks
GetEnforcedBlocksByBucketResp *hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse
GetEnforcedBlocksByBucketErr error
ExistingBuilds []string
ExistingBuildLabels map[string]string
@ -321,3 +328,28 @@ func (svc *MockPackerClientService) PackerServiceUpdateChannel(
return ok, nil
}
func (svc *MockPackerClientService) PackerServiceGetEnforcedBlocksByBucket(
params *hcpPackerService.PackerServiceGetEnforcedBlocksByBucketParams, _ runtime.ClientAuthInfoWriter,
opts ...hcpPackerService.ClientOption,
) (*hcpPackerService.PackerServiceGetEnforcedBlocksByBucketOK, error) {
if svc.TrackCalledServiceMethods {
svc.GetEnforcedBlocksByBucketCalled = true
}
if svc.GetEnforcedBlocksByBucketErr != nil {
return nil, svc.GetEnforcedBlocksByBucketErr
}
ok := &hcpPackerService.PackerServiceGetEnforcedBlocksByBucketOK{}
if svc.GetEnforcedBlocksByBucketResp != nil {
ok.Payload = svc.GetEnforcedBlocksByBucketResp
} else {
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse{
EnforcedBlockDetail: []*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockDetail{},
}
}
return ok, nil
}

@ -0,0 +1,33 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package api
import (
"context"
hcpPackerService "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/client/packer_service"
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
)
// GetEnforcedBlocksForBucket fetches all enforced blocks linked to a bucket.
// This is the key method used during packer build to auto-inject provisioners.
// The response includes EnforcedBlockDetail entries each with an active version
// containing the raw HCL block_content to be parsed and injected.
func (c *Client) GetEnforcedBlocksForBucket(
ctx context.Context,
bucketName string,
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse, error) {
params := hcpPackerService.NewPackerServiceGetEnforcedBlocksByBucketParamsWithContext(ctx)
params.LocationOrganizationID = c.OrganizationID
params.LocationProjectID = c.ProjectID
params.BucketName = bucketName
resp, err := c.Packer.PackerServiceGetEnforcedBlocksByBucket(params, nil)
if err != nil {
return nil, err
}
return resp.Payload, nil
}

@ -91,6 +91,67 @@ func (h *HCLRegistry) VersionStatusSummary() {
h.bucket.Version.statusSummary(h.ui)
}
// FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer
func (h *HCLRegistry) FetchEnforcedBlocks(ctx context.Context) error {
return h.bucket.FetchEnforcedBlocks(ctx)
}
// InjectEnforcedProvisioners injects enforced provisioners into the builds
func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
enforcedBlocks := h.bucket.EnforcedBlocks
if len(enforcedBlocks) == 0 {
return nil
}
var allDiags hcl.Diagnostics
// Parse all enforced blocks into provisioner blocks
for _, eb := range enforcedBlocks {
if eb.BlockContent == "" {
continue
}
provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent)
if diags.HasErrors() {
allDiags = append(allDiags, &hcl.Diagnostic{
Severity: hcl.DiagError,
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))
}
// Inject into each build
for _, build := range builds {
for _, pb := range provBlocks {
// Check if this provisioner should be skipped for this build
if pb.OnlyExcept.Skip(build.Type) {
log.Printf("[DEBUG] skipping enforced provisioner %q for build %q due to only/except rules",
pb.PType, build.Name())
continue
}
coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromBlock(pb, build.Type)
if moreDiags.HasErrors() {
allDiags = append(allDiags, moreDiags...)
continue
}
build.Provisioners = append(build.Provisioners, coreProv)
log.Printf("[INFO] injected enforced provisioner %q from block %q into build %q",
pb.PType, eb.Name, build.Name())
}
}
}
return allDiags
}
func NewHCLRegistry(config *hcl2template.PackerConfig, ui sdkpacker.Ui) (*HCLRegistry, hcl.Diagnostics) {
var diags hcl.Diagnostics
if len(config.Builds) > 1 {

@ -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/hcl2template"
"github.com/hashicorp/packer/packer"
)
@ -113,3 +114,85 @@ func (h *JSONRegistry) VersionStatusSummary() {
func (h *JSONRegistry) Metadata() Metadata {
return h.metadata
}
// FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer
func (h *JSONRegistry) FetchEnforcedBlocks(ctx context.Context) error {
return h.bucket.FetchEnforcedBlocks(ctx)
}
// InjectEnforcedProvisioners injects enforced provisioners into the builds
func (h *JSONRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
enforcedBlocks := h.bucket.EnforcedBlocks
if len(enforcedBlocks) == 0 {
return nil
}
var allDiags hcl.Diagnostics
for _, eb := range enforcedBlocks {
if eb.BlockContent == "" {
continue
}
provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent)
if diags.HasErrors() {
allDiags = append(allDiags, &hcl.Diagnostic{
Severity: hcl.DiagError,
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
injectedProvisioners := 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)
injectedProvisioners = append(injectedProvisioners, coreProv)
log.Printf("[INFO] injected enforced provisioner %q from block %q into legacy JSON build %q",
pb.PType, eb.Name, build.Name())
}
if len(injectedProvisioners) == 0 {
continue
}
if err := build.PrepareProvisioners(injectedProvisioners...); 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 allDiags
}

@ -0,0 +1,122 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package registry
import (
"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()
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)
bucket := NewBucketWithVersion()
bucket.Name = "test-bucket"
registry := &JSONRegistry{
configuration: core,
bucket: bucket,
ui: packer.TestUi(t),
metadata: &MetadataStore{},
}
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"])
}
}

@ -6,6 +6,7 @@ package registry
import (
"context"
"github.com/hashicorp/hcl/v2"
sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/packer"
)
@ -35,3 +36,11 @@ func (r nullRegistry) VersionStatusSummary() {}
func (r nullRegistry) Metadata() Metadata {
return NilMetadata{}
}
func (r nullRegistry) FetchEnforcedBlocks(ctx context.Context) error {
return nil
}
func (r nullRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
return nil
}

@ -20,6 +20,10 @@ type Registry interface {
CompleteBuild(ctx context.Context, build *packer.CoreBuild, artifacts []sdkpacker.Artifact, buildErr error) ([]sdkpacker.Artifact, error)
VersionStatusSummary()
Metadata() Metadata
// FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer
FetchEnforcedBlocks(ctx context.Context) error
// InjectEnforcedProvisioners injects enforced provisioners into the builds
InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics
}
// New instantiates the appropriate registry for the Packer configuration template type.

@ -29,6 +29,16 @@ import (
// build is still alive.
const HeartbeatPeriod = 2 * time.Minute
// EnforcedBlock represents an enforced provisioner block from HCP Packer
type EnforcedBlock struct {
ID string
Name string
BlockContent string // Raw HCL content containing provisioner blocks
VersionID string
Version string
TemplateType string
}
// Bucket represents a single bucket on the HCP Packer registry.
type Bucket struct {
Name string
@ -40,6 +50,7 @@ type Bucket struct {
SourceExternalIdentifierToParentVersions map[string]ParentVersion
RunningBuilds map[string]chan struct{}
Version *Version
EnforcedBlocks []*EnforcedBlock
client *hcpPackerAPI.Client
}
@ -142,6 +153,63 @@ func (bucket *Bucket) Initialize(
return bucket.initializeVersion(ctx, templateType)
}
// FetchEnforcedBlocks retrieves all enforced blocks linked to this bucket from HCP Packer.
// These blocks contain provisioner configurations that should be automatically injected
// into builds for this bucket.
func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error {
if bucket.client == nil {
return errors.New("bucket client not initialized, call Initialize first")
}
log.Printf("[INFO] fetching enforced blocks linked to bucket %q", bucket.Name)
resp, err := bucket.client.GetEnforcedBlocksForBucket(ctx, bucket.Name)
if err != nil {
if hcpPackerAPI.CheckErrorCode(err, codes.NotFound) || hcpPackerAPI.CheckErrorCode(err, codes.Unimplemented) {
// If the API doesn't support enforced blocks yet or returns not found, continue silently.
log.Printf("[DEBUG] fetching enforced blocks for bucket %q: %v", bucket.Name, err)
return nil
}
return fmt.Errorf("failed fetching enforced blocks for bucket %q: %w", bucket.Name, err)
}
if resp == nil {
log.Printf("[INFO] no enforced blocks response returned for bucket %q", bucket.Name)
return nil
}
bucket.EnforcedBlocks = make([]*EnforcedBlock, 0, len(resp.EnforcedBlockDetail))
for _, detail := range resp.EnforcedBlockDetail {
if detail == nil || detail.Version == nil {
continue
}
block := &EnforcedBlock{
ID: detail.ID,
Name: detail.Name,
BlockContent: detail.Version.BlockContent,
VersionID: detail.Version.ID,
Version: detail.Version.Version,
}
if detail.Version.TemplateType != nil {
block.TemplateType = string(*detail.Version.TemplateType)
}
bucket.EnforcedBlocks = append(bucket.EnforcedBlocks, block)
log.Printf("[INFO] linked enforced block found for bucket %q: name=%q id=%q version=%q",
bucket.Name, block.Name, block.ID, block.Version)
}
if len(bucket.EnforcedBlocks) == 0 {
log.Printf("[INFO] no enforced provisioner blocks linked to bucket %q", bucket.Name)
}
log.Printf("[INFO] fetched %d enforced block(s) linked to bucket %q", len(bucket.EnforcedBlocks), bucket.Name)
return nil
}
func (bucket *Bucket) RegisterBuildForComponent(sourceName string) {
if bucket == nil {
return

@ -0,0 +1,117 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package registry
import (
"context"
"errors"
"fmt"
"testing"
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
hcpPackerAPI "github.com/hashicorp/packer/internal/hcp/api"
"google.golang.org/grpc/codes"
)
func TestBucket_FetchEnforcedBlocks_ReturnsAllBlocks(t *testing.T) {
hcl2Type := hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeHCL2
jsonType := hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeJSON
mockService := hcpPackerAPI.NewMockPackerClientService()
mockService.GetEnforcedBlocksByBucketResp = &hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse{
EnforcedBlockDetail: []*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockDetail{
{
ID: "hcl-id",
Name: "hcl-block",
Version: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
ID: "hcl-v1",
Version: "1",
BlockContent: "provisioner \"shell\" {}",
TemplateType: &hcl2Type,
},
},
{
ID: "json-id",
Name: "json-block",
Version: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
ID: "json-v1",
Version: "1",
BlockContent: "{\"provisioner\":[{\"shell\":{}}]}",
TemplateType: &jsonType,
},
},
{
ID: "unset-id",
Name: "unset-block",
Version: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
ID: "unset-v1",
Version: "1",
BlockContent: "provisioner \"shell\" {}",
},
},
},
}
bucket := &Bucket{
Name: "test-bucket",
client: &hcpPackerAPI.Client{
Packer: mockService,
},
}
err := bucket.FetchEnforcedBlocks(context.Background())
if err != nil {
t.Fatalf("FetchEnforcedBlocks() unexpected error: %v", err)
}
if len(bucket.EnforcedBlocks) != 3 {
t.Fatalf("FetchEnforcedBlocks() got %d blocks, want 3", len(bucket.EnforcedBlocks))
}
if bucket.EnforcedBlocks[0].Name != "hcl-block" {
t.Fatalf("first block name = %q, want %q", bucket.EnforcedBlocks[0].Name, "hcl-block")
}
if bucket.EnforcedBlocks[1].Name != "json-block" {
t.Fatalf("second block name = %q, want %q", bucket.EnforcedBlocks[1].Name, "json-block")
}
if bucket.EnforcedBlocks[2].Name != "unset-block" {
t.Fatalf("third block name = %q, want %q", bucket.EnforcedBlocks[2].Name, "unset-block")
}
}
func TestBucket_FetchEnforcedBlocks_ReturnsErrorOnServiceFailure(t *testing.T) {
mockService := hcpPackerAPI.NewMockPackerClientService()
mockService.GetEnforcedBlocksByBucketErr = errors.New("service unavailable")
bucket := &Bucket{
Name: "test-bucket",
client: &hcpPackerAPI.Client{
Packer: mockService,
},
}
err := bucket.FetchEnforcedBlocks(context.Background())
if err == nil {
t.Fatal("FetchEnforcedBlocks() expected error, got nil")
}
}
func TestBucket_FetchEnforcedBlocks_NotFoundIsNonFatal(t *testing.T) {
mockService := hcpPackerAPI.NewMockPackerClientService()
mockService.GetEnforcedBlocksByBucketErr = fmt.Errorf("Code:%d %s", codes.NotFound, codes.NotFound.String())
bucket := &Bucket{
Name: "test-bucket",
client: &hcpPackerAPI.Client{
Packer: mockService,
},
}
err := bucket.FetchEnforcedBlocks(context.Background())
if err != nil {
t.Fatalf("FetchEnforcedBlocks() expected nil error for NotFound, got: %v", err)
}
}

@ -52,6 +52,7 @@ type CoreBuild struct {
onError string
l sync.Mutex
prepareCalled bool
generatedVars []string
SBOMs []SBOM
}
@ -145,6 +146,63 @@ 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)+2)
copy(configs, coreProv.config)
configs = append(configs, packerConfig, 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
}
// SetGeneratedVars stores the builder-generated variables from the initial
// builder preparation so late-injected provisioners can reuse them without
// invoking Builder.Prepare again.
func (b *CoreBuild) SetGeneratedVars(generatedVars []string) {
b.generatedVars = append([]string(nil), generatedVars...)
}
// PrepareProvisioners prepares provisioners injected after the build itself has already been prepared.
func (b *CoreBuild) PrepareProvisioners(provisioners ...CoreBuildProvisioner) error {
if !b.prepareCalled {
return fmt.Errorf("Prepare must be called first")
}
packerConfig := b.packerConfig()
return b.prepareProvisioners(provisioners, packerConfig, b.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 +225,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)
@ -185,35 +233,19 @@ func (b *CoreBuild) Prepare() (warn []string, err error) {
log.Printf("Build '%s' prepare failure: %s\n", b.Type, err)
return
}
b.SetGeneratedVars(generatedVars)
// 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 != "" {
configs := make([]interface{}, len(b.CleanupProvisioner.config), len(b.CleanupProvisioner.config)+1)
configs := make([]interface{}, len(b.CleanupProvisioner.config), len(b.CleanupProvisioner.config)+2)
copy(configs, b.CleanupProvisioner.config)
configs = append(configs, packerConfig)
configs = append(configs, generatedPlaceholderMap)
configs = append(configs, packerConfig, generatedPlaceholderMap)
err = b.CleanupProvisioner.Provisioner.Prepare(configs...)
if err != nil {
return

@ -230,6 +230,92 @@ func TestBuildPrepare_ProvisionerGetsGeneratedMap(t *testing.T) {
}
}
func TestBuild_PrepareProvisioners_ReusesStoredGeneratedVars(t *testing.T) {
packerConfig := testDefaultPackerConfig()
build := testBuild()
builder := build.Builder.(*packersdk.MockBuilder)
builder.GeneratedVars = []string{"PartyVar"}
if _, err := build.Prepare(); err != nil {
t.Fatalf("bad error: %s", err)
}
builder.PrepareCalled = false
lateProv := CoreBuildProvisioner{
PType: "mock-provisioner",
Provisioner: &packersdk.MockProvisioner{},
config: []interface{}{84},
}
if err := build.PrepareProvisioners(lateProv); err != nil {
t.Fatalf("bad error: %s", err)
}
if builder.PrepareCalled {
t.Fatal("builder prepare should not be called again")
}
prov := lateProv.Provisioner.(*packersdk.MockProvisioner)
generated := BasicPlaceholderData()
generated["PartyVar"] = "Build_PartyVar. " + packerbuilderdata.PlaceholderMsg
if !reflect.DeepEqual(prov.PrepConfigs, []interface{}{84, packerConfig, generated}) {
t.Fatalf("bad: %#v", prov.PrepConfigs)
}
}
func TestBuild_PrepareProvisioners_ReusesStoredGeneratedVarsForPreparedBuild(t *testing.T) {
packerConfig := testDefaultPackerConfig()
build := testBuild()
builder := build.Builder.(*packersdk.MockBuilder)
build.Prepared = true
build.SetGeneratedVars([]string{"PartyVar"})
if _, err := build.Prepare(); err != nil {
t.Fatalf("bad error: %s", err)
}
lateProv := CoreBuildProvisioner{
PType: "mock-provisioner",
Provisioner: &packersdk.MockProvisioner{},
config: []interface{}{84},
}
if err := build.PrepareProvisioners(lateProv); err != nil {
t.Fatalf("bad error: %s", err)
}
if builder.PrepareCalled {
t.Fatal("builder prepare should not be called for prepared builds")
}
prov := lateProv.Provisioner.(*packersdk.MockProvisioner)
generated := BasicPlaceholderData()
generated["PartyVar"] = "Build_PartyVar. " + packerbuilderdata.PlaceholderMsg
if !reflect.DeepEqual(prov.PrepConfigs, []interface{}{84, packerConfig, generated}) {
t.Fatalf("bad: %#v", prov.PrepConfigs)
}
}
func TestBuild_PrepareProvisioners_RequiresPrepare(t *testing.T) {
build := testBuild()
lateProv := CoreBuildProvisioner{
PType: "mock-provisioner",
Provisioner: &packersdk.MockProvisioner{},
config: []interface{}{84},
}
err := build.PrepareProvisioners(lateProv)
if err == nil {
t.Fatal("expected error")
}
if err.Error() != "Prepare must be called first" {
t.Fatalf("bad error: %s", err)
}
}
func TestBuild_Run(t *testing.T) {
ui := testUi()

@ -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) {

@ -139,6 +139,43 @@ func (h *ProvisionHook) Run(ctx context.Context, name string, ui packersdk.Ui, c
return nil
}
// ProvisionerWrapOptions contains options for wrapping a provisioner with
// additional behavior like pausing, timeouts, and retries.
type ProvisionerWrapOptions struct {
PauseBefore time.Duration
Timeout time.Duration
MaxRetries int
}
// WrapProvisionerWithOptions wraps a provisioner with additional behavior
// based on the provided options.
func WrapProvisionerWithOptions(provisioner packersdk.Provisioner, opts ProvisionerWrapOptions) packersdk.Provisioner {
wrapped := provisioner
if opts.PauseBefore != 0 {
wrapped = &PausedProvisioner{
PauseBefore: opts.PauseBefore,
Provisioner: wrapped,
}
}
if opts.Timeout != 0 {
wrapped = &TimeoutProvisioner{
Timeout: opts.Timeout,
Provisioner: wrapped,
}
}
if opts.MaxRetries != 0 {
wrapped = &RetriedProvisioner{
MaxRetries: opts.MaxRetries,
Provisioner: wrapped,
}
}
return wrapped
}
// PausedProvisioner is a Provisioner implementation that pauses before
// the provisioner is actually run.
type PausedProvisioner struct {

Loading…
Cancel
Save