mirror of https://github.com/hashicorp/packer
Feature/enforced provisioner (#13591)
* added the parser for the enforced block * Enhance enforced provisioner parsing to support HCL and JSON formats - Updated ParseProvisionerBlocks to handle both HCL and JSON syntax, including legacy JSON format. - Added comprehensive test cases for JSON provisioner parsing. - Improved ExtractBuildProvisionerHCL to merge inline commands from shell provisioners. - Enhanced logging for enforced block operations in HCP Packer. * Remove PublishEnforcedBlocks function from Bucket struct * Remove ExtractBuildProvisionerHCL function and unused imports * Reverted the version upgrade * Added the internal-sdk for the enforcedProvsioner api changes * Enhance enforced provisioner handling and error reporting - Update error handling in FetchEnforcedBlocks to return detailed errors instead of warnings. - Modify GetCoreBuildProvisionerFromBlock to accept build name for overrides. - Add tests for FetchEnforcedBlocks to ensure correct behavior and error handling. - Implement diagnostics for unsupported legacy JSON templates. * Implement enforced provisioner parsing and handling - Introduced a new package `enforcedparser` to handle parsing of enforced provisioner blocks from HCL and JSON formats. - Refactored existing code to utilize the new `ParseProvisionerBlocks` function from the `enforcedparser` package. - Updated `GetCoreBuildProvisionerFromEnforcedBlock` method to convert enforced provisioner blocks into core build provisioners. - Enhanced error handling and logging during the parsing process. - Added tests for the new parsing functionality and ensured existing tests were updated to reflect changes. - Modified `InjectEnforcedProvisioners` method in JSON registry to utilize the new parsing logic. * Add test case for -skip-enforcement flag in BuildArgs * Refactor sensitive variable handling in provisioners and add related tests * Refactor enforced provisioner handling: remove internal parser, update tests, and streamline API interactions * Enhance provisioner block parsing: add error handling for invalid combinations and expand test coverage * Remove internal SDK replacement for enforced block types in go.mod * Update dependencies in go.mod and go.sum: bump hcp-sdk-go and packer-plugin-sdk versions, adjust syft version, and update OpenTelemetry packages * Update hcp-sdk-go dependency to v0.172.0 in go.mod and go.sum * Fix formatting in TestBuildCommand_ParseArgs and add newline at end of json_enforced_test.go * Refactor testJSONRegistryWithBuilds: remove environment variable setup and streamline registry initialization * Rename injected variable for clarity in InjectEnforcedProvisioners function --------- Co-authored-by: Hari Om <58305594+Madhav008@users.noreply.github.com>backport_v1.15.2
parent
16292a5251
commit
3b0f5d512a
@ -0,0 +1,81 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
// GetCoreBuildProvisionerFromBlock converts a ProvisionerBlock to a CoreBuildProvisioner.
|
||||
// This is used for enforced provisioners that need to be injected into builds.
|
||||
func (cfg *PackerConfig) GetCoreBuildProvisionerFromBlock(pb *ProvisionerBlock, buildName string) (packer.CoreBuildProvisioner, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
// Get the provisioner plugin
|
||||
provisioner, err := cfg.parser.PluginConfig.Provisioners.Start(pb.PType)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", pb.PType),
|
||||
Detail: fmt.Sprintf("The provisioner plugin could not be loaded: %s", err.Error()),
|
||||
})
|
||||
return packer.CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
// Create basic builder variables
|
||||
builderVars := map[string]interface{}{
|
||||
"packer_core_version": cfg.CorePackerVersionString,
|
||||
"packer_debug": strconv.FormatBool(cfg.debug),
|
||||
"packer_force": strconv.FormatBool(cfg.force),
|
||||
"packer_on_error": cfg.onError,
|
||||
"packer_sensitive_variables": cfg.sensitiveInputVariableKeys(),
|
||||
}
|
||||
|
||||
// Create evaluation context
|
||||
ectx := cfg.EvalContext(BuildContext, nil)
|
||||
|
||||
// Create the HCL2Provisioner wrapper
|
||||
hclProvisioner := &HCL2Provisioner{
|
||||
Provisioner: provisioner,
|
||||
provisionerBlock: pb,
|
||||
evalContext: ectx,
|
||||
builderVariables: builderVars,
|
||||
}
|
||||
|
||||
if pb.Override != nil {
|
||||
if override, ok := pb.Override[buildName]; ok {
|
||||
if typedOverride, ok := override.(map[string]interface{}); ok {
|
||||
hclProvisioner.override = typedOverride
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the provisioner
|
||||
err = hclProvisioner.HCL2Prepare(nil)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to prepare enforced provisioner %q", pb.PType),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return packer.CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
// Wrap provisioner with any special behavior (pause, timeout, retry)
|
||||
wrappedProvisioner := packer.WrapProvisionerWithOptions(hclProvisioner, packer.ProvisionerWrapOptions{
|
||||
PauseBefore: pb.PauseBefore,
|
||||
Timeout: pb.Timeout,
|
||||
MaxRetries: pb.MaxRetries,
|
||||
})
|
||||
|
||||
return packer.CoreBuildProvisioner{
|
||||
PType: pb.PType,
|
||||
PName: pb.PName,
|
||||
Provisioner: wrappedProvisioner,
|
||||
}, diags
|
||||
}
|
||||
@ -0,0 +1,146 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var standaloneProvisionerSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: buildProvisionerLabel, LabelNames: []string{"type"}},
|
||||
},
|
||||
}
|
||||
|
||||
// ParseProvisionerBlocks parses raw provisioner block content into ProvisionerBlocks.
|
||||
// It accepts HCL, HCL JSON, and the legacy JSON payload used for enforced provisioners.
|
||||
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
parser := &Parser{Parser: hclparse.NewParser()}
|
||||
return parser.parseProvisionerBlocks(blockContent)
|
||||
}
|
||||
|
||||
func (p *Parser) parseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
hclParser := p.Parser
|
||||
if hclParser == nil {
|
||||
hclParser = hclparse.NewParser()
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] parsing provisioner block content as HCL")
|
||||
|
||||
file, diags := hclParser.ParseHCL([]byte(blockContent), "provisioner.pkr.hcl")
|
||||
if !diags.HasErrors() {
|
||||
provisioners, provisionerDiags := p.parseProvisionerBlocksFromFile(file, diags)
|
||||
if provisionerDiags.HasErrors() {
|
||||
return nil, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] parsed provisioner block content as HCL")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] failed to parse provisioner block content as HCL, trying JSON fallback")
|
||||
|
||||
jsonFile, jsonDiags := hclParser.ParseJSON([]byte(blockContent), "provisioner.pkr.json")
|
||||
if jsonDiags.HasErrors() {
|
||||
log.Printf("[DEBUG] failed to parse provisioner block content as JSON")
|
||||
return nil, append(diags, jsonDiags...)
|
||||
}
|
||||
|
||||
provisioners, provisionerDiags := p.parseProvisionerBlocksFromFile(jsonFile, jsonDiags)
|
||||
if !provisionerDiags.HasErrors() && len(provisioners) > 0 {
|
||||
log.Printf("[DEBUG] parsed provisioner block content as JSON")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
|
||||
legacyJSON, ok, err := normalizeLegacyProvisionersJSON(blockContent)
|
||||
if err == nil && ok {
|
||||
legacyFile, legacyDiags := hclParser.ParseJSON([]byte(legacyJSON), "provisioner_legacy.pkr.json")
|
||||
if !legacyDiags.HasErrors() {
|
||||
legacyProvisioners, legacyProvisionerDiags := p.parseProvisionerBlocksFromFile(legacyFile, legacyDiags)
|
||||
if !legacyProvisionerDiags.HasErrors() && len(legacyProvisioners) > 0 {
|
||||
log.Printf("[DEBUG] parsed provisioner block content as legacy JSON")
|
||||
return legacyProvisioners, legacyProvisionerDiags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if provisionerDiags.HasErrors() {
|
||||
return nil, provisionerDiags
|
||||
}
|
||||
log.Printf("[DEBUG] parsed provisioner block content as JSON but found no valid provisioner blocks")
|
||||
return provisioners, provisionerDiags
|
||||
}
|
||||
|
||||
func normalizeLegacyProvisionersJSON(blockContent string) (string, bool, error) {
|
||||
type legacyPayload struct {
|
||||
Provisioners []map[string]interface{} `json:"provisioners"`
|
||||
}
|
||||
|
||||
var payload legacyPayload
|
||||
if err := json.Unmarshal([]byte(blockContent), &payload); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if len(payload.Provisioners) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
normalized := make([]map[string]interface{}, 0, len(payload.Provisioners))
|
||||
for _, provisioner := range payload.Provisioners {
|
||||
typeName, ok := provisioner["type"].(string)
|
||||
if !ok || typeName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cfg := make(map[string]interface{})
|
||||
for key, value := range provisioner {
|
||||
if key == "type" {
|
||||
continue
|
||||
}
|
||||
cfg[key] = value
|
||||
}
|
||||
|
||||
normalized = append(normalized, map[string]interface{}{typeName: cfg})
|
||||
}
|
||||
|
||||
if len(normalized) == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
out := map[string]interface{}{
|
||||
buildProvisionerLabel: normalized,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
return string(b), true, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseProvisionerBlocksFromFile(file *hcl.File, diags hcl.Diagnostics) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
content, moreDiags := file.Body.Content(standaloneProvisionerSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ectx := &hcl.EvalContext{Variables: map[string]cty.Value{}}
|
||||
provisioners := make([]*ProvisionerBlock, 0, len(content.Blocks))
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
provisioner, moreDiags := p.decodeProvisioner(block, ectx)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
provisioners = append(provisioners, provisioner)
|
||||
}
|
||||
|
||||
return provisioners, diags
|
||||
}
|
||||
@ -0,0 +1,496 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCoreBuildProvisionerFromBlock_AppliesOverrideForBuild(t *testing.T) {
|
||||
parser := getBasicParser()
|
||||
cfg := &PackerConfig{
|
||||
parser: parser,
|
||||
CorePackerVersionString: lockedVersion,
|
||||
}
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(`
|
||||
provisioner "shell" {
|
||||
override = {
|
||||
"amazon-ebs.ubuntu" = {
|
||||
bool = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
|
||||
if !ok {
|
||||
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
|
||||
}
|
||||
|
||||
if hclProv.override == nil {
|
||||
t.Fatal("expected override to be applied, got nil")
|
||||
}
|
||||
|
||||
if got, ok := hclProv.override["bool"]; !ok || got != false {
|
||||
t.Fatalf("expected override bool=false, got %#v", hclProv.override["bool"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCoreBuildProvisionerFromBlock_OverrideNotAppliedForOtherBuild(t *testing.T) {
|
||||
parser := getBasicParser()
|
||||
cfg := &PackerConfig{
|
||||
parser: parser,
|
||||
CorePackerVersionString: lockedVersion,
|
||||
}
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(`
|
||||
provisioner "shell" {
|
||||
override = {
|
||||
"amazon-ebs.ubuntu" = {
|
||||
bool = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "virtualbox-iso.base")
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
|
||||
if !ok {
|
||||
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
|
||||
}
|
||||
|
||||
if hclProv.override != nil {
|
||||
t.Fatalf("expected no override to be applied, got %#v", hclProv.override)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCoreBuildProvisionerFromBlock_IncludesSensitiveVariables(t *testing.T) {
|
||||
parser := getBasicParser()
|
||||
cfg := &PackerConfig{
|
||||
parser: parser,
|
||||
CorePackerVersionString: lockedVersion,
|
||||
InputVariables: Variables{
|
||||
"visible": &Variable{Name: "visible"},
|
||||
"secret": &Variable{Name: "secret", Sensitive: true},
|
||||
},
|
||||
}
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(`
|
||||
provisioner "shell" {
|
||||
override = {
|
||||
"amazon-ebs.ubuntu" = {
|
||||
bool = false
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
coreProv, diags := cfg.GetCoreBuildProvisionerFromBlock(blocks[0], "amazon-ebs.ubuntu")
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("GetCoreBuildProvisionerFromBlock() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
hclProv, ok := coreProv.Provisioner.(*HCL2Provisioner)
|
||||
if !ok {
|
||||
t.Fatalf("expected *HCL2Provisioner, got %T", coreProv.Provisioner)
|
||||
}
|
||||
|
||||
sensitiveVars, ok := hclProv.builderVariables["packer_sensitive_variables"].([]string)
|
||||
if !ok {
|
||||
t.Fatalf("expected []string packer_sensitive_variables, got %T", hclProv.builderVariables["packer_sensitive_variables"])
|
||||
}
|
||||
|
||||
if len(sensitiveVars) != 1 || sensitiveVars[0] != "secret" {
|
||||
t.Fatalf("expected sensitive vars [secret], got %#v", sensitiveVars)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blockContent string
|
||||
wantCount int
|
||||
wantTypes []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single shell provisioner",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'Hello from enforced provisioner'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple provisioners",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'First enforced provisioner'"]
|
||||
}
|
||||
|
||||
provisioner "shell" {
|
||||
name = "security-scan"
|
||||
inline = ["echo 'Security scan running...'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with pause_before",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
pause_before = "10s"
|
||||
inline = ["echo 'Waiting before execution'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with max_retries",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
max_retries = 3
|
||||
inline = ["echo 'Retry test'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with only filter",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu"]
|
||||
inline = ["echo 'Only for amazon-ebs.ubuntu'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with except filter",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
except = ["null.test"]
|
||||
inline = ["echo 'Except for null.test'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with both only and except",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu"]
|
||||
except = ["null.test"]
|
||||
inline = ["echo 'invalid filter combination'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty block content",
|
||||
blockContent: "",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace only block content",
|
||||
blockContent: "\n\n\t \n",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid HCL syntax",
|
||||
blockContent: "this is not valid { hcl }}}",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "json single shell provisioner",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo 'Hello from enforced provisioner JSON'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json provisioner with escaped newline in string value",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo first line\necho second line"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "json multiple provisioners",
|
||||
blockContent: `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"inline": ["echo 'first'"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"shell": {
|
||||
"name": "security-scan",
|
||||
"inline": ["echo 'second'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json syntax",
|
||||
blockContent: `{"provisioner": [ { "shell": { "inline": ["test"] } ] }`,
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "legacy json provisioners format",
|
||||
blockContent: `{
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": ["echo legacy json format"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "legacy json provisioners with escaped newline and multiple types",
|
||||
blockContent: `{
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"inline": ["echo legacy line 1\necho legacy line 2"]
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source": "source.txt",
|
||||
"destination": "destination.txt"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "file"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "hcl provisioners with windows newlines",
|
||||
blockContent: "provisioner \"shell\" {\r\n inline = [\"echo first\"]\r\n}\r\n\r\nprovisioner \"file\" {\r\n source = \"source.txt\"\r\n destination = \"destination.txt\"\r\n}\r\n",
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "file"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
blocks, diags := ParseProvisionerBlocks(tt.blockContent)
|
||||
|
||||
if tt.wantErr {
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("ParseProvisionerBlocks() expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
return
|
||||
}
|
||||
|
||||
if len(blocks) != tt.wantCount {
|
||||
t.Errorf("ParseProvisionerBlocks() got %d blocks, want %d", len(blocks), tt.wantCount)
|
||||
return
|
||||
}
|
||||
|
||||
for i, wantType := range tt.wantTypes {
|
||||
if blocks[i].PType != wantType {
|
||||
t.Errorf("ParseProvisionerBlocks() block[%d].PType = %q, want %q", i, blocks[i].PType, wantType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithPauseBefore(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
pause_before = "30s"
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
// pause_before should be parsed as 30 seconds
|
||||
if blocks[0].PauseBefore.Seconds() != 30 {
|
||||
t.Errorf("Expected PauseBefore=30s, got %v", blocks[0].PauseBefore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithMaxRetries(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
max_retries = 5
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
if blocks[0].MaxRetries != 5 {
|
||||
t.Errorf("Expected MaxRetries=5, got %d", blocks[0].MaxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithOnlyExcept(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu", "azure-arm.windows"]
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
// Check only filter
|
||||
if len(blocks[0].OnlyExcept.Only) != 2 {
|
||||
t.Errorf("Expected 2 only values, got %d", len(blocks[0].OnlyExcept.Only))
|
||||
}
|
||||
|
||||
// Skip should return true for sources not in the only list
|
||||
if !blocks[0].OnlyExcept.Skip("null.test") {
|
||||
t.Error("Skip() should return true for source not in only list")
|
||||
}
|
||||
|
||||
// Skip should return false for sources in the only list
|
||||
if blocks[0].OnlyExcept.Skip("amazon-ebs.ubuntu") {
|
||||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksJSONWithOptions(t *testing.T) {
|
||||
blockContent := `{
|
||||
"provisioner": [
|
||||
{
|
||||
"shell": {
|
||||
"pause_before": "15s",
|
||||
"max_retries": 2,
|
||||
"only": ["docker.ubuntu"],
|
||||
"inline": ["echo 'json test'"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
if blocks[0].PauseBefore.Seconds() != 15 {
|
||||
t.Errorf("Expected PauseBefore=15s, got %v", blocks[0].PauseBefore)
|
||||
}
|
||||
|
||||
if blocks[0].MaxRetries != 2 {
|
||||
t.Errorf("Expected MaxRetries=2, got %d", blocks[0].MaxRetries)
|
||||
}
|
||||
|
||||
if blocks[0].OnlyExcept.Skip("docker.ubuntu") {
|
||||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
|
||||
if !blocks[0].OnlyExcept.Skip("null.test") {
|
||||
t.Error("Skip() should return true for source not in only list")
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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"])
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue