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) + } +}