From ed446782f9782536dfdb95bf00942c1784b8352a Mon Sep 17 00:00:00 2001 From: Andy Thompson Date: Sun, 20 Jul 2014 21:58:07 +0100 Subject: [PATCH] Add a docker save post processor --- builder/docker/driver.go | 3 + builder/docker/driver_docker.go | 20 ++++ builder/docker/driver_mock.go | 19 ++++ config.go | 1 + plugin/post-processor-docker-save/main.go | 15 +++ .../post-processor-docker-save/main_test.go | 1 + post-processor/docker-save/post-processor.go | 104 ++++++++++++++++++ .../docker-save/post-processor_test.go | 31 ++++++ 8 files changed, 194 insertions(+) create mode 100644 plugin/post-processor-docker-save/main.go create mode 100644 plugin/post-processor-docker-save/main_test.go create mode 100644 post-processor/docker-save/post-processor.go create mode 100644 post-processor/docker-save/post-processor_test.go diff --git a/builder/docker/driver.go b/builder/docker/driver.go index d54eac5a1..ab326a148 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -26,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) diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index d334eef2a..78c91f76f 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -115,6 +115,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 diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index 8afa4fd85..cf623f011 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -25,6 +25,11 @@ type MockDriver struct { PushName string PushErr error + SaveImageCalled bool + SaveImageId string + SaveImageReader io.Reader + SaveImageError error + TagImageCalled bool TagImageImageId string TagImageRepo string @@ -94,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 diff --git a/config.go b/config.go index 4a6b52547..7c1f54677 100644 --- a/config.go +++ b/config.go @@ -48,6 +48,7 @@ const defaultConfig = ` "vsphere": "packer-post-processor-vsphere", "docker-push": "packer-post-processor-docker-push", "docker-import": "packer-post-processor-docker-import", + "docker-save": "packer-post-processor-docker-save", "docker-tag": "packer-post-processor-docker-tag", "vagrant-cloud": "packer-post-processor-vagrant-cloud" }, 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/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) +}