diff --git a/command/build.go b/command/build.go index 3f3af9bf3..c1ffac044 100644 --- a/command/build.go +++ b/command/build.go @@ -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) diff --git a/command/cli.go b/command/cli.go index 655ab434e..643862e54 100644 --- a/command/cli.go +++ b/command/cli.go @@ -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) { diff --git a/go.mod b/go.mod index 4ec11da2e..61ca8b55e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ce2da1024..4a06de75b 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/hcl2template/enforced_provisioner.go b/hcl2template/enforced_provisioner.go new file mode 100644 index 000000000..9602257fa --- /dev/null +++ b/hcl2template/enforced_provisioner.go @@ -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 +} diff --git a/hcl2template/enforced_provisioner_test.go b/hcl2template/enforced_provisioner_test.go new file mode 100644 index 000000000..ed79b7480 --- /dev/null +++ b/hcl2template/enforced_provisioner_test.go @@ -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") + } +} diff --git a/hcl2template/types.build.hcp_packer_registry.go b/hcl2template/types.build.hcp_packer_registry.go index 6229556c6..392aea935 100644 --- a/hcl2template/types.build.hcp_packer_registry.go +++ b/hcl2template/types.build.hcp_packer_registry.go @@ -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 +} diff --git a/internal/hcp/api/mock_service.go b/internal/hcp/api/mock_service.go index 4c75f1ebe..b743e82cb 100644 --- a/internal/hcp/api/mock_service.go +++ b/internal/hcp/api/mock_service.go @@ -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 +} diff --git a/internal/hcp/api/service_enforced_provisioner.go b/internal/hcp/api/service_enforced_provisioner.go new file mode 100644 index 000000000..4d9704a04 --- /dev/null +++ b/internal/hcp/api/service_enforced_provisioner.go @@ -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 +} diff --git a/internal/hcp/registry/hcl.go b/internal/hcp/registry/hcl.go index 170371248..8560090d2 100644 --- a/internal/hcp/registry/hcl.go +++ b/internal/hcp/registry/hcl.go @@ -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 { diff --git a/internal/hcp/registry/json.go b/internal/hcp/registry/json.go index a9f766e0b..bd777358e 100644 --- a/internal/hcp/registry/json.go +++ b/internal/hcp/registry/json.go @@ -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 +} diff --git a/internal/hcp/registry/null_registry.go b/internal/hcp/registry/null_registry.go index 6856dec52..285a1cd58 100644 --- a/internal/hcp/registry/null_registry.go +++ b/internal/hcp/registry/null_registry.go @@ -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 +} diff --git a/internal/hcp/registry/registry.go b/internal/hcp/registry/registry.go index e77ac55d7..43162bd73 100644 --- a/internal/hcp/registry/registry.go +++ b/internal/hcp/registry/registry.go @@ -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. diff --git a/internal/hcp/registry/types.bucket.go b/internal/hcp/registry/types.bucket.go index 26a54bc62..9e0fdb971 100644 --- a/internal/hcp/registry/types.bucket.go +++ b/internal/hcp/registry/types.bucket.go @@ -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 diff --git a/packer/provisioner.go b/packer/provisioner.go index 7f243a880..0c5da2289 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -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 {