added the parser for the enforced block

feature/enforcedProvisioner
Hari Om 2 months ago committed by Hari Om
parent 33740b7c61
commit 91cae751a6

@ -150,6 +150,20 @@ 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 {
c.Ui.Error(fmt.Sprintf("Warning: failed to fetch enforced provisioners: %s", err))
}
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 +470,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)

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

@ -56,7 +56,7 @@ require (
require (
github.com/CycloneDX/cyclonedx-go v0.10.0
github.com/anchore/syft v1.42.3
github.com/anchore/syft v1.42.4
github.com/go-openapi/strfmt v0.23.0
github.com/oklog/ulid v1.3.1
github.com/pierrec/lz4/v4 v4.1.22
@ -373,11 +373,11 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/sdk v1.41.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
@ -398,4 +398,7 @@ require (
go 1.25.8
replace github.com/zclconf/go-cty => github.com/nywilken/go-cty v1.13.3 // added by packer-sdc fix as noted in github.com/hashicorp/packer-plugin-sdk/issues/187
replace github.com/zclconf/go-cty => github.com/nywilken/go-cty v1.13.3 // added by packer-sdc fix as noted in github.com/hashicorp/issues/187
// The internal Go SDK has the enforced block types not yet available in the public SDK.
replace github.com/hashicorp/hcp-sdk-go => github.com/hashicorp/hcp-sdk-go-internal v0.0.0-20260304114239-45aa9349dd39

@ -152,8 +152,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiE
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
github.com/anchore/stereoscope v0.1.22 h1:L807G/kk0WZzOCGuRGF7knxMKzwW2PGdbPVRystryd8=
github.com/anchore/stereoscope v0.1.22/go.mod h1:FikPtAb/WnbqwgLHAvQA9O+fWez0K4pbjxzghz++iy4=
github.com/anchore/syft v1.42.3 h1:eIeeGyqfXm/C8wpBWU50xFbOjdL37VbLatMj9nEJ6n4=
github.com/anchore/syft v1.42.3/go.mod h1:i2PZ+276IdPcnd/n32aeIv849iO/QqdjRknbIc39yL0=
github.com/anchore/syft v1.42.4 h1:+atKDW8sNzGF4E1cG1fcWV4WO7TrAGJmfckxDuY5fT4=
github.com/anchore/syft v1.42.4/go.mod h1:KZqrhA8moG2hXMXs1eE0w2FPp54hmTmDOAhsWr6nw2Q=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
@ -441,8 +441,8 @@ github.com/github/go-spdx/v2 v2.4.0 h1:+4IwVwJJbm3rzvrQ6P1nI9BDMcy3la4RchRy5uehV
github.com/github/go-spdx/v2 v2.4.0/go.mod h1:/5rwgS0txhGtRdUZwc02bTglzg6HK3FfuEbECKlK2Sg=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-snaps v0.5.20 h1:FGKonEeQPJ12t7RQj6cTPa881fl5c8HYarMLv5vP7sg=
github.com/gkampitakis/go-snaps v0.5.20/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs=
github.com/gkampitakis/go-snaps v0.5.21 h1:SvhSFeZviQXwlT+dnGyAIATVehkhqRVW6qfQZhCZH+Y=
github.com/gkampitakis/go-snaps v0.5.21/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@ -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-internal v0.0.0-20260304114239-45aa9349dd39 h1:HtR5UFigB5Kj5KO0OiMbnsjimqyGlvZXOCsSFQQPgvc=
github.com/hashicorp/hcp-sdk-go-internal v0.0.0-20260304114239-45aa9349dd39/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=
@ -1231,18 +1231,18 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
@ -1760,14 +1760,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/sqlite v1.46.2 h1:gkXQ6R0+AjxFC/fTDaeIVLbNLNrRoOK7YYVz5BKhTcE=
modernc.org/sqlite v1.46.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

@ -0,0 +1,111 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package hcl2template
import (
"fmt"
"strconv"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"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 partial HCL string that contains only
// top-level provisioner blocks and returns the parsed ProvisionerBlock list.
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
parser := &Parser{Parser: hclparse.NewParser()}
file, diags := parser.ParseHCL([]byte(blockContent), "enforced_provisioner.pkr.hcl")
if diags.HasErrors() {
return nil, diags
}
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
}
// 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) (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": []string{},
}
// Create evaluation context
ectx := cfg.EvalContext(BuildContext, nil)
// Create the HCL2Provisioner wrapper
hclProvisioner := &HCL2Provisioner{
Provisioner: provisioner,
provisionerBlock: pb,
evalContext: ectx,
builderVariables: builderVars,
}
// 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,212 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package hcl2template
import (
"testing"
)
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: "empty block content",
blockContent: "",
wantCount: 0,
wantTypes: nil,
wantErr: false,
},
{
name: "invalid HCL syntax",
blockContent: "this is not valid { hcl }}}",
wantCount: 0,
wantTypes: nil,
wantErr: true,
},
}
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")
}
}

