// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 //go:generate packer-sdc struct-markdown //go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config package hcp_packer_image import ( "context" "errors" "fmt" "log" "time" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/hcl/v2/hcldec" hcpPackerDeprecatedModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2021-04-30/models" "github.com/hashicorp/packer-plugin-sdk/common" "github.com/hashicorp/packer-plugin-sdk/hcl2helper" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" hcpapi "github.com/hashicorp/packer/internal/hcp/api" ) type Datasource struct { config Config } type Config struct { common.PackerConfig `mapstructure:",squash"` // The name of the bucket your image is in. Bucket string `mapstructure:"bucket_name" required:"true"` // The name of the channel to use when retrieving your image. // Either this or `iteration_id` MUST be set. // Mutually exclusive with `iteration_id`. // If using several images from a single iteration, you may prefer // sourcing an iteration first, and referencing it for subsequent uses, // as every `hcp-packer-image` with the channel set will generate a // potentially billable HCP Packer request, but if several // `hcp-packer-image`s use a shared `hcp-packer-iteration` that will // only generate one potentially billable request. Channel string `mapstructure:"channel" required:"true"` // The ID of the iteration to use when retrieving your image // Either this or `channel` MUST be set. // Mutually exclusive with `channel` IterationID string `mapstructure:"iteration_id" required:"true"` // The name of the cloud provider that your image is for. For example, // "aws" or "gce". CloudProvider string `mapstructure:"cloud_provider" required:"true"` // The name of the cloud region your image is in. For example "us-east-1". Region string `mapstructure:"region" required:"true"` // The specific Packer builder used to create the image. // For example, "amazon-ebs.example" ComponentType string `mapstructure:"component_type" required:"false"` // TODO: Version string `mapstructure:"version"` // TODO: Fingerprint string `mapstructure:"fingerprint"` // TODO: Label string `mapstructure:"label"` } func (d *Datasource) ConfigSpec() hcldec.ObjectSpec { return d.config.FlatMapstructure().HCL2Spec() } func (d *Datasource) Configure(raws ...interface{}) error { err := config.Decode(&d.config, nil, raws...) if err != nil { return err } var errs *packersdk.MultiError if d.config.Bucket == "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("The `bucket_name` must be specified")) } // Ensure either channel or iteration_id are set, and not both at the same time if d.config.Channel == "" && d.config.IterationID == "" { errs = packersdk.MultiErrorAppend(errs, errors.New( "The `iteration_id` or `channel` must be specified.")) } if d.config.Channel != "" && d.config.IterationID != "" { errs = packersdk.MultiErrorAppend(errs, errors.New( "`iteration_id` and `channel` cannot both be specified.")) } if d.config.Region == "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`region` is "+ "currently a required field.")) } if d.config.CloudProvider == "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`cloud_provider` is "+ "currently a required field.")) } if errs != nil && len(errs.Errors) > 0 { return errs } return nil } // DatasourceOutput Information from []*hcpPackerDeprecatedModels.HashicorpCloudPackerImage with some information // from the parent []*hcpPackerDeprecatedModels.HashicorpCloudPackerBuild included where it seemed // like it might be relevant. Need to copy so we can generate type DatasourceOutput struct { // The name of the cloud provider that the image exists in. For example, // "aws", "azure", or "gce". CloudProvider string `mapstructure:"cloud_provider"` // The specific Packer builder or post-processor used to create the image. ComponentType string `mapstructure:"component_type"` // The date and time at which the image was created. CreatedAt string `mapstructure:"created_at"` // The ID of the build that created the image. This is a ULID, which is a // unique identifier similar to a UUID. It is created by the HCP Packer // Registry when an build is first created, and is unique to this build. BuildID string `mapstructure:"build_id"` // The iteration ID. This is a ULID, which is a unique identifier similar // to a UUID. It is created by the HCP Packer Registry when an iteration is // first created, and is unique to this iteration. IterationID string `mapstructure:"iteration_id"` // The ID of the channel used to query the image iteration. This value will be empty if the `iteration_id` was used // directly instead of a channel. ChannelID string `mapstructure:"channel_id"` // The UUID associated with the Packer run that created this image. PackerRunUUID string `mapstructure:"packer_run_uuid"` // ID or URL of the remote cloud image as given by a build. ID string `mapstructure:"id"` // The cloud region as given by `packer build`. eg. "ap-east-1". // For locally managed clouds, this may map instead to a cluster, server // or datastore. Region string `mapstructure:"region"` // The key:value metadata labels associated with this build. Labels map[string]string `mapstructure:"labels"` } func (d *Datasource) OutputSpec() hcldec.ObjectSpec { return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec() } func (d *Datasource) Execute() (cty.Value, error) { log.Printf("[WARN] Deprecation: `hcp-packer-image` datasource has been deprecated. " + "Please use `hcp-packer-artifact` datasource instead.") ctx := context.TODO() cli, err := hcpapi.NewDeprecatedClient() if err != nil { return cty.NullVal(cty.EmptyObject), err } var iteration *hcpPackerDeprecatedModels.HashicorpCloudPackerIteration var channelID string if d.config.IterationID != "" { log.Printf("[INFO] Reading info from HCP Packer registry (%s) [project_id=%s, organization_id=%s, iteration_id=%s]", d.config.Bucket, cli.ProjectID, cli.OrganizationID, d.config.IterationID) iter, err := cli.GetIteration(ctx, d.config.Bucket, hcpapi.GetIteration_byID(d.config.IterationID)) if err != nil { return cty.NullVal(cty.EmptyObject), fmt.Errorf( "error retrieving image iteration from HCP Packer registry: %s", err) } iteration = iter } else { log.Printf("[INFO] Reading info from HCP Packer registry (%s) [project_id=%s, organization_id=%s, channel=%s]", d.config.Bucket, cli.ProjectID, cli.OrganizationID, d.config.Channel) channel, err := cli.GetChannel(ctx, d.config.Bucket, d.config.Channel) if err != nil { return cty.NullVal(cty.EmptyObject), fmt.Errorf("error retrieving "+ "channel from HCP Packer registry: %s", err.Error()) } if channel.Iteration == nil { return cty.NullVal(cty.EmptyObject), fmt.Errorf("there is no iteration associated with the channel %s", d.config.Channel) } channelID = channel.ID iteration = channel.Iteration } revokeAt := time.Time(iteration.RevokeAt) if !revokeAt.IsZero() && revokeAt.Before(time.Now().UTC()) { // If RevokeAt is not a zero date and is before NOW, it means this iteration is revoked and should not be used // to build new images. return cty.NullVal(cty.EmptyObject), fmt.Errorf("the iteration %s is revoked and can not be used on Packer builds", iteration.ID) } var output DatasourceOutput cloudAndRegions := map[string][]string{} for _, build := range iteration.Builds { if build.CloudProvider != d.config.CloudProvider { continue } for _, image := range build.Images { cloudAndRegions[build.CloudProvider] = append(cloudAndRegions[build.CloudProvider], image.Region) if image.Region == d.config.Region && filterBuildByComponentType(build, d.config.ComponentType) { // This is the desired image. output = DatasourceOutput{ CloudProvider: build.CloudProvider, ComponentType: build.ComponentType, CreatedAt: image.CreatedAt.String(), BuildID: build.ID, IterationID: build.IterationID, ChannelID: channelID, PackerRunUUID: build.PackerRunUUID, ID: image.ImageID, Region: image.Region, Labels: build.Labels, } return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil } } } return cty.NullVal(cty.EmptyObject), fmt.Errorf("could not find a build result matching "+ "[region=%q, cloud_provider=%q, component_type=%q]. Available: %v ", d.config.Region, d.config.CloudProvider, d.config.ComponentType, cloudAndRegions) } func filterBuildByComponentType(build *hcpPackerDeprecatedModels.HashicorpCloudPackerBuild, componentType string) bool { // optional field is not specified, passthrough if componentType == "" { return true } // if specified, only the matched image metadata is returned by this effect return build.ComponentType == componentType }