github.com/hashicorp/packer@v1.14.3/datasource/hcp-packer-artifact/data.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 //go:generate packer-sdc struct-markdown 5 //go:generate packer-sdc mapstructure-to-hcl2 -type DatasourceOutput,Config 6 package hcp_packer_artifact 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "log" 13 14 "github.com/zclconf/go-cty/cty" 15 16 "github.com/hashicorp/hcl/v2/hcldec" 17 hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models" 18 "github.com/hashicorp/packer-plugin-sdk/common" 19 "github.com/hashicorp/packer-plugin-sdk/hcl2helper" 20 packersdk "github.com/hashicorp/packer-plugin-sdk/packer" 21 "github.com/hashicorp/packer-plugin-sdk/template/config" 22 hcpapi "github.com/hashicorp/packer/internal/hcp/api" 23 ) 24 25 type Datasource struct { 26 config Config 27 } 28 29 type Config struct { 30 common.PackerConfig `mapstructure:",squash"` 31 32 // The name of the bucket your artifact is in. 33 BucketName string `mapstructure:"bucket_name" required:"true"` 34 35 // The name of the channel to use when retrieving your artifact. 36 // Either `channel_name` or `version_fingerprint` MUST be set. 37 // If using several artifacts from a single version, you may prefer sourcing a version first, 38 // and referencing it for subsequent uses, as every `hcp_packer_artifact` with the channel set will generate a 39 // potentially billable HCP Packer request, but if several `hcp_packer_artifact`s use a shared `hcp_packer_version` 40 // that will only generate one potentially billable request. 41 ChannelName string `mapstructure:"channel_name" required:"true"` 42 43 // The fingerprint of the version to use when retrieving your artifact. 44 // Either this or `channel_name` MUST be set. 45 // Mutually exclusive with `channel_name` 46 VersionFingerprint string `mapstructure:"version_fingerprint" required:"true"` 47 48 // The name of the platform that your artifact is for. 49 // For example, "aws", "azure", or "gce". 50 Platform string `mapstructure:"platform" required:"true"` 51 52 // The name of the region your artifact is in. 53 // For example "us-east-1". 54 Region string `mapstructure:"region" required:"true"` 55 56 // The specific Packer builder used to create the artifact. 57 // For example, "amazon-ebs.example" 58 ComponentType string `mapstructure:"component_type" required:"false"` 59 } 60 61 func (d *Datasource) ConfigSpec() hcldec.ObjectSpec { 62 return d.config.FlatMapstructure().HCL2Spec() 63 } 64 65 func (d *Datasource) Configure(raws ...interface{}) error { 66 err := config.Decode(&d.config, nil, raws...) 67 if err != nil { 68 return err 69 } 70 71 var errs *packersdk.MultiError 72 73 if d.config.BucketName == "" { 74 errs = packersdk.MultiErrorAppend( 75 errs, fmt.Errorf("the `bucket_name` must be specified"), 76 ) 77 } 78 79 // Ensure either channel_name or version_fingerprint is set, and not both at the same time. 80 if d.config.ChannelName == "" && d.config.VersionFingerprint == "" { 81 errs = packersdk.MultiErrorAppend(errs, errors.New( 82 "`version_fingerprint` or `channel_name` must be specified", 83 )) 84 } 85 if d.config.ChannelName != "" && d.config.VersionFingerprint != "" { 86 errs = packersdk.MultiErrorAppend(errs, errors.New( 87 "`version_fingerprint` and `channel_name` cannot be specified together", 88 )) 89 } 90 91 if d.config.Region == "" { 92 errs = packersdk.MultiErrorAppend(errs, 93 fmt.Errorf("the `region` must be specified"), 94 ) 95 } 96 97 if d.config.Platform == "" { 98 errs = packersdk.MultiErrorAppend(errs, fmt.Errorf( 99 "the `platform` must be specified", 100 )) 101 } 102 103 if errs != nil && len(errs.Errors) > 0 { 104 return errs 105 } 106 return nil 107 } 108 109 // DatasourceOutput Information from []*hcpPackerModels.HashicorpCloudPacker20230101Artifact with some information 110 // from the parent []*hcpPackerModels.HashicorpCloudPacker20230101Build included where it seemed 111 // like it might be relevant. Need to copy so we can generate 112 type DatasourceOutput struct { 113 // The name of the platform that the artifact exists in. 114 // For example, "aws", "azure", or "gce". 115 Platform string `mapstructure:"platform"` 116 117 // The specific Packer builder or post-processor used to create the artifact. 118 ComponentType string `mapstructure:"component_type"` 119 120 // The date and time at which the artifact was created. 121 CreatedAt string `mapstructure:"created_at"` 122 123 // The ID of the build that created the artifact. This is a ULID, which is a 124 // unique identifier similar to a UUID. It is created by the HCP Packer 125 // Registry when a build is first created, and is unique to this build. 126 BuildID string `mapstructure:"build_id"` 127 128 // The version ID. This is a ULID, which is a unique identifier similar 129 // to a UUID. It is created by the HCP Packer Registry when a version is 130 // first created, and is unique to this version. 131 VersionID string `mapstructure:"version_id"` 132 133 // The ID of the channel used to query the version. This value will be empty if the `version_fingerprint` was used 134 // directly instead of a channel. 135 ChannelID string `mapstructure:"channel_id"` 136 137 // The UUID associated with the Packer run that created this artifact. 138 PackerRunUUID string `mapstructure:"packer_run_uuid"` 139 140 // Identifier or URL of the remote artifact as given by a build. 141 // For example, ami-12345. 142 ExternalIdentifier string `mapstructure:"external_identifier"` 143 144 // The region as given by `packer build`. eg. "ap-east-1". 145 // For locally managed clouds, this may map instead to a cluster, server or datastore. 146 Region string `mapstructure:"region"` 147 148 // The key:value metadata labels associated with this build. 149 Labels map[string]string `mapstructure:"labels"` 150 } 151 152 func (d *Datasource) OutputSpec() hcldec.ObjectSpec { 153 return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec() 154 } 155 156 func (d *Datasource) Execute() (cty.Value, error) { 157 ctx := context.TODO() 158 159 cli, err := hcpapi.NewClient() 160 if err != nil { 161 return cty.NullVal(cty.EmptyObject), err 162 } 163 164 var version *hcpPackerModels.HashicorpCloudPacker20230101Version 165 var channelID string 166 if d.config.VersionFingerprint != "" { 167 log.Printf( 168 "[INFO] Reading info from HCP Packer Registry (%s) "+ 169 "[project_id=%s, organization_id=%s, version_fingerprint=%s]", 170 d.config.BucketName, cli.ProjectID, cli.OrganizationID, d.config.VersionFingerprint, 171 ) 172 173 version, err = cli.GetVersion(ctx, d.config.BucketName, d.config.VersionFingerprint) 174 if err != nil { 175 return cty.NullVal(cty.EmptyObject), fmt.Errorf( 176 "error retrieving version from HCP Packer Registry: %s", err, 177 ) 178 } 179 } else { 180 log.Printf( 181 "[INFO] Reading info from HCP Packer Registry (%s) "+ 182 "[project_id=%s, organization_id=%s, channel=%s]", 183 d.config.BucketName, cli.ProjectID, cli.OrganizationID, d.config.ChannelName, 184 ) 185 186 var channel *hcpPackerModels.HashicorpCloudPacker20230101Channel 187 channel, err = cli.GetChannel(ctx, d.config.BucketName, d.config.ChannelName) 188 if err != nil { 189 return cty.NullVal(cty.EmptyObject), fmt.Errorf( 190 "error retrieving channel from HCP Packer Registry: %s", err.Error(), 191 ) 192 } 193 194 if channel.Version == nil { 195 return cty.NullVal(cty.EmptyObject), fmt.Errorf( 196 "there is no version associated with the channel %s", d.config.ChannelName, 197 ) 198 } 199 channelID = channel.ID 200 version = channel.Version 201 } 202 203 if *version.Status == hcpPackerModels.HashicorpCloudPacker20230101VersionStatusVERSIONREVOKED { 204 return cty.NullVal(cty.EmptyObject), fmt.Errorf( 205 "the version %s is revoked and can not be used on Packer builds", version.ID, 206 ) 207 } 208 209 var output DatasourceOutput 210 211 cloudAndRegions := map[string][]string{} 212 for _, build := range version.Builds { 213 if build.Platform != d.config.Platform { 214 continue 215 } 216 for _, artifact := range build.Artifacts { 217 cloudAndRegions[build.Platform] = append(cloudAndRegions[build.Platform], artifact.Region) 218 if artifact.Region == d.config.Region && filterBuildByComponentType(build, d.config.ComponentType) { 219 // This is the desired artifact. 220 output = DatasourceOutput{ 221 Platform: build.Platform, 222 ComponentType: build.ComponentType, 223 CreatedAt: artifact.CreatedAt.String(), 224 BuildID: build.ID, 225 VersionID: build.VersionID, 226 ChannelID: channelID, 227 PackerRunUUID: build.PackerRunUUID, 228 ExternalIdentifier: artifact.ExternalIdentifier, 229 Region: artifact.Region, 230 Labels: build.Labels, 231 } 232 return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil 233 } 234 } 235 } 236 237 return cty.NullVal(cty.EmptyObject), fmt.Errorf( 238 "could not find a build result matching "+ 239 "[region=%q, platform=%q, component_type=%q]. Available: %v ", 240 d.config.Region, d.config.Platform, d.config.ComponentType, cloudAndRegions, 241 ) 242 } 243 244 func filterBuildByComponentType(build *hcpPackerModels.HashicorpCloudPacker20230101Build, componentType string) bool { 245 // optional field is not specified, passthrough 246 if componentType == "" { 247 return true 248 } 249 // if specified, only the matched artifact metadata is returned by this effect 250 return build.ComponentType == componentType 251 }