// Copyright IBM Corp. 2013, 2025 // SPDX-License-Identifier: BUSL-1.1 package registry import ( "context" "fmt" "log" "github.com/hashicorp/hcl/v2" hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models" sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer/hcl2template" "github.com/hashicorp/packer/packer" "github.com/zclconf/go-cty/cty" ) // HCLRegistry is a HCP handler made for handling HCL configurations type HCLRegistry struct { configuration *hcl2template.PackerConfig bucket *Bucket ui sdkpacker.Ui metadata *MetadataStore buildNames map[string]struct{} } const ( // Known HCP Packer Datasource, whose id is the SourceImageId for some build. hcpImageDatasourceType string = "hcp-packer-image" hcpArtifactDatasourceType string = "hcp-packer-artifact" hcpIterationDatasourceType string = "hcp-packer-iteration" hcpVersionDatasourceType string = "hcp-packer-version" buildLabel string = "build" ) // PopulateVersion creates the metadata in HCP Packer Registry for a build func (h *HCLRegistry) PopulateVersion(ctx context.Context) error { err := h.bucket.Initialize(ctx, hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeHCL2) if err != nil { return err } err = h.bucket.populateVersion(ctx) if err != nil { return err } versionID := h.bucket.Version.ID versionFingerprint := h.bucket.Version.Fingerprint // FIXME: Remove h.configuration.HCPVars["iterationID"] = cty.StringVal(versionID) h.configuration.HCPVars["versionFingerprint"] = cty.StringVal(versionFingerprint) sha, err := getGitSHA(h.configuration.Basedir) if err != nil { log.Printf("failed to get GIT SHA from environment, won't set as build labels") } else { h.bucket.Version.AddSHAToBuildLabels(sha) } return nil } // StartBuild is invoked when one build for the configuration is starting to be processed func (h *HCLRegistry) StartBuild(ctx context.Context, build *packer.CoreBuild) error { return h.bucket.startBuild(ctx, h.HCPBuildName(build)) } // CompleteBuild is invoked when one build for the configuration has finished func (h *HCLRegistry) CompleteBuild( ctx context.Context, build *packer.CoreBuild, artifacts []sdkpacker.Artifact, buildErr error, ) ([]sdkpacker.Artifact, error) { buildName := h.HCPBuildName(build) buildMetadata, envMetadata := build.GetMetadata(), h.metadata err := h.bucket.Version.AddMetadataToBuild(ctx, buildName, buildMetadata, envMetadata) if err != nil { return nil, err } 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 func (h *HCLRegistry) VersionStatusSummary() { h.bucket.Version.statusSummary(h.ui) } // FetchEnforcedBlocks fetches enforced provisioner blocks from HCP Packer func (h *HCLRegistry) FetchEnforcedBlocks(ctx context.Context) error { return h.bucket.FetchEnforcedBlocks(ctx) } // InjectEnforcedProvisioners injects enforced provisioners into the builds func (h *HCLRegistry) InjectEnforcedProvisioners(builds []*packer.CoreBuild) hcl.Diagnostics { enforcedBlocks := h.bucket.EnforcedBlocks if len(enforcedBlocks) == 0 { return nil } var allDiags hcl.Diagnostics // Parse all enforced blocks into provisioner blocks for _, eb := range enforcedBlocks { if eb.BlockContent == "" { continue } provBlocks, diags := hcl2template.ParseProvisionerBlocks(eb.BlockContent) if diags.HasErrors() { allDiags = append(allDiags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Failed to parse enforced block %q", eb.Name), Detail: diags.Error(), }) continue } if len(provBlocks) > 0 { h.ui.Say(fmt.Sprintf("Loaded %d enforced provisioner(s) from HCP block %q and template type %q", len(provBlocks), eb.Name, eb.TemplateType)) } // Inject into each build for _, build := range builds { for _, pb := range provBlocks { // Check if this provisioner should be skipped for this build if pb.OnlyExcept.Skip(build.Type) { log.Printf("[DEBUG] skipping enforced provisioner %q for build %q due to only/except rules", pb.PType, build.Name()) continue } coreProv, moreDiags := h.configuration.GetCoreBuildProvisionerFromBlock(pb, build.Type) if moreDiags.HasErrors() { allDiags = append(allDiags, moreDiags...) continue } build.Provisioners = append(build.Provisioners, coreProv) log.Printf("[INFO] injected enforced provisioner %q from block %q into build %q", pb.PType, eb.Name, build.Name()) } } } return allDiags } func NewHCLRegistry(config *hcl2template.PackerConfig, ui sdkpacker.Ui) (*HCLRegistry, hcl.Diagnostics) { var diags hcl.Diagnostics if len(config.Builds) > 1 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Multiple " + buildLabel + " blocks", Detail: fmt.Sprintf("For HCP Packer Registry enabled builds, only one " + buildLabel + " block can be defined. Please remove any additional " + buildLabel + " block(s). If this " + buildLabel + " is not meant for the HCP Packer registry please " + "clear any HCP_PACKER_* environment variables."), }) return nil, diags } registryConfig, rcDiags := config.GetHCPPackerRegistryBlock() diags = diags.Extend(rcDiags) if diags.HasErrors() { return nil, diags } withHCLBucketConfiguration := func(bucket *Bucket) hcl.Diagnostics { bucket.ReadFromHCPPackerRegistryBlock(registryConfig) return nil } // we must use the old strategy when there is only a single build block because // we used to rely on the parent build block for setting some default data if len(config.Builds) == 1 && config.HCPPackerRegistry == nil { withHCLBucketConfiguration = func(bucket *Bucket) hcl.Diagnostics { bb := config.Builds[0] bucket.ReadFromHCLBuildBlock(bb) // If at this point the bucket.Name is still empty, // last try is to use the build.Name if present if bucket.Name == "" && bb.Name != "" { bucket.Name = bb.Name } // If the description is empty, use the one from the build block if bucket.Description == "" && bb.Description != "" { bucket.Description = bb.Description } return nil } } // Capture Datasource configuration data vals, dsDiags := config.Datasources.Values() if dsDiags != nil { diags = append(diags, dsDiags...) } bucket, bucketDiags := createConfiguredBucket( config.Basedir, withPackerEnvConfiguration, withHCLBucketConfiguration, withDeprecatedDatasourceConfiguration(vals, ui), withDatasourceConfiguration(vals), ) if bucketDiags != nil { diags = append(diags, bucketDiags...) } if diags.HasErrors() { return nil, diags } registry := &HCLRegistry{ configuration: config, bucket: bucket, ui: ui, metadata: &MetadataStore{}, buildNames: map[string]struct{}{}, } ui.Say(fmt.Sprintf("Tracking build on HCP Packer with fingerprint %q", bucket.Version.Fingerprint)) return registry, diags.Extend(registry.registerAllComponents()) } func (h *HCLRegistry) registerAllComponents() hcl.Diagnostics { var diags hcl.Diagnostics conflictSources := map[string]struct{}{} // we currently support only one build block but it will change in the near future for _, build := range h.configuration.Builds { for _, source := range build.Sources { // If we encounter the same source twice, we'll defer // its addition to later, using both the build name // and the source type as the name used for HCP Packer. _, ok := h.buildNames[source.String()] if !ok { h.buildNames[source.String()] = struct{}{} continue } conflictSources[source.String()] = struct{}{} // We need to delete it to avoid having a false-positive // when returning the name, since we'll be using // the combination of build name + source.String() delete(h.buildNames, source.String()) } } // Second pass is to take care of conflicting sources // // If the same source is used twice in the configuration, we need to // have a way to differentiate the two on HCP, as each build should have // a locally unique name. // // If that happens, we then use a combination of both the build name, and // the source type. for _, build := range h.configuration.Builds { for _, source := range build.Sources { if _, ok := conflictSources[source.String()]; !ok { continue } buildName := source.String() if build.Name != "" { buildName = fmt.Sprintf("%s.%s", build.Name, buildName) } if _, ok := h.buildNames[buildName]; ok { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Build name conflicts", Subject: &build.HCL2Ref.DefRange, Detail: fmt.Sprintf("Two sources are used in the same build block, causing "+ "a conflict, there must only be one instance of %s", source.String()), }) } h.buildNames[buildName] = struct{}{} } } if diags.HasErrors() { return diags } for buildName := range h.buildNames { h.bucket.RegisterBuildForComponent(buildName) } return diags } func (h *HCLRegistry) Metadata() Metadata { return h.metadata } // HCPBuildName will return the properly formatted string taking name conflict into account func (h *HCLRegistry) HCPBuildName(build *packer.CoreBuild) string { _, ok := h.buildNames[build.Type] if ok { return build.Type } return fmt.Sprintf("%s.%s", build.BuildName, build.Type) }