diff --git a/builder/docker/driver.go b/builder/docker/driver.go index c0fb5fa66..7683c4ca2 100644 --- a/builder/docker/driver.go +++ b/builder/docker/driver.go @@ -6,4 +6,17 @@ package docker type Driver interface { // Pull should pull down the given image. Pull(image string) error + + // StartContainer starts a container and returns the ID for that container, + // along with a potential error. + StartContainer(*ContainerConfig) (string, error) + + // StopContainer forcibly stops a container. + StopContainer(id string) error +} + +// ContainerConfig is the configuration used to start a container. +type ContainerConfig struct { + Image string + Volumes map[string]string } diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 04d338abd..9cd72c649 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -1,8 +1,12 @@ package docker import ( + "bytes" + "fmt" "github.com/mitchellh/packer/packer" + "log" "os/exec" + "strings" ) type DockerDriver struct { @@ -13,3 +17,42 @@ func (d *DockerDriver) Pull(image string) error { cmd := exec.Command("docker", "pull", image) return runAndStream(cmd, d.Ui) } + +func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { + // Args that we're going to pass to Docker + args := []string{"run", "-d", "-i", "-t"} + + if len(config.Volumes) > 0 { + volumes := make([]string, 0, len(config.Volumes)) + for host, guest := range config.Volumes { + volumes = append(volumes, fmt.Sprintf("%s:%s", host, guest)) + } + + args = append(args, "-v", strings.Join(volumes, ",")) + } + + args = append(args, config.Image, "/bin/bash") + + // Start the container + var stdout, stderr bytes.Buffer + cmd := exec.Command("docker", args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + log.Printf("Starting container with args: %v", args) + if err := cmd.Start(); err != nil { + return "", err + } + + log.Println("Waiting for container to finish starting") + if err := cmd.Wait(); err != nil { + return "", err + } + + // Capture the container ID, which is alone on stdout + return strings.TrimSpace(stdout.String()), nil +} + +func (d *DockerDriver) StopContainer(id string) error { + return exec.Command("docker", "kill", id).Run() +} diff --git a/builder/docker/driver_mock.go b/builder/docker/driver_mock.go index f5aad13bc..4917cb369 100644 --- a/builder/docker/driver_mock.go +++ b/builder/docker/driver_mock.go @@ -2,10 +2,17 @@ package docker // MockDriver is a driver implementation that can be used for tests. type MockDriver struct { - PullError error + PullError error + StartID string + StartError error + StopError error - PullCalled bool - PullImage string + PullCalled bool + PullImage string + StartCalled bool + StartConfig *ContainerConfig + StopCalled bool + StopID string } func (d *MockDriver) Pull(image string) error { @@ -13,3 +20,15 @@ func (d *MockDriver) Pull(image string) error { d.PullImage = image return d.PullError } + +func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) { + d.StartCalled = true + d.StartConfig = config + return d.StartID, d.StartError +} + +func (d *MockDriver) StopContainer(id string) error { + d.StopCalled = true + d.StopID = id + return d.StopError +} diff --git a/builder/docker/driver_mock_test.go b/builder/docker/driver_mock_test.go new file mode 100644 index 000000000..b7d144813 --- /dev/null +++ b/builder/docker/driver_mock_test.go @@ -0,0 +1,7 @@ +package docker + +import "testing" + +func TestMockDriver_impl(t *testing.T) { + var _ Driver = new(MockDriver) +} diff --git a/builder/docker/drover_docker_test.go b/builder/docker/drover_docker_test.go new file mode 100644 index 000000000..dbc0ebc11 --- /dev/null +++ b/builder/docker/drover_docker_test.go @@ -0,0 +1,7 @@ +package docker + +import "testing" + +func TestDockerDriver_impl(t *testing.T) { + var _ Driver = new(DockerDriver) +} diff --git a/builder/docker/step_run.go b/builder/docker/step_run.go index 7c4397bf0..27dfeb8d5 100644 --- a/builder/docker/step_run.go +++ b/builder/docker/step_run.go @@ -1,13 +1,9 @@ package docker import ( - "bytes" "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "log" - "os/exec" - "strings" ) type StepRun struct { @@ -16,47 +12,30 @@ type StepRun struct { func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) + driver := state.Get("driver").(Driver) tempDir := state.Get("temp_dir").(string) ui := state.Get("ui").(packer.Ui) - ui.Say("Starting docker container with /bin/bash") - - // Args that we're going to pass to Docker - args := []string{ - "run", - "-d", "-i", "-t", - "-v", fmt.Sprintf("%s:/packer-files", tempDir), - config.Image, - "/bin/bash", + runConfig := ContainerConfig{ + Image: config.Image, + Volumes: map[string]string{ + tempDir: "/packer-files", + }, } - // Start the container - var stdout, stderr bytes.Buffer - cmd := exec.Command("docker", args...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - log.Printf("Starting container with args: %v", args) - if err := cmd.Start(); err != nil { + ui.Say("Starting docker container with /bin/bash") + containerId, err := driver.StartContainer(&runConfig) + if err != nil { err := fmt.Errorf("Error running container: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - if err := cmd.Wait(); err != nil { - err := fmt.Errorf("Error running container: %s\nStderr: %s", - err, stderr.String()) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Capture the container ID, which is alone on stdout - s.containerId = strings.TrimSpace(stdout.String()) - ui.Message(fmt.Sprintf("Container ID: %s", s.containerId)) - + // Save the container ID + s.containerId = containerId state.Put("container_id", s.containerId) + ui.Message(fmt.Sprintf("Container ID: %s", s.containerId)) return multistep.ActionContinue } @@ -68,5 +47,9 @@ func (s *StepRun) Cleanup(state multistep.StateBag) { // Kill the container. We don't handle errors because errors usually // just mean that the container doesn't exist anymore, which isn't a // big deal. - exec.Command("docker", "kill", s.containerId).Run() + driver := state.Get("driver").(Driver) + driver.StopContainer(s.containerId) + + // Reset the container ID so that we're idempotent + s.containerId = "" } diff --git a/builder/docker/step_run_test.go b/builder/docker/step_run_test.go new file mode 100644 index 000000000..9ce556b12 --- /dev/null +++ b/builder/docker/step_run_test.go @@ -0,0 +1,95 @@ +package docker + +import ( + "errors" + "github.com/mitchellh/multistep" + "testing" +) + +func testStepRunState(t *testing.T) multistep.StateBag { + state := testState(t) + state.Put("temp_dir", "/foo") + return state +} + +func TestStepRun_impl(t *testing.T) { + var _ multistep.Step = new(StepRun) +} + +func TestStepRun(t *testing.T) { + state := testStepRunState(t) + step := new(StepRun) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + driver := state.Get("driver").(*MockDriver) + driver.StartID = "foo" + + // 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.StartCalled { + t.Fatal("should've called") + } + if driver.StartConfig.Image != config.Image { + t.Fatalf("bad: %#v", driver.StartConfig.Image) + } + + // verify the ID is saved + idRaw, ok := state.GetOk("container_id") + if !ok { + t.Fatal("should've saved ID") + } + + id := idRaw.(string) + if id != "foo" { + t.Fatalf("bad: %#v", id) + } + + // Verify we haven't called stop yet + if driver.StopCalled { + t.Fatal("should not have stopped") + } + + // Cleanup + step.Cleanup(state) + if !driver.StopCalled { + t.Fatal("should've stopped") + } + if driver.StopID != id { + t.Fatalf("bad: %#v", driver.StopID) + } +} + +func TestStepRun_error(t *testing.T) { + state := testStepRunState(t) + step := new(StepRun) + defer step.Cleanup(state) + + driver := state.Get("driver").(*MockDriver) + driver.StartError = 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("container_id"); ok { + t.Fatal("shouldn't save container ID") + } + + // Verify we haven't called stop yet + if driver.StopCalled { + t.Fatal("should not have stopped") + } + + // Cleanup + step.Cleanup(state) + if driver.StopCalled { + t.Fatal("should not have stopped") + } +}