github.com/hashicorp/packer@v1.14.3/internal/hcp/registry/hcl.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package registry
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"log"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  	hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
    13  	sdkpacker "github.com/hashicorp/packer-plugin-sdk/packer"
    14  	"github.com/hashicorp/packer/hcl2template"
    15  	"github.com/hashicorp/packer/packer"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  // HCLRegistry is a HCP handler made for handling HCL configurations
    20  type HCLRegistry struct {
    21  	configuration *hcl2template.PackerConfig
    22  	bucket        *Bucket
    23  	ui            sdkpacker.Ui
    24  	metadata      *MetadataStore
    25  	buildNames    map[string]struct{}
    26  }
    27  
    28  const (
    29  	// Known HCP Packer Datasource, whose id is the SourceImageId for some build.
    30  	hcpImageDatasourceType    string = "hcp-packer-image"
    31  	hcpArtifactDatasourceType string = "hcp-packer-artifact"
    32  
    33  	hcpIterationDatasourceType string = "hcp-packer-iteration"
    34  	hcpVersionDatasourceType   string = "hcp-packer-version"
    35  
    36  	buildLabel string = "build"
    37  )
    38  
    39  // PopulateVersion creates the metadata in HCP Packer Registry for a build
    40  func (h *HCLRegistry) PopulateVersion(ctx context.Context) error {
    41  	err := h.bucket.Initialize(ctx, hcpPackerModels.HashicorpCloudPacker20230101TemplateTypeHCL2)
    42  	if err != nil {
    43  		return err
    44  	}
    45  
    46  	err = h.bucket.populateVersion(ctx)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	versionID := h.bucket.Version.ID
    52  	versionFingerprint := h.bucket.Version.Fingerprint
    53  
    54  	// FIXME: Remove
    55  	h.configuration.HCPVars["iterationID"] = cty.StringVal(versionID)
    56  	h.configuration.HCPVars["versionFingerprint"] = cty.StringVal(versionFingerprint)
    57  
    58  	sha, err := getGitSHA(h.configuration.Basedir)
    59  	if err != nil {
    60  		log.Printf("failed to get GIT SHA from environment, won't set as build labels")
    61  	} else {
    62  		h.bucket.Version.AddSHAToBuildLabels(sha)
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  // StartBuild is invoked when one build for the configuration is starting to be processed
    69  func (h *HCLRegistry) StartBuild(ctx context.Context, build *packer.CoreBuild) error {
    70  	return h.bucket.startBuild(ctx, h.HCPBuildName(build))
    71  }
    72  
    73  // CompleteBuild is invoked when one build for the configuration has finished
    74  func (h *HCLRegistry) CompleteBuild(
    75  	ctx context.Context,
    76  	build *packer.CoreBuild,
    77  	artifacts []sdkpacker.Artifact,
    78  	buildErr error,
    79  ) ([]sdkpacker.Artifact, error) {
    80  	buildName := h.HCPBuildName(build)
    81  	buildMetadata, envMetadata := build.GetMetadata(), h.metadata
    82  	err := h.bucket.Version.AddMetadataToBuild(ctx, buildName, buildMetadata, envMetadata)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	return h.bucket.completeBuild(ctx, buildName, artifacts, buildErr)
    87  }
    88  
    89  // VersionStatusSummary prints a status report in the UI if the version is not yet done
    90  func (h *HCLRegistry) VersionStatusSummary() {
    91  	h.bucket.Version.statusSummary(h.ui)
    92  }
    93  
    94  func NewHCLRegistry(config *hcl2template.PackerConfig, ui sdkpacker.Ui) (*HCLRegistry, hcl.Diagnostics) {
    95  	var diags hcl.Diagnostics
    96  	if len(config.Builds) > 1 {
    97  		diags = append(diags, &hcl.Diagnostic{
    98  			Severity: hcl.DiagError,
    99  			Summary:  "Multiple " + buildLabel + " blocks",
   100  			Detail: fmt.Sprintf("For HCP Packer Registry enabled builds, only one " + buildLabel +
   101  				" block can be defined. Please remove any additional " + buildLabel +
   102  				" block(s). If this " + buildLabel + " is not meant for the HCP Packer registry please " +
   103  				"clear any HCP_PACKER_* environment variables."),
   104  		})
   105  
   106  		return nil, diags
   107  	}
   108  
   109  	registryConfig, rcDiags := config.GetHCPPackerRegistryBlock()
   110  	diags = diags.Extend(rcDiags)
   111  	if diags.HasErrors() {
   112  		return nil, diags
   113  	}
   114  
   115  	withHCLBucketConfiguration := func(bucket *Bucket) hcl.Diagnostics {
   116  		bucket.ReadFromHCPPackerRegistryBlock(registryConfig)
   117  		return nil
   118  	}
   119  
   120  	// we must use the old strategy when there is only a single build block because
   121  	// we used to rely on the parent build block for setting some default data
   122  	if len(config.Builds) == 1 && config.HCPPackerRegistry == nil {
   123  		withHCLBucketConfiguration = func(bucket *Bucket) hcl.Diagnostics {
   124  			bb := config.Builds[0]
   125  			bucket.ReadFromHCLBuildBlock(bb)
   126  			// If at this point the bucket.Name is still empty,
   127  			// last try is to use the build.Name if present
   128  			if bucket.Name == "" && bb.Name != "" {
   129  				bucket.Name = bb.Name
   130  			}
   131  
   132  			// If the description is empty, use the one from the build block
   133  			if bucket.Description == "" && bb.Description != "" {
   134  				bucket.Description = bb.Description
   135  			}
   136  			return nil
   137  		}
   138  	}
   139  
   140  	// Capture Datasource configuration data
   141  	vals, dsDiags := config.Datasources.Values()
   142  	if dsDiags != nil {
   143  		diags = append(diags, dsDiags...)
   144  	}
   145  
   146  	bucket, bucketDiags := createConfiguredBucket(
   147  		config.Basedir,
   148  		withPackerEnvConfiguration,
   149  		withHCLBucketConfiguration,
   150  		withDeprecatedDatasourceConfiguration(vals, ui),
   151  		withDatasourceConfiguration(vals),
   152  	)
   153  	if bucketDiags != nil {
   154  		diags = append(diags, bucketDiags...)
   155  	}
   156  
   157  	if diags.HasErrors() {
   158  		return nil, diags
   159  	}
   160  
   161  	registry := &HCLRegistry{
   162  		configuration: config,
   163  		bucket:        bucket,
   164  		ui:            ui,
   165  		metadata:      &MetadataStore{},
   166  		buildNames:    map[string]struct{}{},
   167  	}
   168  
   169  	ui.Say(fmt.Sprintf("Tracking build on HCP Packer with fingerprint %q", bucket.Version.Fingerprint))
   170  
   171  	return registry, diags.Extend(registry.registerAllComponents())
   172  }
   173  
   174  func (h *HCLRegistry) registerAllComponents() hcl.Diagnostics {
   175  	var diags hcl.Diagnostics
   176  
   177  	conflictSources := map[string]struct{}{}
   178  
   179  	// we currently support only one build block but it will change in the near future
   180  	for _, build := range h.configuration.Builds {
   181  		for _, source := range build.Sources {
   182  			// If we encounter the same source twice, we'll defer
   183  			// its addition to later, using both the build name
   184  			// and the source type as the name used for HCP Packer.
   185  			_, ok := h.buildNames[source.String()]
   186  			if !ok {
   187  				h.buildNames[source.String()] = struct{}{}
   188  				continue
   189  			}
   190  
   191  			conflictSources[source.String()] = struct{}{}
   192  			// We need to delete it to avoid having a false-positive
   193  			// when returning the name, since we'll be using
   194  			// the combination of build name + source.String()
   195  			delete(h.buildNames, source.String())
   196  		}
   197  	}
   198  
   199  	// Second pass is to take care of conflicting sources
   200  	//
   201  	// If the same source is used twice in the configuration, we need to
   202  	// have a way to differentiate the two on HCP, as each build should have
   203  	// a locally unique name.
   204  	//
   205  	// If that happens, we then use a combination of both the build name, and
   206  	// the source type.
   207  	for _, build := range h.configuration.Builds {
   208  		for _, source := range build.Sources {
   209  			if _, ok := conflictSources[source.String()]; !ok {
   210  				continue
   211  			}
   212  
   213  			buildName := source.String()
   214  			if build.Name != "" {
   215  				buildName = fmt.Sprintf("%s.%s", build.Name, buildName)
   216  			}
   217  
   218  			if _, ok := h.buildNames[buildName]; ok {
   219  				diags = append(diags, &hcl.Diagnostic{
   220  					Severity: hcl.DiagError,
   221  					Summary:  "Build name conflicts",
   222  					Subject:  &build.HCL2Ref.DefRange,
   223  					Detail: fmt.Sprintf("Two sources are used in the same build block, causing "+
   224  						"a conflict, there must only be one instance of %s", source.String()),
   225  				})
   226  			}
   227  			h.buildNames[buildName] = struct{}{}
   228  		}
   229  	}
   230  
   231  	if diags.HasErrors() {
   232  		return diags
   233  	}
   234  
   235  	for buildName := range h.buildNames {
   236  		h.bucket.RegisterBuildForComponent(buildName)
   237  	}
   238  	return diags
   239  }
   240  
   241  func (h *HCLRegistry) Metadata() Metadata {
   242  	return h.metadata
   243  }
   244  
   245  // HCPBuildName will return the properly formatted string taking name conflict into account
   246  func (h *HCLRegistry) HCPBuildName(build *packer.CoreBuild) string {
   247  	_, ok := h.buildNames[build.Type]
   248  	if ok {
   249  		return build.Type
   250  	}
   251  
   252  	return fmt.Sprintf("%s.%s", build.BuildName, build.Type)
   253  }