From 3feab1dea147cba617d9d45312e6b4bffe05fe0a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 16:55:02 -0800 Subject: [PATCH 01/29] builder/docker: boilerplate --- builder/docker/builder.go | 70 ++++++++++++++++++++++++++++++++++ builder/docker/builder_test.go | 10 +++++ builder/docker/config.go | 12 ++++++ 3 files changed, 92 insertions(+) create mode 100644 builder/docker/builder.go create mode 100644 builder/docker/builder_test.go create mode 100644 builder/docker/config.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go new file mode 100644 index 000000000..b9e69151e --- /dev/null +++ b/builder/docker/builder.go @@ -0,0 +1,70 @@ +package docker + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "log" +) + +const BuilderId = "packer.docker" + +type Builder struct { + config Config + runner multistep.Runner +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + md, err := common.DecodeConfig(&b.config, raws...) + if err != nil { + return nil, err + } + + b.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return nil, err + } + + errs := common.CheckUnusedConfig(md) + warnings := make([]string, 0) + + if errs != nil && len(errs.Errors) > 0 { + return warnings, errs + } + + return warnings, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + steps := []multistep.Step{} + + // Setup the state bag and initial state for the steps + state := new(multistep.BasicStateBag) + state.Put("config", &b.config) + + // Run! + if b.config.PackerDebug { + b.runner = &multistep.DebugRunner{ + Steps: steps, + PauseFn: common.MultistepDebugFn(ui), + } + } else { + b.runner = &multistep.BasicRunner{Steps: steps} + } + + b.runner.Run(state) + + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + return nil, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the step runner...") + b.runner.Cancel() + } +} diff --git a/builder/docker/builder_test.go b/builder/docker/builder_test.go new file mode 100644 index 000000000..c8da72224 --- /dev/null +++ b/builder/docker/builder_test.go @@ -0,0 +1,10 @@ +package docker + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestBuilder_implBuilder(t *testing.T) { + var _ packer.Builder = new(Builder) +} diff --git a/builder/docker/config.go b/builder/docker/config.go new file mode 100644 index 000000000..038b00dda --- /dev/null +++ b/builder/docker/config.go @@ -0,0 +1,12 @@ +package docker + +import ( + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + tpl *packer.ConfigTemplate +} From 6047c9a4699ca05bfcc8f7f6d3b5e44b3168a728 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 16:57:13 -0800 Subject: [PATCH 02/29] plugin/builder-docker --- plugin/builder-docker/main.go | 10 ++++++++++ plugin/builder-docker/main_test.go | 1 + 2 files changed, 11 insertions(+) create mode 100644 plugin/builder-docker/main.go create mode 100644 plugin/builder-docker/main_test.go diff --git a/plugin/builder-docker/main.go b/plugin/builder-docker/main.go new file mode 100644 index 000000000..a5b69de0b --- /dev/null +++ b/plugin/builder-docker/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/mitchellh/packer/builder/docker" + "github.com/mitchellh/packer/packer/plugin" +) + +func main() { + plugin.ServeBuilder(new(docker.Builder)) +} diff --git a/plugin/builder-docker/main_test.go b/plugin/builder-docker/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/plugin/builder-docker/main_test.go @@ -0,0 +1 @@ +package main From f9f10ed512b784006bdf8bd214bb9812d56765df Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 16:57:34 -0800 Subject: [PATCH 03/29] main: Default config has docker --- config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config.go b/config.go index a6a1abd4f..26003f8c0 100644 --- a/config.go +++ b/config.go @@ -23,6 +23,7 @@ const defaultConfig = ` "amazon-chroot": "packer-builder-amazon-chroot", "amazon-instance": "packer-builder-amazon-instance", "digitalocean": "packer-builder-digitalocean", + "docker": "packer-builder-docker", "openstack": "packer-builder-openstack", "qemu": "packer-builder-qemu", "virtualbox": "packer-builder-virtualbox", From 034e04cc1e58ae26f2d6d4011c7fe756558ec5fd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 22:00:57 -0800 Subject: [PATCH 04/29] builder/docker: pull images --- builder/docker/builder.go | 6 ++- builder/docker/config.go | 2 + builder/docker/exec.go | 102 ++++++++++++++++++++++++++++++++++++ builder/docker/step_pull.go | 29 ++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 builder/docker/exec.go create mode 100644 builder/docker/step_pull.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index b9e69151e..9f14a20d0 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -36,11 +36,15 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - steps := []multistep.Step{} + steps := []multistep.Step{ + &StepPull{}, + } // 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) // Run! if b.config.PackerDebug { diff --git a/builder/docker/config.go b/builder/docker/config.go index 038b00dda..4bab81bdd 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -8,5 +8,7 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` + Image string + tpl *packer.ConfigTemplate } diff --git a/builder/docker/exec.go b/builder/docker/exec.go new file mode 100644 index 000000000..598702410 --- /dev/null +++ b/builder/docker/exec.go @@ -0,0 +1,102 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/iochan" + "github.com/mitchellh/packer/packer" + "io" + "log" + "os/exec" + "regexp" + "strings" + "sync" + "syscall" +) + +func runAndStream(cmd *exec.Cmd, ui packer.Ui) error { + stdout_r, stdout_w := io.Pipe() + stderr_r, stderr_w := io.Pipe() + defer stdout_w.Close() + defer stderr_w.Close() + + log.Printf("Executing: %s %v", cmd.Path, cmd.Args[1:]) + cmd.Stdout = stdout_w + cmd.Stderr = stderr_w + if err := cmd.Start(); err != nil { + return err + } + + // Create the channels we'll use for data + exitCh := make(chan int, 1) + stdoutCh := iochan.DelimReader(stdout_r, '\n') + stderrCh := iochan.DelimReader(stderr_r, '\n') + + // Start the goroutine to watch for the exit + go func() { + defer stdout_w.Close() + defer stderr_w.Close() + exitStatus := 0 + + err := cmd.Wait() + if exitErr, ok := err.(*exec.ExitError); ok { + exitStatus = 1 + + // There is no process-independent way to get the REAL + // exit status so we just try to go deeper. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } + } + + exitCh <- exitStatus + }() + + // This waitgroup waits for the streaming to end + var streamWg sync.WaitGroup + streamWg.Add(2) + + streamFunc := func(ch <-chan string) { + defer streamWg.Done() + + for data := range ch { + data = cleanOutputLine(data) + if data != "" { + ui.Message(data) + } + } + } + + // Stream stderr/stdout + go streamFunc(stderrCh) + go streamFunc(stdoutCh) + + // Wait for the process to end and then wait for the streaming to end + exitStatus := <-exitCh + streamWg.Wait() + + if exitStatus != 0 { + return fmt.Errorf("Bad exit status: %d", exitStatus) + } + + return nil +} + +// cleanOutputLine cleans up a line so that '\r' don't muck up the +// UI output when we're reading from a remote command. +func cleanOutputLine(line string) string { + // Build a regular expression that will get rid of shell codes + re := regexp.MustCompile("(?i)\x1b\\[([0-9]{1,2}(;[0-9]{1,2})?)?[a|b|m|k]") + line = re.ReplaceAllString(line, "") + + // Trim surrounding whitespace + line = strings.TrimSpace(line) + + // Trim up to the first carriage return, since that text would be + // lost anyways. + idx := strings.LastIndex(line, "\r") + if idx > -1 { + line = line[idx+1:] + } + + return line +} diff --git a/builder/docker/step_pull.go b/builder/docker/step_pull.go new file mode 100644 index 000000000..28662eccb --- /dev/null +++ b/builder/docker/step_pull.go @@ -0,0 +1,29 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "os/exec" +) + +type StepPull struct{} + +func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + ui.Say(fmt.Sprintf("Pulling Docker image: %s", config.Image)) + cmd := exec.Command("docker", "pull", config.Image) + if err := runAndStream(cmd, ui); err != nil { + err := fmt.Errorf("Error pulling Docker image: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepPull) Cleanup(state multistep.StateBag) { +} From 4db609b24ccc95e4b5e5292ef9eb3d0639f63ba3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 22:02:47 -0800 Subject: [PATCH 05/29] builder/docker: tests for some exec stuff --- builder/docker/exec_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 builder/docker/exec_test.go diff --git a/builder/docker/exec_test.go b/builder/docker/exec_test.go new file mode 100644 index 000000000..c035c9eaf --- /dev/null +++ b/builder/docker/exec_test.go @@ -0,0 +1,24 @@ +package docker + +import ( + "testing" +) + +func TestCleanLine(t *testing.T) { + cases := []struct { + input string + output string + }{ + { + "\x1b[0A\x1b[2K\r8dbd9e392a96: Pulling image (precise) from ubuntu\r\x1b[0B\x1b[1A\x1b[2K\r8dbd9e392a96: Pulling image (precise) from ubuntu, endpoint: https://cdn-registry-1.docker.io/v1/\r\x1b[1B", + "8dbd9e392a96: Pulling image (precise) from ubuntu, endpoint: https://cdn-registry-1.docker.io/v1/", + }, + } + + for _, tc := range cases { + actual := cleanOutputLine(tc.input) + if actual != tc.output { + t.Fatalf("bad: %#v %#v", tc.input, actual) + } + } +} From 2e080ece6d6c678936c985a38201ed599d568cad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 22:17:46 -0800 Subject: [PATCH 06/29] builder/docker: start a container --- builder/docker/builder.go | 1 + builder/docker/step_run.go | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 builder/docker/step_run.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index 9f14a20d0..ecc794f26 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -38,6 +38,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { steps := []multistep.Step{ &StepPull{}, + &StepRun{}, } // Setup the state bag and initial state for the steps diff --git a/builder/docker/step_run.go b/builder/docker/step_run.go new file mode 100644 index 000000000..51bc504db --- /dev/null +++ b/builder/docker/step_run.go @@ -0,0 +1,53 @@ +package docker + +import ( + "bytes" + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "os/exec" + "strings" +) + +type StepRun struct { + containerId string +} + +func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Starting docker container with /bin/bash") + + var stdout, stderr bytes.Buffer + cmd := exec.Command("docker", "run", "-d", "-i", "-t", config.Image, "/bin/bash") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Start(); 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", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.containerId = strings.TrimSpace(stdout.String()) + ui.Message(fmt.Sprintf("Container ID: %s", s.containerId)) + + return multistep.ActionContinue +} + +func (s *StepRun) Cleanup(state multistep.StateBag) { + if s.containerId == "" { + return + } + + // TODO(mitchellh): handle errors + exec.Command("docker", "kill", s.containerId).Run() +} From 797c44bfc108598834313f2207b268dcc21ec3fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 23:43:41 -0800 Subject: [PATCH 07/29] builder/docker: a non-working communicator --- builder/docker/builder.go | 2 + builder/docker/communicator.go | 98 +++++++++++++++++++++++++++++ builder/docker/communicator_test.go | 10 +++ builder/docker/step_provision.go | 33 ++++++++++ builder/docker/step_run.go | 25 +++++++- builder/docker/step_temp_dir.go | 38 +++++++++++ 6 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 builder/docker/communicator.go create mode 100644 builder/docker/communicator_test.go create mode 100644 builder/docker/step_provision.go create mode 100644 builder/docker/step_temp_dir.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index ecc794f26..0b343c0db 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -37,8 +37,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { steps := []multistep.Step{ + &StepTempDir{}, &StepPull{}, &StepRun{}, + &StepProvision{}, } // Setup the state bag and initial state for the steps diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go new file mode 100644 index 000000000..dddd27042 --- /dev/null +++ b/builder/docker/communicator.go @@ -0,0 +1,98 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "syscall" +) + +type Communicator struct { + ContainerId string + HostDir string + ContainerDir string +} + +func (c *Communicator) Start(remote *packer.RemoteCmd) error { + cmd := exec.Command("docker", "attach", c.ContainerId) + stdin_w, err := cmd.StdinPipe() + if err != nil { + return err + } + + cmd.Stdout = remote.Stdout + cmd.Stderr = remote.Stderr + + log.Printf("Executing in container %s: %#v", c.ContainerId, remote.Command) + if err := cmd.Start(); err != nil { + return err + } + + go func() { + defer stdin_w.Close() + stdin_w.Write([]byte(remote.Command + "\n")) + }() + + var exitStatus int = 0 + err = cmd.Wait() + if exitErr, ok := err.(*exec.ExitError); ok { + exitStatus = 1 + + // There is no process-independent way to get the REAL + // exit status so we just try to go deeper. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } + } + + if exitStatus != 0 { + return fmt.Errorf("Exit status: %d", exitStatus) + } + + return nil +} + +func (c *Communicator) Upload(dst string, src io.Reader) error { + // Create a temporary file to store the upload + tempfile, err := ioutil.TempFile(c.HostDir, "upload") + if err != nil { + return err + } + defer os.Remove(tempfile.Name()) + + // Copy the contents to the temporary file + _, err = io.Copy(tempfile, src) + tempfile.Close() + if err != nil { + return err + } + + // TODO(mitchellh): Copy the file into place + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("cp %s %s", tempfile.Name(), dst), + } + + if err := c.Start(cmd); err != nil { + return err + } + + // Wait for the copy to complete + cmd.Wait() + if cmd.ExitStatus != 0 { + return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus) + } + + return nil +} + +func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { + return nil +} + +func (c *Communicator) Download(src string, dst io.Writer) error { + return nil +} diff --git a/builder/docker/communicator_test.go b/builder/docker/communicator_test.go new file mode 100644 index 000000000..f75a89d96 --- /dev/null +++ b/builder/docker/communicator_test.go @@ -0,0 +1,10 @@ +package docker + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestCommunicator_impl(t *testing.T) { + var _ packer.Communicator = new(Communicator) +} diff --git a/builder/docker/step_provision.go b/builder/docker/step_provision.go new file mode 100644 index 000000000..ecee2f99d --- /dev/null +++ b/builder/docker/step_provision.go @@ -0,0 +1,33 @@ +package docker + +import ( + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepProvision struct{} + +func (s *StepProvision) Run(state multistep.StateBag) multistep.StepAction { + containerId := state.Get("container_id").(string) + hook := state.Get("hook").(packer.Hook) + tempDir := state.Get("temp_dir").(string) + ui := state.Get("ui").(packer.Ui) + + // Create the communicator that talks to Docker via various + // os/exec tricks. + comm := &Communicator{ + ContainerId: containerId, + HostDir: tempDir, + ContainerDir: "/packer-files", + } + + // Run the provisioning hook + if err := hook.Run(packer.HookProvision, ui, comm, nil); err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepProvision) Cleanup(state multistep.StateBag) {} diff --git a/builder/docker/step_run.go b/builder/docker/step_run.go index 51bc504db..7c4397bf0 100644 --- a/builder/docker/step_run.go +++ b/builder/docker/step_run.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "log" "os/exec" "strings" ) @@ -15,14 +16,27 @@ type StepRun struct { func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) + 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", + } + + // Start the container var stdout, stderr bytes.Buffer - cmd := exec.Command("docker", "run", "-d", "-i", "-t", config.Image, "/bin/bash") + 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 { err := fmt.Errorf("Error running container: %s", err) state.Put("error", err) @@ -31,15 +45,18 @@ func (s *StepRun) Run(state multistep.StateBag) multistep.StepAction { } if err := cmd.Wait(); err != nil { - err := fmt.Errorf("Error running container: %s", err) + 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)) + state.Put("container_id", s.containerId) return multistep.ActionContinue } @@ -48,6 +65,8 @@ func (s *StepRun) Cleanup(state multistep.StateBag) { return } - // TODO(mitchellh): handle errors + // 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() } diff --git a/builder/docker/step_temp_dir.go b/builder/docker/step_temp_dir.go new file mode 100644 index 000000000..c8b2fa7e6 --- /dev/null +++ b/builder/docker/step_temp_dir.go @@ -0,0 +1,38 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "io/ioutil" + "os" +) + +// StepTempDir creates a temporary directory that we use in order to +// share data with the docker container over the communicator. +type StepTempDir struct { + tempDir string +} + +func (s *StepTempDir) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + + ui.Say("Creating a temporary directory for sharing data...") + td, err := ioutil.TempDir("", "packer-docker") + if err != nil { + err := fmt.Errorf("Error making temp dir: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.tempDir = td + state.Put("temp_dir", s.tempDir) + return multistep.ActionContinue +} + +func (s *StepTempDir) Cleanup(state multistep.StateBag) { + if s.tempDir != "" { + os.RemoveAll(s.tempDir) + } +} From 5f76ed68c42ae4d39de78e28f8760689623373aa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Nov 2013 23:59:25 -0800 Subject: [PATCH 08/29] builder/docker: Remote execution works! /cc @mwhooker - WOW. By luck, I had a hunch that maybe something like this might be going on based on straces I was reading. Check: https://github.com/dotcloud/docker/issues/2628 Anyways, this works now. No more blocker! --- builder/docker/communicator.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index dddd27042..58ba732fb 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "syscall" + "time" ) type Communicator struct { @@ -34,6 +35,7 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { go func() { defer stdin_w.Close() + time.Sleep(2 * time.Second) stdin_w.Write([]byte(remote.Command + "\n")) }() @@ -49,9 +51,8 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { } } - if exitStatus != 0 { - return fmt.Errorf("Exit status: %d", exitStatus) - } + // Say that we ended! + remote.SetExited(exitStatus) return nil } From c1f0fe3f3fdc26b7e9e391bd2977ec2a0798547b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 00:05:44 -0800 Subject: [PATCH 09/29] builder/docker: fix upload for copy to work --- builder/docker/communicator.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 58ba732fb..ccb5830c0 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -8,6 +8,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "syscall" "time" ) @@ -25,6 +26,11 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { return err } + // TODO(mitchellh): We need to hijack the command to write the exit + // code to a temporary file in the shared folder so that we can read it + // out. Since we're going over a pty, we can't get the exit code another + // way. + cmd.Stdout = remote.Stdout cmd.Stderr = remote.Stderr @@ -74,7 +80,8 @@ func (c *Communicator) Upload(dst string, src io.Reader) error { // TODO(mitchellh): Copy the file into place cmd := &packer.RemoteCmd{ - Command: fmt.Sprintf("cp %s %s", tempfile.Name(), dst), + Command: fmt.Sprintf("cp %s/%s %s", c.ContainerDir, + filepath.Base(tempfile.Name()), dst), } if err := c.Start(cmd); err != nil { From eabd32f3cea7d0f4c74b492d89f1ff995fc2fb48 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 00:06:45 -0800 Subject: [PATCH 10/29] builder/docker: comment the sleep on remote exec --- builder/docker/communicator.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index ccb5830c0..01889b638 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -41,7 +41,14 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { go func() { defer stdin_w.Close() + + // This sleep needs to be here because of the issue linked to below. + // Basically, without it, Docker will hang on reading stdin forever, + // and won't see what we write, for some reason. + // + // https://github.com/dotcloud/docker/issues/2628 time.Sleep(2 * time.Second) + stdin_w.Write([]byte(remote.Command + "\n")) }() From 2e7574e360670c5ad26d9a985fa0a1592b8e09b5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 00:33:36 -0800 Subject: [PATCH 11/29] builder/docker: command output and exit codes work /cc @mwhooker - CCing you on this because it is also ridiculous. See the big comments --- builder/docker/communicator.go | 93 +++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 01889b638..2576a4452 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -1,6 +1,7 @@ package docker import ( + "bytes" "fmt" "github.com/mitchellh/packer/packer" "io" @@ -9,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "syscall" "time" ) @@ -20,21 +22,39 @@ type Communicator struct { } func (c *Communicator) Start(remote *packer.RemoteCmd) error { + // Create a temporary file to store the output. Because of a bug in + // Docker, sometimes all the output doesn't properly show up. This + // file will capture ALL of the output, and we'll read that. + // + // https://github.com/dotcloud/docker/issues/2625 + outputFile, err := ioutil.TempFile(c.HostDir, "cmd") + if err != nil { + return err + } + outputFile.Close() + defer os.Remove(outputFile.Name()) + + // This file will store the exit code of the command once it is complete. + exitCodePath := outputFile.Name() + "-exit" + + // Modify the remote command so that all the output of the commands + // go to a single file and so that the exit code is redirected to + // a single file. This lets us determine both when the command + // is truly complete (because the file will have data), what the + // exit status is (because Docker loses it because of the pty, not + // Docker's fault), and get the output (Docker bug). + remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s", + remote.Command, + filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())), + filepath.Join(c.ContainerDir, filepath.Base(exitCodePath))) + cmd := exec.Command("docker", "attach", c.ContainerId) stdin_w, err := cmd.StdinPipe() if err != nil { return err } - // TODO(mitchellh): We need to hijack the command to write the exit - // code to a temporary file in the shared folder so that we can read it - // out. Since we're going over a pty, we can't get the exit code another - // way. - - cmd.Stdout = remote.Stdout - cmd.Stderr = remote.Stderr - - log.Printf("Executing in container %s: %#v", c.ContainerId, remote.Command) + log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd) if err := cmd.Start(); err != nil { return err } @@ -49,23 +69,68 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { // https://github.com/dotcloud/docker/issues/2628 time.Sleep(2 * time.Second) - stdin_w.Write([]byte(remote.Command + "\n")) + stdin_w.Write([]byte(remoteCmd + "\n")) }() - var exitStatus int = 0 err = cmd.Wait() if exitErr, ok := err.(*exec.ExitError); ok { - exitStatus = 1 + exitStatus := 1 // There is no process-independent way to get the REAL // exit status so we just try to go deeper. if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { exitStatus = status.ExitStatus() } + + // Say that we ended, since if Docker itself failed, then + // the command must've not run, or so we assume + remote.SetExited(exitStatus) + return nil } - // Say that we ended! - remote.SetExited(exitStatus) + // Wait for the exit code to appear in our file... + log.Println("Waiting for exit code to appear for remote command...") + for { + fi, err := os.Stat(exitCodePath) + if err == nil && fi.Size() > 0 { + break + } + + time.Sleep(1 * time.Second) + } + + // Read the exit code + exitRaw, err := ioutil.ReadFile(exitCodePath) + if err != nil { + return err + } + + exitStatus, err := strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0) + if err != nil { + return err + } + log.Printf("Executed command exit status: %d", exitStatus) + + // Read the output + f, err := os.Open(outputFile.Name()) + if err != nil { + return err + } + defer f.Close() + + if remote.Stdout != nil { + io.Copy(remote.Stdout, f) + } else { + output, err := ioutil.ReadAll(f) + if err != nil { + return err + } + + log.Printf("Command output: %s", string(output)) + } + + // Finally, we're done + remote.SetExited(int(exitStatus)) return nil } From d27ceaf509865f9bcfa76544bb77766e5dd7fae2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 00:47:37 -0800 Subject: [PATCH 12/29] builder/docker: remove the exit code file when we're done --- builder/docker/communicator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 2576a4452..45ad0ee92 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -36,6 +36,7 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { // This file will store the exit code of the command once it is complete. exitCodePath := outputFile.Name() + "-exit" + defer os.Remove(exitCodePath) // Modify the remote command so that all the output of the commands // go to a single file and so that the exit code is redirected to From d5ce8ddb4a82c087f0a58573f6792611a0e2a0de Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 09:48:36 -0800 Subject: [PATCH 13/29] builder/docker: export the final image --- builder/docker/builder.go | 1 + builder/docker/config.go | 3 +- builder/docker/step_export.go | 61 +++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 builder/docker/step_export.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index 0b343c0db..09c5826a7 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -41,6 +41,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &StepPull{}, &StepRun{}, &StepProvision{}, + &StepExport{}, } // Setup the state bag and initial state for the steps diff --git a/builder/docker/config.go b/builder/docker/config.go index 4bab81bdd..86f3da982 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -8,7 +8,8 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` - Image string + ExportPath string `mapstructure:"export_path"` + Image string tpl *packer.ConfigTemplate } diff --git a/builder/docker/step_export.go b/builder/docker/step_export.go new file mode 100644 index 000000000..de50844b9 --- /dev/null +++ b/builder/docker/step_export.go @@ -0,0 +1,61 @@ +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. +type StepExport struct{} + +func (s *StepExport) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + 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 { + err := fmt.Errorf("Error creating output file: %s", err) + state.Put("error", err) + 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 + + 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 + } + + return multistep.ActionContinue +} + +func (s *StepExport) Cleanup(state multistep.StateBag) {} From 44a41451f0a51ed9bce7945522b70cd706fedfbc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 10:13:27 -0800 Subject: [PATCH 14/29] builder/docker: Communicator.Start doesn't block --- builder/docker/communicator.go | 204 +++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 89 deletions(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 45ad0ee92..d757e89cb 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "strconv" + "sync" "syscall" "time" ) @@ -19,6 +20,8 @@ type Communicator struct { ContainerId string HostDir string ContainerDir string + + lock sync.Mutex } func (c *Communicator) Start(remote *packer.RemoteCmd) error { @@ -38,100 +41,14 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { exitCodePath := outputFile.Name() + "-exit" defer os.Remove(exitCodePath) - // Modify the remote command so that all the output of the commands - // go to a single file and so that the exit code is redirected to - // a single file. This lets us determine both when the command - // is truly complete (because the file will have data), what the - // exit status is (because Docker loses it because of the pty, not - // Docker's fault), and get the output (Docker bug). - remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s", - remote.Command, - filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())), - filepath.Join(c.ContainerDir, filepath.Base(exitCodePath))) - cmd := exec.Command("docker", "attach", c.ContainerId) stdin_w, err := cmd.StdinPipe() if err != nil { return err } - log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd) - if err := cmd.Start(); err != nil { - return err - } - - go func() { - defer stdin_w.Close() - - // This sleep needs to be here because of the issue linked to below. - // Basically, without it, Docker will hang on reading stdin forever, - // and won't see what we write, for some reason. - // - // https://github.com/dotcloud/docker/issues/2628 - time.Sleep(2 * time.Second) - - stdin_w.Write([]byte(remoteCmd + "\n")) - }() - - err = cmd.Wait() - if exitErr, ok := err.(*exec.ExitError); ok { - exitStatus := 1 - - // There is no process-independent way to get the REAL - // exit status so we just try to go deeper. - if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { - exitStatus = status.ExitStatus() - } - - // Say that we ended, since if Docker itself failed, then - // the command must've not run, or so we assume - remote.SetExited(exitStatus) - return nil - } - - // Wait for the exit code to appear in our file... - log.Println("Waiting for exit code to appear for remote command...") - for { - fi, err := os.Stat(exitCodePath) - if err == nil && fi.Size() > 0 { - break - } - - time.Sleep(1 * time.Second) - } - - // Read the exit code - exitRaw, err := ioutil.ReadFile(exitCodePath) - if err != nil { - return err - } - - exitStatus, err := strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0) - if err != nil { - return err - } - log.Printf("Executed command exit status: %d", exitStatus) - - // Read the output - f, err := os.Open(outputFile.Name()) - if err != nil { - return err - } - defer f.Close() - - if remote.Stdout != nil { - io.Copy(remote.Stdout, f) - } else { - output, err := ioutil.ReadAll(f) - if err != nil { - return err - } - - log.Printf("Command output: %s", string(output)) - } - - // Finally, we're done - remote.SetExited(int(exitStatus)) + // Run the actual command in a goroutine so that Start doesn't block + go c.run(cmd, remote, stdin_w, outputFile, exitCodePath) return nil } @@ -151,7 +68,8 @@ func (c *Communicator) Upload(dst string, src io.Reader) error { return err } - // TODO(mitchellh): Copy the file into place + // Copy the file into place by copying the temporary file we put + // into the shared folder into the proper location in the container cmd := &packer.RemoteCmd{ Command: fmt.Sprintf("cp %s/%s %s", c.ContainerDir, filepath.Base(tempfile.Name()), dst), @@ -177,3 +95,111 @@ func (c *Communicator) UploadDir(dst string, src string, exclude []string) error func (c *Communicator) Download(src string, dst io.Writer) error { return nil } + +// Runs the given command and blocks until completion +func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.WriteCloser, outputFile *os.File, exitCodePath string) { + // For Docker, remote communication must be serialized since it + // only supports single execution. + c.lock.Lock() + defer c.lock.Unlock() + + // Modify the remote command so that all the output of the commands + // go to a single file and so that the exit code is redirected to + // a single file. This lets us determine both when the command + // is truly complete (because the file will have data), what the + // exit status is (because Docker loses it because of the pty, not + // Docker's fault), and get the output (Docker bug). + remoteCmd := fmt.Sprintf("(%s) >%s 2>&1; echo $? >%s", + remote.Command, + filepath.Join(c.ContainerDir, filepath.Base(outputFile.Name())), + filepath.Join(c.ContainerDir, filepath.Base(exitCodePath))) + + // Start the command + log.Printf("Executing in container %s: %#v", c.ContainerId, remoteCmd) + if err := cmd.Start(); err != nil { + log.Printf("Error executing: %s", err) + remote.SetExited(254) + return + } + + go func() { + defer stdin_w.Close() + + // This sleep needs to be here because of the issue linked to below. + // Basically, without it, Docker will hang on reading stdin forever, + // and won't see what we write, for some reason. + // + // https://github.com/dotcloud/docker/issues/2628 + time.Sleep(2 * time.Second) + + stdin_w.Write([]byte(remoteCmd + "\n")) + }() + + err := cmd.Wait() + if exitErr, ok := err.(*exec.ExitError); ok { + exitStatus := 1 + + // There is no process-independent way to get the REAL + // exit status so we just try to go deeper. + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitStatus = status.ExitStatus() + } + + // Say that we ended, since if Docker itself failed, then + // the command must've not run, or so we assume + remote.SetExited(exitStatus) + return + } + + // Wait for the exit code to appear in our file... + log.Println("Waiting for exit code to appear for remote command...") + for { + fi, err := os.Stat(exitCodePath) + if err == nil && fi.Size() > 0 { + break + } + + time.Sleep(1 * time.Second) + } + + // Read the exit code + exitRaw, err := ioutil.ReadFile(exitCodePath) + if err != nil { + log.Printf("Error executing: %s", err) + remote.SetExited(254) + return + } + + exitStatus, err := strconv.ParseInt(string(bytes.TrimSpace(exitRaw)), 10, 0) + if err != nil { + log.Printf("Error executing: %s", err) + remote.SetExited(254) + return + } + log.Printf("Executed command exit status: %d", exitStatus) + + // Read the output + f, err := os.Open(outputFile.Name()) + if err != nil { + log.Printf("Error executing: %s", err) + remote.SetExited(254) + return + } + defer f.Close() + + if remote.Stdout != nil { + io.Copy(remote.Stdout, f) + } else { + output, err := ioutil.ReadAll(f) + if err != nil { + log.Printf("Error executing: %s", err) + remote.SetExited(254) + return + } + + log.Printf("Command output: %s", string(output)) + } + + // Finally, we're done + remote.SetExited(int(exitStatus)) +} From 23ad5442ece00e904d358a17181c8d4f526b7d4f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 10:15:25 -0800 Subject: [PATCH 15/29] builder/docker: perform cleanup in run method, not prematurely --- builder/docker/communicator.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index d757e89cb..118fc0956 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -35,15 +35,17 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error { return err } outputFile.Close() - defer os.Remove(outputFile.Name()) // This file will store the exit code of the command once it is complete. exitCodePath := outputFile.Name() + "-exit" - defer os.Remove(exitCodePath) cmd := exec.Command("docker", "attach", c.ContainerId) stdin_w, err := cmd.StdinPipe() if err != nil { + // We have to do some cleanup since run was never called + os.Remove(outputFile.Name()) + os.Remove(exitCodePath) + return err } @@ -103,6 +105,10 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W c.lock.Lock() defer c.lock.Unlock() + // Clean up after ourselves by removing our temporary files + defer os.Remove(outputFile.Name()) + defer os.Remove(exitCodePath) + // Modify the remote command so that all the output of the commands // go to a single file and so that the exit code is redirected to // a single file. This lets us determine both when the command From da683afde0224d72bc5aa7170ca0a4c381f441a8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 11:47:32 -0800 Subject: [PATCH 16/29] builder/docker: config validation test --- builder/docker/builder.go | 6 ++++ builder/docker/config.go | 29 +++++++++++++++ builder/docker/config_test.go | 67 +++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 builder/docker/config_test.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index 09c5826a7..67515b645 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -25,9 +25,15 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { return nil, err } + // Accumulate any errors errs := common.CheckUnusedConfig(md) warnings := make([]string, 0) + // Validate the configuration + cwarns, cerrs := b.config.Prepare() + errs = packer.MultiErrorAppend(errs, cerrs...) + warnings = append(warnings, cwarns...) + if errs != nil && len(errs.Errors) > 0 { return warnings, errs } diff --git a/builder/docker/config.go b/builder/docker/config.go index 86f3da982..09ef02a06 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -1,6 +1,7 @@ package docker import ( + "fmt" "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/packer" ) @@ -13,3 +14,31 @@ type Config struct { tpl *packer.ConfigTemplate } + +func (c *Config) Prepare() ([]string, []error) { + errs := make([]error, 0) + + templates := map[string]*string{ + "export_path": &c.ExportPath, + "image": &c.Image, + } + + for n, ptr := range templates { + var err error + *ptr, err = c.tpl.Process(*ptr, nil) + if err != nil { + errs = append( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if c.ExportPath == "" { + errs = append(errs, fmt.Errorf("export_path must be specified")) + } + + if c.Image == "" { + errs = append(errs, fmt.Errorf("image must be specified")) + } + + return nil, errs +} diff --git a/builder/docker/config_test.go b/builder/docker/config_test.go new file mode 100644 index 000000000..8d083bf70 --- /dev/null +++ b/builder/docker/config_test.go @@ -0,0 +1,67 @@ +package docker + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func testConfigStruct(t *testing.T) *Config { + tpl, err := packer.NewConfigTemplate() + if err != nil { + t.Fatalf("err: %s", err) + } + + return &Config{ + ExportPath: "foo", + Image: "bar", + tpl: tpl, + } +} + +func TestConfigPrepare_exportPath(t *testing.T) { + c := testConfigStruct(t) + + // No export path + c.ExportPath = "" + warns, errs := c.Prepare() + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if len(errs) <= 0 { + t.Fatalf("bad: %#v", errs) + } + + // Good export path + c.ExportPath = "path" + warns, errs = c.Prepare() + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if len(errs) > 0 { + t.Fatalf("bad: %#v", errs) + } +} + +func TestConfigPrepare_image(t *testing.T) { + c := testConfigStruct(t) + + // No image + c.Image = "" + warns, errs := c.Prepare() + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if len(errs) <= 0 { + t.Fatalf("bad: %#v", errs) + } + + // Good image + c.Image = "path" + warns, errs = c.Prepare() + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if len(errs) > 0 { + t.Fatalf("bad: %#v", errs) + } +} From 2da9233655880aeb3559c8eb16779862fffa246c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 11:58:20 -0800 Subject: [PATCH 17/29] builder/docker: test StepTempDir --- builder/docker/step_temp_dir_test.go | 44 ++++++++++++++++++++++++++++ builder/docker/step_test.go | 19 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 builder/docker/step_temp_dir_test.go create mode 100644 builder/docker/step_test.go diff --git a/builder/docker/step_temp_dir_test.go b/builder/docker/step_temp_dir_test.go new file mode 100644 index 000000000..a7d495f65 --- /dev/null +++ b/builder/docker/step_temp_dir_test.go @@ -0,0 +1,44 @@ +package docker + +import ( + "github.com/mitchellh/multistep" + "os" + "testing" +) + +func TestStepTempDir_impl(t *testing.T) { + var _ multistep.Step = new(StepTempDir) +} + +func TestStepTempDir(t *testing.T) { + state := testState(t) + step := new(StepTempDir) + defer step.Cleanup(state) + + // sanity test + if _, ok := state.GetOk("temp_dir"); ok { + t.Fatalf("temp_dir should not be in state yet") + } + + // run the step + if action := step.Run(state); action != multistep.ActionContinue { + t.Fatalf("bad action: %#v", action) + } + + // Verify that we got the temp dir + dirRaw, ok := state.GetOk("temp_dir") + if !ok { + t.Fatalf("should've made temp_dir") + } + dir := dirRaw.(string) + + if _, err := os.Stat(dir); err != nil { + t.Fatalf("err: %s", err) + } + + // Cleanup + step.Cleanup(state) + if _, err := os.Stat(dir); err == nil { + t.Fatalf("dir should be gone") + } +} diff --git a/builder/docker/step_test.go b/builder/docker/step_test.go new file mode 100644 index 000000000..c6487c55f --- /dev/null +++ b/builder/docker/step_test.go @@ -0,0 +1,19 @@ +package docker + +import ( + "bytes" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "testing" +) + +func testState(t *testing.T) multistep.StateBag { + state := new(multistep.BasicStateBag) + state.Put("config", testConfigStruct(t)) + state.Put("hook", &packer.MockHook{}) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + return state +} From 62b81dc432f4a8a02ea0b0ce928f45f5c5f38e34 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 12:12:23 -0800 Subject: [PATCH 18/29] builder/docker: StepPull test, driver abstraction for tests --- builder/docker/builder.go | 5 +++ builder/docker/driver.go | 9 ++++++ builder/docker/driver_docker.go | 15 +++++++++ builder/docker/driver_mock.go | 15 +++++++++ builder/docker/step_pull.go | 5 ++- builder/docker/step_pull_test.go | 52 ++++++++++++++++++++++++++++++++ builder/docker/step_test.go | 1 + 7 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 builder/docker/driver.go create mode 100644 builder/docker/driver_docker.go create mode 100644 builder/docker/driver_mock.go create mode 100644 builder/docker/step_pull_test.go diff --git a/builder/docker/builder.go b/builder/docker/builder.go index 67515b645..4c52827ed 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -56,6 +56,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe state.Put("hook", hook) state.Put("ui", ui) + // Setup the driver that will talk to Docker + state.Put("driver", &DockerDriver{ + Ui: ui, + }) + // Run! if b.config.PackerDebug { b.runner = &multistep.DebugRunner{ diff --git a/builder/docker/driver.go b/builder/docker/driver.go new file mode 100644 index 000000000..c0fb5fa66 --- /dev/null +++ b/builder/docker/driver.go @@ -0,0 +1,9 @@ +package docker + +// 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 { + // 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 new file mode 100644 index 000000000..04d338abd --- /dev/null +++ b/builder/docker/driver_docker.go @@ -0,0 +1,15 @@ +package docker + +import ( + "github.com/mitchellh/packer/packer" + "os/exec" +) + +type DockerDriver struct { + Ui packer.Ui +} + +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 new file mode 100644 index 000000000..f5aad13bc --- /dev/null +++ b/builder/docker/driver_mock.go @@ -0,0 +1,15 @@ +package docker + +// MockDriver is a driver implementation that can be used for tests. +type MockDriver struct { + PullError error + + PullCalled bool + PullImage string +} + +func (d *MockDriver) Pull(image string) error { + d.PullCalled = true + d.PullImage = image + return d.PullError +} diff --git a/builder/docker/step_pull.go b/builder/docker/step_pull.go index 28662eccb..19e5a69ab 100644 --- a/builder/docker/step_pull.go +++ b/builder/docker/step_pull.go @@ -4,18 +4,17 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" - "os/exec" ) type StepPull struct{} func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction { config := state.Get("config").(*Config) + driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) ui.Say(fmt.Sprintf("Pulling Docker image: %s", config.Image)) - cmd := exec.Command("docker", "pull", config.Image) - if err := runAndStream(cmd, ui); err != nil { + if err := driver.Pull(config.Image); err != nil { err := fmt.Errorf("Error pulling Docker image: %s", err) state.Put("error", err) ui.Error(err.Error()) diff --git a/builder/docker/step_pull_test.go b/builder/docker/step_pull_test.go new file mode 100644 index 000000000..93e9cf830 --- /dev/null +++ b/builder/docker/step_pull_test.go @@ -0,0 +1,52 @@ +package docker + +import ( + "errors" + "github.com/mitchellh/multistep" + "testing" +) + +func TestStepPull_impl(t *testing.T) { + var _ multistep.Step = new(StepPull) +} + +func TestStepPull(t *testing.T) { + state := testState(t) + step := new(StepPull) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + 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.PullCalled { + t.Fatal("should've pulled") + } + if driver.PullImage != config.Image { + t.Fatalf("bad: %#v", driver.PullImage) + } +} + +func TestStepPull_error(t *testing.T) { + state := testState(t) + step := new(StepPull) + defer step.Cleanup(state) + + driver := state.Get("driver").(*MockDriver) + driver.PullError = 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") + } +} diff --git a/builder/docker/step_test.go b/builder/docker/step_test.go index c6487c55f..0bc190830 100644 --- a/builder/docker/step_test.go +++ b/builder/docker/step_test.go @@ -10,6 +10,7 @@ import ( func testState(t *testing.T) multistep.StateBag { state := new(multistep.BasicStateBag) state.Put("config", testConfigStruct(t)) + state.Put("driver", &MockDriver{}) state.Put("hook", &packer.MockHook{}) state.Put("ui", &packer.BasicUi{ Reader: new(bytes.Buffer), From 0e3011cbce3ba6fbc2d1c15317be85e66cd5f8f8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 13:03:01 -0800 Subject: [PATCH 19/29] builder/docker: StepRun tests --- builder/docker/driver.go | 13 ++++ builder/docker/driver_docker.go | 43 +++++++++++++ builder/docker/driver_mock.go | 25 +++++++- builder/docker/driver_mock_test.go | 7 ++ builder/docker/drover_docker_test.go | 7 ++ builder/docker/step_run.go | 51 +++++---------- builder/docker/step_run_test.go | 95 ++++++++++++++++++++++++++++ 7 files changed, 204 insertions(+), 37 deletions(-) create mode 100644 builder/docker/driver_mock_test.go create mode 100644 builder/docker/drover_docker_test.go create mode 100644 builder/docker/step_run_test.go 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") + } +} From a58754b97439f51bb865e9769aedd53e29fa2cf6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 13:15:51 -0800 Subject: [PATCH 20/29] builder/docker: StepExport tests --- builder/docker/driver.go | 7 +++ builder/docker/driver_docker.go | 21 +++++++ builder/docker/driver_mock.go | 42 ++++++++++--- builder/docker/foo | 0 builder/docker/step_export.go | 31 ++-------- builder/docker/step_export_test.go | 99 ++++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 builder/docker/foo create mode 100644 builder/docker/step_export_test.go 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") + } +} From 06b6cb1af1b74f08e3e7b45e6fb6b8b8f197f036 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 13:22:13 -0800 Subject: [PATCH 21/29] builder/docker: artifact --- builder/docker/artifact_export.go | 32 ++++++++++++++++++++++++++ builder/docker/artifact_export_test.go | 10 ++++++++ builder/docker/builder.go | 4 +++- 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 builder/docker/artifact_export.go create mode 100644 builder/docker/artifact_export_test.go diff --git a/builder/docker/artifact_export.go b/builder/docker/artifact_export.go new file mode 100644 index 000000000..29cbefb48 --- /dev/null +++ b/builder/docker/artifact_export.go @@ -0,0 +1,32 @@ +package docker + +import ( + "fmt" + "os" +) + +// ExportArtifact is an Artifact implementation for when a container is +// exported from docker into a single flat file. +type ExportArtifact struct { + path string +} + +func (*ExportArtifact) BuilderId() string { + return BuilderId +} + +func (a *ExportArtifact) Files() []string { + return []string{a.path} +} + +func (*ExportArtifact) Id() string { + return "Container" +} + +func (a *ExportArtifact) String() string { + return fmt.Sprintf("Exported Docker file: %s", a.path) +} + +func (a *ExportArtifact) Destroy() error { + return os.Remove(a.path) +} diff --git a/builder/docker/artifact_export_test.go b/builder/docker/artifact_export_test.go new file mode 100644 index 000000000..be7ce2444 --- /dev/null +++ b/builder/docker/artifact_export_test.go @@ -0,0 +1,10 @@ +package docker + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestExportArtifact_impl(t *testing.T) { + var _ packer.Artifact = new(ExportArtifact) +} diff --git a/builder/docker/builder.go b/builder/docker/builder.go index 4c52827ed..f42e4b30d 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -78,7 +78,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, rawErr.(error) } - return nil, nil + // No errors, must've worked + artifact := &ExportArtifact{path: b.config.ExportPath} + return artifact, nil } func (b *Builder) Cancel() { From 3a13e47f34eaed460ade62afbb8262b02d3e241c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 13:28:00 -0800 Subject: [PATCH 22/29] website: start documenting the builder --- .../source/docs/builders/docker.html.markdown | 41 +++++++++++++++++++ website/source/layouts/docs.erb | 1 + 2 files changed, 42 insertions(+) create mode 100644 website/source/docs/builders/docker.html.markdown diff --git a/website/source/docs/builders/docker.html.markdown b/website/source/docs/builders/docker.html.markdown new file mode 100644 index 000000000..a38691f76 --- /dev/null +++ b/website/source/docs/builders/docker.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "docs" +--- + +# Docker Builder + +Type: `docker` + +The Docker builder builds [Docker](http://www.docker.io) images using +Docker. The builder starts a Docker container, runs provisioners within +this container, then exports the container for re-use. + +The Docker builder must run on a machine that supports Docker. + +## Basic Example + +Below is a fully functioning example. It doesn't do anything useful, since +no provisioners are defined, but it will effectively repackage an image. + +
+{
+  "type": "docker",
+  "image": "ubuntu",
+  "export_path": "image.tar"
+}
+
+ +## Configuration Reference + +All configuration options are currently required. + +* `export_path` (string) - The path where the final container will be exported + as a tar file. + +* `image` (string) - The base image for the Docker container that will + be started. This image will be pulled from the Docker registry if it + doesn't already exist. + +## Dockerfiles + +TODO diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index f400ec930..d340b6edc 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -32,6 +32,7 @@
  • Builders

  • Amazon EC2 (AMI)
  • DigitalOcean
  • +
  • Docker
  • OpenStack
  • QEMU
  • VirtualBox
  • From 8f39edf935f030166a0511a5690996c19db02249 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 13:59:34 -0800 Subject: [PATCH 23/29] website: document dockerfile --- website/source/docs/builders/docker.html.markdown | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/website/source/docs/builders/docker.html.markdown b/website/source/docs/builders/docker.html.markdown index a38691f76..a6807aac4 100644 --- a/website/source/docs/builders/docker.html.markdown +++ b/website/source/docs/builders/docker.html.markdown @@ -38,4 +38,11 @@ All configuration options are currently required. ## Dockerfiles -TODO +This builder allows you to build Docker images _without_ Dockerfiles. If +you have a Dockerfile already made, it is simple to just run `docker build` +manually. + +With this builder, you can repeatably create Docker images without the use +a Dockerfile. You don't need to know the syntax or semantics of Dockerfiles. +Instead, you can just provide shell scripts, Chef recipes, Puppet manifests, +etc. to provision your Docker container just like you would a regular machine. From 0287cdd6149124e39b35e4aad9c63798fd10b6f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 17:07:14 -0800 Subject: [PATCH 24/29] builder/docker: config refactor --- builder/docker/builder.go | 27 +++------------ builder/docker/config.go | 29 ++++++++++++---- builder/docker/config_test.go | 63 +++++++++++++++++++---------------- 3 files changed, 62 insertions(+), 57 deletions(-) diff --git a/builder/docker/builder.go b/builder/docker/builder.go index f42e4b30d..824f1697f 100644 --- a/builder/docker/builder.go +++ b/builder/docker/builder.go @@ -10,33 +10,16 @@ import ( const BuilderId = "packer.docker" type Builder struct { - config Config + config *Config runner multistep.Runner } func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { - md, err := common.DecodeConfig(&b.config, raws...) - if err != nil { - return nil, err - } - - b.config.tpl, err = packer.NewConfigTemplate() - if err != nil { - return nil, err - } - - // Accumulate any errors - errs := common.CheckUnusedConfig(md) - warnings := make([]string, 0) - - // Validate the configuration - cwarns, cerrs := b.config.Prepare() - errs = packer.MultiErrorAppend(errs, cerrs...) - warnings = append(warnings, cwarns...) - - if errs != nil && len(errs.Errors) > 0 { + c, warnings, errs := NewConfig(raws...) + if errs != nil { return warnings, errs } + b.config = c return warnings, nil } @@ -52,7 +35,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe // Setup the state bag and initial state for the steps state := new(multistep.BasicStateBag) - state.Put("config", &b.config) + state.Put("config", b.config) state.Put("hook", hook) state.Put("ui", ui) diff --git a/builder/docker/config.go b/builder/docker/config.go index 09ef02a06..e62930fd2 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -15,8 +15,19 @@ type Config struct { tpl *packer.ConfigTemplate } -func (c *Config) Prepare() ([]string, []error) { - errs := make([]error, 0) +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := new(Config) + md, err := common.DecodeConfig(c, raws...) + if err != nil { + return nil, nil, err + } + + c.tpl, err = packer.NewConfigTemplate() + if err != nil { + return nil, nil, err + } + + errs := common.CheckUnusedConfig(md) templates := map[string]*string{ "export_path": &c.ExportPath, @@ -27,18 +38,24 @@ func (c *Config) Prepare() ([]string, []error) { var err error *ptr, err = c.tpl.Process(*ptr, nil) if err != nil { - errs = append( + errs = packer.MultiErrorAppend( errs, fmt.Errorf("Error processing %s: %s", n, err)) } } if c.ExportPath == "" { - errs = append(errs, fmt.Errorf("export_path must be specified")) + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("export_path must be specified")) } if c.Image == "" { - errs = append(errs, fmt.Errorf("image must be specified")) + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("image must be specified")) } - return nil, errs + if errs != nil && len(errs.Errors) > 0 { + return nil, nil, errs + } + + return c, nil, nil } diff --git a/builder/docker/config_test.go b/builder/docker/config_test.go index 8d083bf70..d26c21973 100644 --- a/builder/docker/config_test.go +++ b/builder/docker/config_test.go @@ -1,67 +1,72 @@ package docker import ( - "github.com/mitchellh/packer/packer" "testing" ) -func testConfigStruct(t *testing.T) *Config { - tpl, err := packer.NewConfigTemplate() - if err != nil { - t.Fatalf("err: %s", err) - } - - return &Config{ - ExportPath: "foo", - Image: "bar", - tpl: tpl, +func testConfig() map[string]interface{} { + return map[string]interface{}{ + "export_path": "foo", + "image": "bar", } } +func testConfigStruct(t *testing.T) *Config { + c, warns, errs := NewConfig(testConfig()) + if len(warns) > 0 { + t.Fatalf("bad: %#v", len(warns)) + } + if errs != nil { + t.Fatalf("bad: %#v", errs) + } + + return c +} + func TestConfigPrepare_exportPath(t *testing.T) { - c := testConfigStruct(t) + raw := testConfig() // No export path - c.ExportPath = "" - warns, errs := c.Prepare() + delete(raw, "export_path") + _, warns, errs := NewConfig(raw) if len(warns) > 0 { t.Fatalf("bad: %#v", warns) } - if len(errs) <= 0 { - t.Fatalf("bad: %#v", errs) + if errs == nil { + t.Fatal("should error") } // Good export path - c.ExportPath = "path" - warns, errs = c.Prepare() + raw["export_path"] = "good" + _, warns, errs = NewConfig(raw) if len(warns) > 0 { t.Fatalf("bad: %#v", warns) } - if len(errs) > 0 { - t.Fatalf("bad: %#v", errs) + if errs != nil { + t.Fatalf("bad: %s", errs) } } func TestConfigPrepare_image(t *testing.T) { - c := testConfigStruct(t) + raw := testConfig() // No image - c.Image = "" - warns, errs := c.Prepare() + delete(raw, "image") + _, warns, errs := NewConfig(raw) if len(warns) > 0 { t.Fatalf("bad: %#v", warns) } - if len(errs) <= 0 { - t.Fatalf("bad: %#v", errs) + if errs == nil { + t.Fatal("should error") } // Good image - c.Image = "path" - warns, errs = c.Prepare() + raw["image"] = "path" + _, warns, errs = NewConfig(raw) if len(warns) > 0 { t.Fatalf("bad: %#v", warns) } - if len(errs) > 0 { - t.Fatalf("bad: %#v", errs) + if errs != nil { + t.Fatalf("bad: %s", errs) } } From 8ec68031d0660587035e487410c9669d72b95eeb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 17:21:24 -0800 Subject: [PATCH 25/29] builder/docker: ability to disable pull --- builder/docker/config.go | 14 ++++++++ builder/docker/config_test.go | 62 ++++++++++++++++++++------------ builder/docker/step_pull.go | 6 ++++ builder/docker/step_pull_test.go | 21 +++++++++++ 4 files changed, 81 insertions(+), 22 deletions(-) diff --git a/builder/docker/config.go b/builder/docker/config.go index e62930fd2..864ad1fee 100644 --- a/builder/docker/config.go +++ b/builder/docker/config.go @@ -11,6 +11,7 @@ type Config struct { ExportPath string `mapstructure:"export_path"` Image string + Pull bool tpl *packer.ConfigTemplate } @@ -27,6 +28,19 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) { return nil, nil, err } + // Default Pull if it wasn't set + hasPull := false + for _, k := range md.Keys { + if k == "Pull" { + hasPull = true + break + } + } + + if !hasPull { + c.Pull = true + } + errs := common.CheckUnusedConfig(md) templates := map[string]*string{ diff --git a/builder/docker/config_test.go b/builder/docker/config_test.go index d26c21973..3b279ab22 100644 --- a/builder/docker/config_test.go +++ b/builder/docker/config_test.go @@ -23,28 +23,36 @@ func testConfigStruct(t *testing.T) *Config { return c } +func testConfigErr(t *testing.T, warns []string, err error) { + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err == nil { + t.Fatal("should error") + } +} + +func testConfigOk(t *testing.T, warns []string, err error) { + if len(warns) > 0 { + t.Fatalf("bad: %#v", warns) + } + if err != nil { + t.Fatalf("bad: %s", err) + } +} + func TestConfigPrepare_exportPath(t *testing.T) { raw := testConfig() // No export path delete(raw, "export_path") _, warns, errs := NewConfig(raw) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if errs == nil { - t.Fatal("should error") - } + testConfigErr(t, warns, errs) // Good export path raw["export_path"] = "good" _, warns, errs = NewConfig(raw) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if errs != nil { - t.Fatalf("bad: %s", errs) - } + testConfigOk(t, warns, errs) } func TestConfigPrepare_image(t *testing.T) { @@ -53,20 +61,30 @@ func TestConfigPrepare_image(t *testing.T) { // No image delete(raw, "image") _, warns, errs := NewConfig(raw) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) - } - if errs == nil { - t.Fatal("should error") - } + testConfigErr(t, warns, errs) // Good image raw["image"] = "path" _, warns, errs = NewConfig(raw) - if len(warns) > 0 { - t.Fatalf("bad: %#v", warns) + testConfigOk(t, warns, errs) +} + +func TestConfigPrepare_pull(t *testing.T) { + raw := testConfig() + + // No pull set + delete(raw, "pull") + c, warns, errs := NewConfig(raw) + testConfigOk(t, warns, errs) + if !c.Pull { + t.Fatal("should pull by default") } - if errs != nil { - t.Fatalf("bad: %s", errs) + + // Pull set + raw["pull"] = false + c, warns, errs = NewConfig(raw) + testConfigOk(t, warns, errs) + if c.Pull { + t.Fatal("should not pull") } } diff --git a/builder/docker/step_pull.go b/builder/docker/step_pull.go index 19e5a69ab..6571a6563 100644 --- a/builder/docker/step_pull.go +++ b/builder/docker/step_pull.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "log" ) type StepPull struct{} @@ -13,6 +14,11 @@ func (s *StepPull) Run(state multistep.StateBag) multistep.StepAction { driver := state.Get("driver").(Driver) ui := state.Get("ui").(packer.Ui) + if !config.Pull { + log.Println("Pull disabled, won't docker pull") + return multistep.ActionContinue + } + ui.Say(fmt.Sprintf("Pulling Docker image: %s", config.Image)) if err := driver.Pull(config.Image); err != nil { err := fmt.Errorf("Error pulling Docker image: %s", err) diff --git a/builder/docker/step_pull_test.go b/builder/docker/step_pull_test.go index 93e9cf830..5c66425bb 100644 --- a/builder/docker/step_pull_test.go +++ b/builder/docker/step_pull_test.go @@ -50,3 +50,24 @@ func TestStepPull_error(t *testing.T) { t.Fatal("should have error") } } + +func TestStepPull_noPull(t *testing.T) { + state := testState(t) + step := new(StepPull) + defer step.Cleanup(state) + + config := state.Get("config").(*Config) + config.Pull = 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.PullCalled { + t.Fatal("shouldn't have pulled") + } +} From 865adeb4b3451f5cee5d6edca069055067837695 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 17:22:39 -0800 Subject: [PATCH 26/29] website: doc pull --- website/source/docs/builders/docker.html.markdown | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/website/source/docs/builders/docker.html.markdown b/website/source/docs/builders/docker.html.markdown index a6807aac4..c3f9f8018 100644 --- a/website/source/docs/builders/docker.html.markdown +++ b/website/source/docs/builders/docker.html.markdown @@ -27,7 +27,10 @@ no provisioners are defined, but it will effectively repackage an image. ## Configuration Reference -All configuration options are currently required. +Configuration options are organized below into two categories: required and optional. Within +each category, the available options are alphabetized and described. + +Required: * `export_path` (string) - The path where the final container will be exported as a tar file. @@ -36,6 +39,12 @@ All configuration options are currently required. be started. This image will be pulled from the Docker registry if it doesn't already exist. +Optional: + +* `pull` (bool) - If true, the configured image will be pulled using + `docker pull` prior to use. Otherwise, it is assumed the image already + exists and can be used. This defaults to true if not set. + ## Dockerfiles This builder allows you to build Docker images _without_ Dockerfiles. If From ab5f719734c7fa4fbc12f0a1cbe68ce811662e9a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 17:27:30 -0800 Subject: [PATCH 27/29] builder/docker: show stderr if docker fails to run --- builder/docker/driver_docker.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/builder/docker/driver_docker.go b/builder/docker/driver_docker.go index 15029d202..932d46101 100644 --- a/builder/docker/driver_docker.go +++ b/builder/docker/driver_docker.go @@ -67,6 +67,11 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) { log.Println("Waiting for container to finish starting") if err := cmd.Wait(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("Docker exited with a non-zero exit status.\nStderr: %s", + stderr.String()) + } + return "", err } From 2b89da50b3e81d9bd15b098203b6c101bb60a475 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 18:44:43 -0800 Subject: [PATCH 28/29] builder/docker: UploadDir --- builder/docker/communicator.go | 78 +++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 118fc0956..741a4fccb 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -91,11 +91,87 @@ func (c *Communicator) Upload(dst string, src io.Reader) error { } func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { + // Create the temporary directory that will store the contents of "src" + // for copying into the container. + td, err := ioutil.TempDir(c.HostDir, "dirupload") + if err != nil { + return err + } + defer os.RemoveAll(td) + + walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relpath, err := filepath.Rel(src, path) + if err != nil { + return err + } + hostpath := filepath.Join(td, relpath) + + // If it is a directory, just create it + if info.IsDir() { + return os.MkdirAll(hostpath, info.Mode()) + } + + // It is a file, copy it over, including mode. + src, err := os.Open(path) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(hostpath) + if err != nil { + return err + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return err + } + + si, err := src.Stat() + if err != nil { + return err + } + + return dst.Chmod(si.Mode()) + } + + // Copy the entire directory tree to the temporary directory + if err := filepath.Walk(src, walkFn); err != nil { + return err + } + + // Determine the destination directory + containerSrc := filepath.Join(c.ContainerDir, filepath.Base(td)) + containerDst := dst + if src[len(src)-1] != '/' { + containerDst = filepath.Join(dst, filepath.Base(src)) + } + + // Make the directory, then copy into it + cmd := &packer.RemoteCmd{ + Command: fmt.Sprintf("set -e; mkdir -p %s; cp -R %s/* %s", + containerDst, containerSrc, containerDst), + } + if err := c.Start(cmd); err != nil { + return err + } + + // Wait for the copy to complete + cmd.Wait() + if cmd.ExitStatus != 0 { + return fmt.Errorf("Upload failed with non-zero exit status: %d", cmd.ExitStatus) + } + return nil } func (c *Communicator) Download(src string, dst io.Writer) error { - return nil + panic("not implemented") } // Runs the given command and blocks until completion From 7bcfd83bdd6e80251dd4ee285bda8e5c2f900e92 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 9 Nov 2013 19:06:03 -0800 Subject: [PATCH 29/29] builder/docker: stream output from commands --- builder/docker/communicator.go | 49 ++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 741a4fccb..649a135f5 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -3,6 +3,7 @@ package docker import ( "bytes" "fmt" + "github.com/ActiveState/tail" "github.com/mitchellh/packer/packer" "io" "io/ioutil" @@ -185,6 +186,19 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W defer os.Remove(outputFile.Name()) defer os.Remove(exitCodePath) + // Tail the output file and send the data to the stdout listener + tail, err := tail.TailFile(outputFile.Name(), tail.Config{ + Poll: true, + ReOpen: true, + Follow: true, + }) + if err != nil { + log.Printf("Error tailing output file: %s", err) + remote.SetExited(254) + return + } + defer tail.Stop() + // Modify the remote command so that all the output of the commands // go to a single file and so that the exit code is redirected to // a single file. This lets us determine both when the command @@ -217,7 +231,18 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W stdin_w.Write([]byte(remoteCmd + "\n")) }() - err := cmd.Wait() + // Start a goroutine to read all the lines out of the logs + go func() { + for line := range tail.Lines { + if remote.Stdout != nil { + remote.Stdout.Write([]byte(line.Text + "\n")) + } else { + log.Printf("Command stdout: %#v", line.Text) + } + } + }() + + err = cmd.Wait() if exitErr, ok := err.(*exec.ExitError); ok { exitStatus := 1 @@ -260,28 +285,6 @@ func (c *Communicator) run(cmd *exec.Cmd, remote *packer.RemoteCmd, stdin_w io.W } log.Printf("Executed command exit status: %d", exitStatus) - // Read the output - f, err := os.Open(outputFile.Name()) - if err != nil { - log.Printf("Error executing: %s", err) - remote.SetExited(254) - return - } - defer f.Close() - - if remote.Stdout != nil { - io.Copy(remote.Stdout, f) - } else { - output, err := ioutil.ReadAll(f) - if err != nil { - log.Printf("Error executing: %s", err) - remote.SetExited(254) - return - } - - log.Printf("Command output: %s", string(output)) - } - // Finally, we're done remote.SetExited(int(exitStatus)) }