diff --git a/builder/docker/driver.go b/builder/docker/driver.go index 7683c4ca2..04f4e3fd5 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -1,9 +1,16 @@ package docker +import ( + "io" +) + // Driver is the interface that has to be implemented to communicate with // Docker. The Driver interface also allows the steps to be tested since // a mock driver can be shimmed in. type Driver interface { + // Export exports the container with the given ID to the given writer. + Export(id string, dst io.Writer) error + // Pull should pull down the given image. Pull(image string) error diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 9cd72c649..15029d202 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "github.com/mitchellh/packer/packer" + "io" "log" "os/exec" "strings" @@ -13,6 +14,26 @@ type DockerDriver struct { Ui packer.Ui } +func (d *DockerDriver) Export(id string, dst io.Writer) error { + var stderr bytes.Buffer + cmd := exec.Command("docker", "export", id) + cmd.Stdout = dst + cmd.Stderr = &stderr + + log.Printf("Exporting container: %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) Pull(image string) error { cmd := exec.Command("docker", "pull", image) return runAndStream(cmd, d.Ui) diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index 4917cb369..9cf2fb31b 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -1,18 +1,40 @@ package docker +import ( + "io" +) + // MockDriver is a driver implementation that can be used for tests. type MockDriver struct { - PullError error - StartID string - StartError error - StopError error + ExportReader io.Reader + ExportError error + PullError error + StartID string + StartError error + StopError error - PullCalled bool - PullImage string - StartCalled bool - StartConfig *ContainerConfig - StopCalled bool - StopID string + ExportCalled bool + ExportID string + PullCalled bool + PullImage string + StartCalled bool + StartConfig *ContainerConfig + StopCalled bool + StopID string +} + +func (d *MockDriver) Export(id string, dst io.Writer) error { + d.ExportCalled = true + d.ExportID = id + + if d.ExportReader != nil { + _, err := io.Copy(dst, d.ExportReader) + if err != nil { + return err + } + } + + return d.ExportError } func (d *MockDriver) Pull(image string) error { diff --git a/builder/docker/foo b/builder/docker/foo new file mode 100644 index 000000000..e69de29bb diff --git a/builder/docker/step_export.go b/builder/docker/step_export.go index de50844b9..69d6f483c 100644 --- a/builder/docker/step_export.go +++ b/builder/docker/step_export.go @@ -1,13 +1,10 @@ package docker import ( - "bytes" "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "log" "os" - "os/exec" ) // StepExport exports the container to a flat tar file. @@ -15,14 +12,10 @@ type StepExport struct{} func (s *StepExport) 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) - ui.Say("Exporting the container") - - // Args that we're going to pass to Docker - args := []string{"export", containerId} - // Open the file that we're going to write to f, err := os.Create(config.ExportPath) if err != nil { @@ -31,30 +24,18 @@ func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { ui.Error(err.Error()) return multistep.ActionHalt } - defer f.Close() - // Export the thing, take stderr and point it to the file - var stderr bytes.Buffer - cmd := exec.Command("docker", args...) - cmd.Stdout = f - cmd.Stderr = &stderr + ui.Say("Exporting the container") + if err := driver.Export(containerId, f); err != nil { + f.Close() + os.Remove(f.Name()) - log.Printf("Starting container with args: %v", args) - if err := cmd.Start(); err != nil { - err := fmt.Errorf("Error exporting: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - if err := cmd.Wait(); err != nil { - err := fmt.Errorf("Error exporting: %s\nStderr: %s", - err, stderr.String()) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } + f.Close() return multistep.ActionContinue } diff --git a/builder/docker/step_export_test.go b/builder/docker/step_export_test.go new file mode 100644 index 000000000..d07d547c2 --- /dev/null +++ b/builder/docker/step_export_test.go @@ -0,0 +1,99 @@ +package docker + +import ( + "bytes" + "errors" + "github.com/mitchellh/multistep" + "io/ioutil" + "os" + "testing" +) + +func testStepExportState(t *testing.T) multistep.StateBag { + state := testState(t) + state.Put("container_id", "foo") + return state +} + +func TestStepExport_impl(t *testing.T) { + var _ multistep.Step = new(StepExport) +} + +func TestStepExport(t *testing.T) { + state := testStepExportState(t) + step := new(StepExport) + defer step.Cleanup(state) + + // Create a tempfile for our output path + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + defer os.Remove(tf.Name()) + + config := state.Get("config").(*Config) + config.ExportPath = tf.Name() + driver := state.Get("driver").(*MockDriver) + driver.ExportReader = bytes.NewReader([]byte("data!")) + + // 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("should've exported") + } + if driver.ExportID != "foo" { + t.Fatalf("bad: %#v", driver.ExportID) + } + + // verify the data exported to the file + contents, err := ioutil.ReadFile(tf.Name()) + if err != nil { + t.Fatalf("err: %s", err) + } + + if string(contents) != "data!" { + t.Fatalf("bad: %#v", string(contents)) + } +} + +func TestStepExport_error(t *testing.T) { + state := testStepExportState(t) + step := new(StepExport) + defer step.Cleanup(state) + + // Create a tempfile for our output path + tf, err := ioutil.TempFile("", "packer") + if err != nil { + t.Fatalf("err: %s", err) + } + tf.Close() + + if err := os.Remove(tf.Name()); err != nil { + t.Fatalf("err: %s", err) + } + + config := state.Get("config").(*Config) + config.ExportPath = tf.Name() + driver := state.Get("driver").(*MockDriver) + driver.ExportError = errors.New("foo") + + // run the step + if action := step.Run(state); action != multistep.ActionHalt { + t.Fatalf("bad action: %#v", action) + } + + // verify we have an error + if _, ok := state.GetOk("error"); !ok { + t.Fatal("should have error") + } + + // verify we didn't make that file + if _, err := os.Stat(tf.Name()); err == nil { + t.Fatal("export path shouldn't exist") + } +}