diff --git a/command/core_wrapper.go b/command/core_wrapper.go index 30f935bff5e..1d0f3290d10 100644 --- a/command/core_wrapper.go +++ b/command/core_wrapper.go @@ -3,7 +3,6 @@ package command import ( "github.com/hashicorp/hcl/v2" packerregistry "github.com/hashicorp/packer/internal/packer_registry" - "github.com/hashicorp/packer/internal/packer_registry/env" "github.com/hashicorp/packer/packer" plugingetter "github.com/hashicorp/packer/packer/plugin-getter" ) @@ -40,9 +39,10 @@ func (c *CoreWrapper) PluginRequirements() (plugingetter.Requirements, hcl.Diagn // ConfiguredArtifactMetadataPublisher returns a configured image bucket that can be used for publishing // build image artifacts to a configured Packer Registry destination. func (c *CoreWrapper) ConfiguredArtifactMetadataPublisher() (*packerregistry.Bucket, hcl.Diagnostics) { - // JSON can only configure its Bucket through Env. variables so if not in PAR mode - // we don't really care if bucket is nil or set to a bunch of zero values. - if !env.InPARMode() { + bucket := c.Core.GetRegistryBucket() + + // If at this point the bucket is nil, it means PAR is not enabled + if bucket == nil { return nil, hcl.Diagnostics{ &hcl.Diagnostic{ Summary: "Publishing build artifacts to HCP Packer Registry not enabled", @@ -53,7 +53,6 @@ func (c *CoreWrapper) ConfiguredArtifactMetadataPublisher() (*packerregistry.Buc } } - bucket := c.Core.GetRegistryBucket() err := bucket.Validate() if err != nil { return nil, hcl.Diagnostics{ diff --git a/go.mod b/go.mod index 96d4b5bf1d9..05ad096260f 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/hashicorp/go-uuid v1.0.2 github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/hcl/v2 v2.10.0 - github.com/hashicorp/hcp-sdk-go v0.10.1-0.20210717111725-9bb3c21afe93 + github.com/hashicorp/hcp-sdk-go v0.10.1-0.20210727200019-239ce8d80646 github.com/hashicorp/packer-plugin-alicloud v1.0.0 github.com/hashicorp/packer-plugin-amazon v1.0.0 github.com/hashicorp/packer-plugin-ansible v1.0.0 diff --git a/go.sum b/go.sum index 4c649e2304c..f6b66975cd7 100644 --- a/go.sum +++ b/go.sum @@ -675,8 +675,8 @@ github.com/hashicorp/hcl/v2 v2.8.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yI github.com/hashicorp/hcl/v2 v2.9.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/hashicorp/hcl/v2 v2.10.0 h1:1S1UnuhDGlv3gRFV4+0EdwB+znNP5HmcGbIqwnSCByg= github.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= -github.com/hashicorp/hcp-sdk-go v0.10.1-0.20210717111725-9bb3c21afe93 h1:tCWmkmMKLZQIi53OpIAdqDw1PmtqbqFLBQYcY2sKIFQ= -github.com/hashicorp/hcp-sdk-go v0.10.1-0.20210717111725-9bb3c21afe93/go.mod h1:Tm9BAlTkp6jknZ0YNxF/556JBC/meCN1LUmWFN38HsU= +github.com/hashicorp/hcp-sdk-go v0.10.1-0.20210727200019-239ce8d80646 h1:4WbtYgGIxeRrGC0hGBtOi6XuJ23024H7r724rCL+eXI= +github.com/hashicorp/hcp-sdk-go v0.10.1-0.20210727200019-239ce8d80646/go.mod h1:Tm9BAlTkp6jknZ0YNxF/556JBC/meCN1LUmWFN38HsU= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= diff --git a/hcl2template/common_test.go b/hcl2template/common_test.go index c9f7b978934..1765a3fa4ab 100644 --- a/hcl2template/common_test.go +++ b/hcl2template/common_test.go @@ -6,7 +6,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" @@ -14,6 +13,7 @@ import ( "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer/builder/null" . "github.com/hashicorp/packer/hcl2template/internal" + packerregistry "github.com/hashicorp/packer/internal/packer_registry" "github.com/hashicorp/packer/packer" "github.com/zclconf/go-cty/cty" ) @@ -354,10 +354,16 @@ var cmpOpts = []cmp.Option{ packer.CoreBuildProvisioner{}, packer.CoreBuildPostProcessor{}, null.Builder{}, + packerregistry.Bucket{}, + packerregistry.Iteration{}, + packer.RegistryBuilder{}, ), cmpopts.IgnoreFields(PackerConfig{}, "Cwd", // Cwd will change for every os type ), + cmpopts.IgnoreFields(packerregistry.Iteration{}, + "Fingerprint", // Fingerprint will change everytime + ), cmpopts.IgnoreFields(VariableAssignment{}, "Expr", // its an interface ), diff --git a/hcl2template/testdata/hcp_par/build-description.pkr.hcl b/hcl2template/testdata/hcp_par/build-description.pkr.hcl new file mode 100644 index 00000000000..e4ec3410ce8 --- /dev/null +++ b/hcl2template/testdata/hcp_par/build-description.pkr.hcl @@ -0,0 +1,15 @@ +build { + description = < override the env. - if env.InPARMode() { - var err error - cfg.bucket, err = packerregistry.NewBucketWithIteration(packerregistry.IterationOptions{ - TemplateBaseDir: cfg.Basedir, - }) - cfg.bucket.LoadDefaultSettingsFromEnv() - if build.Name != "" { - cfg.bucket.Slug = build.Name - } - if err != nil { - diags = append(diags, &hcl.Diagnostic{ - Summary: "Unable to create a valid bucket object for HCP Packer Registry", - Detail: fmt.Sprintf("%s", err), - Severity: hcl.DiagError, - }) - } - } - for _, buildFrom := range b.FromSources { ref := sourceRefFromString(buildFrom) @@ -132,14 +117,14 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB Detail: "A " + sourceLabel + " type is made of three parts that are" + "split by a dot `.`; each part must start with a letter and " + "may contain only letters, digits, underscores, and dashes." + - "A valid source reference looks like: `source.type.name`", Subject: block.DefRange.Ptr(), + "A valid source reference looks like: `source.type.name`", + Subject: block.DefRange.Ptr(), }) continue } // source with no body build.Sources = append(build.Sources, SourceUseBlock{SourceRef: ref}) - cfg.bucket.RegisterBuildForComponent(ref.String()) } body = b.Config @@ -150,6 +135,21 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB } for _, block := range content.Blocks { switch block.Type { + case buildHCPPackerRegistryLabel: + if build.HCPPackerRegistry != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Only one " + buildHCPPackerRegistryLabel + " is allowed"), + Subject: block.DefRange.Ptr(), + }) + continue + } + hcpPackerRegistry, moreDiags := p.decodeHCPRegistry(block) + diags = append(diags, moreDiags...) + if moreDiags.HasErrors() { + continue + } + build.HCPPackerRegistry = hcpPackerRegistry case sourceLabel: ref, moreDiags := p.decodeBuildSource(block) diags = append(diags, moreDiags...) @@ -211,5 +211,38 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB } } + // Creates a bucket if either a hcp_packer_registry block is set or PAR is enabled via + // environment variable + if build.HCPPackerRegistry != nil || env.IsPAREnabled() { + var err error + cfg.bucket, err = packerregistry.NewBucketWithIteration(packerregistry.IterationOptions{ + TemplateBaseDir: cfg.Basedir, + }) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Summary: "Unable to create a valid bucket object for HCP Packer Registry", + Detail: fmt.Sprintf("%s", err), + Severity: hcl.DiagError, + }) + } + cfg.bucket.LoadDefaultSettingsFromEnv() + build.HCPPackerRegistry.WriteToBucketConfig(cfg.bucket) + + // If at this point the bucket.Slug is still empty, + // last try is to use the build.Name if present + if cfg.bucket.Slug == "" && build.Name != "" { + cfg.bucket.Slug = build.Name + } + + // If the description is empty, use the one from the build block + if cfg.bucket.Description == "" { + cfg.bucket.Description = build.Description + } + + for _, source := range build.Sources { + cfg.bucket.RegisterBuildForComponent(source.String()) + } + } + return build, diags } diff --git a/hcl2template/types.build.hcp_packer_registry.go b/hcl2template/types.build.hcp_packer_registry.go new file mode 100644 index 00000000000..31384b3ccd5 --- /dev/null +++ b/hcl2template/types.build.hcp_packer_registry.go @@ -0,0 +1,64 @@ +package hcl2template + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + packerregistry "github.com/hashicorp/packer/internal/packer_registry" +) + +type HCPPackerRegistryBlock struct { + // Bucket slug + Slug string + // Bucket description + Description string + // Bucket labels + Labels map[string]string + + HCL2Ref +} + +func (b *HCPPackerRegistryBlock) WriteToBucketConfig(bucket *packerregistry.Bucket) { + if b == nil { + return + } + bucket.Description = b.Description + bucket.Labels = b.Labels + // If there's already a Slug this was set from env variable. + // In Packer, env variable overrides config values so we keep it that way for consistency. + if bucket.Slug == "" && b.Slug != "" { + bucket.Slug = b.Slug + } +} + +func (p *Parser) decodeHCPRegistry(block *hcl.Block) (*HCPPackerRegistryBlock, hcl.Diagnostics) { + par := &HCPPackerRegistryBlock{} + body := block.Body + + var b struct { + Slug string `hcl:"slug,optional"` + Description string `hcl:"description,optional"` + Labels map[string]string `hcl:"labels,optional"` + Config hcl.Body `hcl:",remain"` + } + diags := gohcl.DecodeBody(body, nil, &b) + if diags.HasErrors() { + return nil, diags + } + + if len(b.Description) > 255 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf(buildHCPPackerRegistryLabel + ".description should have a maximum length of 255 characters"), + Subject: block.DefRange.Ptr(), + }) + return nil, diags + } + + par.Slug = b.Slug + par.Description = b.Description + par.Labels = b.Labels + + return par, diags +} diff --git a/hcl2template/types.build.hcp_packer_registry_test.go b/hcl2template/types.build.hcp_packer_registry_test.go new file mode 100644 index 00000000000..21f7c61e4d1 --- /dev/null +++ b/hcl2template/types.build.hcp_packer_registry_test.go @@ -0,0 +1,398 @@ +package hcl2template + +import ( + "path/filepath" + "testing" + + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer/internal/packer_registry" + "github.com/hashicorp/packer/packer" +) + +func Test_ParseHCPPackerRegistryBlock(t *testing.T) { + defaultParser := getBasicParser() + + tests := []parseTest{ + {"complete working build with hcp_packer_registry block", + defaultParser, + parseTestArgs{"testdata/hcp_par/complete.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + Sources: map[SourceRef]SourceBlock{ + refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"}, + refAWSEBSUbuntu1604: {Type: "amazon-ebs", Name: "ubuntu-1604"}, + }, + Builds: Builds{ + &BuildBlock{ + Name: "bucket-slug", + HCPPackerRegistry: &HCPPackerRegistryBlock{ + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + }, + Sources: []SourceUseBlock{ + { + SourceRef: refVBIsoUbuntu1204, + }, + { + SourceRef: SourceRef{Type: "amazon-ebs", Name: "ubuntu-1604"}, + LocalName: "aws-ubuntu-16.04", + }, + }, + }, + }, + }, + false, false, + []packersdk.Build{ + &packer.CoreBuild{ + BuildName: "bucket-slug", + Type: "virtualbox-iso.ubuntu-1204", + Prepared: true, + Builder: &packer.RegistryBuilder{ + Name: "virtualbox-iso.ubuntu-1204", + Builder: emptyMockBuilder, + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", // this will be different everytime so it's ignored + }, + }, + }, + Provisioners: []packer.CoreBuildProvisioner{}, + PostProcessors: [][]packer.CoreBuildPostProcessor{ + { + { + PostProcessor: &packer.RegistryPostProcessor{ + BuilderType: "virtualbox-iso.ubuntu-1204", + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", + }, + }, + }, + }, + }, + }, + }, + &packer.CoreBuild{ + BuildName: "bucket-slug", + Type: "amazon-ebs.aws-ubuntu-16.04", + Prepared: true, + Builder: &packer.RegistryBuilder{ + Name: "amazon-ebs.aws-ubuntu-16.04", + Builder: emptyMockBuilder, + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", // this will be different everytime so it's ignored + }, + }, + }, + Provisioners: []packer.CoreBuildProvisioner{}, + PostProcessors: [][]packer.CoreBuildPostProcessor{ + { + { + PostProcessor: &packer.RegistryPostProcessor{ + BuilderType: "amazon-ebs.aws-ubuntu-16.04", + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", + }, + }, + }, + }, + }, + }, + }, + }, + false, + }, + {"set slug in hcp_packer_registry block", + defaultParser, + parseTestArgs{"testdata/hcp_par/slug.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + Sources: map[SourceRef]SourceBlock{ + refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"}, + }, + Builds: Builds{ + &BuildBlock{ + Name: "bucket-slug", + HCPPackerRegistry: &HCPPackerRegistryBlock{ + Slug: "real-bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + }, + Sources: []SourceUseBlock{ + { + SourceRef: refVBIsoUbuntu1204, + }, + }, + }, + }, + }, + false, false, + []packersdk.Build{ + &packer.CoreBuild{ + BuildName: "bucket-slug", + Type: "virtualbox-iso.ubuntu-1204", + Prepared: true, + Builder: &packer.RegistryBuilder{ + Name: "virtualbox-iso.ubuntu-1204", + Builder: emptyMockBuilder, + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "real-bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", // this will be different everytime so it's ignored + }, + }, + }, + Provisioners: []packer.CoreBuildProvisioner{}, + PostProcessors: [][]packer.CoreBuildPostProcessor{ + { + { + PostProcessor: &packer.RegistryPostProcessor{ + BuilderType: "virtualbox-iso.ubuntu-1204", + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "real-bucket-slug", + Description: "Some description\n", + Labels: map[string]string{"foo": "bar"}, + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", + }, + }, + }, + }, + }, + }, + }, + }, + false, + }, + {"use build description", + defaultParser, + parseTestArgs{"testdata/hcp_par/build-description.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + Sources: map[SourceRef]SourceBlock{ + refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"}, + }, + Builds: Builds{ + &BuildBlock{ + Description: "Some build description\n", + HCPPackerRegistry: &HCPPackerRegistryBlock{ + Slug: "bucket-slug", + }, + Sources: []SourceUseBlock{ + { + SourceRef: refVBIsoUbuntu1204, + }, + }, + }, + }, + }, + false, false, + []packersdk.Build{ + &packer.CoreBuild{ + Type: "virtualbox-iso.ubuntu-1204", + Prepared: true, + Builder: &packer.RegistryBuilder{ + Name: "virtualbox-iso.ubuntu-1204", + Builder: emptyMockBuilder, + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some build description\n", + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", // this will be different everytime so it's ignored + }, + }, + }, + Provisioners: []packer.CoreBuildProvisioner{}, + PostProcessors: [][]packer.CoreBuildPostProcessor{ + { + { + PostProcessor: &packer.RegistryPostProcessor{ + BuilderType: "virtualbox-iso.ubuntu-1204", + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some build description\n", + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", + }, + }, + }, + }, + }, + }, + }, + }, + false, + }, + {"override build description with hcp_packer_registry description", + defaultParser, + parseTestArgs{"testdata/hcp_par/override-build-description.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + Sources: map[SourceRef]SourceBlock{ + refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"}, + }, + Builds: Builds{ + &BuildBlock{ + Description: "Some build description\n", + HCPPackerRegistry: &HCPPackerRegistryBlock{ + Slug: "bucket-slug", + Description: "Some override description\n", + }, + Sources: []SourceUseBlock{ + { + SourceRef: refVBIsoUbuntu1204, + }, + }, + }, + }, + }, + false, false, + []packersdk.Build{ + &packer.CoreBuild{ + Type: "virtualbox-iso.ubuntu-1204", + Prepared: true, + Builder: &packer.RegistryBuilder{ + Name: "virtualbox-iso.ubuntu-1204", + Builder: emptyMockBuilder, + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some override description\n", + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", // this will be different everytime so it's ignored + }, + }, + }, + Provisioners: []packer.CoreBuildProvisioner{}, + PostProcessors: [][]packer.CoreBuildPostProcessor{ + { + { + PostProcessor: &packer.RegistryPostProcessor{ + BuilderType: "virtualbox-iso.ubuntu-1204", + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Description: "Some override description\n", + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", + }, + }, + }, + }, + }, + }, + }, + }, + false, + }, + {"duplicate hcp_packer_registry blocks", + defaultParser, + parseTestArgs{"testdata/hcp_par/duplicate.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + }, + true, true, + nil, + false, + }, + {"empty hcp_packer_registry block", + defaultParser, + parseTestArgs{"testdata/hcp_par/empty.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + Sources: map[SourceRef]SourceBlock{ + refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"}, + }, + Builds: Builds{ + &BuildBlock{ + Name: "bucket-slug", + HCPPackerRegistry: &HCPPackerRegistryBlock{}, + Sources: []SourceUseBlock{ + { + SourceRef: refVBIsoUbuntu1204, + }, + }, + }, + }, + }, + false, false, + []packersdk.Build{ + &packer.CoreBuild{ + BuildName: "bucket-slug", + Type: "virtualbox-iso.ubuntu-1204", + Prepared: true, + Builder: &packer.RegistryBuilder{ + Name: "virtualbox-iso.ubuntu-1204", + Builder: emptyMockBuilder, + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", // this will be different everytime so it's ignored + }, + }, + }, + Provisioners: []packer.CoreBuildProvisioner{}, + PostProcessors: [][]packer.CoreBuildPostProcessor{ + { + { + PostProcessor: &packer.RegistryPostProcessor{ + BuilderType: "virtualbox-iso.ubuntu-1204", + ArtifactMetadataPublisher: &packer_registry.Bucket{ + Slug: "bucket-slug", + Iteration: &packer_registry.Iteration{ + Fingerprint: "ignored-fingerprint", + }, + }, + }, + }, + }, + }, + }, + }, + false, + }, + {"invalid hcp_packer_registry config", + defaultParser, + parseTestArgs{"testdata/hcp_par/invalid.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + }, + true, true, + nil, + false, + }, + {"long hcp_packer_registry.description", + defaultParser, + parseTestArgs{"testdata/hcp_par/long-description.pkr.hcl", nil, nil}, + &PackerConfig{ + CorePackerVersionString: lockedVersion, + Basedir: filepath.Join("testdata", "hcp_par"), + }, + true, true, + nil, + false, + }, + } + testParse(t, tests) +} diff --git a/internal/packer_registry/client.go b/internal/packer_registry/client.go index 8a6fed60af2..cad10f0efd9 100644 --- a/internal/packer_registry/client.go +++ b/internal/packer_registry/client.go @@ -1,18 +1,21 @@ package packer_registry import ( - "errors" + "fmt" packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/preview/2021-04-30/client/packer_service" - "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/organization_service" - "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/project_service" + organizationSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/organization_service" + projectSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/project_service" + rmmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/models" "github.com/hashicorp/hcp-sdk-go/httpclient" + "github.com/hashicorp/packer/internal/packer_registry/env" ) -// ClientConfig specifies configuration for the client that interacts with HCP -type ClientConfig struct { - ClientID string - ClientSecret string +// Client is an HCP client capable of making requests on behalf of a service principal +type Client struct { + Packer packerSvc.ClientService + Organization organizationSvc.ClientService + Project projectSvc.ClientService // OrganizationID is the organization unique identifier on HCP. OrganizationID string @@ -21,58 +24,75 @@ type ClientConfig struct { ProjectID string } -func (cfg ClientConfig) Validate() error { - if cfg.OrganizationID == "" { - return &ClientError{ +// NewClient returns an authenticated client to a HCP Packer Registry. +// Client authentication requires the following environment variables be set HCP_CLIENT_ID and HCP_CLIENT_SECRET. +// Upon error a HCPClientError will be returned. +func NewClient() (*Client, error) { + if !env.HasHCPCredentials() { + return nil, &ClientError{ StatusCode: InvalidClientConfig, - Err: errors.New(`no valid HCP Organization ID found, check that HCP_PACKER_REGISTRY is in the format "HCP_ORG_ID/HCP_PROJ_ID"`), + Err: fmt.Errorf("the client authentication requires both HCP_CLIENT_ID and HCP_CLIENT_SECRET environment variables to be set"), } - } - if cfg.ProjectID == "" { - return &ClientError{ + cl, err := httpclient.New(httpclient.Config{}) + if err != nil { + return nil, &ClientError{ StatusCode: InvalidClientConfig, - Err: errors.New(`no valid HCP Project ID found, check that HCP_PACKER_REGISTRY is in the format "HCP_ORG_ID/HCP_PROJ_ID"`), + Err: err, } } - return nil -} - -// Client is an HCP client capable of making requests on behalf of a service principal -type Client struct { - Config ClientConfig - - Organization organization_service.ClientService - Project project_service.ClientService - Packer packerSvc.ClientService -} + client := &Client{ + Packer: packerSvc.New(cl, nil), + Organization: organizationSvc.New(cl, nil), + Project: projectSvc.New(cl, nil), + } -// NewClient returns an authenticated client to a HCP Packer Registry. -// Client authentication requires the following environment variables be set HCP_CLIENT_ID, HCP_CLIENT_SECRET, and HCP_PACKER_REGISTRY. -// if not explicitly provided via a valid ClientConfig cfg. -// Upon error a HCPClientError will be returned. -func NewClient(cfg ClientConfig) (*Client, error) { - if err := cfg.Validate(); err != nil { + if err := client.loadOrganizationID(); err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: err, } } - - cl, err := httpclient.New(httpclient.Config{}) - if err != nil { + if err := client.loadProjectID(); err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: err, } } - svc := packerSvc.New(cl, nil) - return &Client{ - Packer: svc, - Config: cfg, - }, nil + return client, nil +} + +func (c *Client) loadOrganizationID() error { + // Get the organization ID. + listOrgParams := organizationSvc.NewOrganizationServiceListParams() + listOrgResp, err := c.Organization.OrganizationServiceList(listOrgParams, nil) + if err != nil { + return fmt.Errorf("unable to fetch organization list: %v", err) + } + orgLen := len(listOrgResp.Payload.Organizations) + if orgLen != 1 { + return fmt.Errorf("unexpected number of organizations: expected 1, actual: %v", orgLen) + } + c.OrganizationID = listOrgResp.Payload.Organizations[0].ID + return nil +} +func (c *Client) loadProjectID() error { + // Get the project using the organization ID. + listProjParams := projectSvc.NewProjectServiceListParams() + listProjParams.ScopeID = &c.OrganizationID + scopeType := string(rmmodels.HashicorpCloudResourcemanagerResourceIDResourceTypeORGANIZATION) + listProjParams.ScopeType = &scopeType + listProjResp, err := c.Project.ProjectServiceList(listProjParams, nil) + if err != nil { + return fmt.Errorf("unable to fetch project id: %v", err) + } + if len(listProjResp.Payload.Projects) > 1 { + return fmt.Errorf("this version of Packer does not support multiple projects") + } + c.ProjectID = listProjResp.Payload.Projects[0].ID + return nil } diff --git a/internal/packer_registry/env/env.go b/internal/packer_registry/env/env.go index c67a1dfdd17..9f8f4961256 100644 --- a/internal/packer_registry/env/env.go +++ b/internal/packer_registry/env/env.go @@ -1,6 +1,9 @@ package env -import "os" +import ( + "os" + "strings" +) func HasClientID() bool { _, ok := os.LookupEnv(HCPClientID) @@ -12,21 +15,15 @@ func HasClientSecret() bool { return ok } -func HasPackerRegistryDestionation() bool { - _, ok := os.LookupEnv(HCPPackerRegistry) - return ok -} - func HasPackerRegistryBucket() bool { _, ok := os.LookupEnv(HCPPackerBucket) return ok } -func InPARMode() bool { +func HasHCPCredentials() bool { checks := []func() bool{ HasClientID, HasClientSecret, - HasPackerRegistryDestionation, } for _, check := range checks { @@ -37,3 +34,8 @@ func InPARMode() bool { return true } + +func IsPAREnabled() bool { + val, ok := os.LookupEnv(HCPPackerRegistry) + return ok && strings.ToLower(val) != "off" && val != "0" +} diff --git a/internal/packer_registry/env/env_test.go b/internal/packer_registry/env/env_test.go new file mode 100644 index 00000000000..5d3a9afa438 --- /dev/null +++ b/internal/packer_registry/env/env_test.go @@ -0,0 +1,53 @@ +package env + +import ( + "os" + "testing" +) + +func Test_IsPAREnabled(t *testing.T) { + tcs := []struct { + name string + value string + output bool + }{ + { + name: "set with 1", + value: "1", + output: true, + }, + { + name: "set with ON", + value: "ON", + output: true, + }, + { + name: "set with 0", + value: "0", + output: false, + }, + { + name: "set with OFF", + value: "OFF", + output: false, + }, + { + name: "unset", + value: "", + output: false, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + if tc.value != "" { + _ = os.Setenv(HCPPackerRegistry, tc.value) + defer os.Unsetenv(HCPPackerRegistry) + } + out := IsPAREnabled() + if out != tc.output { + t.Fatalf("unexpected output: %t", out) + } + }) + } +} diff --git a/internal/packer_registry/registry.go b/internal/packer_registry/registry.go index 9de26707f9e..26f49b64b3d 100644 --- a/internal/packer_registry/registry.go +++ b/internal/packer_registry/registry.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/go-openapi/runtime" packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/preview/2021-04-30/client/packer_service" "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/preview/2021-04-30/models" "google.golang.org/grpc/codes" @@ -14,11 +13,11 @@ import ( func CreateBucket(ctx context.Context, client *Client, input *models.HashicorpCloudPackerCreateBucketRequest) (string, error) { params := packerSvc.NewCreateBucketParamsWithContext(ctx) - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.Body = input - resp, err := client.Packer.CreateBucket(params, nil, func(*runtime.ClientOperation) {}) + resp, err := client.Packer.CreateBucket(params, nil) if err != nil { return "", err } @@ -41,14 +40,14 @@ func UpsertBucket(ctx context.Context, client *Client, input *models.HashicorpCl } params := packerSvc.NewUpdateBucketParamsWithContext(ctx) - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.BucketSlug = input.BucketSlug params.Body = &models.HashicorpCloudPackerUpdateBucketRequest{ Description: input.Description, Labels: input.Labels, } - _, err = client.Packer.UpdateBucket(params, nil, func(*runtime.ClientOperation) {}) + _, err = client.Packer.UpdateBucket(params, nil) return err } @@ -62,12 +61,12 @@ input: *models.HashicorpCloudPackerCreateIterationRequest{BucketSlug: "bucket na func CreateIteration(ctx context.Context, client *Client, input *models.HashicorpCloudPackerCreateIterationRequest) (string, error) { // Create/find iteration params := packerSvc.NewCreateIterationParamsWithContext(ctx) - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.BucketSlug = input.BucketSlug params.Body = input - it, err := client.Packer.CreateIteration(params, nil, func(*runtime.ClientOperation) {}) + it, err := client.Packer.CreateIteration(params, nil) if err != nil { return "", err } @@ -78,14 +77,14 @@ func CreateIteration(ctx context.Context, client *Client, input *models.Hashicor func GetIteration(ctx context.Context, client *Client, bucketslug string, fingerprint string) (string, error) { // Create/find iteration params := packerSvc.NewGetIterationParamsWithContext(ctx) - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.BucketSlug = bucketslug // identifier can be either fingerprint, iterationid, or incremental version // for now, we only care about fingerprint so we're hardcoding it. params.Fingerprint = &fingerprint - it, err := client.Packer.GetIteration(params, nil, func(*runtime.ClientOperation) {}) + it, err := client.Packer.GetIteration(params, nil) if err != nil { return "", err } @@ -95,13 +94,13 @@ func GetIteration(ctx context.Context, client *Client, bucketslug string, finger func CreateBuild(ctx context.Context, client *Client, input *models.HashicorpCloudPackerCreateBuildRequest) (string, error) { params := packerSvc.NewCreateBuildParamsWithContext(ctx) - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.BucketSlug = input.BucketSlug params.BuildIterationID = input.Build.IterationID params.Body = input - resp, err := client.Packer.CreateBuild(params, nil, func(*runtime.ClientOperation) {}) + resp, err := client.Packer.CreateBuild(params, nil) if err != nil { return "", err } @@ -111,12 +110,12 @@ func CreateBuild(ctx context.Context, client *Client, input *models.HashicorpClo func ListBuilds(ctx context.Context, client *Client, bucketSlug string, iterationID string) ([]*models.HashicorpCloudPackerBuild, error) { params := packerSvc.NewListBuildsParamsWithContext(ctx) - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.BucketSlug = bucketSlug params.IterationID = iterationID - resp, err := client.Packer.ListBuilds(params, nil, func(*runtime.ClientOperation) {}) + resp, err := client.Packer.ListBuilds(params, nil) if err != nil { return []*models.HashicorpCloudPackerBuild{}, err } @@ -127,11 +126,11 @@ func ListBuilds(ctx context.Context, client *Client, bucketSlug string, iteratio func UpdateBuild(ctx context.Context, client *Client, input *models.HashicorpCloudPackerUpdateBuildRequest) (string, error) { params := packerSvc.NewUpdateBuildParamsWithContext(ctx) params.BuildID = input.BuildID - params.LocationOrganizationID = client.Config.OrganizationID - params.LocationProjectID = client.Config.ProjectID + params.LocationOrganizationID = client.OrganizationID + params.LocationProjectID = client.ProjectID params.Body = input - resp, err := client.Packer.UpdateBuild(params, nil, func(*runtime.ClientOperation) {}) + resp, err := client.Packer.UpdateBuild(params, nil) if err != nil { return "", err } diff --git a/internal/packer_registry/types.bucket.go b/internal/packer_registry/types.bucket.go index 9c177c32f44..afa1256922c 100644 --- a/internal/packer_registry/types.bucket.go +++ b/internal/packer_registry/types.bucket.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "os" - "strings" "sync" "github.com/hashicorp/go-multierror" @@ -20,7 +19,6 @@ type Bucket struct { Description string Destination string Labels map[string]string - Config ClientConfig *Iteration client *Client } @@ -47,11 +45,6 @@ func (b *Bucket) Validate() error { if b.Slug == "" { return fmt.Errorf("no Packer bucket name defined; either the environment variable %q is undefined or the HCL configuration has no build name", env.HCPPackerBucket) } - - if b.Destination == "" { - return fmt.Errorf("no Packer registry defined; either the environment variable %q is undefined or the HCL configuration has no build name", env.HCPPackerRegistry) - } - return nil } @@ -69,6 +62,8 @@ func (b *Bucket) Initialize(ctx context.Context) error { } } + b.Destination = fmt.Sprintf("%s/%s", b.client.OrganizationID, b.client.ProjectID) + bucketInput := &models.HashicorpCloudPackerCreateBucketRequest{ BucketSlug: b.Slug, Description: b.Description, @@ -125,7 +120,7 @@ func (b *Bucket) Initialize(ctx context.Context) error { found = true log.Printf("build of component type %s already exists; skipping the create call", expected) - if *existing.Status == models.HashicorpCloudPackerBuildStatusDONE { + if existing.Status == models.HashicorpCloudPackerBuildStatusDONE { // We also need to remove the builds that are _complete_ from the // Iteration's expectedBuilds so we don't overwrite them. //b.Iteration.expectedBuilds = append(b.Iteration.expectedBuilds[:i], b.Iteration.expectedBuilds[i+1:]...) @@ -180,7 +175,7 @@ func (b *Bucket) Initialize(ctx context.Context) error { // connect initializes a client connection to a remote HCP Packer Registry service on HCP. // Upon a successful connection the initialized client is persisted on the Bucket b for later usage. func (b *Bucket) connect() error { - registryClient, err := NewClient(b.Config) + registryClient, err := NewClient() if err != nil { return errors.New("Failed to create client connection to artifact registry: " + err.Error()) } @@ -220,7 +215,7 @@ func (b *Bucket) PublishBuildStatus(ctx context.Context, name string, status mod Updates: &models.HashicorpCloudPackerBuildUpdates{ PackerRunUUID: buildToUpdate.RunUUID, Labels: buildToUpdate.Metadata, - Status: &status, + Status: status, }, } @@ -259,7 +254,7 @@ func (b *Bucket) CreateInitialBuildForIteration(ctx context.Context, name string ComponentType: name, IterationID: b.Iteration.ID, PackerRunUUID: b.Iteration.RunUUID, - Status: &status, + Status: status, }, } @@ -331,39 +326,11 @@ func (b *Bucket) AddBuildMetadata(name string, data map[string]string) error { // Load defaults from environment variables func (b *Bucket) LoadDefaultSettingsFromEnv() { - if b.Config.ClientID == "" { - b.Config.ClientID = os.Getenv(env.HCPClientID) - } - - if b.Config.ClientSecret == "" { - b.Config.ClientSecret = os.Getenv(env.HCPClientSecret) - } - // Configure HCP registry destination if b.Slug == "" { b.Slug = os.Getenv(env.HCPPackerBucket) } - loc := os.Getenv(env.HCPPackerRegistry) - locParts := strings.Split(loc, "/") - if len(locParts) != 2 { - // we want an error here. Or at least when we try to create the registry client we fail - return - } - orgID, projID := locParts[0], locParts[1] - - if b.Destination == "" { - b.Destination = loc - } - - if b.Config.OrganizationID == "" { - b.Config.OrganizationID = orgID - } - - if b.Config.ProjectID == "" { - b.Config.ProjectID = projID - } - // Set some iteration values. For Packer RunUUID should always be set. // Creating a bucket differently? Let's not overwrite a UUID that might be set. if b.Iteration.RunUUID == "" { diff --git a/packer/core.go b/packer/core.go index 38777a1d5d6..87da34ec28b 100644 --- a/packer/core.go +++ b/packer/core.go @@ -142,7 +142,7 @@ func (core *Core) Initialize() error { packersdk.LogSecretFilter.Set(secret) } - if env.InPARMode() { + if env.IsPAREnabled() { var err error core.bucket, err = packerregistry.NewBucketWithIteration(packerregistry.IterationOptions{ TemplateBaseDir: filepath.Dir(core.Template.Path), @@ -390,7 +390,7 @@ func (c *Core) Build(n string) (packersdk.Build, error) { "post-processor type not found: %s", rawP.Type) } - if env.InPARMode() && c.bucket != nil { + if c.bucket != nil { postProcessor = &RegistryPostProcessor{ BuilderType: n, ArtifactMetadataPublisher: c.bucket, @@ -414,7 +414,7 @@ func (c *Core) Build(n string) (packersdk.Build, error) { postProcessors = append(postProcessors, current) } - if env.InPARMode() && c.bucket != nil { + if c.bucket != nil { postProcessors = append(postProcessors, []CoreBuildPostProcessor{ { PostProcessor: &RegistryPostProcessor{ @@ -427,7 +427,7 @@ func (c *Core) Build(n string) (packersdk.Build, error) { // TODO hooks one day - if env.InPARMode() && c.bucket != nil { + if c.bucket != nil { builder = &RegistryBuilder{ Name: n, ArtifactMetadataPublisher: c.bucket, 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 new file mode 100644 index 00000000000..8cd8bded3d8 --- /dev/null +++ b/website/content/docs/templates/hcl_templates/blocks/build/hcp_packer_registry.mdx @@ -0,0 +1,50 @@ +--- +description: > + The hcp_packer_registry allows operators the ability to customize the metadata sent to HCP Packer Registry. + It configures the base details of an image that is created or updated within HCP PAR. +page_title: hcp_packer_registry - build - Blocks +--- + +# The `hcp_packer_registry` block + +The `hcp_packer_registry` block allows operators the ability to customize the metadata sent to +HCP Packer Registry. It configures the details of an image that is created or updated within HCP PAR. + +The presence of a `hcp_packer_registry` block will enable HCP PAR mode and all the builds within that build block +will be pushed to the remote registry if the appropriate HCP credentials are set (`HCP_CLIENT_ID` and `HCP_CLIENT_SECRET`). If no HCP credentials are set Packer will fail the build and exit immediately to avoid any potential artifact drift between the build providers and the Packer registry. + +```hcl +# file: builds.pkr.hcl +source "file" "basic-example" { + content = "Lorem ipsum dolor sit amet" + target = "sample_artifact" +} + +build { + hcp_packer_registry { + slug = "sample-artifact" + + description = <hcp_packer_registry", + "path": "templates/hcl_templates/blocks/build/hcp_packer_registry" + }, { "title": "source", "path": "templates/hcl_templates/blocks/build/source"