mirror of https://github.com/hashicorp/packer
parent
33740b7c61
commit
91cae751a6
@ -0,0 +1,111 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var enforcedProvisionerSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: buildProvisionerLabel, LabelNames: []string{"type"}},
|
||||
},
|
||||
}
|
||||
|
||||
// ParseProvisionerBlocks parses a partial HCL string that contains only
|
||||
// top-level provisioner blocks and returns the parsed ProvisionerBlock list.
|
||||
func ParseProvisionerBlocks(blockContent string) ([]*ProvisionerBlock, hcl.Diagnostics) {
|
||||
parser := &Parser{Parser: hclparse.NewParser()}
|
||||
file, diags := parser.ParseHCL([]byte(blockContent), "enforced_provisioner.pkr.hcl")
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
content, moreDiags := file.Body.Content(enforcedProvisionerSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
if diags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
ectx := &hcl.EvalContext{Variables: map[string]cty.Value{}}
|
||||
provisioners := make([]*ProvisionerBlock, 0, len(content.Blocks))
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
prov, moreDiags := parser.decodeProvisioner(block, ectx)
|
||||
diags = append(diags, moreDiags...)
|
||||
if moreDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
provisioners = append(provisioners, prov)
|
||||
}
|
||||
|
||||
return provisioners, diags
|
||||
}
|
||||
|
||||
// GetCoreBuildProvisionerFromBlock converts a ProvisionerBlock to a CoreBuildProvisioner.
|
||||
// This is used for enforced provisioners that need to be injected into builds.
|
||||
func (cfg *PackerConfig) GetCoreBuildProvisionerFromBlock(pb *ProvisionerBlock) (packer.CoreBuildProvisioner, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
// Get the provisioner plugin
|
||||
provisioner, err := cfg.parser.PluginConfig.Provisioners.Start(pb.PType)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to start enforced provisioner %q", pb.PType),
|
||||
Detail: fmt.Sprintf("The provisioner plugin could not be loaded: %s", err.Error()),
|
||||
})
|
||||
return packer.CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
// Create basic builder variables
|
||||
builderVars := map[string]interface{}{
|
||||
"packer_core_version": cfg.CorePackerVersionString,
|
||||
"packer_debug": strconv.FormatBool(cfg.debug),
|
||||
"packer_force": strconv.FormatBool(cfg.force),
|
||||
"packer_on_error": cfg.onError,
|
||||
"packer_sensitive_variables": []string{},
|
||||
}
|
||||
|
||||
// Create evaluation context
|
||||
ectx := cfg.EvalContext(BuildContext, nil)
|
||||
|
||||
// Create the HCL2Provisioner wrapper
|
||||
hclProvisioner := &HCL2Provisioner{
|
||||
Provisioner: provisioner,
|
||||
provisionerBlock: pb,
|
||||
evalContext: ectx,
|
||||
builderVariables: builderVars,
|
||||
}
|
||||
|
||||
// Prepare the provisioner
|
||||
err = hclProvisioner.HCL2Prepare(nil)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to prepare enforced provisioner %q", pb.PType),
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return packer.CoreBuildProvisioner{}, diags
|
||||
}
|
||||
|
||||
// Wrap provisioner with any special behavior (pause, timeout, retry)
|
||||
wrappedProvisioner := packer.WrapProvisionerWithOptions(hclProvisioner, packer.ProvisionerWrapOptions{
|
||||
PauseBefore: pb.PauseBefore,
|
||||
Timeout: pb.Timeout,
|
||||
MaxRetries: pb.MaxRetries,
|
||||
})
|
||||
|
||||
return packer.CoreBuildProvisioner{
|
||||
PType: pb.PType,
|
||||
PName: pb.PName,
|
||||
Provisioner: wrappedProvisioner,
|
||||
}, diags
|
||||
}
|
||||
@ -0,0 +1,212 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package hcl2template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseProvisionerBlocks(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blockContent string
|
||||
wantCount int
|
||||
wantTypes []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single shell provisioner",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'Hello from enforced provisioner'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple provisioners",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
inline = ["echo 'First enforced provisioner'"]
|
||||
}
|
||||
|
||||
provisioner "shell" {
|
||||
name = "security-scan"
|
||||
inline = ["echo 'Security scan running...'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 2,
|
||||
wantTypes: []string{"shell", "shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with pause_before",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
pause_before = "10s"
|
||||
inline = ["echo 'Waiting before execution'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with max_retries",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
max_retries = 3
|
||||
inline = ["echo 'Retry test'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with only filter",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu"]
|
||||
inline = ["echo 'Only for amazon-ebs.ubuntu'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "provisioner with except filter",
|
||||
blockContent: `
|
||||
provisioner "shell" {
|
||||
except = ["null.test"]
|
||||
inline = ["echo 'Except for null.test'"]
|
||||
}
|
||||
`,
|
||||
wantCount: 1,
|
||||
wantTypes: []string{"shell"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty block content",
|
||||
blockContent: "",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid HCL syntax",
|
||||
blockContent: "this is not valid { hcl }}}",
|
||||
wantCount: 0,
|
||||
wantTypes: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
blocks, diags := ParseProvisionerBlocks(tt.blockContent)
|
||||
|
||||
if tt.wantErr {
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("ParseProvisionerBlocks() expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if diags.HasErrors() {
|
||||
t.Errorf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
return
|
||||
}
|
||||
|
||||
if len(blocks) != tt.wantCount {
|
||||
t.Errorf("ParseProvisionerBlocks() got %d blocks, want %d", len(blocks), tt.wantCount)
|
||||
return
|
||||
}
|
||||
|
||||
for i, wantType := range tt.wantTypes {
|
||||
if blocks[i].PType != wantType {
|
||||
t.Errorf("ParseProvisionerBlocks() block[%d].PType = %q, want %q", i, blocks[i].PType, wantType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithPauseBefore(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
pause_before = "30s"
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
// pause_before should be parsed as 30 seconds
|
||||
if blocks[0].PauseBefore.Seconds() != 30 {
|
||||
t.Errorf("Expected PauseBefore=30s, got %v", blocks[0].PauseBefore)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithMaxRetries(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
max_retries = 5
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
if blocks[0].MaxRetries != 5 {
|
||||
t.Errorf("Expected MaxRetries=5, got %d", blocks[0].MaxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseProvisionerBlocksWithOnlyExcept(t *testing.T) {
|
||||
blockContent := `
|
||||
provisioner "shell" {
|
||||
only = ["amazon-ebs.ubuntu", "azure-arm.windows"]
|
||||
inline = ["echo 'test'"]
|
||||
}
|
||||
`
|
||||
blocks, diags := ParseProvisionerBlocks(blockContent)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("ParseProvisionerBlocks() unexpected error: %v", diags)
|
||||
}
|
||||
|
||||
if len(blocks) != 1 {
|
||||
t.Fatalf("Expected 1 block, got %d", len(blocks))
|
||||
}
|
||||
|
||||
// Check only filter
|
||||
if len(blocks[0].OnlyExcept.Only) != 2 {
|
||||
t.Errorf("Expected 2 only values, got %d", len(blocks[0].OnlyExcept.Only))
|
||||
}
|
||||
|
||||
// Skip should return true for sources not in the only list
|
||||
if !blocks[0].OnlyExcept.Skip("null.test") {
|
||||
t.Error("Skip() should return true for source not in only list")
|
||||
}
|
||||
|
||||
// Skip should return false for sources in the only list
|
||||
if blocks[0].OnlyExcept.Skip("amazon-ebs.ubuntu") {
|
||||
t.Error("Skip() should return false for source in only list")
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
// Copyright IBM Corp. 2013, 2025
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
hcpPackerService "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/client/packer_service"
|
||||
hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
|
||||
)
|
||||
|
||||
// CreateEnforcedBlock creates a new enforced block in the HCP Packer registry.
|
||||
// The block content contains raw HCL provisioner configuration that will be
|
||||
// enforced on all builds for buckets linked to this enforced block.
|
||||
func (c *Client) CreateEnforcedBlock(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
blockContent string,
|
||||
version string,
|
||||
templateType hcpPackerModels.HashicorpCloudPacker20230101TemplateType,
|
||||
description string,
|
||||
labels map[string]string,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceCreateEnforcedBlockParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
params.Body = &hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockBody{
|
||||
Name: name,
|
||||
BlockContent: blockContent,
|
||||
Version: version,
|
||||
TemplateType: &templateType,
|
||||
AdditionalDescription: description,
|
||||
Labels: labels,
|
||||
}
|
||||
|
||||
resp, err := c.Packer.PackerServiceCreateEnforcedBlock(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
|
||||
// GetEnforcedBlock retrieves a single enforced block by its ID.
|
||||
func (c *Client) GetEnforcedBlock(
|
||||
ctx context.Context,
|
||||
enforcedBlockID string,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceGetEnforcedBlockParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
params.EnforcedBlockID = enforcedBlockID
|
||||
|
||||
resp, err := c.Packer.PackerServiceGetEnforcedBlock(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
|
||||
// ListEnforcedBlocks lists all enforced blocks in the current project.
|
||||
func (c *Client) ListEnforcedBlocks(
|
||||
ctx context.Context,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101ListEnforcedBlocksResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceListEnforcedBlocksParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
|
||||
resp, err := c.Packer.PackerServiceListEnforcedBlocks(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
|
||||
// CreateEnforcedBlockVersion creates a new version of an existing enforced block.
|
||||
// This allows updating the block content while keeping a version history.
|
||||
func (c *Client) CreateEnforcedBlockVersion(
|
||||
ctx context.Context,
|
||||
enforcedBlockID string,
|
||||
blockContent string,
|
||||
version string,
|
||||
templateType hcpPackerModels.HashicorpCloudPacker20230101TemplateType,
|
||||
description string,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockVersionResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceCreateEnforcedBlockVersionParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
params.EnforcedBlockID = enforcedBlockID
|
||||
params.Body = &hcpPackerModels.HashicorpCloudPacker20230101CreateEnforcedBlockVersionBody{
|
||||
BlockContent: blockContent,
|
||||
Version: version,
|
||||
TemplateType: &templateType,
|
||||
AdditionalDescription: description,
|
||||
}
|
||||
|
||||
resp, err := c.Packer.PackerServiceCreateEnforcedBlockVersion(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
|
||||
// GetEnforcedBlockVersions retrieves all versions of an enforced block.
|
||||
func (c *Client) GetEnforcedBlockVersions(
|
||||
ctx context.Context,
|
||||
enforcedBlockID string,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlockVersionsResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceGetEnforcedBlockVersionsParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
params.EnforcedBlockID = enforcedBlockID
|
||||
|
||||
resp, err := c.Packer.PackerServiceGetEnforcedBlockVersions(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
|
||||
// GetEnforcedBlocksForBucket fetches all enforced blocks linked to a bucket.
|
||||
// This is the key method used during packer build to auto-inject provisioners.
|
||||
// The response includes EnforcedBlockDetail entries each with an active version
|
||||
// containing the raw HCL block_content to be parsed and injected.
|
||||
func (c *Client) GetEnforcedBlocksForBucket(
|
||||
ctx context.Context,
|
||||
bucketName string,
|
||||
) (*hcpPackerModels.HashicorpCloudPacker20230101GetEnforcedBlocksByBucketResponse, error) {
|
||||
|
||||
params := hcpPackerService.NewPackerServiceGetEnforcedBlocksByBucketParamsWithContext(ctx)
|
||||
params.LocationOrganizationID = c.OrganizationID
|
||||
params.LocationProjectID = c.ProjectID
|
||||
params.BucketName = bucketName
|
||||
|
||||
resp, err := c.Packer.PackerServiceGetEnforcedBlocksByBucket(params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Payload, nil
|
||||
}
|
||||
Loading…
Reference in new issue