From b763b79d9faf6c484787e46e601bd76a614e28a1 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Fri, 8 Oct 2021 15:05:35 -0700 Subject: [PATCH] implement contextual variable packer.iteration_id --- command/build.go | 37 ++++++++++---- hcl2template/types.packer_config.go | 12 ++++- internal/registry/types.bucket.go | 17 ++++--- internal/registry/types.bucket_test.go | 48 +++++++++++++++---- .../hcl_templates/contextual-variables.mdx | 34 +++++++++++++ 5 files changed, 121 insertions(+), 27 deletions(-) diff --git a/command/build.go b/command/build.go index e67a710b4..b90a176fd 100644 --- a/command/build.go +++ b/command/build.go @@ -156,6 +156,29 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int return ret } + // This build currently enforces a 1:1 mapping that one publisher can be assigned to a single packer config file. + // It also requires that each config type implements this ConfiguredArtifactMetadataPublisher to return a configured bucket. + // TODO find an option that is not managed by a globally shared Publisher. + ArtifactMetadataPublisher, diags := packerStarter.ConfiguredArtifactMetadataPublisher() + if diags.HasErrors() { + return writeDiags(c.Ui, nil, diags) + } + + // We need to create a bucket and an empty iteration before we retrieve builds + // so that we can add the iteration ID to the build's eval context + if ArtifactMetadataPublisher != nil { + if err := ArtifactMetadataPublisher.Initialize(buildCtx); err != nil { + diags := hcl.Diagnostics{ + &hcl.Diagnostic{ + Summary: "HCP Packer Registry iteration initialization failed", + Detail: fmt.Sprintf("Failed to initialize iteration for %q\n %s", ArtifactMetadataPublisher.Slug, err), + Severity: hcl.DiagError, + }, + } + return writeDiags(c.Ui, nil, diags) + } + } + builds, diags := packerStarter.GetBuilds(packer.GetBuildsOptions{ Only: cla.Only, Except: cla.Except, @@ -172,19 +195,13 @@ func (c *BuildCommand) RunContext(buildCtx context.Context, cla *BuildArgs) int c.Ui.Say("Debug mode enabled. Builds will not be parallelized.") } - // This build currently enforces a 1:1 mapping that one publisher can be assigned to a single packer config file. - // It also requires that each config type implements this ConfiguredArtifactMetadataPublisher to return a configured bucket. - // TODO find an option that is not managed by a globally shared Publisher. - ArtifactMetadataPublisher, diags := packerStarter.ConfiguredArtifactMetadataPublisher() - if diags.HasErrors() { - return writeDiags(c.Ui, nil, diags) - } - + // Now that builds have been retrieved, we can populate the iteration with + // the builds we expect to run. if ArtifactMetadataPublisher != nil { - if err := ArtifactMetadataPublisher.Initialize(buildCtx); err != nil { + if err := ArtifactMetadataPublisher.PopulateIteration(buildCtx); err != nil { diags := hcl.Diagnostics{ &hcl.Diagnostic{ - Summary: "HCP Packer Registry initialization failed", + Summary: "HCP Packer Registry build initialization failed", Detail: fmt.Sprintf("Failed to initialize build for %q\n %s", ArtifactMetadataPublisher.Slug, err), Severity: hcl.DiagError, }, diff --git a/hcl2template/types.packer_config.go b/hcl2template/types.packer_config.go index 89aa64cc8..f91d3ae4d 100644 --- a/hcl2template/types.packer_config.go +++ b/hcl2template/types.packer_config.go @@ -109,8 +109,8 @@ func (cfg *PackerConfig) EvalContext(ctx BlockContext, variables map[string]cty. }), buildAccessor: cty.UnknownVal(cty.EmptyObject), packerAccessor: cty.ObjectVal(map[string]cty.Value{ - "version": cty.StringVal(cfg.CorePackerVersionString), - "iteration_id": cty.UnknownVal(cty.String), + "version": cty.StringVal(cfg.CorePackerVersionString), + "iterationID": cty.UnknownVal(cty.String), }), pathVariablesAccessor: cty.ObjectVal(map[string]cty.Value{ "cwd": cty.StringVal(strings.ReplaceAll(cfg.Cwd, `\`, `/`)), @@ -119,6 +119,14 @@ func (cfg *PackerConfig) EvalContext(ctx BlockContext, variables map[string]cty. }, } + // Store the iteration_id, if it exists. Otherwise, it'll be "unknown" + if cfg.bucket != nil { + ectx.Variables[packerAccessor] = cty.ObjectVal(map[string]cty.Value{ + "version": cty.StringVal(cfg.CorePackerVersionString), + "iterationID": cty.StringVal(cfg.bucket.Iteration.ID), + }) + } + // In the future we'd like to load and execute HCL blocks using a graph // dependency tree, so that any block can use any block whatever the // order. diff --git a/internal/registry/types.bucket.go b/internal/registry/types.bucket.go index 27380e2e0..e78442820 100644 --- a/internal/registry/types.bucket.go +++ b/internal/registry/types.bucket.go @@ -289,16 +289,11 @@ func (b *Bucket) createIteration() (*models.HashicorpCloudPackerIteration, error return iterationResp, nil } -// initializeIteration populates the bucket iteration with the details needed for tracking builds for a Packer run. -// If an existing Packer registry iteration exists for the said iteration fingerprint, calling initialize on iteration -// that doesn't yet exist will call createIteration to create the entry on the HCP packer registry for the given bucket. -// All build details will be created (if they don't exists) and added to b.Iteration.builds for tracking during runtime. func (b *Bucket) initializeIteration(ctx context.Context) error { - // load existing iteration using fingerprint. iterationResp, err := GetIteration(ctx, b.client, b.Slug, b.Iteration.Fingerprint) if checkErrorCode(err, codes.Aborted) { - //probably means Iteration doesn't exist need a way to check the error + // probably means Iteration doesn't exist need a way to check the error iterationResp, err = b.createIteration() } @@ -321,9 +316,17 @@ func (b *Bucket) initializeIteration(ctx context.Context) error { "If you wish to add a new build to this image a new iteration must be created by changing the build fingerprint.", b.Iteration.Fingerprint) } + return nil +} + +// populateIteration populates the bucket iteration with the details needed for tracking builds for a Packer run. +// If an existing Packer registry iteration exists for the said iteration fingerprint, calling initialize on iteration +// that doesn't yet exist will call createIteration to create the entry on the HCP packer registry for the given bucket. +// All build details will be created (if they don't exists) and added to b.Iteration.builds for tracking during runtime. +func (b *Bucket) PopulateIteration(ctx context.Context) error { // list all this iteration's builds so we can figure out which ones // we want to run against. TODO: pagination? - existingBuilds, err := ListBuilds(ctx, b.client, b.Slug, iterationResp.ID) + existingBuilds, err := ListBuilds(ctx, b.client, b.Slug, b.Iteration.ID) if err != nil { return fmt.Errorf("error listing builds for this existing iteration: %s", err) } diff --git a/internal/registry/types.bucket_test.go b/internal/registry/types.bucket_test.go index 459552969..5a8cbdf7d 100644 --- a/internal/registry/types.bucket_test.go +++ b/internal/registry/types.bucket_test.go @@ -42,14 +42,22 @@ func TestInitialize_NewBucketNewIteration(t *testing.T) { t.Errorf("expected a call to CreateIteration but it didn't happen") } - if !mockService.CreateBuildCalled { - t.Errorf("expected a call to CreateBuild but it didn't happen") + if mockService.CreateBuildCalled { + t.Errorf("Didn't expect a call to CreateBuild") } if b.Iteration.ID != "iteration-id" { t.Errorf("expected an iteration to created but it didn't") } + err = b.PopulateIteration(context.TODO()) + if err != nil { + t.Errorf("unexpected failure: %v", err) + } + if !mockService.CreateBuildCalled { + t.Errorf("Expected a call to CreateBuild but it didn't happen") + } + if _, ok := b.Iteration.builds.Load("happycloud.image"); !ok { t.Errorf("expected a basic build entry to be created but it didn't") } @@ -91,14 +99,22 @@ func TestInitialize_ExistingBucketNewIteration(t *testing.T) { t.Errorf("expected a call to CreateIteration but it didn't happen") } - if !mockService.CreateBuildCalled { - t.Errorf("expected a call to CreateBuild but it didn't happen") + if mockService.CreateBuildCalled { + t.Errorf("Didn't expect a call to CreateBuild") } if b.Iteration.ID != "iteration-id" { t.Errorf("expected an iteration to created but it didn't") } + err = b.PopulateIteration(context.TODO()) + if err != nil { + t.Errorf("unexpected failure: %v", err) + } + if !mockService.CreateBuildCalled { + t.Errorf("Expected a call to CreateBuild but it didn't happen") + } + if _, ok := b.Iteration.builds.Load("happycloud.image"); !ok { t.Errorf("expected a basic build entry to be created but it didn't") } @@ -133,6 +149,10 @@ func TestInitialize_ExistingBucketExistingIteration(t *testing.T) { if err != nil { t.Errorf("unexpected failure: %v", err) } + err = b.PopulateIteration(context.TODO()) + if err != nil { + t.Errorf("unexpected failure: %v", err) + } if mockService.CreateBucketCalled { t.Errorf("unexpected call to CreateBucket") @@ -158,6 +178,10 @@ func TestInitialize_ExistingBucketExistingIteration(t *testing.T) { t.Errorf("expected an iteration to created but it didn't") } + err = b.PopulateIteration(context.TODO()) + if err != nil { + t.Errorf("unexpected failure: %v", err) + } loadedBuild, ok := b.Iteration.builds.Load("happycloud.image") if !ok { t.Errorf("expected a basic build entry to be created but it didn't") @@ -201,11 +225,11 @@ func TestInitialize_ExistingBucketCompleteIteration(t *testing.T) { err = b.Initialize(context.TODO()) if err == nil { - t.Errorf("Calling initialize on a completed Iteration should fail hard") + t.Errorf("unexpected failure: %v", err) } if mockService.CreateIterationCalled { - t.Errorf("unexpected a call to CreateIteration") + t.Errorf("unexpected call to CreateIteration") } if !mockService.GetIterationCalled { @@ -213,11 +237,11 @@ func TestInitialize_ExistingBucketCompleteIteration(t *testing.T) { } if mockService.CreateBuildCalled { - t.Errorf("unexpected a call to CreateBuild") + t.Errorf("unexpected call to CreateBuild") } if b.Iteration.ID != "iteration-id" { - t.Errorf("expected an iteration to returned but it didn't") + t.Errorf("expected an iteration to be returned but it wasn't") } } @@ -249,6 +273,10 @@ func TestUpdateBuildStatus(t *testing.T) { if err != nil { t.Errorf("unexpected failure: %v", err) } + err = b.PopulateIteration(context.TODO()) + if err != nil { + t.Errorf("unexpected failure: %v", err) + } loadedBuild, ok := b.Iteration.builds.Load("happycloud.image") if !ok { @@ -312,6 +340,10 @@ func TestUpdateBuildStatus_DONENoImages(t *testing.T) { if err != nil { t.Errorf("unexpected failure: %v", err) } + err = b.PopulateIteration(context.TODO()) + if err != nil { + t.Errorf("unexpected failure: %v", err) + } loadedBuild, ok := b.Iteration.builds.Load("happycloud.image") if !ok { diff --git a/website/content/docs/templates/hcl_templates/contextual-variables.mdx b/website/content/docs/templates/hcl_templates/contextual-variables.mdx index 929218f14..17977d0e1 100644 --- a/website/content/docs/templates/hcl_templates/contextual-variables.mdx +++ b/website/content/docs/templates/hcl_templates/contextual-variables.mdx @@ -129,3 +129,37 @@ null.first-example: output will be in this color. Make sure to wrap your variable in single quotes in order to escape the string that is returned; if you are running a dev version of packer the parenthesis may through off your shell escaping otherwise. + +# HCP Packer Iteration ID + +If your build is pushing metadata to the HCP Packer reigstry, this variable is +set to the value of the Iteration ID associated with this run. + +```hcl +source "amazon-ebs" "cannonical-ubuntu-server" { + ami_name = "packer-example" + // ... + run_volume_tags = { + hcp_iteration_id = packer.iterationID + } +} +``` + +```shell-session +==> vanilla.amazon-ebs.cannonical-ubuntu-server: Adding tags to source instance + vanilla.amazon-ebs.cannonical-ubuntu-server: Adding tag: "Name": "Packer Builder" + vanilla.amazon-ebs.cannonical-ubuntu-server: Adding tag: "hcp_iteration_id": "01FHGF3M2AK4TS6PCZES4VX5E7" +``` + +You can also add this value to post-processors, for example to add to a manifest file: + +```hcl + post-processor "manifest" { + output = "manifest.json" + strip_path = true + custom_data = { + iteration = "${packer.iterationID}" + } + } + +``` \ No newline at end of file