diff --git a/builder/docker/builder.go b/builder/docker/builder.go index 3b6ae22f5..a3f807da4 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -5,6 +5,7 @@ import ( "log" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer/builder" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/multistep" @@ -29,7 +30,9 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { return nil, warnings, errs } - return nil, warnings, nil + return []string{ + "ImageSha256", + }, warnings, nil } func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { @@ -44,6 +47,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack } log.Printf("[DEBUG] Docker version: %s", version.String()) + // Setup the state bag and initial state for the steps + state := new(multistep.BasicStateBag) + state.Put("config", &b.config) + state.Put("hook", hook) + state.Put("ui", ui) + generatedData := &builder.GeneratedData{State: state} + + // Setup the driver that will talk to Docker + state.Put("driver", driver) + steps := []multistep.Step{ &StepTempDir{}, &StepPull{}, @@ -67,7 +80,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack log.Print("[DEBUG] Container will be discarded") } else if b.config.Commit { log.Print("[DEBUG] Container will be committed") - steps = append(steps, new(StepCommit)) + steps = append(steps, + new(StepCommit), + &StepSetGeneratedData{ // Adds ImageSha256 variable available after StepCommit + GeneratedData: generatedData, + }) } else if b.config.ExportPath != "" { log.Printf("[DEBUG] Container will be exported to %s", b.config.ExportPath) steps = append(steps, new(StepExport)) @@ -75,15 +92,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack return nil, errArtifactNotUsed } - // Setup the state bag and initial state for the steps - state := new(multistep.BasicStateBag) - state.Put("config", &b.config) - state.Put("hook", hook) - state.Put("ui", ui) - - // Setup the driver that will talk to Docker - state.Put("driver", driver) - // Run! b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) b.runner.Run(ctx, state) diff --git a/builder/docker/driver.go b/builder/docker/driver.go index 816c49f1a..43effdf8e 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -26,6 +26,9 @@ type Driver interface { // for external access. IPAddress(id string) (string, error) + // Sha256 returns the sha256 id of the image + Sha256(id string) (string, error) + // Login. This will lock the driver from performing another Login // until Logout is called. Therefore, any users MUST call Logout. Login(repo, username, password string) error diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 10e78c7d3..ba5693b91 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -159,6 +159,23 @@ func (d *DockerDriver) IPAddress(id string) (string, error) { return strings.TrimSpace(stdout.String()), nil } +func (d *DockerDriver) Sha256(id string) (string, error) { + var stderr, stdout bytes.Buffer + cmd := exec.Command( + "docker", + "inspect", + "--format", + "{{ .Id }}", + id) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("Error: %s\n\nStderr: %s", err, stderr.String()) + } + + return strings.TrimSpace(stdout.String()), nil +} + func (d *DockerDriver) Login(repo, user, pass string) error { d.l.Lock() diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index 71af7f9a0..b4e3036f6 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -28,6 +28,11 @@ type MockDriver struct { IPAddressResult string IPAddressErr error + Sha256Called bool + Sha256Id string + Sha256Result string + Sha256Err error + KillCalled bool KillID string KillError error @@ -118,6 +123,12 @@ func (d *MockDriver) IPAddress(id string) (string, error) { return d.IPAddressResult, d.IPAddressErr } +func (d *MockDriver) Sha256(id string) (string, error) { + d.Sha256Called = true + d.Sha256Id = id + return d.Sha256Result, d.Sha256Err +} + func (d *MockDriver) Login(r, u, p string) error { d.LoginCalled = true d.LoginRepo = r diff --git a/builder/docker/step_set_generated_data.go b/builder/docker/step_set_generated_data.go new file mode 100644 index 000000000..5ab9df8b9 --- /dev/null +++ b/builder/docker/step_set_generated_data.go @@ -0,0 +1,30 @@ +package docker + +import ( + "context" + + "github.com/hashicorp/packer/builder" + "github.com/hashicorp/packer/helper/multistep" +) + +type StepSetGeneratedData struct { + GeneratedData *builder.GeneratedData +} + +func (s *StepSetGeneratedData) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + driver := state.Get("driver").(Driver) + + sha256 := "ERR_IMAGE_SHA256_NOT_FOUND" + if imageId, ok := state.GetOk("image_id"); ok { + s256, err := driver.Sha256(imageId.(string)) + if err == nil { + sha256 = s256 + } + } + s.GeneratedData.Put("ImageSha256", sha256) + return multistep.ActionContinue +} + +func (s *StepSetGeneratedData) Cleanup(_ multistep.StateBag) { + // No cleanup... +} diff --git a/builder/docker/step_set_generated_data_test.go b/builder/docker/step_set_generated_data_test.go new file mode 100644 index 000000000..b1e5e2bea --- /dev/null +++ b/builder/docker/step_set_generated_data_test.go @@ -0,0 +1,51 @@ +package docker + +import ( + "context" + "testing" + + "github.com/hashicorp/packer/builder" + "github.com/hashicorp/packer/helper/multistep" +) + +func TestStepSetGeneratedData_Run(t *testing.T) { + state := testState(t) + step := new(StepSetGeneratedData) + step.GeneratedData = &builder.GeneratedData{State: state} + driver := state.Get("driver").(*MockDriver) + driver.Sha256Result = "80B3BB1B1696E73A9B19DEEF92F664F8979F948DF348088B61F9A3477655AF64" + state.Put("image_id", "12345") + + if action := step.Run(context.TODO(), state); action != multistep.ActionContinue { + t.Fatalf("Should not halt") + } + if !driver.Sha256Called { + t.Fatalf("driver.SHA256 should be called") + } + if driver.Sha256Id != "12345" { + t.Fatalf("driver.SHA256 got wrong image it: %s", driver.Sha256Id) + } + genData := state.Get("generated_data").(map[string]interface{}) + imgSha256 := genData["ImageSha256"].(string) + if imgSha256 != driver.Sha256Result { + t.Fatalf("Expected ImageSha256 to be %s but was %s", driver.Sha256Result, imgSha256) + } + + // Image ID not implement + state = testState(t) + step.GeneratedData = &builder.GeneratedData{State: state} + driver = state.Get("driver").(*MockDriver) + notImplementedMsg := "ERR_IMAGE_SHA256_NOT_FOUND" + + if action := step.Run(context.TODO(), state); action != multistep.ActionContinue { + t.Fatalf("Should not halt") + } + if driver.Sha256Called { + t.Fatalf("driver.SHA256 should not be called") + } + genData = state.Get("generated_data").(map[string]interface{}) + imgSha256 = genData["ImageSha256"].(string) + if imgSha256 != notImplementedMsg { + t.Fatalf("Expected ImageSha256 to be %s but was %s", notImplementedMsg, imgSha256) + } +} diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index a362b2e46..43cde6855 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -175,7 +175,10 @@ func (p *PostProcessor) PostProcessProvider(name string, provider Provider, ui p return nil, false, err } - customVagrantfile = string(customBytes) + customVagrantfile, err = interpolate.Render(string(customBytes), &config.ctx) + if err != nil { + return nil, false, err + } } f, err := os.Create(filepath.Join(dir, "Vagrantfile")) diff --git a/website/pages/docs/builders/docker.mdx b/website/pages/docs/builders/docker.mdx index 2c33dff3b..f62cae8da 100644 --- a/website/pages/docs/builders/docker.mdx +++ b/website/pages/docs/builders/docker.mdx @@ -215,6 +215,16 @@ You must specify (only) one of `commit`, `discard`, or `export_path`. @include 'builder/docker/Config-not-required.mdx' +## Build Shared Information Variables + +This build shares generated data with provisioners and post-processors via [template engines](/docs/templates/engine) +for JSON and [contextual variables](/docs/from-1.5/contextual-variables) for HCL2. + +The generated variable available for this builder is: + +- `ImageSha256` - When committing a container to an image, this will give the image SHA256. Because the image is not available at the provision step, +this variable is only available for post-processors. + ## Using the Artifact: Export Once the tar artifact has been generated, you will likely want to import, tag, diff --git a/website/pages/docs/post-processors/vagrant.mdx b/website/pages/docs/post-processors/vagrant.mdx index b06441a29..9c718c5a0 100644 --- a/website/pages/docs/post-processors/vagrant.mdx +++ b/website/pages/docs/post-processors/vagrant.mdx @@ -102,7 +102,8 @@ more details about certain options in following sections. `lxc`, `scaleway`, `hyperv`, `parallels`, `aws`, or `google`. - `vagrantfile_template` (string) - Path to a template to use for the - Vagrantfile that is packaged with the box. + Vagrantfile that is packaged with the box. This option supports the usage of the [template engine](/docs/templates/engine) + for JSON and the [contextual variables](/docs/from-1.5/contextual-variables) for HCL2. - `vagrantfile_template_generated` (boolean) - By default, Packer will exit with an error if the file specified using the