@ -6,9 +6,11 @@ package hcl2template
import (
"fmt"
"regexp"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
"github.com/hashicorp/hcl/v2/hclwrite"
)
type HCPPackerRegistryBlock struct {
@ -97,3 +99,43 @@ func (p *Parser) decodeHCPRegistry(block *hcl.Block, cfg *PackerConfig) (*HCPPac
return par, diags
}
// ExtractBuildProvisionerHCL extracts all provisioner blocks from the build
// blocks in the configuration and returns them as raw HCL content.
// This is used to publish provisioner configurations as enforced blocks
// to HCP Packer, so that other builds against the same bucket will
// automatically have these provisioners injected.
func (cfg *PackerConfig) ExtractBuildProvisionerHCL() (string, error) {
sourceFiles := cfg.parser.Files()
var buf strings.Builder
for filename, file := range sourceFiles {
// hclwrite only supports HCL native syntax, skip JSON and variable files
if !strings.HasSuffix(filename, hcl2FileExt) {
continue
}
wf, diags := hclwrite.ParseConfig(file.Bytes, filename, hcl.Pos{Line: 1, Column: 1})
if diags.HasErrors() {
continue
}
for _, block := range wf.Body().Blocks() {
if block.Type() != buildLabel {
continue
}
for _, inner := range block.Body().Blocks() {
if inner.Type() != buildProvisionerLabel {
continue
}
buf.Write(inner.BuildTokens(nil).Bytes())
buf.WriteString("\n")
}
}
}
return strings.TrimSpace(buf.String()), nil
}

@ -25,6 +25,11 @@ type MockPackerClientService struct {
UpdateChannelCalled bool
TrackCalledServiceMethods bool
// Enforced block tracking
CreateEnforcedBlockCalled, GetEnforcedBlockCalled, ListEnforcedBlocksCalled bool
CreateEnforcedBlockVersionCalled, GetEnforcedBlockVersionsCalled bool
GetEnforcedBlocksByBucketCalled bool
// Mock Creates
CreateBucketResp *hcpPackerModels.HashicorpCloudPacker20230101CreateBucketResponse
CreateVersionResp *hcpPackerModels.HashicorpCloudPacker20230101CreateVersionResponse
@ -33,6 +38,20 @@ type MockPackerClientService struct {
// Mock Gets
GetVersionResp *hcpPackerModels.HashicorpCloudPacker20230101GetVersionResponse
// Mock enforced blocks
CreateEnforcedBlockResp *hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockResponse
CreateEnforcedBlockErr error
GetEnforcedBlockResp *hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockResponse
GetEnforcedBlockErr error
ListEnforcedBlocksResp *hcpPackerModels.HashicorpCloudPacker20230101ListEnforcedBlocksResponse
ListEnforcedBlocksErr error
CreateEnforcedBlockVersionResp *hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockVersionResponse
CreateEnforcedBlockVersionErr error
GetEnforcedBlockVersionsResp *hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockVersionsResponse
GetEnforcedBlockVersionsErr error
GetEnforcedBlocksByBucketResp *hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse
GetEnforcedBlocksByBucketErr error
ExistingBuilds []string
ExistingBuildLabels map[string]string
@ -321,3 +340,163 @@ func (svc *MockPackerClientService) PackerServiceUpdateChannel(
return ok, nil
}
func (svc *MockPackerClientService) PackerServiceCreateEnforcedBlock(
params *hcpPackerService.PackerServiceCreateEnforcedBlockParams, _ runtime.ClientAuthInfoWriter,
opts ...hcpPackerService.ClientOption,
) (*hcpPackerService.PackerServiceCreateEnforcedBlockOK, error) {
if svc.TrackCalledServiceMethods {
svc.CreateEnforcedBlockCalled = true
}
if svc.CreateEnforcedBlockErr != nil {
return nil, svc.CreateEnforcedBlockErr
}
ok := &hcpPackerService.PackerServiceCreateEnforcedBlockOK{}
if svc.CreateEnforcedBlockResp != nil {
ok.Payload = svc.CreateEnforcedBlockResp
} else {
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockResponse{
EnforcedBlock: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlock{
ID: "enforced-block-id",
Name: params.Body.Name,
},
}
}
return ok, nil
}
func (svc *MockPackerClientService) PackerServiceGetEnforcedBlock(
params *hcpPackerService.PackerServiceGetEnforcedBlockParams, _ runtime.ClientAuthInfoWriter,
opts ...hcpPackerService.ClientOption,
) (*hcpPackerService.PackerServiceGetEnforcedBlockOK, error) {
if svc.TrackCalledServiceMethods {
svc.GetEnforcedBlockCalled = true
}
if svc.GetEnforcedBlockErr != nil {
return nil, svc.GetEnforcedBlockErr
}
ok := &hcpPackerService.PackerServiceGetEnforcedBlockOK{}
if svc.GetEnforcedBlockResp != nil {
ok.Payload = svc.GetEnforcedBlockResp
} else {
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockResponse{
EnforcedBlock: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlock{
ID: params.EnforcedBlockID,
},
}
}
return ok, nil
}
func (svc *MockPackerClientService) PackerServiceListEnforcedBlocks(
params *hcpPackerService.PackerServiceListEnforcedBlocksParams, _ runtime.ClientAuthInfoWriter,
opts ...hcpPackerService.ClientOption,
) (*hcpPackerService.PackerServiceListEnforcedBlocksOK, error) {
if svc.TrackCalledServiceMethods {
svc.ListEnforcedBlocksCalled = true
}
if svc.ListEnforcedBlocksErr != nil {
return nil, svc.ListEnforcedBlocksErr
}
ok := &hcpPackerService.PackerServiceListEnforcedBlocksOK{}
if svc.ListEnforcedBlocksResp != nil {
ok.Payload = svc.ListEnforcedBlocksResp
} else {
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101ListEnforcedBlocksResponse{
EnforcedBlocks: []*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlock{},
}
}
return ok, nil
}
func (svc *MockPackerClientService) PackerServiceCreateEnforcedBlockVersion(
params *hcpPackerService.PackerServiceCreateEnforcedBlockVersionParams, _ runtime.ClientAuthInfoWriter,
opts ...hcpPackerService.ClientOption,
) (*hcpPackerService.PackerServiceCreateEnforcedBlockVersionOK, error) {
if svc.TrackCalledServiceMethods {
svc.CreateEnforcedBlockVersionCalled = true
}
if svc.CreateEnforcedBlockVersionErr != nil {
return nil, svc.CreateEnforcedBlockVersionErr
}
ok := &hcpPackerService.PackerServiceCreateEnforcedBlockVersionOK{}
if svc.CreateEnforcedBlockVersionResp != nil {
ok.Payload = svc.CreateEnforcedBlockVersionResp
} else {
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockVersionResponse{
EnforcedBlockVersion: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
ID: "enforced-block-version-id",
EnforcedBlockID: params.EnforcedBlockID,
BlockContent: params.Body.BlockContent,
Version: params.Body.Version,
},
}
}
return ok, nil
}
func (svc *MockPackerClientService) PackerServiceGetEnforcedBlockVersions(
params *hcpPackerService.PackerServiceGetEnforcedBlockVersionsParams, _ runtime.ClientAuthInfoWriter,
opts ...hcpPackerService.ClientOption,
) (*hcpPackerService.PackerServiceGetEnforcedBlockVersionsOK, error) {
if svc.TrackCalledServiceMethods {
svc.GetEnforcedBlockVersionsCalled = true
}
if svc.GetEnforcedBlockVersionsErr != nil {
return nil, svc.GetEnforcedBlockVersionsErr
}
ok := &hcpPackerService.PackerServiceGetEnforcedBlockVersionsOK{}
if svc.GetEnforcedBlockVersionsResp != nil {
ok.Payload = svc.GetEnforcedBlockVersionsResp
} else {
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockVersionsResponse{
EnforcedBlockDetail: []*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockDetail{},
}
}
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,151 @@
// 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"
)
// CreateEnforcedBlock creates a new enforced block in the HCP Packer registry.
// The block content contains raw HCL provisioner configuration that will be
// enforced on all builds for buckets linked to this enforced block.
func (c *Client) CreateEnforcedBlock(
ctx context.Context,
name string,
blockContent string,
version string,
templateType hcpPackerModels.HashicorpCloudPacker20230101TemplateType,
description string,
labels map[string]string,
) (*hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockResponse, error) {
params := hcpPackerService.NewPackerServiceCreateEnforcedBlockParamsWithContext(ctx)
params.LocationOrganizationID = c.OrganizationID
params.LocationProjectID = c.ProjectID
params.Body = &hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockBody{
Name: name,
BlockContent: blockContent,
Version: version,
TemplateType: &templateType,
AdditionalDescription: description,
Labels: labels,
}
resp, err := c.Packer.PackerServiceCreateEnforcedBlock(params, nil)
if err != nil {
return nil, err
}
return resp.Payload, nil
}
// GetEnforcedBlock retrieves a single enforced block by its ID.
func (c *Client) GetEnforcedBlock(
ctx context.Context,
enforcedBlockID string,
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockResponse, error) {
params := hcpPackerService.NewPackerServiceGetEnforcedBlockParamsWithContext(ctx)
params.LocationOrganizationID = c.OrganizationID
params.LocationProjectID = c.ProjectID
params.EnforcedBlockID = enforcedBlockID
resp, err := c.Packer.PackerServiceGetEnforcedBlock(params, nil)
if err != nil {
return nil, err
}
return resp.Payload, nil
}
// ListEnforcedBlocks lists all enforced blocks in the current project.
func (c *Client) ListEnforcedBlocks(
ctx context.Context,
) (*hcpPackerModels.HashicorpCloudPacker20230101ListEnforcedBlocksResponse, error) {
params := hcpPackerService.NewPackerServiceListEnforcedBlocksParamsWithContext(ctx)
params.LocationOrganizationID = c.OrganizationID
params.LocationProjectID = c.ProjectID
resp, err := c.Packer.PackerServiceListEnforcedBlocks(params, nil)
if err != nil {
return nil, err
}
return resp.Payload, nil
}
// CreateEnforcedBlockVersion creates a new version of an existing enforced block.
// This allows updating the block content while keeping a version history.
func (c *Client) CreateEnforcedBlockVersion(
ctx context.Context,
enforcedBlockID string,
blockContent string,
version string,
templateType hcpPackerModels.HashicorpCloudPacker20230101TemplateType,
description string,
) (*hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockVersionResponse, error) {
params := hcpPackerService.NewPackerServiceCreateEnforcedBlockVersionParamsWithContext(ctx)
params.LocationOrganizationID = c.OrganizationID
params.LocationProjectID = c.ProjectID
params.EnforcedBlockID = enforcedBlockID
params.Body = &hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockVersionBody{
BlockContent: blockContent,
Version: version,
TemplateType: &templateType,
AdditionalDescription: description,
}
resp, err := c.Packer.PackerServiceCreateEnforcedBlockVersion(params, nil)
if err != nil {
return nil, err
}
return resp.Payload, nil
}
// GetEnforcedBlockVersions retrieves all versions of an enforced block.
func (c *Client) GetEnforcedBlockVersions(
ctx context.Context,
enforcedBlockID string,
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockVersionsResponse, error) {
params := hcpPackerService.NewPackerServiceGetEnforcedBlockVersionsParamsWithContext(ctx)
params.LocationOrganizationID = c.OrganizationID
params.LocationProjectID = c.ProjectID
params.EnforcedBlockID = enforcedBlockID
resp, err := c.Packer.PackerServiceGetEnforcedBlockVersions(params, nil)
if err != nil {
return nil, err
}
return resp.Payload, nil
}
// 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
}

@ -43,6 +43,21 @@ func (h *HCLRegistry) PopulateVersion(ctx context.Context) error {
return err
}
// Extract provisioner blocks from the build and publish them as enforced
// blocks to HCP Packer, so other builds against the same bucket will
// automatically have these provisioners injected.
blockContent, err := h.configuration.ExtractBuildProvisionerHCL()
if err != nil {
log.Printf("[WARN] failed to extract provisioner blocks for enforced publishing: %v", err)
} else if blockContent != "" {
blockName := h.bucket.Name + "-provisioners"
if pubErr := h.bucket.PublishEnforcedBlocks(
ctx, blockName, blockContent, hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeHCL2,
); pubErr != nil {
log.Printf("[WARN] failed to publish enforced blocks for bucket %q: %v", h.bucket.Name, pubErr)
}
}
err = h.bucket.populateVersion(ctx)
if err != nil {
return err
@ -91,6 +106,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.DiagWarning,
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", len(provBlocks), eb.Name))
}
// 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)
if moreDiags.HasErrors() {
allDiags = append(allDiags, moreDiags...)
continue
}
log.Printf("[INFO] injecting enforced provisioner %q from block %q into build %q",
pb.PType, eb.Name, build.Name())
build.Provisioners = append(build.Provisioners, coreProv)
}
}
}
return allDiags
}
func NewHCLRegistry(config *hcl2template.PackerConfig, ui sdkpacker.Ui) (*HCLRegistry, hcl.Diagnostics) {
var diags hcl.Diagnostics
if len(config.Builds) > 1 {

@ -113,3 +113,17 @@ 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
// 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 {
h.ui.Say("Warning: Enforced provisioners are not supported for legacy JSON templates")
}
return nil
}

