mirror of
https://github.com/hashicorp/packer.git
synced 2026-06-14 19:20:04 -04:00
Feature/enforced provisioner (#13591)
Some checks are pending
build / get-go-version (push) Waiting to run
build / set-product-version (push) Waiting to run
build / generate-metadata-file (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} solaris 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} windows 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} solaris amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} windows amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux arm64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux ppc64le build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} darwin amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} darwin arm64 build (push) Blocked by required conditions
build / Docker light 386 build (push) Blocked by required conditions
build / Docker light amd64 build (push) Blocked by required conditions
build / Docker light arm build (push) Blocked by required conditions
build / Docker light arm64 build (push) Blocked by required conditions
build / Docker full 386 build (push) Blocked by required conditions
build / Docker full amd64 build (push) Blocked by required conditions
build / Docker full arm build (push) Blocked by required conditions
build / Docker full arm64 build (push) Blocked by required conditions
Go Test / get-go-version (push) Waiting to run
Go Test / Linux go tests (push) Blocked by required conditions
Go Test / Darwin go tests (push) Blocked by required conditions
Go Test / Windows go tests (push) Blocked by required conditions
Go Validate / get-go-version (push) Waiting to run
Go Validate / Go Mod Tidy (push) Blocked by required conditions
Go Validate / Lint (push) Blocked by required conditions
Go Validate / Fmt check (push) Blocked by required conditions
Go Validate / Generate check (push) Blocked by required conditions
Some checks are pending
build / get-go-version (push) Waiting to run
build / set-product-version (push) Waiting to run
build / generate-metadata-file (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} solaris 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} windows 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} solaris amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} windows amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux 386 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux arm build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux arm64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} linux ppc64le build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} darwin amd64 build (push) Blocked by required conditions
build / Go ${{ needs.get-go-version.outputs.go-version }} darwin arm64 build (push) Blocked by required conditions
build / Docker light 386 build (push) Blocked by required conditions
build / Docker light amd64 build (push) Blocked by required conditions
build / Docker light arm build (push) Blocked by required conditions
build / Docker light arm64 build (push) Blocked by required conditions
build / Docker full 386 build (push) Blocked by required conditions
build / Docker full amd64 build (push) Blocked by required conditions
build / Docker full arm build (push) Blocked by required conditions
build / Docker full arm64 build (push) Blocked by required conditions
Go Test / get-go-version (push) Waiting to run
Go Test / Linux go tests (push) Blocked by required conditions
Go Test / Darwin go tests (push) Blocked by required conditions
Go Test / Windows go tests (push) Blocked by required conditions
Go Validate / get-go-version (push) Waiting to run
Go Validate / Go Mod Tidy (push) Blocked by required conditions
Go Validate / Lint (push) Blocked by required conditions
Go Validate / Fmt check (push) Blocked by required conditions
Go Validate / Generate check (push) Blocked by required conditions
* added the parser for the enforced block * Enhance enforced provisioner parsing to support HCL and JSON formats - Updated ParseProvisionerBlocks to handle both HCL and JSON syntax, including legacy JSON format. - Added comprehensive test cases for JSON provisioner parsing. - Improved ExtractBuildProvisionerHCL to merge inline commands from shell provisioners. - Enhanced logging for enforced block operations in HCP Packer. * Remove PublishEnforcedBlocks function from Bucket struct * Remove ExtractBuildProvisionerHCL function and unused imports * Reverted the version upgrade * Added the internal-sdk for the enforcedProvsioner api changes * Enhance enforced provisioner handling and error reporting - Update error handling in FetchEnforcedBlocks to return detailed errors instead of warnings. - Modify GetCoreBuildProvisionerFromBlock to accept build name for overrides. - Add tests for FetchEnforcedBlocks to ensure correct behavior and error handling. - Implement diagnostics for unsupported legacy JSON templates. * Implement enforced provisioner parsing and handling - Introduced a new package `enforcedparser` to handle parsing of enforced provisioner blocks from HCL and JSON formats. - Refactored existing code to utilize the new `ParseProvisionerBlocks` function from the `enforcedparser` package. - Updated `GetCoreBuildProvisionerFromEnforcedBlock` method to convert enforced provisioner blocks into core build provisioners. - Enhanced error handling and logging during the parsing process. - Added tests for the new parsing functionality and ensured existing tests were updated to reflect changes. - Modified `InjectEnforcedProvisioners` method in JSON registry to utilize the new parsing logic. * Add test case for -skip-enforcement flag in BuildArgs * Refactor sensitive variable handling in provisioners and add related tests * Refactor enforced provisioner handling: remove internal parser, update tests, and streamline API interactions * Enhance provisioner block parsing: add error handling for invalid combinations and expand test coverage * Remove internal SDK replacement for enforced block types in go.mod * Update dependencies in go.mod and go.sum: bump hcp-sdk-go and packer-plugin-sdk versions, adjust syft version, and update OpenTelemetry packages * Update hcp-sdk-go dependency to v0.172.0 in go.mod and go.sum * Fix formatting in TestBuildCommand_ParseArgs and add newline at end of json_enforced_test.go * Refactor testJSONRegistryWithBuilds: remove environment variable setup and streamline registry initialization * Rename injected variable for clarity in InjectEnforcedProvisioners function --------- Co-authored-by: Hari Om <58305594+Madhav008@users.noreply.github.com>
This commit is contained in:
parent
ea4bb3d39a
commit
eee3805c06
24 changed files with 1578 additions and 55 deletions
|
|
@ -150,6 +150,26 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int
|
|||
return ret
|
||||
}
|
||||
|
||||
// Fetch and inject enforced provisioners from HCP Packer (if configured)
|
||||
if !cla.SkipEnforcement {
|
||||
if err := hcpRegistry.FetchEnforcedBlocks(buildCtx); err != nil {
|
||||
return writeDiags(c.Ui, nil, hcl.Diagnostics{
|
||||
&hcl.Diagnostic{
|
||||
Summary: "HCP: fetching enforced provisioners failed",
|
||||
Severity: hcl.DiagError,
|
||||
Detail: err.Error(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
diags := hcpRegistry.InjectEnforcedProvisioners(builds)
|
||||
if diags.HasErrors() {
|
||||
return writeDiags(c.Ui, nil, diags)
|
||||
}
|
||||
} else {
|
||||
c.Ui.Say("Skipping HCP Packer enforced provisioners (--skip-enforcement flag set)")
|
||||
}
|
||||
|
||||
if cla.Debug {
|
||||
c.Ui.Say("Debug mode enabled. Builds will not be parallelized.")
|
||||
}
|
||||
|
|
@ -456,6 +476,7 @@ Options:
|
|||
-warn-on-undeclared-var Display warnings for user variable files containing undeclared variables.
|
||||
-ignore-prerelease-plugins Disable the loading of prerelease plugin binaries (x.y.z-dev).
|
||||
-use-sequential-evaluation Fallback to using a sequential approach for local/datasource evaluation.
|
||||
-skip-enforcement Skip injection of HCP Packer enforced provisioners.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
|
|
|
|||
|
|
@ -1131,6 +1131,16 @@ func TestBuildCommand_ParseArgs(t *testing.T) {
|
|||
},
|
||||
0,
|
||||
},
|
||||
{fields{defaultMeta},
|
||||
args{[]string{"-skip-enforcement", "file.json"}},
|
||||
&BuildArgs{
|
||||
MetaArgs: MetaArgs{Path: "file.json"},
|
||||
ParallelBuilds: math.MaxInt64,
|
||||
Color: true,
|
||||
SkipEnforcement: true,
|
||||
},
|
||||
0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%s", tt.args.args), func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ func (ba *BuildArgs) AddFlagSets(flags *flag.FlagSet) {
|
|||
|
||||
flags.BoolVar(&ba.ReleaseOnly, "ignore-prerelease-plugins", false, "Disable the loading of prerelease plugin binaries (x.y.z-dev).")
|
||||
|
||||
flags.BoolVar(&ba.SkipEnforcement, "skip-enforcement", false, "Skip injection of HCP Packer enforced provisioners. Requires admin privileges.")
|
||||
|
||||
ba.MetaArgs.AddFlagSets(flags)
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +138,7 @@ type BuildArgs struct {
|
|||
ParallelBuilds int64
|
||||
OnError string
|
||||
ReleaseOnly bool
|
||||
SkipEnforcement bool
|
||||
}
|
||||
|
||||
func (ia *InitArgs) AddFlagSets(flags *flag.FlagSet) {
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -22,7 +22,7 @@ require (
|
|||
github.com/hashicorp/go-uuid v1.0.3
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/hashicorp/hcl/v2 v2.24.0
|
||||
github.com/hashicorp/hcp-sdk-go v0.167.0
|
||||
github.com/hashicorp/hcp-sdk-go v0.172.0
|
||||
github.com/hashicorp/packer-plugin-sdk v0.6.7
|
||||
github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869
|
||||
github.com/klauspost/compress v1.18.5
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -732,8 +732,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
|
||||
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
|
||||
github.com/hashicorp/hcp-sdk-go v0.167.0 h1:t2v+mm3gN1z4qvdJ7g9RuDdXDvIExMtjV1Fvzn2LuVc=
|
||||
github.com/hashicorp/hcp-sdk-go v0.167.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
|
||||
github.com/hashicorp/hcp-sdk-go v0.172.0 h1:j4VrSN2yd8prFb8Y0gQWQbTpsV5uVPgYEUozOGfPOOc=
|
||||
github.com/hashicorp/hcp-sdk-go v0.172.0/go.mod h1:v2vbpNIrmgUTelW4Z+ur+aQuSPxeaVK3xytFdpEXvSg=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
||||
|
|
|
|||
81
hcl2template/enforced_provisioner.go
Normal file
81
hcl2template/enforced_provisioner.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
// GetCoreBuildProvisionerFromBlock converts a ProvisionerBlock to a CoreBuildProvisioner.
|
||||
// This is used for enforced provisioners that need to be injected into builds.
|
||||
func (cfg *PackerConfig) GetCoreBuildProvisionerFromBlock(pb *ProvisionerBlock, buildName string) (packer.CoreBuildProvisioner, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
// Get the provisioner plugin
|
||||
provisioner, err := cfg.parser.PluginConfig.Provisioners.Start(pb.PType)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", pb.PType),
|
||||
Detail: fmt.Sprintf("The provisioner plugin could not be loaded: %s", err.Error()),
|
||||
})
|
||||
return packer.CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
// Create basic builder variables
|
||||
builderVars := map[string]interface{}{
|
||||
"packer_core_version": cfg.CorePackerVersionString,
|
||||
"packer_debug": strconv.FormatBool(cfg.debug),
|
||||
"packer_force": strconv.FormatBool(cfg.force),
|
||||
"packer_on_error": cfg.onError,
|
||||
"packer_sensitive_variables": cfg.sensitiveInputVariableKeys(),
|
||||
}
|
||||
|
||||
// Create evaluation context
|
||||
ectx := cfg.EvalContext(BuildContext, nil)
|
||||
|
||||
// Create the HCL2Provisioner wrapper
|
||||
hclProvisioner := &HCL2Provisioner{
|
||||
Provisioner: provisioner,
|
||||
provisionerBlock: pb,
|
||||
evalContext: ectx,
|
||||
builderVariables: builderVars,
|
||||
}
|
||||
|
||||
if pb.Override != nil {
|
||||
if override, ok := pb.Override[buildName]; ok {
|
||||
if typedOverride, ok := override.(map[string]interface{}); ok {
|
||||
hclProvisioner.override = typedOverride
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the provisioner
|
||||
err = hclProvisioner.HCL2Prepare(nil)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to prepare enforced provisioner %q", pb.PType),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return packer.CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
// Wrap provisioner with any special behavior (pause, timeout, retry)
|
||||
wrappedProvisioner := packer.WrapProvisionerWithOptions(hclProvisioner, packer.ProvisionerWrapOptions{
|
||||
PauseBefore: pb.PauseBefore,
|
||||
Timeout: pb.Timeout,
|
||||
MaxRetries: pb.MaxRetries,
|
||||
})
|
||||
|
||||
return packer.CoreBuildProvisioner{
|
||||
PType: pb.PType,
|
||||
PName: pb.PName,
|
||||
Provisioner: wrappedProvisioner,
|
||||
}, diags
|
||||
}
|
||||
146
hcl2template/enforced_provisioner_parser.go
Normal file
146
hcl2template/enforced_provisioner_parser.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var standaloneProvisionerSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: buildProvisionerLabel, LabelNames: []string{"type"}},
|
||||
},
|
||||
}
|
||||
|
||||
// ParseProvisionerBlocks parses raw provisioner block content into ProvisionerBlocks.
|
||||
// It accepts HCL, HCL JSON, and the legacy JSON payload used for enforced provisioners.
|
||||
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
parser := &Parser{Parser: hclparse.NewParser()}
|
||||
return parser.parseProvisionerBlocks(blockContent)
|
||||
}
|
||||
|
||||
func (p *Parser) parseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
hclParser := p.Parser
|
||||
if hclParser == nil {
|
||||
hclParser = hclparse.NewParser()
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] parsing provisioner block content as HCL")
|
||||
|
||||
file, diags := hclParser.ParseHCL([]byte(blockContent), "provisioner.pkr.hcl")
|
||||
if !diags.HasErrors() {
|
||||
provisioners, provisionerDiags := p.parseProvisionerBlocksFromFile(file, diags)
|
||||
if provisionerDiags.HasErrors() {
|
||||
return nil, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] parsed provisioner block content as HCL")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] failed to parse provisioner block content as HCL, trying JSON fallback")
|
||||
|
||||
jsonFile, jsonDiags := hclParser.ParseJSON([]byte(blockContent), "provisioner.pkr.json")
|
||||
if jsonDiags.HasErrors() {
|
||||
log.Printf("[DEBUG] failed to parse provisioner block content as JSON")
|
||||
return nil, append(diags, jsonDiags...)
|
||||
}
|
||||
|
||||
provisioners, provisionerDiags := p.parseProvisionerBlocksFromFile(jsonFile, jsonDiags)
|
||||
if !provisionerDiags.HasErrors() && len(provisioners) > 0 {
|
||||
log.Printf("[DEBUG] parsed provisioner block content as JSON")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
|
||||
legacyJSON, ok, err := normalizeLegacyProvisionersJSON(blockContent)
|
||||
if err == nil && ok {
|
||||
legacyFile, legacyDiags := hclParser.ParseJSON([]byte(legacyJSON), "provisioner_legacy.pkr.json")
|
||||
if !legacyDiags.HasErrors() {
|
||||
legacyProvisioners, legacyProvisionerDiags := p.parseProvisionerBlocksFromFile(legacyFile, legacyDiags)
|
||||
if !legacyProvisionerDiags.HasErrors() && len(legacyProvisioners) > 0 {
|
||||
log.Printf("[DEBUG] parsed provisioner block content as legacy JSON")
|
||||
return legacyProvisioners, legacyProvisionerDiags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if provisionerDiags.HasErrors() {
|
||||
return nil, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] parsed provisioner block content as JSON but found no valid provisioner blocks")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
|
||||
func normalizeLegacyProvisionersJSON(blockContent string) (string, bool, error) {
|
||||
type legacyPayload struct {
|
||||
Provisioners []map[string]interface{} `json:"provisioners"`
|
||||
}
|
||||
|
||||
var payload legacyPayload
|
||||
if err := json.Unmarshal([]byte(blockContent), &payload); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if len(payload.Provisioners) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
normalized := make([]map[string]interface{}, 0, len(payload.Provisioners))
|
||||
for _, provisioner := range payload.Provisioners {
|
||||
typeName, ok := provisioner["type"].(string)
|
||||
if !ok || typeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg := make(map[string]interface{})
|
||||
for key, value := range provisioner {
|
||||
if key == "type" {
|
||||
continue
|
||||
}
|
||||
cfg[key] = value
|
||||
}
|
||||
|
||||
normalized = append(normalized, map[string]interface{}{typeName: cfg})
|
||||
}
|
||||
|
||||
if len(normalized) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
out := map[string]interface{}{
|
||||
buildProvisionerLabel: normalized,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return string(b), true, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseProvisionerBlocksFromFile(file *hcl.File, diags hcl.Diagnostics) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
content, moreDiags := file.Body.Content(standaloneProvisionerSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ectx := &hcl.EvalContext{Variables: map[string]cty.Value{}}
|
||||
provisioners := make([]*ProvisionerBlock, 0, len(content.Blocks))
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
provisioner, moreDiags := p.decodeProvisioner(block, ectx)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
provisioners = append(provisioners, provisioner)
|
||||
}
|
||||
|
||||
return provisioners, diags
|
||||
}
|
||||
496
hcl2template/enforced_provisioner_test.go
Normal file
496
hcl2template/enforced_provisioner_test.go
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCoreBuildProvisionerFromBlock_AppliesOverrideForBuild(t *testing.T) {
|
||||
parser := getBasicParser()
|
||||
cfg := &PackerConfig{
|
||||
parser: parser,
|
||||
CorePackerVersionString: lockedVersion,
|
||||
}
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(`
|
||||
provisioner "shell" {
|
||||
override = {
|
||||
"amazon-ebs.ubuntu" = {
|
||||
bool = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
|
||||
if !ok {
|
||||
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
|
||||
}
|
||||
|
||||
if hclProv.override == nil {
|
||||
t.Fatal("expected override to be applied, got nil")
|
||||
}
|
||||
|
||||
if got, ok := hclProv.override["bool"]; !ok || got != false {
|
||||
t.Fatalf("expected override bool=false, got %#v", hclProv.override["bool"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCoreBuildProvisionerFromBlock_OverrideNotAppliedForOtherBuild(t *testing.T) {
|
||||
parser := getBasicParser()
|
||||
cfg := &PackerConfig{
|
||||
parser: parser,
|
||||
CorePackerVersionString: lockedVersion,
|
||||
}
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(`
|
||||
provisioner "shell" {
|
||||
override = {
|
||||
"amazon-ebs.ubuntu" = {
|
||||
bool = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "virtualbox-iso.base")
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
|
||||
if !ok {
|
||||
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
|
||||
}
|
||||
|
||||
if hclProv.override != nil {
|
||||
t.Fatalf("expected no override to be applied, got %#v", hclProv.override)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCoreBuildProvisionerFromBlock_IncludesSensitiveVariables(t *testing.T) {
|
||||
parser := getBasicParser()
|
||||
cfg := &PackerConfig{
|
||||
parser: parser,
|
||||
CorePackerVersionString: lockedVersion,
|
||||
InputVariables: Variables{
|
||||
"visible": &Variable{Name: "visible"},
|
||||
"secret": &Variable{Name: "secret", Sensitive: true},
|
||||
},
|
||||
}
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(`
|
||||
provisioner "shell" {
|
||||
override = {
|
||||
"amazon-ebs.ubuntu" = {
|
||||
bool = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
|
||||
if !ok {
|
||||
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
|
||||
}
|
||||
|
||||
sensitiveVars, ok := hclProv.builderVariables["packer_sensitive_variables"].([]string)
|
||||
if !ok {
|
||||
t.Fatalf("expected []string packer_sensitive_variables, got %T", hclProv.builderVariables["packer_sensitive_variables"])
|
||||
}
|
||||
|
||||
if len(sensitiveVars) != 1 || sensitiveVars[0] != "secret" {
|
||||
t.Fatalf("expected sensitive vars [secret], got %#v", sensitiveVars)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blockContent string
|
||||
wantCount int
|
||||
wantTypes []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single shell provisioner",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'Hello from enforced provisioner'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple provisioners",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'First enforced provisioner'"]
|
||||
}
|
||||
|
||||
provisioner "shell" {
|
||||
name = "security-scan"
|
||||
inline = ["echo 'Security scan running...'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with pause_before",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
pause_before = "10s"
|
||||
inline = ["echo 'Waiting before execution'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with max_retries",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
max_retries = 3
|
||||
inline = ["echo 'Retry test'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with only filter",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu"]
|
||||
inline = ["echo 'Only for amazon-ebs.ubuntu'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with except filter",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
except = ["null.test"]
|
||||
inline = ["echo 'Except for null.test'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with both only and except",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu"]
|
||||
except = ["null.test"]
|
||||
inline = ["echo 'invalid filter combination'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty block content",
|
||||
blockContent: "",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace only block content",
|
||||
blockContent: "\n\n\t \n",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid HCL syntax",
|
||||
blockContent: "this is not valid { hcl }}}",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "json single shell provisioner",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo 'Hello from enforced provisioner JSON'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json provisioner with escaped newline in string value",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo first line\necho second line"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json multiple provisioners",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo 'first'"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"shell": {
|
||||
"name": "security-scan",
|
||||
"inline": ["echo 'second'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json syntax",
|
||||
blockContent: `{"provisioner": [ { "shell": { "inline": ["test"] } ] }`,
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "legacy json provisioners format",
|
||||
blockContent: `{
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": ["echo legacy json format"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "legacy json provisioners with escaped newline and multiple types",
|
||||
blockContent: `{
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": ["echo legacy line 1\necho legacy line 2"]
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "source.txt",
|
||||
"destination": "destination.txt"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "file"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "hcl provisioners with windows newlines",
|
||||
blockContent: "provisioner \"shell\" {\r\n inline = [\"echo first\"]\r\n}\r\n\r\nprovisioner \"file\" {\r\n source = \"source.txt\"\r\n destination = \"destination.txt\"\r\n}\r\n",
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "file"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
blocks, diags := ParseProvisionerBlocks(tt.blockContent)
|
||||
|
||||
if tt.wantErr {
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("ParseProvisionerBlocks() expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
return
|
||||
}
|
||||
|
||||
if len(blocks) != tt.wantCount {
|
||||
t.Errorf("ParseProvisionerBlocks() got %d blocks, want %d", len(blocks), tt.wantCount)
|
||||
return
|
||||
}
|
||||
|
||||
for i, wantType := range tt.wantTypes {
|
||||
if blocks[i].PType != wantType {
|
||||
t.Errorf("ParseProvisionerBlocks() block[%d].PType = %q, want %q", i, blocks[i].PType, wantType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithPauseBefore(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
pause_before = "30s"
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
// pause_before should be parsed as 30 seconds
|
||||
if blocks[0].PauseBefore.Seconds() != 30 {
|
||||
t.Errorf("Expected PauseBefore=30s, got %v", blocks[0].PauseBefore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithMaxRetries(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
max_retries = 5
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
if blocks[0].MaxRetries != 5 {
|
||||
t.Errorf("Expected MaxRetries=5, got %d", blocks[0].MaxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithOnlyExcept(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu", "azure-arm.windows"]
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
// Check only filter
|
||||
if len(blocks[0].OnlyExcept.Only) != 2 {
|
||||
t.Errorf("Expected 2 only values, got %d", len(blocks[0].OnlyExcept.Only))
|
||||
}
|
||||
|
||||
// Skip should return true for sources not in the only list
|
||||
if !blocks[0].OnlyExcept.Skip("null.test") {
|
||||
t.Error("Skip() should return true for source not in only list")
|
||||
}
|
||||
|
||||
// Skip should return false for sources in the only list
|
||||
if blocks[0].OnlyExcept.Skip("amazon-ebs.ubuntu") {
|
||||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksJSONWithOptions(t *testing.T) {
|
||||
blockContent := `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"pause_before": "15s",
|
||||
"max_retries": 2,
|
||||
"only": ["docker.ubuntu"],
|
||||
"inline": ["echo 'json test'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
if blocks[0].PauseBefore.Seconds() != 15 {
|
||||
t.Errorf("Expected PauseBefore=15s, got %v", blocks[0].PauseBefore)
|
||||
}
|
||||
|
||||
if blocks[0].MaxRetries != 2 {
|
||||
t.Errorf("Expected MaxRetries=2, got %d", blocks[0].MaxRetries)
|
||||
}
|
||||
|
||||
if blocks[0].OnlyExcept.Skip("docker.ubuntu") {
|
||||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
|
||||
if !blocks[0].OnlyExcept.Skip("null.test") {
|
||||
t.Error("Skip() should return true for source not in only list")
|
||||
}
|
||||
}
|
||||
|
|
@ -185,16 +185,7 @@ func (cfg *PackerConfig) startProvisioner(source SourceUseBlock, pb *Provisioner
|
|||
builderVars["packer_debug"] = strconv.FormatBool(cfg.debug)
|
||||
builderVars["packer_force"] = strconv.FormatBool(cfg.force)
|
||||
builderVars["packer_on_error"] = cfg.onError
|
||||
|
||||
sensitiveVars := make([]string, 0, len(cfg.InputVariables))
|
||||
|
||||
for key, variable := range cfg.InputVariables {
|
||||
if variable.Sensitive {
|
||||
sensitiveVars = append(sensitiveVars, key)
|
||||
}
|
||||
}
|
||||
|
||||
builderVars["packer_sensitive_variables"] = sensitiveVars
|
||||
builderVars["packer_sensitive_variables"] = cfg.sensitiveInputVariableKeys()
|
||||
|
||||
hclProvisioner := &HCL2Provisioner{
|
||||
Provisioner: provisioner,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ func (p *HCL2Provisioner) HCL2Prepare(buildVars map[string]interface{}) error {
|
|||
ectx = p.evalContext.NewChild()
|
||||
buildValues := map[string]cty.Value{}
|
||||
if !p.evalContext.Variables[buildAccessor].IsNull() {
|
||||
buildValues = p.evalContext.Variables[buildAccessor].AsValueMap()
|
||||
for k, v := range p.evalContext.Variables[buildAccessor].AsValueMap() {
|
||||
buildValues[k] = v
|
||||
}
|
||||
}
|
||||
for k, v := range buildVars {
|
||||
val, err := ConvertPluginConfigValueToHCLValue(v)
|
||||
|
|
|
|||
|
|
@ -827,14 +827,8 @@ func (cfg *PackerConfig) GetBuilds(opts packer.GetBuildsOptions) ([]*packer.Core
|
|||
pcb.Provisioners = provisioners
|
||||
pcb.PostProcessors = pps
|
||||
pcb.Prepared = true
|
||||
|
||||
pcb.SensitiveVars = make([]string, 0, len(cfg.InputVariables))
|
||||
|
||||
for key, variable := range cfg.InputVariables {
|
||||
if variable.Sensitive {
|
||||
pcb.SensitiveVars = append(pcb.SensitiveVars, key)
|
||||
}
|
||||
}
|
||||
pcb.SetGeneratedVars(generatedVars)
|
||||
pcb.SensitiveVars = cfg.sensitiveInputVariableKeys()
|
||||
|
||||
// Prepare just sets the "prepareCalled" flag on CoreBuild, since
|
||||
// we did all the prep here.
|
||||
|
|
@ -926,6 +920,18 @@ func (p *PackerConfig) printVariables() string {
|
|||
return out.String()
|
||||
}
|
||||
|
||||
func (cfg *PackerConfig) sensitiveInputVariableKeys() []string {
|
||||
sensitiveVars := make([]string, 0, len(cfg.InputVariables))
|
||||
|
||||
for key, variable := range cfg.InputVariables {
|
||||
if variable.Sensitive {
|
||||
sensitiveVars = append(sensitiveVars, key)
|
||||
}
|
||||
}
|
||||
|
||||
return sensitiveVars
|
||||
}
|
||||
|
||||
func (p *PackerConfig) printBuilds() string {
|
||||
out := &strings.Builder{}
|
||||
out.WriteString("> builds:\n")
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ type MockPackerClientService struct {
|
|||
UpdateChannelCalled bool
|
||||
TrackCalledServiceMethods bool
|
||||
|
||||
// Enforced block tracking
|
||||
GetEnforcedBlocksByBucketCalled bool
|
||||
|
||||
// Mock Creates
|
||||
CreateBucketResp *hcpPackerModels.HashicorpCloudPacker20230101CreateBucketResponse
|
||||
CreateVersionResp *hcpPackerModels.HashicorpCloudPacker20230101CreateVersionResponse
|
||||
|
|
@ -33,6 +36,10 @@ type MockPackerClientService struct {
|
|||
// Mock Gets
|
||||
GetVersionResp *hcpPackerModels.HashicorpCloudPacker20230101GetVersionResponse
|
||||
|
||||
// Mock enforced blocks
|
||||
GetEnforcedBlocksByBucketResp *hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse
|
||||
GetEnforcedBlocksByBucketErr error
|
||||
|
||||
ExistingBuilds []string
|
||||
ExistingBuildLabels map[string]string
|
||||
|
||||
|
|
@ -321,3 +328,28 @@ func (svc *MockPackerClientService) PackerServiceUpdateChannel(
|
|||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (svc *MockPackerClientService) PackerServiceGetEnforcedBlocksByBucket(
|
||||
params *hcpPackerService.PackerServiceGetEnforcedBlocksByBucketParams, _ runtime.ClientAuthInfoWriter,
|
||||
opts ...hcpPackerService.ClientOption,
|
||||
) (*hcpPackerService.PackerServiceGetEnforcedBlocksByBucketOK, error) {
|
||||
|
||||
if svc.TrackCalledServiceMethods {
|
||||
svc.GetEnforcedBlocksByBucketCalled = true
|
||||
}
|
||||
|
||||
if svc.GetEnforcedBlocksByBucketErr != nil {
|
||||
return nil, svc.GetEnforcedBlocksByBucketErr
|
||||
}
|
||||
|
||||
ok := &hcpPackerService.PackerServiceGetEnforcedBlocksByBucketOK{}
|
||||
if svc.GetEnforcedBlocksByBucketResp != nil {
|
||||
ok.Payload = svc.GetEnforcedBlocksByBucketResp
|
||||
} else {
|
||||
ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse{
|
||||
EnforcedBlockDetail: []*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockDetail{},
|
||||
}
|
||||
}
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
|
|
|||
33
internal/hcp/api/service_enforced_provisioner.go
Normal file
33
internal/hcp/api/service_enforced_provisioner.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
hcpPackerService "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/client/packer_service"
|
||||
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
|
||||
)
|
||||
|
||||
// GetEnforcedBlocksForBucket fetches all enforced blocks linked to a bucket.
|
||||
// This is the key method used during packer build to auto-inject provisioners.
|
||||
// The response includes EnforcedBlockDetail entries each with an active version
|
||||
// containing the raw HCL block_content to be parsed and injected.
|
||||
func (c *Client) GetEnforcedBlocksForBucket(
|
||||
ctx context.Context,
|
||||
bucketName string,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceGetEnforcedBlocksByBucketParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
params.BucketName = bucketName
|
||||
|
||||
resp, err := c.Packer.PackerServiceGetEnforcedBlocksByBucket(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
|
|
@ -91,6 +91,67 @@ func (h *HCLRegistry) VersionStatusSummary() {
|
|||
h.bucket.Version.statusSummary(h.ui)
|
||||
}
|
||||
|
||||
// FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer
|
||||
func (h *HCLRegistry) FetchEnforcedBlocks(ctx context.Context) error {
|
||||
return h.bucket.FetchEnforcedBlocks(ctx)
|
||||
}
|
||||
|
||||
// InjectEnforcedProvisioners injects enforced provisioners into the builds
|
||||
func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
|
||||
enforcedBlocks := h.bucket.EnforcedBlocks
|
||||
if len(enforcedBlocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var allDiags hcl.Diagnostics
|
||||
|
||||
// Parse all enforced blocks into provisioner blocks
|
||||
for _, eb := range enforcedBlocks {
|
||||
if eb.BlockContent == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent)
|
||||
if diags.HasErrors() {
|
||||
allDiags = append(allDiags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to parse enforced block %q", eb.Name),
|
||||
Detail: diags.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(provBlocks) > 0 {
|
||||
h.ui.Say(fmt.Sprintf("Loaded %d enforced provisioner(s) from HCP block %q and template type %q", len(provBlocks), eb.Name, eb.TemplateType))
|
||||
}
|
||||
|
||||
// Inject into each build
|
||||
for _, build := range builds {
|
||||
for _, pb := range provBlocks {
|
||||
// Check if this provisioner should be skipped for this build
|
||||
if pb.OnlyExcept.Skip(build.Type) {
|
||||
log.Printf("[DEBUG] skipping enforced provisioner %q for build %q due to only/except rules",
|
||||
pb.PType, build.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromBlock(pb, build.Type)
|
||||
if moreDiags.HasErrors() {
|
||||
allDiags = append(allDiags, moreDiags...)
|
||||
continue
|
||||
}
|
||||
|
||||
build.Provisioners = append(build.Provisioners, coreProv)
|
||||
|
||||
log.Printf("[INFO] injected enforced provisioner %q from block %q into build %q",
|
||||
pb.PType, eb.Name, build.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allDiags
|
||||
}
|
||||
|
||||
func NewHCLRegistry(config *hcl2template.PackerConfig, ui sdkpacker.Ui) (*HCLRegistry, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
if len(config.Builds) > 1 {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/hashicorp/hcl/v2"
|
||||
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
|
||||
sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||
"github.com/hashicorp/packer/hcl2template"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
|
|
@ -113,3 +114,85 @@ func (h *JSONRegistry) VersionStatusSummary() {
|
|||
func (h *JSONRegistry) Metadata() Metadata {
|
||||
return h.metadata
|
||||
}
|
||||
|
||||
// FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer
|
||||
func (h *JSONRegistry) FetchEnforcedBlocks(ctx context.Context) error {
|
||||
return h.bucket.FetchEnforcedBlocks(ctx)
|
||||
}
|
||||
|
||||
// InjectEnforcedProvisioners injects enforced provisioners into the builds
|
||||
func (h *JSONRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
|
||||
enforcedBlocks := h.bucket.EnforcedBlocks
|
||||
if len(enforcedBlocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var allDiags hcl.Diagnostics
|
||||
|
||||
for _, eb := range enforcedBlocks {
|
||||
if eb.BlockContent == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent)
|
||||
if diags.HasErrors() {
|
||||
allDiags = append(allDiags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to parse enforced block %q", eb.Name),
|
||||
Detail: diags.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(provBlocks) > 0 {
|
||||
h.ui.Say(fmt.Sprintf("Loaded %d enforced provisioner(s) from HCP block %q and template type %q", len(provBlocks), eb.Name, eb.TemplateType))
|
||||
}
|
||||
|
||||
for _, build := range builds {
|
||||
buildName := build.Type
|
||||
injectedProvisioners := make([]packer.CoreBuildProvisioner, 0, len(provBlocks))
|
||||
|
||||
for _, pb := range provBlocks {
|
||||
if pb.OnlyExcept.Skip(buildName) {
|
||||
log.Printf("[DEBUG] skipping enforced provisioner %q for legacy JSON build %q due to only/except rules",
|
||||
pb.PType, build.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
coreProv, moreDiags := h.configuration.GenerateCoreBuildProvisionerFromHCLBody(
|
||||
pb.PType,
|
||||
pb.Rest,
|
||||
pb.Override,
|
||||
pb.PauseBefore,
|
||||
pb.MaxRetries,
|
||||
pb.Timeout,
|
||||
buildName,
|
||||
)
|
||||
if moreDiags.HasErrors() {
|
||||
allDiags = append(allDiags, moreDiags...)
|
||||
continue
|
||||
}
|
||||
|
||||
build.Provisioners = append(build.Provisioners, coreProv)
|
||||
injectedProvisioners = append(injectedProvisioners, coreProv)
|
||||
|
||||
log.Printf("[INFO] injected enforced provisioner %q from block %q into legacy JSON build %q",
|
||||
pb.PType, eb.Name, build.Name())
|
||||
}
|
||||
|
||||
if len(injectedProvisioners) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := build.PrepareProvisioners(injectedProvisioners...); err != nil {
|
||||
allDiags = append(allDiags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to prepare enforced provisioners for legacy JSON build %q", build.Name()),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allDiags
|
||||
}
|
||||
|
|
|
|||
122
internal/hcp/registry/json_enforced_test.go
Normal file
122
internal/hcp/registry/json_enforced_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||
packertemplate "github.com/hashicorp/packer-plugin-sdk/template"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func testJSONRegistryWithBuilds(t *testing.T, builderNames ...string) (*JSONRegistry, []*packer.CoreBuild, *packersdk.MockProvisioner) {
|
||||
t.Helper()
|
||||
|
||||
coreConfig := packer.TestCoreConfig(t)
|
||||
packer.TestBuilder(t, coreConfig, "test")
|
||||
provisioner := packer.TestProvisioner(t, coreConfig, "test")
|
||||
|
||||
builders := make(map[string]*packertemplate.Builder, len(builderNames))
|
||||
for _, name := range builderNames {
|
||||
builders[name] = &packertemplate.Builder{
|
||||
Name: name,
|
||||
Type: "test",
|
||||
Config: map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
coreConfig.Template = &packertemplate.Template{
|
||||
Path: "test.json",
|
||||
Builders: builders,
|
||||
}
|
||||
|
||||
core := packer.TestCore(t, coreConfig)
|
||||
bucket := NewBucketWithVersion()
|
||||
bucket.Name = "test-bucket"
|
||||
|
||||
registry := &JSONRegistry{
|
||||
configuration: core,
|
||||
bucket: bucket,
|
||||
ui: packer.TestUi(t),
|
||||
metadata: &MetadataStore{},
|
||||
}
|
||||
|
||||
builds, diags := core.GetBuilds(packer.GetBuildsOptions{})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetBuilds() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
return registry, builds, provisioner
|
||||
}
|
||||
|
||||
func TestJSONRegistry_InjectEnforcedProvisioners_AppliesOverride(t *testing.T) {
|
||||
registry, builds, provisioner := testJSONRegistryWithBuilds(t, "app")
|
||||
registry.bucket.EnforcedBlocks = []*EnforcedBlock{{
|
||||
Name: "enforced",
|
||||
BlockContent: `provisioner "test" {
|
||||
override = {
|
||||
app = {
|
||||
foo = "bar"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}}
|
||||
|
||||
diags := registry.InjectEnforcedProvisioners(builds)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("InjectEnforcedProvisioners() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if got := len(builds[0].Provisioners); got != 1 {
|
||||
t.Fatalf("build provisioner count = %d, want 1", got)
|
||||
}
|
||||
|
||||
if !provisioner.PrepCalled {
|
||||
t.Fatal("expected injected legacy JSON provisioner to be prepared")
|
||||
}
|
||||
|
||||
foundOverride := false
|
||||
for _, raw := range provisioner.PrepConfigs {
|
||||
config, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if value, ok := config["foo"]; ok && value == "bar" {
|
||||
foundOverride = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundOverride {
|
||||
t.Fatal("expected override config to be passed to injected provisioner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONRegistry_InjectEnforcedProvisioners_RespectsOnlyExcept(t *testing.T) {
|
||||
registry, builds, _ := testJSONRegistryWithBuilds(t, "app", "other")
|
||||
registry.bucket.EnforcedBlocks = []*EnforcedBlock{{
|
||||
Name: "enforced",
|
||||
BlockContent: `provisioner "test" {
|
||||
only = ["app"]
|
||||
}`,
|
||||
}}
|
||||
|
||||
diags := registry.InjectEnforcedProvisioners(builds)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("InjectEnforcedProvisioners() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
provisionerCounts := make(map[string]int, len(builds))
|
||||
for _, build := range builds {
|
||||
provisionerCounts[build.Type] = len(build.Provisioners)
|
||||
}
|
||||
|
||||
if provisionerCounts["app"] != 1 {
|
||||
t.Fatalf("app build provisioner count = %d, want 1", provisionerCounts["app"])
|
||||
}
|
||||
|
||||
if provisionerCounts["other"] != 0 {
|
||||
t.Fatalf("other build provisioner count = %d, want 0", provisionerCounts["other"])
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ package registry
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
|
@ -35,3 +36,11 @@ func (r nullRegistry) VersionStatusSummary() {}
|
|||
func (r nullRegistry) Metadata() Metadata {
|
||||
return NilMetadata{}
|
||||
}
|
||||
|
||||
func (r nullRegistry) FetchEnforcedBlocks(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r nullRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ type Registry interface {
|
|||
CompleteBuild(ctx context.Context, build *packer.CoreBuild, artifacts []sdkpacker.Artifact, buildErr error) ([]sdkpacker.Artifact, error)
|
||||
VersionStatusSummary()
|
||||
Metadata() Metadata
|
||||
// FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer
|
||||
FetchEnforcedBlocks(ctx context.Context) error
|
||||
// InjectEnforcedProvisioners injects enforced provisioners into the builds
|
||||
InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics
|
||||
}
|
||||
|
||||
// New instantiates the appropriate registry for the Packer configuration template type.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,16 @@ import (
|
|||
// build is still alive.
|
||||
const HeartbeatPeriod = 2 * time.Minute
|
||||
|
||||
// EnforcedBlock represents an enforced provisioner block from HCP Packer
|
||||
type EnforcedBlock struct {
|
||||
ID string
|
||||
Name string
|
||||
BlockContent string // Raw HCL content containing provisioner blocks
|
||||
VersionID string
|
||||
Version string
|
||||
TemplateType string
|
||||
}
|
||||
|
||||
// Bucket represents a single bucket on the HCP Packer registry.
|
||||
type Bucket struct {
|
||||
Name string
|
||||
|
|
@ -40,6 +50,7 @@ type Bucket struct {
|
|||
SourceExternalIdentifierToParentVersions map[string]ParentVersion
|
||||
RunningBuilds map[string]chan struct{}
|
||||
Version *Version
|
||||
EnforcedBlocks []*EnforcedBlock
|
||||
client *hcpPackerAPI.Client
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +153,63 @@ func (bucket *Bucket) Initialize(
|
|||
return bucket.initializeVersion(ctx, templateType)
|
||||
}
|
||||
|
||||
// FetchEnforcedBlocks retrieves all enforced blocks linked to this bucket from HCP Packer.
|
||||
// These blocks contain provisioner configurations that should be automatically injected
|
||||
// into builds for this bucket.
|
||||
func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error {
|
||||
if bucket.client == nil {
|
||||
return errors.New("bucket client not initialized, call Initialize first")
|
||||
}
|
||||
|
||||
log.Printf("[INFO] fetching enforced blocks linked to bucket %q", bucket.Name)
|
||||
|
||||
resp, err := bucket.client.GetEnforcedBlocksForBucket(ctx, bucket.Name)
|
||||
if err != nil {
|
||||
if hcpPackerAPI.CheckErrorCode(err, codes.NotFound) || hcpPackerAPI.CheckErrorCode(err, codes.Unimplemented) {
|
||||
// If the API doesn't support enforced blocks yet or returns not found, continue silently.
|
||||
log.Printf("[DEBUG] fetching enforced blocks for bucket %q: %v", bucket.Name, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed fetching enforced blocks for bucket %q: %w", bucket.Name, err)
|
||||
}
|
||||
|
||||
if resp == nil {
|
||||
log.Printf("[INFO] no enforced blocks response returned for bucket %q", bucket.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
bucket.EnforcedBlocks = make([]*EnforcedBlock, 0, len(resp.EnforcedBlockDetail))
|
||||
for _, detail := range resp.EnforcedBlockDetail {
|
||||
if detail == nil || detail.Version == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
block := &EnforcedBlock{
|
||||
ID: detail.ID,
|
||||
Name: detail.Name,
|
||||
BlockContent: detail.Version.BlockContent,
|
||||
VersionID: detail.Version.ID,
|
||||
Version: detail.Version.Version,
|
||||
}
|
||||
|
||||
if detail.Version.TemplateType != nil {
|
||||
block.TemplateType = string(*detail.Version.TemplateType)
|
||||
}
|
||||
|
||||
bucket.EnforcedBlocks = append(bucket.EnforcedBlocks, block)
|
||||
log.Printf("[INFO] linked enforced block found for bucket %q: name=%q id=%q version=%q",
|
||||
bucket.Name, block.Name, block.ID, block.Version)
|
||||
}
|
||||
|
||||
if len(bucket.EnforcedBlocks) == 0 {
|
||||
log.Printf("[INFO] no enforced provisioner blocks linked to bucket %q", bucket.Name)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] fetched %d enforced block(s) linked to bucket %q", len(bucket.EnforcedBlocks), bucket.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bucket *Bucket) RegisterBuildForComponent(sourceName string) {
|
||||
if bucket == nil {
|
||||
return
|
||||
|
|
|
|||
117
internal/hcp/registry/types.bucket_enforced_test.go
Normal file
117
internal/hcp/registry/types.bucket_enforced_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
|
||||
hcpPackerAPI "github.com/hashicorp/packer/internal/hcp/api"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func TestBucket_FetchEnforcedBlocks_ReturnsAllBlocks(t *testing.T) {
|
||||
hcl2Type := hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeHCL2
|
||||
jsonType := hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeJSON
|
||||
|
||||
mockService := hcpPackerAPI.NewMockPackerClientService()
|
||||
mockService.GetEnforcedBlocksByBucketResp = &hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse{
|
||||
EnforcedBlockDetail: []*hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockDetail{
|
||||
{
|
||||
ID: "hcl-id",
|
||||
Name: "hcl-block",
|
||||
Version: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
|
||||
ID: "hcl-v1",
|
||||
Version: "1",
|
||||
BlockContent: "provisioner \"shell\" {}",
|
||||
TemplateType: &hcl2Type,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "json-id",
|
||||
Name: "json-block",
|
||||
Version: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
|
||||
ID: "json-v1",
|
||||
Version: "1",
|
||||
BlockContent: "{\"provisioner\":[{\"shell\":{}}]}",
|
||||
TemplateType: &jsonType,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "unset-id",
|
||||
Name: "unset-block",
|
||||
Version: &hcpPackerModels.HashicorpCloudPacker20230101EnforcedBlockVersion{
|
||||
ID: "unset-v1",
|
||||
Version: "1",
|
||||
BlockContent: "provisioner \"shell\" {}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bucket := &Bucket{
|
||||
Name: "test-bucket",
|
||||
client: &hcpPackerAPI.Client{
|
||||
Packer: mockService,
|
||||
},
|
||||
}
|
||||
|
||||
err := bucket.FetchEnforcedBlocks(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("FetchEnforcedBlocks() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(bucket.EnforcedBlocks) != 3 {
|
||||
t.Fatalf("FetchEnforcedBlocks() got %d blocks, want 3", len(bucket.EnforcedBlocks))
|
||||
}
|
||||
|
||||
if bucket.EnforcedBlocks[0].Name != "hcl-block" {
|
||||
t.Fatalf("first block name = %q, want %q", bucket.EnforcedBlocks[0].Name, "hcl-block")
|
||||
}
|
||||
|
||||
if bucket.EnforcedBlocks[1].Name != "json-block" {
|
||||
t.Fatalf("second block name = %q, want %q", bucket.EnforcedBlocks[1].Name, "json-block")
|
||||
}
|
||||
|
||||
if bucket.EnforcedBlocks[2].Name != "unset-block" {
|
||||
t.Fatalf("third block name = %q, want %q", bucket.EnforcedBlocks[2].Name, "unset-block")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucket_FetchEnforcedBlocks_ReturnsErrorOnServiceFailure(t *testing.T) {
|
||||
mockService := hcpPackerAPI.NewMockPackerClientService()
|
||||
mockService.GetEnforcedBlocksByBucketErr = errors.New("service unavailable")
|
||||
|
||||
bucket := &Bucket{
|
||||
Name: "test-bucket",
|
||||
client: &hcpPackerAPI.Client{
|
||||
Packer: mockService,
|
||||
},
|
||||
}
|
||||
|
||||
err := bucket.FetchEnforcedBlocks(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("FetchEnforcedBlocks() expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBucket_FetchEnforcedBlocks_NotFoundIsNonFatal(t *testing.T) {
|
||||
mockService := hcpPackerAPI.NewMockPackerClientService()
|
||||
mockService.GetEnforcedBlocksByBucketErr = fmt.Errorf("Code:%d %s", codes.NotFound, codes.NotFound.String())
|
||||
|
||||
bucket := &Bucket{
|
||||
Name: "test-bucket",
|
||||
client: &hcpPackerAPI.Client{
|
||||
Packer: mockService,
|
||||
},
|
||||
}
|
||||
|
||||
err := bucket.FetchEnforcedBlocks(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("FetchEnforcedBlocks() expected nil error for NotFound, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ type CoreBuild struct {
|
|||
onError string
|
||||
l sync.Mutex
|
||||
prepareCalled bool
|
||||
generatedVars []string
|
||||
|
||||
SBOMs []SBOM
|
||||
}
|
||||
|
|
@ -145,6 +146,63 @@ func (b *CoreBuild) Name() string {
|
|||
return b.Type
|
||||
}
|
||||
|
||||
func (b *CoreBuild) packerConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
common.BuildNameConfigKey: b.Type,
|
||||
common.BuilderTypeConfigKey: b.BuilderType,
|
||||
common.CoreVersionConfigKey: version.FormattedVersion(),
|
||||
common.DebugConfigKey: b.debug,
|
||||
common.ForceConfigKey: b.force,
|
||||
common.OnErrorConfigKey: b.onError,
|
||||
common.TemplatePathKey: b.TemplatePath,
|
||||
common.UserVariablesConfigKey: b.Variables,
|
||||
common.SensitiveVarsConfigKey: b.SensitiveVars,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *CoreBuild) prepareProvisioners(provisioners []CoreBuildProvisioner, packerConfig map[string]interface{}, generatedVars []string) error {
|
||||
generatedPlaceholderMap := placeholderDataFromGeneratedVars(generatedVars)
|
||||
|
||||
for _, coreProv := range provisioners {
|
||||
configs := make([]interface{}, len(coreProv.config), len(coreProv.config)+2)
|
||||
copy(configs, coreProv.config)
|
||||
configs = append(configs, packerConfig, generatedPlaceholderMap)
|
||||
|
||||
if err := coreProv.Provisioner.Prepare(configs...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func placeholderDataFromGeneratedVars(generatedVars []string) map[string]string {
|
||||
generatedPlaceholderMap := BasicPlaceholderData()
|
||||
for _, k := range generatedVars {
|
||||
generatedPlaceholderMap[k] = fmt.Sprintf("Build_%s. "+
|
||||
packerbuilderdata.PlaceholderMsg, k)
|
||||
}
|
||||
|
||||
return generatedPlaceholderMap
|
||||
}
|
||||
|
||||
// SetGeneratedVars stores the builder-generated variables from the initial
|
||||
// builder preparation so late-injected provisioners can reuse them without
|
||||
// invoking Builder.Prepare again.
|
||||
func (b *CoreBuild) SetGeneratedVars(generatedVars []string) {
|
||||
b.generatedVars = append([]string(nil), generatedVars...)
|
||||
}
|
||||
|
||||
// PrepareProvisioners prepares provisioners injected after the build itself has already been prepared.
|
||||
func (b *CoreBuild) PrepareProvisioners(provisioners ...CoreBuildProvisioner) error {
|
||||
if !b.prepareCalled {
|
||||
return fmt.Errorf("Prepare must be called first")
|
||||
}
|
||||
|
||||
packerConfig := b.packerConfig()
|
||||
return b.prepareProvisioners(provisioners, packerConfig, b.generatedVars)
|
||||
}
|
||||
|
||||
// Prepare prepares the build by doing some initialization for the builder
|
||||
// and any hooks. This _must_ be called prior to Run. The parameter is the
|
||||
// overrides for the variables within the template (if any).
|
||||
|
|
@ -167,17 +225,7 @@ func (b *CoreBuild) Prepare() (warn []string, err error) {
|
|||
// a custom json area instead of just aborting early for HCL.
|
||||
b.prepareCalled = true
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
common.BuildNameConfigKey: b.Type,
|
||||
common.BuilderTypeConfigKey: b.BuilderType,
|
||||
common.CoreVersionConfigKey: version.FormattedVersion(),
|
||||
common.DebugConfigKey: b.debug,
|
||||
common.ForceConfigKey: b.force,
|
||||
common.OnErrorConfigKey: b.onError,
|
||||
common.TemplatePathKey: b.TemplatePath,
|
||||
common.UserVariablesConfigKey: b.Variables,
|
||||
common.SensitiveVarsConfigKey: b.SensitiveVars,
|
||||
}
|
||||
packerConfig := b.packerConfig()
|
||||
|
||||
// Prepare the builder
|
||||
generatedVars, warn, err := b.Builder.Prepare(b.BuilderConfig, packerConfig)
|
||||
|
|
@ -185,35 +233,19 @@ func (b *CoreBuild) Prepare() (warn []string, err error) {
|
|||
log.Printf("Build '%s' prepare failure: %s\n", b.Type, err)
|
||||
return
|
||||
}
|
||||
b.SetGeneratedVars(generatedVars)
|
||||
|
||||
// If the builder has provided a list of to-be-generated variables that
|
||||
// should be made accessible to provisioners, pass that list into
|
||||
// the provisioner prepare() so that the provisioner can appropriately
|
||||
// validate user input against what will become available.
|
||||
generatedPlaceholderMap := BasicPlaceholderData()
|
||||
for _, k := range generatedVars {
|
||||
generatedPlaceholderMap[k] = fmt.Sprintf("Build_%s. "+
|
||||
packerbuilderdata.PlaceholderMsg, k)
|
||||
if err = b.prepareProvisioners(b.Provisioners, packerConfig, generatedVars); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare the provisioners
|
||||
for _, coreProv := range b.Provisioners {
|
||||
configs := make([]interface{}, len(coreProv.config), len(coreProv.config)+1)
|
||||
copy(configs, coreProv.config)
|
||||
configs = append(configs, packerConfig)
|
||||
configs = append(configs, generatedPlaceholderMap)
|
||||
|
||||
if err = coreProv.Provisioner.Prepare(configs...); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
generatedPlaceholderMap := placeholderDataFromGeneratedVars(generatedVars)
|
||||
|
||||
// Prepare the on-error-cleanup provisioner
|
||||
if b.CleanupProvisioner.PType != "" {
|
||||
configs := make([]interface{}, len(b.CleanupProvisioner.config), len(b.CleanupProvisioner.config)+1)
|
||||
configs := make([]interface{}, len(b.CleanupProvisioner.config), len(b.CleanupProvisioner.config)+2)
|
||||
copy(configs, b.CleanupProvisioner.config)
|
||||
configs = append(configs, packerConfig)
|
||||
configs = append(configs, generatedPlaceholderMap)
|
||||
configs = append(configs, packerConfig, generatedPlaceholderMap)
|
||||
err = b.CleanupProvisioner.Provisioner.Prepare(configs...)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -230,6 +230,92 @@ func TestBuildPrepare_ProvisionerGetsGeneratedMap(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuild_PrepareProvisioners_ReusesStoredGeneratedVars(t *testing.T) {
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
|
||||
build := testBuild()
|
||||
builder := build.Builder.(*packersdk.MockBuilder)
|
||||
builder.GeneratedVars = []string{"PartyVar"}
|
||||
|
||||
if _, err := build.Prepare(); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
builder.PrepareCalled = false
|
||||
|
||||
lateProv := CoreBuildProvisioner{
|
||||
PType: "mock-provisioner",
|
||||
Provisioner: &packersdk.MockProvisioner{},
|
||||
config: []interface{}{84},
|
||||
}
|
||||
|
||||
if err := build.PrepareProvisioners(lateProv); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
if builder.PrepareCalled {
|
||||
t.Fatal("builder prepare should not be called again")
|
||||
}
|
||||
|
||||
prov := lateProv.Provisioner.(*packersdk.MockProvisioner)
|
||||
generated := BasicPlaceholderData()
|
||||
generated["PartyVar"] = "Build_PartyVar. " + packerbuilderdata.PlaceholderMsg
|
||||
if !reflect.DeepEqual(prov.PrepConfigs, []interface{}{84, packerConfig, generated}) {
|
||||
t.Fatalf("bad: %#v", prov.PrepConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_PrepareProvisioners_ReusesStoredGeneratedVarsForPreparedBuild(t *testing.T) {
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
|
||||
build := testBuild()
|
||||
builder := build.Builder.(*packersdk.MockBuilder)
|
||||
build.Prepared = true
|
||||
build.SetGeneratedVars([]string{"PartyVar"})
|
||||
|
||||
if _, err := build.Prepare(); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
lateProv := CoreBuildProvisioner{
|
||||
PType: "mock-provisioner",
|
||||
Provisioner: &packersdk.MockProvisioner{},
|
||||
config: []interface{}{84},
|
||||
}
|
||||
|
||||
if err := build.PrepareProvisioners(lateProv); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
if builder.PrepareCalled {
|
||||
t.Fatal("builder prepare should not be called for prepared builds")
|
||||
}
|
||||
|
||||
prov := lateProv.Provisioner.(*packersdk.MockProvisioner)
|
||||
generated := BasicPlaceholderData()
|
||||
generated["PartyVar"] = "Build_PartyVar. " + packerbuilderdata.PlaceholderMsg
|
||||
if !reflect.DeepEqual(prov.PrepConfigs, []interface{}{84, packerConfig, generated}) {
|
||||
t.Fatalf("bad: %#v", prov.PrepConfigs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_PrepareProvisioners_RequiresPrepare(t *testing.T) {
|
||||
build := testBuild()
|
||||
lateProv := CoreBuildProvisioner{
|
||||
PType: "mock-provisioner",
|
||||
Provisioner: &packersdk.MockProvisioner{},
|
||||
config: []interface{}{84},
|
||||
}
|
||||
|
||||
err := build.PrepareProvisioners(lateProv)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if err.Error() != "Prepare must be called first" {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_Run(t *testing.T) {
|
||||
ui := testUi()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ttmp "text/template"
|
||||
|
||||
|
|
@ -18,12 +19,15 @@ import (
|
|||
multierror "github.com/hashicorp/go-multierror"
|
||||
version "github.com/hashicorp/go-version"
|
||||
hcl "github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hcldec"
|
||||
"github.com/hashicorp/packer-plugin-sdk/didyoumean"
|
||||
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||
"github.com/hashicorp/packer-plugin-sdk/template"
|
||||
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||
hcl2shim "github.com/hashicorp/packer/hcl2template/shim"
|
||||
plugingetter "github.com/hashicorp/packer/packer/plugin-getter"
|
||||
packerversion "github.com/hashicorp/packer/version"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// Core is the main executor of Packer. If Packer is being used as a
|
||||
|
|
@ -259,6 +263,12 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName
|
|||
"provisioner failed to be started and did not error: %s", rawP.Type)
|
||||
}
|
||||
|
||||
return c.generateCoreBuildProvisionerWithProvisioner(rawP, rawName, provisioner)
|
||||
}
|
||||
|
||||
func (c *Core) generateCoreBuildProvisionerWithProvisioner(rawP *template.Provisioner, rawName string, provisioner packersdk.Provisioner) (CoreBuildProvisioner, error) {
|
||||
cbp := CoreBuildProvisioner{}
|
||||
|
||||
// Get the configuration
|
||||
config := make([]interface{}, 1, 2)
|
||||
config[0] = rawP.Config
|
||||
|
|
@ -312,6 +322,79 @@ func (c *Core) generateCoreBuildProvisioner(rawP *template.Provisioner, rawName
|
|||
return cbp, nil
|
||||
}
|
||||
|
||||
// GenerateCoreBuildProvisionerFromHCLBody converts a parsed enforced provisioner body into
|
||||
// a legacy JSON core build provisioner, using the same runtime behavior as normal JSON templates.
|
||||
func (c *Core) GenerateCoreBuildProvisionerFromHCLBody(
|
||||
provisionerType string,
|
||||
configBody hcl.Body,
|
||||
override map[string]interface{},
|
||||
pauseBefore time.Duration,
|
||||
maxRetries int,
|
||||
timeout time.Duration,
|
||||
rawName string,
|
||||
) (CoreBuildProvisioner, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
if !c.components.PluginConfig.Provisioners.Has(provisionerType) {
|
||||
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", provisionerType),
|
||||
Detail: fmt.Sprintf("The provisioner plugin %q could not be loaded.", provisionerType),
|
||||
}}
|
||||
}
|
||||
|
||||
provisioner, err := c.components.PluginConfig.Provisioners.Start(provisionerType)
|
||||
if err != nil {
|
||||
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", provisionerType),
|
||||
Detail: fmt.Sprintf("The provisioner plugin could not be loaded: %s", err.Error()),
|
||||
}}
|
||||
}
|
||||
if provisioner == nil {
|
||||
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", provisionerType),
|
||||
Detail: "The provisioner failed to start and returned no instance.",
|
||||
}}
|
||||
}
|
||||
|
||||
flatProvisionerCfg, moreDiags := hcldec.Decode(configBody, provisioner.ConfigSpec(), &hcl.EvalContext{Variables: map[string]cty.Value{}})
|
||||
diags = append(diags, moreDiags...)
|
||||
if diags.HasErrors() {
|
||||
return CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
flatProvisionerCfg = hcl2shim.WriteUnknownPlaceholderValues(flatProvisionerCfg)
|
||||
decodedConfig := hcl2shim.ConfigValueFromHCL2(flatProvisionerCfg)
|
||||
configMap, _ := decodedConfig.(map[string]interface{})
|
||||
if configMap == nil {
|
||||
configMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
rawProvisioner := &template.Provisioner{
|
||||
Type: provisionerType,
|
||||
Config: configMap,
|
||||
Override: override,
|
||||
PauseBefore: pauseBefore,
|
||||
Timeout: timeout,
|
||||
}
|
||||
if maxRetries > 0 {
|
||||
rawProvisioner.MaxRetries = strconv.Itoa(maxRetries)
|
||||
}
|
||||
|
||||
coreProvisioner, err := c.generateCoreBuildProvisionerWithProvisioner(rawProvisioner, rawName, provisioner)
|
||||
if err != nil {
|
||||
return CoreBuildProvisioner{}, hcl.Diagnostics{&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to prepare enforced provisioner %q", provisionerType),
|
||||
Detail: err.Error(),
|
||||
}}
|
||||
}
|
||||
|
||||
return coreProvisioner, diags
|
||||
}
|
||||
|
||||
// This is used for json templates to launch the build plugins.
|
||||
// They will be prepared via b.Prepare() later.
|
||||
func (c *Core) GetBuilds(opts GetBuildsOptions) ([]*CoreBuild, hcl.Diagnostics) {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,43 @@ func (h *ProvisionHook) Run(ctx context.Context, name string, ui packersdk.Ui, c
|
|||
return nil
|
||||
}
|
||||
|
||||
// ProvisionerWrapOptions contains options for wrapping a provisioner with
|
||||
// additional behavior like pausing, timeouts, and retries.
|
||||
type ProvisionerWrapOptions struct {
|
||||
PauseBefore time.Duration
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// WrapProvisionerWithOptions wraps a provisioner with additional behavior
|
||||
// based on the provided options.
|
||||
func WrapProvisionerWithOptions(provisioner packersdk.Provisioner, opts ProvisionerWrapOptions) packersdk.Provisioner {
|
||||
wrapped := provisioner
|
||||
|
||||
if opts.PauseBefore != 0 {
|
||||
wrapped = &PausedProvisioner{
|
||||
PauseBefore: opts.PauseBefore,
|
||||
Provisioner: wrapped,
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Timeout != 0 {
|
||||
wrapped = &TimeoutProvisioner{
|
||||
Timeout: opts.Timeout,
|
||||
Provisioner: wrapped,
|
||||
}
|
||||
}
|
||||
|
||||
if opts.MaxRetries != 0 {
|
||||
wrapped = &RetriedProvisioner{
|
||||
MaxRetries: opts.MaxRetries,
|
||||
Provisioner: wrapped,
|
||||
}
|
||||
}
|
||||
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// PausedProvisioner is a Provisioner implementation that pauses before
|
||||
// the provisioner is actually run.
|
||||
type PausedProvisioner struct {
|
||||
|
|
|
|||
Loading…
Reference in a new issue