diff --git a/builder/docker/builder.go b/builder/docker/builder.go index f8c3832e9..a4659219d 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -35,6 +35,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepPull{}, &StepRun{}, &StepProvision{}, + &StepCommit{}, &StepExport{}, } @@ -64,8 +65,17 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, rawErr.(error) } + var artifact packer.Artifact // No errors, must've worked - artifact := &ExportArtifact{path: b.config.ExportPath} + if b.config.Export { + artifact = &ExportArtifact{path: b.config.ExportPath} + } else { + artifact = &ImportArtifact{ + IdValue: state.Get("image_id").(string), + BuilderIdValue: "packer.post-processor.docker-import", + Driver: driver, + } + } return artifact, nil } diff --git a/builder/docker/config.go b/builder/docker/config.go index 045bf19b8..1045ff26a 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -10,6 +10,7 @@ type Config struct { common.PackerConfig `mapstructure:",squash"` ExportPath string `mapstructure:"export_path"` + Export bool Image string Pull bool RunCommand []string `mapstructure:"run_command"` @@ -71,10 +72,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { } } - if c.ExportPath == "" { - errs = packer.MultiErrorAppend(errs, - fmt.Errorf("export_path must be specified")) - } + c.Export = c.ExportPath != "" if c.Image == "" { errs = packer.MultiErrorAppend(errs, diff --git a/builder/docker/config_test.go b/builder/docker/config_test.go index 3b279ab22..ea65d8114 100644 --- a/builder/docker/config_test.go +++ b/builder/docker/config_test.go @@ -46,13 +46,19 @@ func TestConfigPrepare_exportPath(t *testing.T) { // No export path delete(raw, "export_path") - _, warns, errs := NewConfig(raw) - testConfigErr(t, warns, errs) + c, warns, errs := NewConfig(raw) + testConfigOk(t, warns, errs) + if c.Export { + t.Fatal("should not export") + } // Good export path raw["export_path"] = "good" - _, warns, errs = NewConfig(raw) + c, warns, errs = NewConfig(raw) testConfigOk(t, warns, errs) + if !c.Export { + t.Fatal("should export") + } } func TestConfigPrepare_image(t *testing.T) { diff --git a/builder/docker/driver.go b/builder/docker/driver.go index aaed2fc52..ab326a148 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -8,6 +8,9 @@ import ( // Docker. The Driver interface also allows the steps to be tested since // a mock driver can be shimmed in. type Driver interface { + // Commit the container to a tag + Commit(id string) (string, error) + // Delete an image that is imported into Docker DeleteImage(id string) error @@ -23,6 +26,9 @@ type Driver interface { // Push pushes an image to a Docker index/registry. Push(name string) error + // Save an image with the given ID to the given writer. + SaveImage(id string, dst io.Writer) error + // StartContainer starts a container and returns the ID for that container, // along with a potential error. StartContainer(*ContainerConfig) (string, error) @@ -30,6 +36,9 @@ type Driver interface { // StopContainer forcibly stops a container. StopContainer(id string) error + // TagImage tags the image with the given ID + TagImage(id string, repo string) error + // Verify verifies that the driver can run Verify() error } diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 7d7ed4af8..737a5ff43 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -35,6 +35,27 @@ func (d *DockerDriver) DeleteImage(id string) error { return nil } +func (d *DockerDriver) Commit(id string) (string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + + cmd := exec.Command("docker", "commit", id) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + return "", err + } + + if err := cmd.Wait(); err != nil { + err = fmt.Errorf("Error committing container: %s\nStderr: %s", + err, stderr.String()) + return "", err + } + + return strings.TrimSpace(stdout.String()), nil +} + func (d *DockerDriver) Export(id string, dst io.Writer) error { var stderr bytes.Buffer cmd := exec.Command("docker", "export", id) @@ -98,6 +119,26 @@ func (d *DockerDriver) Push(name string) error { return runAndStream(cmd, d.Ui) } +func (d *DockerDriver) SaveImage(id string, dst io.Writer) error { + var stderr bytes.Buffer + cmd := exec.Command("docker", "save", id) + cmd.Stdout = dst + cmd.Stderr = &stderr + + log.Printf("Exporting image: %s", id) + if err := cmd.Start(); err != nil { + return err + } + + if err := cmd.Wait(); err != nil { + err = fmt.Errorf("Error exporting: %s\nStderr: %s", + err, stderr.String()) + return err + } + + return nil +} + func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { // Build up the template data var tplData startContainerTemplate @@ -156,6 +197,24 @@ func (d *DockerDriver) StopContainer(id string) error { return exec.Command("docker", "rm", id).Run() } +func (d *DockerDriver) TagImage(id string, repo string) error { + var stderr bytes.Buffer + cmd := exec.Command("docker", "tag", id, repo) + cmd.Stderr = &stderr + + if err := cmd.Start(); err != nil { + return err + } + + if err := cmd.Wait(); err != nil { + err = fmt.Errorf("Error tagging image: %s\nStderr: %s", + err, stderr.String()) + return err + } + + return nil +} + func (d *DockerDriver) Verify() error { if _, err := exec.LookPath("docker"); err != nil { return err diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index a48bb99f8..cf623f011 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -6,6 +6,11 @@ import ( // MockDriver is a driver implementation that can be used for tests. type MockDriver struct { + CommitCalled bool + CommitContainerId string + CommitImageId string + CommitErr error + DeleteImageCalled bool DeleteImageId string DeleteImageErr error @@ -20,6 +25,16 @@ type MockDriver struct { PushName string PushErr error + SaveImageCalled bool + SaveImageId string + SaveImageReader io.Reader + SaveImageError error + + TagImageCalled bool + TagImageImageId string + TagImageRepo string + TagImageErr error + ExportReader io.Reader ExportError error PullError error @@ -39,6 +54,12 @@ type MockDriver struct { VerifyCalled bool } +func (d *MockDriver) Commit(id string) (string, error) { + d.CommitCalled = true + d.CommitContainerId = id + return d.CommitImageId, d.CommitErr +} + func (d *MockDriver) DeleteImage(id string) error { d.DeleteImageCalled = true d.DeleteImageId = id @@ -78,6 +99,20 @@ func (d *MockDriver) Push(name string) error { return d.PushErr } +func (d *MockDriver) SaveImage(id string, dst io.Writer) error { + d.SaveImageCalled = true + d.SaveImageId = id + + if d.SaveImageReader != nil { + _, err := io.Copy(dst, d.SaveImageReader) + if err != nil { + return err + } + } + + return d.SaveImageError +} + func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) { d.StartCalled = true d.StartConfig = config @@ -90,6 +125,13 @@ func (d *MockDriver) StopContainer(id string) error { return d.StopError } +func (d *MockDriver) TagImage(id string, repo string) error { + d.TagImageCalled = true + d.TagImageImageId = id + d.TagImageRepo = repo + return d.TagImageErr +} + func (d *MockDriver) Verify() error { d.VerifyCalled = true return d.VerifyError diff --git a/builder/docker/step_commit.go b/builder/docker/step_commit.go new file mode 100644 index 000000000..8b6489495 --- /dev/null +++ b/builder/docker/step_commit.go @@ -0,0 +1,40 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +// StepCommit commits the container to a image. +type StepCommit struct { + imageId string +} + +func (s *StepCommit) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + driver := state.Get("driver").(Driver) + containerId := state.Get("container_id").(string) + ui := state.Get("ui").(packer.Ui) + + if config.Export { + return multistep.ActionContinue + } + + ui.Say("Committing the container") + imageId, err := driver.Commit(containerId) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Save the container ID + s.imageId = imageId + state.Put("image_id", s.imageId) + ui.Message(fmt.Sprintf("Image ID: %s", s.imageId)) + + return multistep.ActionContinue +} + +func (s *StepCommit) Cleanup(state multistep.StateBag) {} diff --git a/builder/docker/step_commit_test.go b/builder/docker/step_commit_test.go new file mode 100644 index 000000000..6e1520dea --- /dev/null +++ b/builder/docker/step_commit_test.go @@ -0,0 +1,95 @@ +package docker + +import ( + "errors" + "github.com/mitchellh/multistep" + "testing" +) + +func testStepCommitState(t *testing.T) multistep.StateBag { + state := testState(t) + state.Put("container_id", "foo") + return state +} + +func TestStepCommit_impl(t *testing.T) { + var _ multistep.Step = new(StepCommit) +} + +func TestStepCommit(t *testing.T) { + state := testStepCommitState(t) + step := new(StepCommit) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + config.Export = false + driver := state.Get("driver").(*MockDriver) + driver.CommitImageId = "bar" + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // verify we did the right thing + if !driver.CommitCalled { + t.Fatal("should've called") + } + + // verify the ID is saved + idRaw, ok := state.GetOk("image_id") + if !ok { + t.Fatal("should've saved ID") + } + + id := idRaw.(string) + if id != driver.CommitImageId { + t.Fatalf("bad: %#v", id) + } +} + +func TestStepCommit_skip(t *testing.T) { + state := testStepCommitState(t) + step := new(StepCommit) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + config.Export = true + driver := state.Get("driver").(*MockDriver) + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // verify we did the right thing + if driver.CommitCalled { + t.Fatal("shouldn't have called") + } + + // verify the ID is not saved + if _, ok := state.GetOk("image_id"); ok { + t.Fatal("shouldn't save image ID") + } +} + +func TestStepCommit_error(t *testing.T) { + state := testStepCommitState(t) + step := new(StepCommit) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + config.Export = false + driver := state.Get("driver").(*MockDriver) + driver.CommitErr = errors.New("foo") + + // run the step + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + // verify the ID is not saved + if _, ok := state.GetOk("image_id"); ok { + t.Fatal("shouldn't save image ID") + } +} diff --git a/builder/docker/step_export.go b/builder/docker/step_export.go index 69d6f483c..4530236f8 100644 --- a/builder/docker/step_export.go +++ b/builder/docker/step_export.go @@ -12,6 +12,11 @@ type StepExport struct{} func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) + + if !config.Export { + return multistep.ActionContinue + } + driver := state.Get("driver").(Driver) containerId := state.Get("container_id").(string) ui := state.Get("ui").(packer.Ui) diff --git a/builder/docker/step_export_test.go b/builder/docker/step_export_test.go index d07d547c2..d0232965b 100644 --- a/builder/docker/step_export_test.go +++ b/builder/docker/step_export_test.go @@ -34,6 +34,7 @@ func TestStepExport(t *testing.T) { config := state.Get("config").(*Config) config.ExportPath = tf.Name() + config.Export = true driver := state.Get("driver").(*MockDriver) driver.ExportReader = bytes.NewReader([]byte("data!")) @@ -61,6 +62,26 @@ func TestStepExport(t *testing.T) { } } +func TestStepExport_skip(t *testing.T) { + state := testStepExportState(t) + step := new(StepExport) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + config.Export = false + driver := state.Get("driver").(*MockDriver) + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // verify we did the right thing + if driver.ExportCalled { + t.Fatal("shouldn't have exported") + } +} + func TestStepExport_error(t *testing.T) { state := testStepExportState(t) step := new(StepExport) @@ -79,6 +100,7 @@ func TestStepExport_error(t *testing.T) { config := state.Get("config").(*Config) config.ExportPath = tf.Name() + config.Export = true driver := state.Get("driver").(*MockDriver) driver.ExportError = errors.New("foo") diff --git a/plugin/post-processor-docker-save/main.go b/plugin/post-processor-docker-save/main.go new file mode 100644 index 000000000..d5f5ec636 --- /dev/null +++ b/plugin/post-processor-docker-save/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/post-processor/docker-save" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterPostProcessor(new(dockersave.PostProcessor)) + server.Serve() +} diff --git a/plugin/post-processor-docker-save/main_test.go b/plugin/post-processor-docker-save/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/post-processor-docker-save/main_test.go @@ -0,0 +1 @@ +package main diff --git a/plugin/post-processor-docker-tag/main.go b/plugin/post-processor-docker-tag/main.go new file mode 100644 index 000000000..e226ff697 --- /dev/null +++ b/plugin/post-processor-docker-tag/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "github.com/mitchellh/packer/packer/plugin" + "github.com/mitchellh/packer/post-processor/docker-tag" +) + +func main() { + server, err := plugin.Server() + if err != nil { + panic(err) + } + server.RegisterPostProcessor(new(dockertag.PostProcessor)) + server.Serve() +} diff --git a/plugin/post-processor-docker-tag/main_test.go b/plugin/post-processor-docker-tag/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/post-processor-docker-tag/main_test.go @@ -0,0 +1 @@ +package main diff --git a/post-processor/docker-save/post-processor.go b/post-processor/docker-save/post-processor.go new file mode 100644 index 000000000..6a2d86298 --- /dev/null +++ b/post-processor/docker-save/post-processor.go @@ -0,0 +1,104 @@ +package dockersave + +import ( + "fmt" + "github.com/mitchellh/packer/builder/docker" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/post-processor/docker-import" + "os" +) + +const BuilderId = "packer.post-processor.docker-save" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Path string `mapstructure:"path"` + + tpl *packer.ConfigTemplate +} + +type PostProcessor struct { + Driver docker.Driver + + config Config +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + _, err := common.DecodeConfig(&p.config, raws...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + // Accumulate any errors + errs := new(packer.MultiError) + + templates := map[string]*string{ + "path": &p.config.Path, + } + + for key, ptr := range templates { + if *ptr == "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("%s must be set", key)) + } + + *ptr, err = p.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", key, err)) + } + } + + if len(errs.Errors) > 0 { + return errs + } + + return nil + +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + if artifact.BuilderId() != dockerimport.BuilderId { + err := fmt.Errorf( + "Unknown artifact type: %s\nCan only save Docker builder artifacts.", + artifact.BuilderId()) + return nil, false, err + } + + path := p.config.Path + + // Open the file that we're going to write to + f, err := os.Create(path) + if err != nil { + err := fmt.Errorf("Error creating output file: %s", err) + return nil, false, err + } + + driver := p.Driver + if driver == nil { + // If no driver is set, then we use the real driver + driver = &docker.DockerDriver{Tpl: p.config.tpl, Ui: ui} + } + + ui.Message("Saving image: " + artifact.Id()) + + if err := driver.SaveImage(artifact.Id(), f); err != nil { + f.Close() + os.Remove(f.Name()) + + return nil, false, err + } + + f.Close() + ui.Message("Saved to: " + path) + + return artifact, true, nil +} diff --git a/post-processor/docker-save/post-processor_test.go b/post-processor/docker-save/post-processor_test.go new file mode 100644 index 000000000..2d29d58b9 --- /dev/null +++ b/post-processor/docker-save/post-processor_test.go @@ -0,0 +1,31 @@ +package dockersave + +import ( + "bytes" + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{} +} + +func testPP(t *testing.T) *PostProcessor { + var p PostProcessor + if err := p.Configure(testConfig()); err != nil { + t.Fatalf("err: %s", err) + } + + return &p +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } +} + +func TestPostProcessor_ImplementsPostProcessor(t *testing.T) { + var _ packer.PostProcessor = new(PostProcessor) +} diff --git a/post-processor/docker-tag/post-processor.go b/post-processor/docker-tag/post-processor.go new file mode 100644 index 000000000..d68b48e4c --- /dev/null +++ b/post-processor/docker-tag/post-processor.go @@ -0,0 +1,103 @@ +package dockertag + +import ( + "fmt" + "github.com/mitchellh/packer/builder/docker" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/post-processor/docker-import" +) + +const BuilderId = "packer.post-processor.docker-tag" + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + Repository string `mapstructure:"repository"` + Tag string `mapstructure:"tag"` + + tpl *packer.ConfigTemplate +} + +type PostProcessor struct { + Driver docker.Driver + + config Config +} + +func (p *PostProcessor) Configure(raws ...interface{}) error { + _, err := common.DecodeConfig(&p.config, raws...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + // Accumulate any errors + errs := new(packer.MultiError) + + templates := map[string]*string{ + "repository": &p.config.Repository, + "tag": &p.config.Tag, + } + + for key, ptr := range templates { + if *ptr == "" { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("%s must be set", key)) + } + + *ptr, err = p.config.tpl.Process(*ptr, nil) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", key, err)) + } + } + + if len(errs.Errors) > 0 { + return errs + } + + return nil + +} + +func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + if artifact.BuilderId() != dockerimport.BuilderId { + err := fmt.Errorf( + "Unknown artifact type: %s\nCan only tag from Docker builder artifacts.", + artifact.BuilderId()) + return nil, false, err + } + + driver := p.Driver + if driver == nil { + // If no driver is set, then we use the real driver + driver = &docker.DockerDriver{Tpl: p.config.tpl, Ui: ui} + } + + importRepo := p.config.Repository + if p.config.Tag != "" { + importRepo += ":" + p.config.Tag + } + + ui.Message("Tagging image: " + artifact.Id()) + ui.Message("Repository: " + importRepo) + err := driver.TagImage(artifact.Id(), importRepo) + if err != nil { + return nil, false, err + } + + // Build the artifact + artifact = &docker.ImportArtifact{ + BuilderIdValue: BuilderId, + Driver: driver, + IdValue: importRepo, + } + + return artifact, true, nil +} diff --git a/post-processor/docker-tag/post-processor_test.go b/post-processor/docker-tag/post-processor_test.go new file mode 100644 index 000000000..925419a10 --- /dev/null +++ b/post-processor/docker-tag/post-processor_test.go @@ -0,0 +1,72 @@ +package dockertag + +import ( + "bytes" + "github.com/mitchellh/packer/builder/docker" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/post-processor/docker-import" + "testing" +) + +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "repository": "foo", + "tag": "bar", + } +} + +func testPP(t *testing.T) *PostProcessor { + var p PostProcessor + if err := p.Configure(testConfig()); err != nil { + t.Fatalf("err: %s", err) + } + + return &p +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } +} + +func TestPostProcessor_ImplementsPostProcessor(t *testing.T) { + var _ packer.PostProcessor = new(PostProcessor) +} + +func TestPostProcessor_PostProcess(t *testing.T) { + driver := &docker.MockDriver{} + p := &PostProcessor{Driver: driver} + _, err := common.DecodeConfig(&p.config, testConfig()) + if err != nil { + t.Fatalf("err %s", err) + } + + artifact := &packer.MockArtifact{ + BuilderIdValue: dockerimport.BuilderId, + IdValue: "1234567890abcdef", + } + + result, keep, err := p.PostProcess(testUi(), artifact) + if _, ok := result.(packer.Artifact); !ok { + t.Fatal("should be instance of Artifact") + } + if !keep { + t.Fatal("should keep") + } + if err != nil { + t.Fatalf("err: %s", err) + } + + if !driver.TagImageCalled { + t.Fatal("should call TagImage") + } + if driver.TagImageImageId != "1234567890abcdef" { + t.Fatal("bad image id") + } + if driver.TagImageRepo != "foo:bar" { + t.Fatal("bad repo") + } +}