diff --git a/builder/docker/communicator.go b/builder/docker/communicator.go index 4fcd9b658..fb88a4491 100644 --- a/builder/docker/communicator.go +++ b/builder/docker/communicator.go @@ -1,6 +1,7 @@ package docker import ( + "archive/tar" "bytes" "fmt" "io" @@ -194,8 +195,42 @@ func (c *Communicator) UploadDir(dst string, src string, exclude []string) error return nil } +// Download pulls a file out of a container using `docker cp`. We have a source +// path and want to write to an io.Writer, not a file. We use - to make docker +// cp to write to stdout, and then copy the stream to our destination io.Writer. func (c *Communicator) Download(src string, dst io.Writer) error { - panic("not implemented") + log.Printf("Downloading file from container: %s:%s", c.ContainerId, src) + localCmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", c.ContainerId, src), "-") + + pipe, err := localCmd.StdoutPipe() + if err != nil { + return fmt.Errorf("Failed to open pipe: %s", err) + } + + if err = localCmd.Start(); err != nil { + return fmt.Errorf("Failed to start download: %s", err) + } + + // When you use - to send docker cp to stdout it is streamed as a tar; this + // enables it to work with directories. We don't actually support + // directories in Download() but we still need to handle the tar format. + archive := tar.NewReader(pipe) + _, err = archive.Next() + if err != nil { + return fmt.Errorf("Failed to read header from tar stream: %s", err) + } + + numBytes, err := io.Copy(dst, archive) + if err != nil { + return fmt.Errorf("Failed to pipe download: %s", err) + } + log.Printf("Copied %d bytes for %s", numBytes, src) + + if err = localCmd.Wait(); err != nil { + return fmt.Errorf("Failed to download '%s' from container: %s", src, err) + } + + return nil } // canExec tells us whether `docker exec` is supported diff --git a/builder/docker/communicator_test.go b/builder/docker/communicator_test.go index f75a89d96..db0bfcfe8 100644 --- a/builder/docker/communicator_test.go +++ b/builder/docker/communicator_test.go @@ -1,10 +1,129 @@ package docker import ( - "github.com/mitchellh/packer/packer" + "crypto/sha256" + "io/ioutil" + "os" + "os/exec" + "runtime" + "strings" "testing" + + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/provisioner/file" + "github.com/mitchellh/packer/template" ) func TestCommunicator_impl(t *testing.T) { var _ packer.Communicator = new(Communicator) } + +func TestUploadDownload(t *testing.T) { + ui := packer.TestUi(t) + cache := &packer.FileCache{CacheDir: os.TempDir()} + + tpl, err := template.Parse(strings.NewReader(dockerBuilderConfig)) + if err != nil { + t.Fatalf("Unable to parse config: %s", err) + } + + // Make sure we only run this on linux hosts + if os.Getenv("PACKER_ACC") == "" { + t.Skip("This test is only run with PACKER_ACC=1") + } + if runtime.GOOS != "linux" { + t.Skip("This test is only supported on linux") + } + cmd := exec.Command("docker", "-v") + cmd.Run() + if !cmd.ProcessState.Success() { + t.Error("docker command not found; please make sure docker is installed") + } + + // Setup the builder + builder := &Builder{} + warnings, err := builder.Prepare(tpl.Builders["docker"].Config) + if err != nil { + t.Fatalf("Error preparing configuration %s", err) + } + if len(warnings) > 0 { + t.Fatal("Encountered configuration warnings; aborting") + } + + // Setup the provisioners + upload := &file.Provisioner{} + err = upload.Prepare(tpl.Provisioners[0].Config) + if err != nil { + t.Fatalf("Error preparing upload: %s", err) + } + download := &file.Provisioner{} + err = download.Prepare(tpl.Provisioners[1].Config) + if err != nil { + t.Fatalf("Error preparing download: %s", err) + } + // Preemptive cleanup. Honestly I don't know why you would want to get rid + // of my strawberry cake. It's so tasty! Do you not like cake? Are you a + // cake-hater? Or are you keeping all the cake all for yourself? So selfish! + defer os.Remove("my-strawberry-cake") + + // Add hooks so the provisioners run during the build + hooks := map[string][]packer.Hook{} + hooks[packer.HookProvision] = []packer.Hook{ + &packer.ProvisionHook{ + Provisioners: []packer.Provisioner{ + upload, + download, + }, + }, + } + hook := &packer.DispatchHook{Mapping: hooks} + + // Run things + artifact, err := builder.Run(ui, hook, cache) + if err != nil { + t.Fatalf("Error running build %s", err) + } + // Preemptive cleanup + defer artifact.Destroy() + + // Verify that the thing we downloaded is the same thing we sent up. + // Complain loudly if it isn't. + inputFile, err := ioutil.ReadFile("test-fixtures/onecakes/strawberry") + if err != nil { + t.Fatalf("Unable to read input file: %s", err) + } + outputFile, err := ioutil.ReadFile("my-strawberry-cake") + if err != nil { + t.Fatalf("Unable to read output file: %s", err) + } + if sha256.Sum256(inputFile) != sha256.Sum256(outputFile) { + t.Fatalf("Input and output files do not match\n"+ + "Input:\n%s\nOutput:\n%s\n", inputFile, outputFile) + } +} + +const dockerBuilderConfig = ` +{ + "builders": [ + { + "type": "docker", + "image": "alpine", + "export_path": "alpine.tar", + "run_command": ["-d", "-i", "-t", "{{.Image}}", "/bin/sh"] + } + ], + "provisioners": [ + { + "type": "file", + "source": "test-fixtures/onecakes/strawberry", + "destination": "/strawberry-cake" + }, + { + "type": "file", + "source": "/strawberry-cake", + "destination": "my-strawberry-cake", + "direction": "download" + } + ] +} +` diff --git a/builder/docker/test-fixtures/manycakes/chocolate b/builder/docker/test-fixtures/manycakes/chocolate new file mode 100644 index 000000000..a2286c928 --- /dev/null +++ b/builder/docker/test-fixtures/manycakes/chocolate @@ -0,0 +1 @@ +chocolate! diff --git a/builder/docker/test-fixtures/manycakes/vanilla b/builder/docker/test-fixtures/manycakes/vanilla new file mode 100644 index 000000000..000a45578 --- /dev/null +++ b/builder/docker/test-fixtures/manycakes/vanilla @@ -0,0 +1 @@ +vanilla! diff --git a/builder/docker/test-fixtures/onecakes/strawberry b/builder/docker/test-fixtures/onecakes/strawberry new file mode 100644 index 000000000..b663de3a9 --- /dev/null +++ b/builder/docker/test-fixtures/onecakes/strawberry @@ -0,0 +1 @@ +strawberry!