@ -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,15 @@ 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
}
// Bucket represents a single bucket on the HCP Packer registry.
type Bucket struct {
Name string
@ -40,6 +49,7 @@ type Bucket struct {
SourceExternalIdentifierToParentVersions map[string]ParentVersion
RunningBuilds map[string]chan struct{}
Version *Version
EnforcedBlocks []*EnforcedBlock
client *hcpPackerAPI.Client
}
@ -142,6 +152,115 @@ 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")
}
resp, err := bucket.client.GetEnforcedBlocksForBucket(ctx, bucket.Name)
if err != nil {
// 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
}
if resp == nil {
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,
}
bucket.EnforcedBlocks = append(bucket.EnforcedBlocks, block)
}
log.Printf("[INFO] fetched %d enforced block(s) for bucket %q", len(bucket.EnforcedBlocks), bucket.Name)
return nil
}
// PublishEnforcedBlocks publishes the given provisioner block content as an enforced block
// on HCP Packer, linked to this bucket. If an enforced block with the given name already
// exists and the content has changed, a new version is created. If it doesn't exist,
// a new enforced block is created.
func (bucket *Bucket) PublishEnforcedBlocks(
ctx context.Context,
blockName string,
blockContent string,
templateType hcpPackerModels.HashicorpCloudPacker20230101TemplateType,
) error {
if bucket.client == nil {
return errors.New("bucket client not initialized, call Initialize first")
}
if blockContent == "" {
log.Printf("[DEBUG] no provisioner content to publish as enforced blocks for bucket %q", bucket.Name)
return nil
}
// List existing enforced blocks to check for duplicates
existingResp, err := bucket.client.ListEnforcedBlocks(ctx)
if err != nil {
log.Printf("[WARN] failed to list existing enforced blocks: %v", err)
// Continue anyway — create will fail if there's a conflict
}
// Build a map of existing enforced blocks by name for quick lookup
existingByName := make(map[string]*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlock)
if existingResp != nil {
for _, eb := range existingResp.EnforcedBlocks {
if eb != nil && eb.Name != "" {
existingByName[eb.Name] = eb
}
}
}
version := "1"
existing, found := existingByName[blockName]
if found {
// Enforced block already exists — check if content changed
if existing.LatestVersion != nil && existing.LatestVersion.BlockContent == blockContent {
log.Printf("[INFO] enforced block %q already up-to-date, skipping", blockName)
return nil
}
// Content changed — create a new version
log.Printf("[INFO] updating enforced block %q with new version", blockName)
_, err := bucket.client.CreateEnforcedBlockVersion(
ctx, existing.ID, blockContent, version, templateType, "",
)
if err != nil {
return fmt.Errorf("failed to create new version for enforced block %q: %w", blockName, err)
}
log.Printf("[INFO] created new version for enforced block %q", blockName)
} else {
// Create new enforced block
log.Printf("[INFO] creating enforced block %q for bucket %q", blockName, bucket.Name)
_, err := bucket.client.CreateEnforcedBlock(
ctx, blockName, blockContent, version, templateType, "", nil,
)
if err != nil {
return fmt.Errorf("failed to create enforced block %q: %w", blockName, err)
}
log.Printf("[INFO] created enforced block %q", blockName)
}
return nil
}
func (bucket *Bucket) RegisterBuildForComponent(sourceName string) {
if bucket == nil {
return

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