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 new file mode 100644 index 000000000..824f1697f --- /dev/null +++ b/builder/docker/builder.go @@ -0,0 +1,74 @@ +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) { + c, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs + } + b.config = c + + return warnings, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + steps := []multistep.Step{ + &StepTempDir{}, + &StepPull{}, + &StepRun{}, + &StepProvision{}, + &StepExport{}, + } + + // 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) + + // Setup the driver that will talk to Docker + state.Put("driver", &DockerDriver{ + Ui: ui, + }) + + // 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) + } + + // No errors, must've worked + artifact := &ExportArtifact{path: b.config.ExportPath} + return artifact, 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/communicator.go b/builder/docker/communicator.go new file mode 100644 index 000000000..649a135f5 --- /dev/null +++ b/builder/docker/communicator.go @@ -0,0 +1,290 @@ +package docker + +import ( + "bytes" + "fmt" + "github.com/ActiveState/tail" + "github.com/mitchellh/packer/packer" + "io" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "sync" + "syscall" + "time" +) + +type Communicator struct { + ContainerId string + HostDir string + ContainerDir string + + lock sync.Mutex +} + +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() + + // This file will store the exit code of the command once it is complete. + exitCodePath := outputFile.Name() + "-exit" + + 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 + } + + // Run the actual command in a goroutine so that Start doesn't block + go c.run(cmd, remote, stdin_w, outputFile, exitCodePath) + + 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 + } + + // 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), + } + + 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 { + // 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 { + panic("not implemented") +} + +// 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() + + // Clean up after ourselves by removing our temporary files + 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 + // 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")) + }() + + // 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 + + // 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) + + // Finally, we're done + remote.SetExited(int(exitStatus)) +} 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/config.go b/builder/docker/config.go new file mode 100644 index 000000000..864ad1fee --- /dev/null +++ b/builder/docker/config.go @@ -0,0 +1,75 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + ExportPath string `mapstructure:"export_path"` + Image string + Pull bool + + tpl *packer.ConfigTemplate +} + +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 + } + + // 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{ + "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 = packer.MultiErrorAppend( + errs, fmt.Errorf("Error processing %s: %s", n, err)) + } + } + + if c.ExportPath == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("export_path must be specified")) + } + + if c.Image == "" { + errs = packer.MultiErrorAppend(errs, + fmt.Errorf("image must be specified")) + } + + 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 new file mode 100644 index 000000000..3b279ab22 --- /dev/null +++ b/builder/docker/config_test.go @@ -0,0 +1,90 @@ +package docker + +import ( + "testing" +) + +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 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) + testConfigErr(t, warns, errs) + + // Good export path + raw["export_path"] = "good" + _, warns, errs = NewConfig(raw) + testConfigOk(t, warns, errs) +} + +func TestConfigPrepare_image(t *testing.T) { + raw := testConfig() + + // No image + delete(raw, "image") + _, warns, errs := NewConfig(raw) + testConfigErr(t, warns, errs) + + // Good image + raw["image"] = "path" + _, warns, errs = NewConfig(raw) + 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") + } + + // 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/driver.go b/builder/docker/driver.go new file mode 100644 index 000000000..04f4e3fd5 --- /dev/null +++ b/builder/docker/driver.go @@ -0,0 +1,29 @@ +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 + + // 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 new file mode 100644 index 000000000..932d46101 --- /dev/null +++ b/builder/docker/driver_docker.go @@ -0,0 +1,84 @@ +package docker + +import ( + "bytes" + "fmt" + "github.com/mitchellh/packer/packer" + "io" + "log" + "os/exec" + "strings" +) + +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) +} + +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 { + if _, ok := err.(*exec.ExitError); ok { + err = fmt.Errorf("Docker exited with a non-zero exit status.\nStderr: %s", + stderr.String()) + } + + 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 new file mode 100644 index 000000000..9cf2fb31b --- /dev/null +++ b/builder/docker/driver_mock.go @@ -0,0 +1,56 @@ +package docker + +import ( + "io" +) + +// MockDriver is a driver implementation that can be used for tests. +type MockDriver struct { + ExportReader io.Reader + ExportError error + PullError error + StartID string + StartError error + StopError error + + 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 { + d.PullCalled = true + 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/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/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) + } + } +} 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 new file mode 100644 index 000000000..69d6f483c --- /dev/null +++ b/builder/docker/step_export.go @@ -0,0 +1,42 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "os" +) + +// 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) + driver := state.Get("driver").(Driver) + containerId := state.Get("container_id").(string) + ui := state.Get("ui").(packer.Ui) + + // 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 + } + + ui.Say("Exporting the container") + if err := driver.Export(containerId, f); err != nil { + f.Close() + os.Remove(f.Name()) + + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + f.Close() + return multistep.ActionContinue +} + +func (s *StepExport) Cleanup(state multistep.StateBag) {} 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") + } +} 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_pull.go b/builder/docker/step_pull.go new file mode 100644 index 000000000..6571a6563 --- /dev/null +++ b/builder/docker/step_pull.go @@ -0,0 +1,34 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +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) + + 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) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepPull) Cleanup(state multistep.StateBag) { +} diff --git a/builder/docker/step_pull_test.go b/builder/docker/step_pull_test.go new file mode 100644 index 000000000..5c66425bb --- /dev/null +++ b/builder/docker/step_pull_test.go @@ -0,0 +1,73 @@ +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") + } +} + +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") + } +} diff --git a/builder/docker/step_run.go b/builder/docker/step_run.go new file mode 100644 index 000000000..27dfeb8d5 --- /dev/null +++ b/builder/docker/step_run.go @@ -0,0 +1,55 @@ +package docker + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type StepRun struct { + containerId string +} + +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) + + runConfig := ContainerConfig{ + Image: config.Image, + Volumes: map[string]string{ + tempDir: "/packer-files", + }, + } + + 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 + } + + // 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 +} + +func (s *StepRun) Cleanup(state multistep.StateBag) { + if s.containerId == "" { + return + } + + // 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. + 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") + } +} 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) + } +} 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..0bc190830 --- /dev/null +++ b/builder/docker/step_test.go @@ -0,0 +1,20 @@ +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("driver", &MockDriver{}) + state.Put("hook", &packer.MockHook{}) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + return state +} 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", 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 diff --git a/website/source/docs/builders/docker.html.markdown b/website/source/docs/builders/docker.html.markdown new file mode 100644 index 000000000..c3f9f8018 --- /dev/null +++ b/website/source/docs/builders/docker.html.markdown @@ -0,0 +1,57 @@ +--- +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 + +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. + +* `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. + +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 +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. 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