diff --git a/command/build.go b/command/build.go index c1ffac044..3f49634fb 100644 --- a/command/build.go +++ b/command/build.go @@ -153,7 +153,13 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int // Fetch and inject enforced provisioners from HCP Packer (if configured) if !cla.SkipEnforcement { if err := hcpRegistry.FetchEnforcedBlocks(buildCtx); err != nil { - c.Ui.Error(fmt.Sprintf("Warning: failed to fetch enforced provisioners: %s", err)) + 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) diff --git a/hcl2template/enforced_provisioner.go b/hcl2template/enforced_provisioner.go index d940a8ead..2695784d0 100644 --- a/hcl2template/enforced_provisioner.go +++ b/hcl2template/enforced_provisioner.go @@ -142,7 +142,7 @@ func parseProvisionerBlocksFromFile(parser *Parser, file *hcl.File, diags hcl.Di // 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) { +func (cfg *PackerConfig) GetCoreBuildProvisionerFromBlock(pb *ProvisionerBlock, buildName string) (packer.CoreBuildProvisioner, hcl.Diagnostics) { var diags hcl.Diagnostics // Get the provisioner plugin @@ -176,6 +176,14 @@ func (cfg *PackerConfig) GetCoreBuildProvisionerFromBlock(pb *ProvisionerBlock) 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 { diff --git a/hcl2template/enforced_provisioner_test.go b/hcl2template/enforced_provisioner_test.go index 7c2d81acf..4df1d7ba1 100644 --- a/hcl2template/enforced_provisioner_test.go +++ b/hcl2template/enforced_provisioner_test.go @@ -7,6 +7,88 @@ 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 TestParseProvisionerBlocks(t *testing.T) { tests := []struct { name string diff --git a/internal/hcp/registry/hcl.go b/internal/hcp/registry/hcl.go index de45e7a44..19eb14baa 100644 --- a/internal/hcp/registry/hcl.go +++ b/internal/hcp/registry/hcl.go @@ -114,7 +114,7 @@ func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent) if diags.HasErrors() { allDiags = append(allDiags, &hcl.Diagnostic{ - Severity: hcl.DiagWarning, + Severity: hcl.DiagError, Summary: fmt.Sprintf("Failed to parse enforced block %q", eb.Name), Detail: diags.Error(), }) @@ -135,7 +135,7 @@ func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl continue } - coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromBlock(pb) + coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromBlock(pb, build.Type) if moreDiags.HasErrors() { allDiags = append(allDiags, moreDiags...) continue diff --git a/internal/hcp/registry/json.go b/internal/hcp/registry/json.go index bd777358e..b3e782ab1 100644 --- a/internal/hcp/registry/json.go +++ b/internal/hcp/registry/json.go @@ -123,7 +123,13 @@ func (h *JSONRegistry) FetchEnforcedBlocks(ctx context.Context) error { // Note: JSON templates don't support enforced provisioners as they are a legacy format func (h *JSONRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics { if len(h.bucket.EnforcedBlocks) > 0 { - h.ui.Say("Warning: Enforced provisioners are not supported for legacy JSON templates") + return hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Enforced provisioners are not supported for legacy JSON templates", + Detail: "Linked enforced blocks were found for this bucket, but the current build is a legacy JSON template.", + }, + } } return nil } diff --git a/internal/hcp/registry/types.bucket.go b/internal/hcp/registry/types.bucket.go index aa56c689b..5195aef86 100644 --- a/internal/hcp/registry/types.bucket.go +++ b/internal/hcp/registry/types.bucket.go @@ -165,9 +165,13 @@ func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error { resp, err := bucket.client.GetEnforcedBlocksForBucket(ctx, bucket.Name) if err != nil { - // If the API doesn't support enforced blocks yet or returns not found, continue silently - log.Printf("[DEBUG] fetching enforced blocks for bucket %q: %v", bucket.Name, err) - return nil + if 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 { @@ -192,6 +196,7 @@ func (bucket *Bucket) FetchEnforcedBlocks(ctx context.Context) error { 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) diff --git a/internal/hcp/registry/types.bucket_enforced_test.go b/internal/hcp/registry/types.bucket_enforced_test.go new file mode 100644 index 000000000..fa9852c67 --- /dev/null +++ b/internal/hcp/registry/types.bucket_enforced_test.go @@ -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) + } +}