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/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/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/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 264852492..3d3546e24 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,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, ®istryArtifact{ BuildName: buildName, BucketName: bucket.Name, diff --git a/internal/hcp/registry/types.bucket_test.go b/internal/hcp/registry/types.bucket_test.go index 6cd49503c..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" @@ -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") + } +} 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 91a103585..921f86e05 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 @@ -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.