From e9230b0b649ada5a7301eb60c737e2fe5fb0c1c8 Mon Sep 17 00:00:00 2001 From: Hari Om <58305594+Madhav008@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:50:00 +0530 Subject: [PATCH] added the parser for the enforced block --- command/build.go | 15 ++ command/cli.go | 3 + go.mod | 52 +++-- go.sum | 110 +++++---- hcl2template/enforced_provisioner.go | 111 +++++++++ hcl2template/enforced_provisioner_test.go | 212 ++++++++++++++++++ .../types.build.hcp_packer_registry.go | 42 ++++ internal/hcp/api/mock_service.go | 179 +++++++++++++++ .../hcp/api/service_enforced_provisioner.go | 151 +++++++++++++ internal/hcp/registry/hcl.go | 76 +++++++ internal/hcp/registry/json.go | 14 ++ internal/hcp/registry/null_registry.go | 9 + internal/hcp/registry/registry.go | 4 + internal/hcp/registry/types.bucket.go | 119 ++++++++++ packer/provisioner.go | 37 +++ test.pkr.hcl | 56 +++++ 16 files changed, 1123 insertions(+), 67 deletions(-) create mode 100644 hcl2template/enforced_provisioner.go create mode 100644 hcl2template/enforced_provisioner_test.go create mode 100644 internal/hcp/api/service_enforced_provisioner.go create mode 100644 test.pkr.hcl 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 64f0a6691..cd63e7131 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/dsnet/compress v0.0.1 github.com/go-git/go-git/v5 v5.16.5 - github.com/go-openapi/runtime v0.26.2 + github.com/go-openapi/runtime v0.28.0 github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.8.1 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect @@ -39,14 +39,14 @@ require ( github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db // indirect github.com/pkg/sftp v1.13.2 // indirect github.com/posener/complete v1.2.3 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/ulikunitz/xz v0.5.15 github.com/zclconf/go-cty v1.13.3 github.com/zclconf/go-cty-yaml v1.0.1 golang.org/x/crypto v0.46.0 // indirect golang.org/x/mod v0.30.0 golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.27.0 + golang.org/x/oauth2 v0.30.0 golang.org/x/sync v0.19.0 golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect @@ -56,7 +56,7 @@ require ( require ( github.com/CycloneDX/cyclonedx-go v0.9.1 - github.com/go-openapi/strfmt v0.21.10 + github.com/go-openapi/strfmt v0.23.0 github.com/oklog/ulid v1.3.1 github.com/pierrec/lz4/v4 v4.1.18 github.com/shirou/gopsutil/v3 v3.23.4 @@ -104,18 +104,29 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/analysis v0.21.5 // indirect - github.com/go-openapi/errors v0.21.0 // indirect - github.com/go-openapi/jsonpointer v0.20.1 // indirect - github.com/go-openapi/jsonreference v0.20.3 // indirect - github.com/go-openapi/loads v0.21.3 // indirect - github.com/go-openapi/spec v0.20.12 // indirect - github.com/go-openapi/swag v0.22.5 // indirect - github.com/go-openapi/validate v0.22.4 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/go-openapi/validate v0.24.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/hashicorp/consul/api v1.25.1 // indirect @@ -150,7 +161,7 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -182,11 +193,11 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect - go.mongodb.org/mongo-driver v1.13.1 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.17.0 // indirect - go.opentelemetry.io/otel/metric v1.17.0 // indirect - go.opentelemetry.io/otel/trace v1.17.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect @@ -202,4 +213,7 @@ require ( go 1.24.12 -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 8f6258fab..eee37e7ad 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,6 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= -github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -154,26 +152,48 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/analysis v0.21.5 h1:3tHfEBh6Ia8eKc4M7khOGjPOAlWKJ10d877Cr9teujI= -github.com/go-openapi/analysis v0.21.5/go.mod h1:25YcZosX9Lwz2wBsrFrrsL8bmjjXdlyP6zsr2AMy29M= -github.com/go-openapi/errors v0.21.0 h1:FhChC/duCnfoLj1gZ0BgaBmzhJC2SL/sJr8a2vAobSY= -github.com/go-openapi/errors v0.21.0/go.mod h1:jxNTMUxRCKj65yb/okJGEtahVd7uvWnuWfj53bse4ho= -github.com/go-openapi/jsonpointer v0.20.1 h1:MkK4VEIEZMj4wT9PmjaUmGflVBr9nvud4Q4UVFbDoBE= -github.com/go-openapi/jsonpointer v0.20.1/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.3 h1:EjGcjTW8pD1mRis6+w/gmoBdqv5+RbE9B85D1NgDOVQ= -github.com/go-openapi/jsonreference v0.20.3/go.mod h1:FviDZ46i9ivh810gqzFLl5NttD5q3tSlMLqLr6okedM= -github.com/go-openapi/loads v0.21.3 h1:8sSH2FIm/SnbDUGv572md4YqVMFne/a9Eubvcd3anew= -github.com/go-openapi/loads v0.21.3/go.mod h1:Y3aMR24iHbKHppOj91nQ/SHc0cuPbAr4ndY4a02xydc= -github.com/go-openapi/runtime v0.26.2 h1:elWyB9MacRzvIVgAZCBJmqTi7hBzU0hlKD4IvfX0Zl0= -github.com/go-openapi/runtime v0.26.2/go.mod h1:O034jyRZ557uJKzngbMDJXkcKJVzXJiymdSfgejrcRw= -github.com/go-openapi/spec v0.20.12 h1:cgSLbrsmziAP2iais+Vz7kSazwZ8rsUZd6TUzdDgkVI= -github.com/go-openapi/spec v0.20.12/go.mod h1:iSCgnBcwbMW9SfzJb8iYynXvcY6C/QFrI7otzF7xGM4= -github.com/go-openapi/strfmt v0.21.10 h1:JIsly3KXZB/Qf4UzvzJpg4OELH/0ASDQsyk//TTBDDk= -github.com/go-openapi/strfmt v0.21.10/go.mod h1:vNDMwbilnl7xKiO/Ve/8H8Bb2JIInBnH+lqiw6QWgis= -github.com/go-openapi/swag v0.22.5 h1:fVS63IE3M0lsuWRzuom3RLwUMVI2peDH01s6M70ugys= -github.com/go-openapi/swag v0.22.5/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0= -github.com/go-openapi/validate v0.22.4 h1:5v3jmMyIPKTR8Lv9syBAIRxG6lY0RqeBPB1LKEijzk8= -github.com/go-openapi/validate v0.22.4/go.mod h1:qm6O8ZIcPVdSY5219468Jv7kBdGvkiZLPOmqnqTUZ2A= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= +github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= @@ -201,7 +221,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -229,8 +248,8 @@ github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -304,8 +323,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.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= -github.com/hashicorp/hcp-sdk-go v0.136.0 h1:NNtb/dYoj7YrVQVvWZ2T7PY2Pwn8vQ5YKIAgaqaKk6A= -github.com/hashicorp/hcp-sdk-go v0.136.0/go.mod h1:vQ4fzdL1AmhIAbCw+4zmFe5Hbpajj3NvRWkJoVuxmAk= +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.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM= @@ -374,8 +393,8 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3v github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20250927112105-5f8e6c707321 h1:AKIJL2PfBX2uie0Mn5pxtG1+zut3hAVMZbRfoXecFzI= @@ -438,7 +457,6 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= @@ -536,8 +554,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/tidwall/transform v0.0.0-20201103190739-32f242e2dbde h1:AMNpJRc7P+GTwVbl8DkK2I9I8BBUzNiHuH/tlxrpan0= @@ -560,16 +578,12 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -577,18 +591,18 @@ github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY3 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8= github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= -go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= -go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= -go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= -go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= -go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= -go.opentelemetry.io/otel/sdk v1.17.0 h1:FLN2X66Ke/k5Sg3V623Q7h7nt3cHXaW1FOvKKrW0IpE= -go.opentelemetry.io/otel/sdk v1.17.0/go.mod h1:U87sE0f5vQB7hwUoW98pW5Rz4ZDuCFBZFNUBlSgmDFQ= -go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= -go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -639,8 +653,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= -golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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 79124b2b9..43d517806 100644 --- a/packer/provisioner.go +++ b/packer/provisioner.go @@ -140,6 +140,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 { diff --git a/test.pkr.hcl b/test.pkr.hcl new file mode 100644 index 000000000..97ee42d88 --- /dev/null +++ b/test.pkr.hcl @@ -0,0 +1,56 @@ +packer { + required_plugins { + docker = { + version = ">= 1.1.0" + source = "github.com/hashicorp/docker" + } + } +} + +# HCP Packer registry — provisioner blocks below will be +# automatically published as enforced blocks to this bucket. +hcp_packer_registry { + bucket_name = "ubuntu-test" + description = "Test Ubuntu image with enforced provisioners" + + bucket_labels = { + "team" = "platform" + "os" = "ubuntu" + "purpose" = "testing" + } +} + +source "docker" "ubuntu" { + image = "ubuntu:22.04" + commit = true +} + +build { + name = "ubuntu-test" + + sources = ["source.docker.ubuntu"] + + provisioner "shell" { + inline = [ + "apt-get update -y", + "apt-get install -y curl wget jq" + ] + } + + provisioner "shell" { + inline = [ + "echo 'Creating app user...'", + "useradd -m -s /bin/bash appuser", + "mkdir -p /opt/app", + "chown appuser:appuser /opt/app" + ] + } + + provisioner "shell" { + inline = [ + "echo 'Applying security hardening...'", + "echo 'net.ipv4.ip_forward = 0' >> /etc/sysctl.conf", + "echo 'Build complete!'" + ] + } +}