Merge pull request #13532 from hashicorp/channel-assignment
Some checks failed
build / get-go-version (push) Has been cancelled
build / set-product-version (push) Has been cancelled
Go Test / get-go-version (push) Has been cancelled
Go Validate / get-go-version (push) Has been cancelled
build / generate-metadata-file (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd 386 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd 386 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd 386 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} solaris 386 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} windows 386 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} solaris amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} windows amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} freebsd arm build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} netbsd arm build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} openbsd arm build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} linux 386 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} linux amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} linux arm build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} linux arm64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} linux ppc64le build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} darwin amd64 build (push) Has been cancelled
build / Go ${{ needs.get-go-version.outputs.go-version }} darwin arm64 build (push) Has been cancelled
build / Docker light 386 build (push) Has been cancelled
build / Docker light amd64 build (push) Has been cancelled
build / Docker light arm build (push) Has been cancelled
build / Docker light arm64 build (push) Has been cancelled
build / Docker full 386 build (push) Has been cancelled
build / Docker full amd64 build (push) Has been cancelled
build / Docker full arm build (push) Has been cancelled
build / Docker full arm64 build (push) Has been cancelled
Go Test / Linux go tests (push) Has been cancelled
Go Test / Darwin go tests (push) Has been cancelled
Go Test / Windows go tests (push) Has been cancelled
Go Validate / Go Mod Tidy (push) Has been cancelled
Go Validate / Lint (push) Has been cancelled
Go Validate / Fmt check (push) Has been cancelled
Go Validate / Generate check (push) Has been cancelled

FEAT: Adds support for updating HCP Packer registry channels
This commit is contained in:
Tanmay Jain 2025-12-15 12:27:51 +05:30 committed by GitHub
commit 2f8390857a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 267 additions and 5 deletions

View file

@ -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{

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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,28 @@ 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, ui packerSDK.Ui) error {
if len(bucket.Channels) == 0 {
return nil
}
body := &hcpPackerModels.HashicorpCloudPacker20230101UpdateChannelBody{
VersionFingerprint: bucket.Version.Fingerprint,
UpdateMask: "versionFingerprint",
}
for _, channel := range bucket.Channels {
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 assigning version `%s` to channel `%s`: %v", bucket.Version.Fingerprint, 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.
@ -627,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]
@ -651,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 {
@ -666,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 {
@ -726,6 +752,12 @@ func (bucket *Bucket) doCompleteBuild(
parErr)
}
// Update channels after build is marked complete
channelErr := bucket.updateChannels(ctx, ui)
if channelErr != nil {
log.Printf("[ERROR] Failed to update channels after completing build %s: %s", buildName, channelErr)
}
return append(packerSDKArtifacts, &registryArtifact{
BuildName: buildName,
BucketName: bucket.Name,

View file

@ -5,6 +5,8 @@ package registry
import (
"context"
"io"
"os"
"reflect"
"strconv"
"sync"
@ -373,6 +375,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",
},
},
},
}
@ -494,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)
@ -509,3 +543,140 @@ 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(), &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)
}
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, &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)
}
// 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")
}
}

View file

@ -15,7 +15,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).
@ -82,5 +82,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.