From 0e354e503b5e190f565665c52dc803f7c9467392 Mon Sep 17 00:00:00 2001 From: Tanmay Jain Date: Fri, 21 Nov 2025 12:43:46 +0530 Subject: [PATCH 1/4] FEAT: Adds support for updating HCP Packer registry channels Enables specifying channels in the build configuration to automatically update existing HCP Packer registry channels to point to the new build version upon completion. Improves workflow automation and reduces manual channel management for users. --- .../types.build.hcp_packer_registry.go | 4 +++ internal/hcp/api/service_build.go | 20 ++++++++++++ internal/hcp/registry/types.bucket.go | 32 +++++++++++++++++++ .../blocks/build/hcp_packer_registry.mdx | 5 ++- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/hcl2template/types.build.hcp_packer_registry.go b/hcl2template/types.build.hcp_packer_registry.go index 640359aa1..2b7c1f71e 100644 --- a/hcl2template/types.build.hcp_packer_registry.go +++ b/hcl2template/types.build.hcp_packer_registry.go @@ -20,6 +20,8 @@ type HCPPackerRegistryBlock struct { BucketLabels map[string]string // Build labels BuildLabels map[string]string + // Channels + Channels []string HCL2Ref } @@ -37,6 +39,7 @@ func (p *Parser) decodeHCPRegistry(block *hcl.Block, cfg *PackerConfig) (*HCPPac Labels map[string]string `hcl:"labels,optional"` BucketLabels map[string]string `hcl:"bucket_labels,optional"` BuildLabels map[string]string `hcl:"build_labels,optional"` + Channels []string `hcl:"channels,optional"` Config hcl.Body `hcl:",remain"` } ectx := cfg.EvalContext(BuildContext, nil) @@ -69,6 +72,7 @@ func (p *Parser) decodeHCPRegistry(block *hcl.Block, cfg *PackerConfig) (*HCPPac par.Slug = b.Slug par.Description = b.Description + par.Channels = b.Channels if len(b.Labels) > 0 && len(b.BucketLabels) > 0 { diags = append(diags, &hcl.Diagnostic{ diff --git a/internal/hcp/api/service_build.go b/internal/hcp/api/service_build.go index a857192c3..51a0f37f6 100644 --- a/internal/hcp/api/service_build.go +++ b/internal/hcp/api/service_build.go @@ -118,3 +118,23 @@ func (c *Client) UploadSbom( _, err := c.Packer.PackerServiceUploadSbom(params, nil) return err } + +func (c *Client) UpdateChannel( + ctx context.Context, + bucketName, channelName string, + body *hcpPackerModels.HashicorpCloudPacker20230101UpdateChannelBody, +) (*hcpPackerAPI.PackerServiceUpdateChannelOK, error) { + + params := hcpPackerAPI.NewPackerServiceUpdateChannelParamsWithContext(ctx) + params.LocationOrganizationID = c.OrganizationID + params.LocationProjectID = c.ProjectID + params.BucketName = bucketName + params.ChannelName = channelName + params.Body = body + resp, err := c.Packer.PackerServiceUpdateChannel(params, nil) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/internal/hcp/registry/types.bucket.go b/internal/hcp/registry/types.bucket.go index 264852492..c81f03412 100644 --- a/internal/hcp/registry/types.bucket.go +++ b/internal/hcp/registry/types.bucket.go @@ -36,6 +36,7 @@ type Bucket struct { Destination string BucketLabels map[string]string BuildLabels map[string]string + Channels []string SourceExternalIdentifierToParentVersions map[string]ParentVersion RunningBuilds map[string]chan struct{} Version *Version @@ -94,6 +95,7 @@ func (bucket *Bucket) ReadFromHCPPackerRegistryBlock(registryBlock *hcl2template bucket.Description = registryBlock.Description bucket.BucketLabels = registryBlock.BucketLabels bucket.BuildLabels = registryBlock.BuildLabels + bucket.Channels = registryBlock.Channels // If there's already a Name this was set from env variable. // In Packer, env variable overrides config values so we keep it that way for consistency. if bucket.Name == "" && registryBlock.Slug != "" { @@ -244,6 +246,30 @@ func (bucket *Bucket) uploadSbom(ctx context.Context, buildName string, sbom pac return bucket.client.UploadSbom(ctx, bucket.Name, bucket.Version.Fingerprint, buildToUpdate.ID, sbom) } +func (bucket *Bucket) updateChannels(ctx context.Context) error { + if len(bucket.Channels) == 0 { + return nil + } + + log.Printf("[INFO] Updating %d channel(s) to point to version %s", len(bucket.Channels), bucket.Version.ID) + + body := &hcpPackerModels.HashicorpCloudPacker20230101UpdateChannelBody{ + VersionFingerprint: bucket.Version.Fingerprint, + UpdateMask: "versionFingerprint", + } + + for _, channel := range bucket.Channels { + log.Printf("[INFO] Updating channel %s to version %s", channel, bucket.Version.ID) + _, err := bucket.client.UpdateChannel(ctx, bucket.Name, channel, body) + if err != nil { + log.Printf("[ERROR] Failed to update channel %s: %s", channel, err) + return fmt.Errorf("failed to update channel %s: %w", channel, err) + } + } + + return nil +} + // markBuildComplete should be called to set a build on the HCP Packer registry to DONE. // Upon a successful call markBuildComplete will publish all artifacts created by the named build, // and set the build to done. A build with no artifacts can not be set to DONE. @@ -726,6 +752,12 @@ func (bucket *Bucket) doCompleteBuild( parErr) } + // Update channels after build is marked complete + channelErr := bucket.updateChannels(ctx) + if channelErr != nil { + log.Printf("[ERROR] Failed to update channels after completing build %s: %s", buildName, channelErr) + } + return append(packerSDKArtifacts, ®istryArtifact{ BuildName: buildName, BucketName: bucket.Name, diff --git a/website/content/docs/templates/hcl_templates/blocks/build/hcp_packer_registry.mdx b/website/content/docs/templates/hcl_templates/blocks/build/hcp_packer_registry.mdx index 503ba23d4..e649e194e 100644 --- a/website/content/docs/templates/hcl_templates/blocks/build/hcp_packer_registry.mdx +++ b/website/content/docs/templates/hcl_templates/blocks/build/hcp_packer_registry.mdx @@ -10,7 +10,7 @@ This topic provides reference information about the `hcp_packer_registry` block. ## Description -The `hcp_packer_registry` block configures details about an image Packer creates or updates in the HCP Packer registry. Use the `hcp_packer_registry` block to customize the metadata Packer sends to HCP Packer Registry. +The `hcp_packer_registry` block configures details about an image Packer creates or updates in the HCP Packer registry. Use the `hcp_packer_registry` block to customize the metadata Packer sends to HCP Packer Registry. To get started with HCP Packer, refer to the [HCP Packer documentation](/hcp/docs/packer) or try the [Get Started with HCP Packer tutorials](/packer/tutorials/hcp-get-started). @@ -77,5 +77,8 @@ Some nice description about the image which artifact is being published to HCP P Packer registry. Should contain a maximum of 255 characters. Defaults to `build.description` if not set. +- `channels` ([]string) - List of channel to update to point to the new build + once the build is complete. Channels must already exist in the HCP Packer registry. + - `labels` (map[string]string) - Deprecated in Packer 1.7.9. See [`bucket_labels`](#bucket_labels) for details. From 52448da979a008c8b8d8828af9efa526fd10a302 Mon Sep 17 00:00:00 2001 From: Tanmay Jain Date: Fri, 21 Nov 2025 14:35:51 +0530 Subject: [PATCH 2/4] Added Test Cases for Channel Assignment --- internal/hcp/api/mock_service.go | 32 +++++ internal/hcp/registry/types.bucket_test.go | 157 +++++++++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/internal/hcp/api/mock_service.go b/internal/hcp/api/mock_service.go index db2f64a7a..72718a814 100644 --- a/internal/hcp/api/mock_service.go +++ b/internal/hcp/api/mock_service.go @@ -22,6 +22,7 @@ type MockPackerClientService struct { CreateBucketCalled, UpdateBucketCalled, GetBucketCalled, BucketNotFound bool CreateVersionCalled, GetVersionCalled, VersionAlreadyExist, VersionCompleted bool CreateBuildCalled, UpdateBuildCalled, ListBuildsCalled, BuildAlreadyDone bool + UpdateChannelCalled bool TrackCalledServiceMethods bool // Mock Creates @@ -289,3 +290,34 @@ func (svc *MockPackerClientService) PackerServiceListBuilds( return ok, nil } + +func (svc *MockPackerClientService) PackerServiceUpdateChannel( + params *hcpPackerService.PackerServiceUpdateChannelParams, _ runtime.ClientAuthInfoWriter, + opts ...hcpPackerService.ClientOption, +) (*hcpPackerService.PackerServiceUpdateChannelOK, error) { + if params.BucketName == "" { + return nil, errors.New("no valid BucketName was passed in") + } + + if params.ChannelName == "" { + return nil, errors.New("no valid ChannelName was passed in") + } + + if params.Body == nil { + return nil, errors.New("no valid update body was passed in") + } + + if svc.TrackCalledServiceMethods { + svc.UpdateChannelCalled = true + } + + ok := hcpPackerService.NewPackerServiceUpdateChannelOK() + ok.Payload = &hcpPackerModels.HashicorpCloudPacker20230101UpdateChannelResponse{ + Channel: &hcpPackerModels.HashicorpCloudPacker20230101Channel{ + Name: params.ChannelName, + BucketName: params.BucketName, + }, + } + + return ok, nil +} diff --git a/internal/hcp/registry/types.bucket_test.go b/internal/hcp/registry/types.bucket_test.go index 6cd49503c..fe5c1bd4f 100644 --- a/internal/hcp/registry/types.bucket_test.go +++ b/internal/hcp/registry/types.bucket_test.go @@ -373,6 +373,34 @@ func TestReadFromHCLBuildBlock(t *testing.T) { "version": "1.7.0", "based_off": "alpine", }, + Channels: nil, + }, + }, + { + desc: "configure bucket with channels", + buildBlock: &hcl2template.BuildBlock{ + HCPPackerRegistry: &hcl2template.HCPPackerRegistryBlock{ + Slug: "channel-test-bucket", + Description: "bucket with channel configuration", + Channels: []string{"production", "staging", "development"}, + BucketLabels: map[string]string{ + "team": "infrastructure", + }, + BuildLabels: map[string]string{ + "version": "2.0.0", + }, + }, + }, + expectedBucket: &Bucket{ + Name: "channel-test-bucket", + Description: "bucket with channel configuration", + Channels: []string{"production", "staging", "development"}, + BucketLabels: map[string]string{ + "team": "infrastructure", + }, + BuildLabels: map[string]string{ + "version": "2.0.0", + }, }, }, } @@ -509,3 +537,132 @@ func TestCompleteBuild(t *testing.T) { }) } } + +func TestBucket_UpdateChannels(t *testing.T) { + tests := []struct { + name string + channels []string + wantErr bool + wantCalled bool + }{ + { + name: "no channels", + channels: []string{}, + wantErr: false, + wantCalled: false, + }, + { + name: "single channel", + channels: []string{"production"}, + wantErr: false, + wantCalled: true, + }, + { + name: "multiple channels", + channels: []string{"staging", "production", "dev"}, + wantErr: false, + wantCalled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockService := hcpPackerAPI.NewMockPackerClientService() + mockService.TrackCalledServiceMethods = true + + b := &Bucket{ + Name: "test-bucket", + Channels: tt.channels, + client: &hcpPackerAPI.Client{ + Packer: mockService, + }, + } + + // Initialize version + b.Version = &Version{ + ID: "test-version-id", + Fingerprint: "test-fingerprint", + } + + err := b.updateChannels(context.Background()) + + if (err != nil) != tt.wantErr { + t.Errorf("updateChannels() error = %v, wantErr %v", err, tt.wantErr) + } + + if mockService.UpdateChannelCalled != tt.wantCalled { + t.Errorf("UpdateChannelCalled = %v, want %v", mockService.UpdateChannelCalled, tt.wantCalled) + } + }) + } +} + +func TestBucket_DoCompleteBuild_WithChannels(t *testing.T) { + mockService := hcpPackerAPI.NewMockPackerClientService() + mockService.VersionAlreadyExist = true + mockService.TrackCalledServiceMethods = true + + b := &Bucket{ + Name: "TestBucket", + Channels: []string{"production", "staging"}, + client: &hcpPackerAPI.Client{ + Packer: mockService, + }, + } + + b.Version = NewVersion() + err := b.Version.Initialize() + if err != nil { + t.Fatalf("unexpected failure initializing version: %v", err) + } + + b.Version.expectedBuilds = append(b.Version.expectedBuilds, "happycloud.image") + mockService.ExistingBuilds = append(mockService.ExistingBuilds, "happycloud.image") + + err = b.Initialize(context.TODO(), models.HashicorpCloudPacker20230101TemplateTypeHCL2) + if err != nil { + t.Fatalf("unexpected failure initializing bucket: %v", err) + } + + err = b.populateVersion(context.TODO()) + if err != nil { + t.Fatalf("unexpected failure populating version: %v", err) + } + + // Create mock HCP-compatible artifacts + mockArtifacts := []packer.Artifact{ + &packer.MockArtifact{ + BuilderIdValue: "builder.test", + FilesValue: []string{"file.one"}, + IdValue: "test-artifact", + StateValues: map[string]interface{}{ + "builder.test": "OK", + image.ArtifactStateURI: &image.Image{ + ImageID: "hcp-test-image", + ProviderName: "test-provider", + ProviderRegion: "test-region", + Labels: map[string]string{}, + SourceImageID: "", + }, + }, + DestroyCalled: false, + StringValue: "", + }, + } + + // Complete the build + _, err = b.doCompleteBuild(context.TODO(), "happycloud.image", mockArtifacts, nil) + if err != nil { + t.Errorf("doCompleteBuild() should have completed successfully for build happycloud.image, got err: %v", err) + } + + // Verify that UpdateChannel was called for channel updates + if !mockService.UpdateChannelCalled { + t.Error("UpdateChannelCalled should be true after completing build with channels") + } + + // Verify that UpdateBuild was called for marking build complete + if !mockService.UpdateBuildCalled { + t.Error("UpdateBuildCalled should be true after completing build") + } +} From aa2b628319fced0c3d1c5990e3c824c3e1a87806 Mon Sep 17 00:00:00 2001 From: Tanmay Jain Date: Mon, 8 Dec 2025 16:08:49 +0530 Subject: [PATCH 3/4] Adding `ui` object support in Bucket for Logs --- internal/hcp/registry/hcl.go | 2 +- internal/hcp/registry/json.go | 2 +- internal/hcp/registry/types.bucket.go | 14 +++++++------- internal/hcp/registry/types.bucket_test.go | 20 +++++++++++++++++--- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/internal/hcp/registry/hcl.go b/internal/hcp/registry/hcl.go index 5c6e9d15f..e569e4ddd 100644 --- a/internal/hcp/registry/hcl.go +++ b/internal/hcp/registry/hcl.go @@ -83,7 +83,7 @@ func (h *HCLRegistry) CompleteBuild( if err != nil { return nil, err } - return h.bucket.completeBuild(ctx, buildName, artifacts, buildErr) + return h.bucket.completeBuild(ctx, buildName, artifacts, h.ui, buildErr) } // VersionStatusSummary prints a status report in the UI if the version is not yet done diff --git a/internal/hcp/registry/json.go b/internal/hcp/registry/json.go index f3f142ee0..173308689 100644 --- a/internal/hcp/registry/json.go +++ b/internal/hcp/registry/json.go @@ -101,7 +101,7 @@ func (h *JSONRegistry) CompleteBuild( if err != nil { return nil, err } - return h.bucket.completeBuild(ctx, buildName, artifacts, buildErr) + return h.bucket.completeBuild(ctx, buildName, artifacts, h.ui, buildErr) } // VersionStatusSummary prints a status report in the UI if the version is not yet done diff --git a/internal/hcp/registry/types.bucket.go b/internal/hcp/registry/types.bucket.go index c81f03412..77775bff4 100644 --- a/internal/hcp/registry/types.bucket.go +++ b/internal/hcp/registry/types.bucket.go @@ -246,23 +246,21 @@ func (bucket *Bucket) uploadSbom(ctx context.Context, buildName string, sbom pac return bucket.client.UploadSbom(ctx, bucket.Name, bucket.Version.Fingerprint, buildToUpdate.ID, sbom) } -func (bucket *Bucket) updateChannels(ctx context.Context) error { +func (bucket *Bucket) updateChannels(ctx context.Context, ui packerSDK.Ui) error { if len(bucket.Channels) == 0 { return nil } - log.Printf("[INFO] Updating %d channel(s) to point to version %s", len(bucket.Channels), bucket.Version.ID) - body := &hcpPackerModels.HashicorpCloudPacker20230101UpdateChannelBody{ VersionFingerprint: bucket.Version.Fingerprint, UpdateMask: "versionFingerprint", } for _, channel := range bucket.Channels { - log.Printf("[INFO] Updating channel %s to version %s", channel, bucket.Version.ID) + ui.Say(fmt.Sprintf("Assigning version %s to channel `%s`", bucket.Version.ID, channel)) _, err := bucket.client.UpdateChannel(ctx, bucket.Name, channel, body) if err != nil { - log.Printf("[ERROR] Failed to update channel %s: %s", channel, err) + ui.Error(fmt.Sprintf("Failed to update channel %s: %s", channel, err)) return fmt.Errorf("failed to update channel %s: %w", channel, err) } } @@ -653,6 +651,7 @@ func (bucket *Bucket) completeBuild( ctx context.Context, buildName string, packerSDKArtifacts []packerSDK.Artifact, + ui packerSDK.Ui, buildErr error, ) ([]packerSDK.Artifact, error) { doneCh, ok := bucket.RunningBuilds[buildName] @@ -677,7 +676,7 @@ func (bucket *Bucket) completeBuild( return packerSDKArtifacts, fmt.Errorf("build failed, not uploading artifacts") } - artifacts, err := bucket.doCompleteBuild(ctx, buildName, packerSDKArtifacts, buildErr) + artifacts, err := bucket.doCompleteBuild(ctx, buildName, packerSDKArtifacts, ui, buildErr) if err != nil { err := bucket.UpdateBuildStatus(ctx, buildName, hcpPackerModels.HashicorpCloudPacker20230101BuildStatusBUILDFAILED) if err != nil { @@ -692,6 +691,7 @@ func (bucket *Bucket) doCompleteBuild( ctx context.Context, buildName string, packerSDKArtifacts []packerSDK.Artifact, + ui packerSDK.Ui, buildErr error, ) ([]packerSDK.Artifact, error) { for _, art := range packerSDKArtifacts { @@ -753,7 +753,7 @@ func (bucket *Bucket) doCompleteBuild( } // Update channels after build is marked complete - channelErr := bucket.updateChannels(ctx) + channelErr := bucket.updateChannels(ctx, ui) if channelErr != nil { log.Printf("[ERROR] Failed to update channels after completing build %s: %s", buildName, channelErr) } diff --git a/internal/hcp/registry/types.bucket_test.go b/internal/hcp/registry/types.bucket_test.go index fe5c1bd4f..9d7dcedb1 100644 --- a/internal/hcp/registry/types.bucket_test.go +++ b/internal/hcp/registry/types.bucket_test.go @@ -5,6 +5,8 @@ package registry import ( "context" + "io" + "os" "reflect" "strconv" "sync" @@ -522,7 +524,11 @@ func TestCompleteBuild(t *testing.T) { Status: models.HashicorpCloudPacker20230101BuildStatusBUILDRUNNING, }) - _, err := dummyBucket.completeBuild(context.Background(), "test-build", tt.artifactsToUse, nil) + _, err := dummyBucket.completeBuild(context.Background(), "test-build", tt.artifactsToUse, &packer.BasicUi{ + Reader: os.Stdin, + Writer: io.Discard, + ErrorWriter: io.Discard, + }, nil) if err != nil != tt.expectError { t.Errorf("expected %t error; got %t", tt.expectError, err != nil) t.Logf("error was: %s", err) @@ -584,7 +590,11 @@ func TestBucket_UpdateChannels(t *testing.T) { Fingerprint: "test-fingerprint", } - err := b.updateChannels(context.Background()) + err := b.updateChannels(context.Background(), &packer.BasicUi{ + Reader: os.Stdin, + Writer: io.Discard, + ErrorWriter: io.Discard, + }) if (err != nil) != tt.wantErr { t.Errorf("updateChannels() error = %v, wantErr %v", err, tt.wantErr) @@ -651,7 +661,11 @@ func TestBucket_DoCompleteBuild_WithChannels(t *testing.T) { } // Complete the build - _, err = b.doCompleteBuild(context.TODO(), "happycloud.image", mockArtifacts, nil) + _, err = b.doCompleteBuild(context.TODO(), "happycloud.image", mockArtifacts, &packer.BasicUi{ + Reader: os.Stdin, + Writer: io.Discard, + ErrorWriter: io.Discard, + }, nil) if err != nil { t.Errorf("doCompleteBuild() should have completed successfully for build happycloud.image, got err: %v", err) } From 651ea426be3fd59ccd10d508642beebbcaa9c383 Mon Sep 17 00:00:00 2001 From: Devashish Date: Fri, 12 Dec 2025 13:20:11 -0500 Subject: [PATCH 4/4] Verbiage change --- internal/hcp/registry/types.bucket.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/hcp/registry/types.bucket.go b/internal/hcp/registry/types.bucket.go index 77775bff4..3d3546e24 100644 --- a/internal/hcp/registry/types.bucket.go +++ b/internal/hcp/registry/types.bucket.go @@ -257,10 +257,10 @@ func (bucket *Bucket) updateChannels(ctx context.Context, ui packerSDK.Ui) error } for _, channel := range bucket.Channels { - ui.Say(fmt.Sprintf("Assigning version %s to channel `%s`", bucket.Version.ID, channel)) + ui.Say(fmt.Sprintf("==> Assigning version `%s` to channel `%s`", bucket.Version.Fingerprint, channel)) _, err := bucket.client.UpdateChannel(ctx, bucket.Name, channel, body) if err != nil { - ui.Error(fmt.Sprintf("Failed to update channel %s: %s", channel, err)) + ui.Error(fmt.Sprintf("==> Failed assigning version `%s` to channel `%s`: %v", bucket.Version.Fingerprint, channel, err)) return fmt.Errorf("failed to update channel %s: %w", channel, err) } }