Merge pull request #7444 from hashicorp/docker_windows

Docker windows
This commit is contained in:
Megan Marsh 2019-04-04 09:30:43 -07:00 committed by GitHub
commit 196028a7ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 314 additions and 45 deletions

View File

@ -50,7 +50,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
Host: commHost,
SSHConfig: b.config.Comm.SSHConfigFunc(),
CustomConnect: map[string]multistep.Step{
"docker": &StepConnectDocker{},
"docker": &StepConnectDocker{},
"dockerWindowsContainer": &StepConnectDocker{},
},
},
&common.StepProvision{},

View File

@ -25,6 +25,7 @@ type Communicator struct {
Config *Config
ContainerUser string
lock sync.Mutex
EntryPoint []string
}
func (c *Communicator) Start(remote *packer.RemoteCmd) error {
@ -32,10 +33,9 @@ func (c *Communicator) Start(remote *packer.RemoteCmd) error {
"exec",
"-i",
c.ContainerID,
"/bin/sh",
"-c",
fmt.Sprintf("(%s)", remote.Command),
}
dockerArgs = append(dockerArgs, c.EntryPoint...)
dockerArgs = append(dockerArgs, fmt.Sprintf("(%s)", remote.Command))
if c.Config.Pty {
dockerArgs = append(dockerArgs[:2], append([]string{"-t"}, dockerArgs[2:]...)...)

View File

@ -23,21 +23,22 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Author string
Changes []string
Commit bool
ContainerDir string `mapstructure:"container_dir"`
Discard bool
ExecUser string `mapstructure:"exec_user"`
ExportPath string `mapstructure:"export_path"`
Image string
Message string
Privileged bool `mapstructure:"privileged"`
Pty bool
Pull bool
RunCommand []string `mapstructure:"run_command"`
Volumes map[string]string
FixUploadOwner bool `mapstructure:"fix_upload_owner"`
Author string
Changes []string
Commit bool
ContainerDir string `mapstructure:"container_dir"`
Discard bool
ExecUser string `mapstructure:"exec_user"`
ExportPath string `mapstructure:"export_path"`
Image string
Message string
Privileged bool `mapstructure:"privileged"`
Pty bool
Pull bool
RunCommand []string `mapstructure:"run_command"`
Volumes map[string]string
FixUploadOwner bool `mapstructure:"fix_upload_owner"`
WindowsContainer bool `mapstructure:"windows_container"`
// This is used to login to dockerhub to pull a private base container. For
// pushing to dockerhub, see the docker post-processors
@ -74,6 +75,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Defaults
if len(c.RunCommand) == 0 {
c.RunCommand = []string{"-d", "-i", "-t", "--entrypoint=/bin/sh", "--", "{{.Image}}"}
if c.WindowsContainer {
c.RunCommand = []string{"-d", "-i", "-t", "--entrypoint=powershell", "--", "{{.Image}}"}
}
}
// Default Pull if it wasn't set
@ -92,6 +96,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
// Default to the normal Docker type
if c.Comm.Type == "" {
c.Comm.Type = "docker"
if c.WindowsContainer {
c.Comm.Type = "dockerWindowsContainer"
}
}
var errs *packer.MultiError

View File

@ -46,7 +46,10 @@ type Driver interface {
// along with a potential error.
StartContainer(*ContainerConfig) (string, error)
// StopContainer forcibly stops a container.
// KillContainer forcibly stops a container.
KillContainer(id string) error
// StopContainer gently stops a container.
StopContainer(id string) error
// TagImage tags the image with the given ID

View File

@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"regexp"
"runtime"
"strings"
"sync"
@ -270,11 +269,6 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) {
args = append(args, "--privileged")
}
for host, guest := range config.Volumes {
if runtime.GOOS == "windows" {
// docker-toolbox can't handle the normal C:\filepath format in CLI
host = strings.Replace(host, "\\", "/", -1)
host = strings.Replace(host, "C:/", "/c/", 1)
}
args = append(args, "-v", fmt.Sprintf("%s:%s", host, guest))
}
for _, v := range config.RunCommand {
@ -314,6 +308,13 @@ func (d *DockerDriver) StartContainer(config *ContainerConfig) (string, error) {
}
func (d *DockerDriver) StopContainer(id string) error {
if err := exec.Command("docker", "stop", id).Run(); err != nil {
return err
}
return nil
}
func (d *DockerDriver) KillContainer(id string) error {
if err := exec.Command("docker", "kill", id).Run(); err != nil {
return err
}

View File

@ -28,6 +28,10 @@ type MockDriver struct {
IPAddressResult string
IPAddressErr error
KillCalled bool
KillID string
KillError error
LoginCalled bool
LoginUsername string
LoginPassword string
@ -160,6 +164,12 @@ func (d *MockDriver) StartContainer(config *ContainerConfig) (string, error) {
return d.StartID, d.StartError
}
func (d *MockDriver) KillContainer(id string) error {
d.KillCalled = true
d.KillID = id
return d.KillError
}
func (d *MockDriver) StopContainer(id string) error {
d.StopCalled = true
d.StopID = id

View File

@ -19,6 +19,16 @@ func (s *StepCommit) Run(_ context.Context, state multistep.StateBag) multistep.
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
if config.WindowsContainer {
// docker can't commit a running Windows container
err := driver.StopContainer(containerId)
if err != nil {
state.Put("error", err)
ui.Error(fmt.Sprintf("Error halting windows container for commit: %s",
err.Error()))
return multistep.ActionHalt
}
}
ui.Say("Committing the container")
imageId, err := driver.Commit(containerId, config.Author, config.Changes, config.Message)
if err != nil {

View File

@ -32,16 +32,31 @@ func (s *StepConnectDocker) Run(_ context.Context, state multistep.StateBag) mul
// Create the communicator that talks to Docker via various
// os/exec tricks.
comm := &Communicator{
ContainerID: containerId,
HostDir: tempDir,
ContainerDir: config.ContainerDir,
Version: version,
Config: config,
ContainerUser: containerUser,
}
if config.WindowsContainer {
comm := &WindowsContainerCommunicator{Communicator{
ContainerID: containerId,
HostDir: tempDir,
ContainerDir: config.ContainerDir,
Version: version,
Config: config,
ContainerUser: containerUser,
EntryPoint: []string{"powershell"},
},
}
state.Put("communicator", comm)
state.Put("communicator", comm)
} else {
comm := &Communicator{
ContainerID: containerId,
HostDir: tempDir,
ContainerDir: config.ContainerDir,
Version: version,
Config: config,
ContainerUser: containerUser,
EntryPoint: []string{"/bin/sh", "-c"},
}
state.Put("communicator", comm)
}
return multistep.ActionContinue
}

View File

@ -58,7 +58,7 @@ func (s *StepRun) Cleanup(state multistep.StateBag) {
// just mean that the container doesn't exist anymore, which isn't a
// big deal.
ui.Say(fmt.Sprintf("Killing the container: %s", s.containerId))
driver.StopContainer(s.containerId)
driver.KillContainer(s.containerId)
// Reset the container ID so that we're idempotent
s.containerId = ""

View File

@ -52,16 +52,16 @@ func TestStepRun(t *testing.T) {
}
// Verify we haven't called stop yet
if driver.StopCalled {
if driver.KillCalled {
t.Fatal("should not have stopped")
}
// Cleanup
step.Cleanup(state)
if !driver.StopCalled {
if !driver.KillCalled {
t.Fatal("should've stopped")
}
if driver.StopID != id {
if driver.KillID != id {
t.Fatalf("bad: %#v", driver.StopID)
}
}
@ -85,13 +85,13 @@ func TestStepRun_error(t *testing.T) {
}
// Verify we haven't called stop yet
if driver.StopCalled {
if driver.KillCalled {
t.Fatal("should not have stopped")
}
// Cleanup
step.Cleanup(state)
if driver.StopCalled {
if driver.KillCalled {
t.Fatal("should not have stopped")
}
}

View File

@ -0,0 +1,188 @@
package docker
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"github.com/hashicorp/packer/packer"
)
// Windows containers are a special beast in Docker; you can't use docker cp
// to move files between the container and host.
// This communicator works around that limitation by reusing all possible
// methods and fields of the normal Docker Communicator, but we overwrite the
// Upload, Download, and UploadDir methods to utilize a mounted directory and
// native powershell commands rather than relying on docker cp.
type WindowsContainerCommunicator struct {
Communicator
}
// Upload uses docker exec to copy the file from the host to the container
func (c *WindowsContainerCommunicator) Upload(dst string, src io.Reader, fi *os.FileInfo) 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)
if err != nil {
return err
}
if fi != nil {
tempfile.Chmod((*fi).Mode())
}
tempfile.Close()
// 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("Copy-Item -Path %s/%s -Destination %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 *WindowsContainerCommunicator) 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())
}
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
dest, err := os.Readlink(path)
if err != nil {
return err
}
return os.Symlink(dest, hostpath)
}
// 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
}
return nil
}
// 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("Copy-Item %s -Destination %s -Recurse",
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
}
// Download pulls a file out of a container using `docker cp`. We have a source
// path and want to write to an io.Writer
func (c *WindowsContainerCommunicator) Download(src string, dst io.Writer) error {
log.Printf("Downloading file from container: %s:%s", c.ContainerID, src)
// Copy file onto temp file on mounted volume inside container
var stdout, stderr bytes.Buffer
cmd := &packer.RemoteCmd{
Command: fmt.Sprintf("Copy-Item -Path %s -Destination %s/%s", src, c.ContainerDir,
filepath.Base(src)),
Stdout: &stdout,
Stderr: &stderr,
}
if err := c.Start(cmd); err != nil {
return err
}
// Wait for the copy to complete
cmd.Wait()
if cmd.ExitStatus != 0 {
return fmt.Errorf("Failed to copy file to shared drive: %s, %s, %d", stderr.String(), stdout.String(), cmd.ExitStatus)
}
// Read that copied file into a new file opened on host machine
fsrc, err := os.Open(filepath.Join(c.HostDir, filepath.Base(src)))
if err != nil {
return err
}
defer fsrc.Close()
defer os.Remove(fsrc.Name())
_, err = io.Copy(dst, fsrc)
if err != nil {
return err
}
return nil
}

View File

@ -212,7 +212,7 @@ func (c *Config) Prepare(ctx *interpolate.Context) []error {
if es := c.prepareWinRM(ctx); len(es) > 0 {
errs = append(errs, es...)
}
case "docker", "none":
case "docker", "dockerWindowsContainer", "none":
break
default:
return []error{fmt.Errorf("Communicator type %s is invalid", c.Type)}

View File

@ -40,7 +40,7 @@ type Config struct {
// This is used in the template generation to format environment variables
// inside the `ExecuteCommand` template.
EnvVarFormat string
EnvVarFormat string `mapstructure:"env_var_format"`
ctx interpolate.Context
}

View File

@ -210,13 +210,20 @@ You must specify (only) one of `commit`, `discard`, or `export_path`.
- `run_command` (array of strings) - An array of arguments to pass to
`docker run` in order to run the container. By default this is set to
`["-d", "-i", "-t", "{{.Image}}", "/bin/sh"]`. As you can see, you have a
couple template variables to customize, as well.
`["-d", "-i", "-t", "--entrypoint=/bin/sh", "--", "{{.Image}}"]` if you are
using a linux container, and
`["-d", "-i", "-t", "--entrypoint=powershell", "--", "{{.Image}}"]` if you
are running a windows container. {{.Image}} is a template variable that
corresponds to the `image` template option.
- `volumes` (map of strings to strings) - A mapping of additional volumes to
mount into this container. The key of the object is the host path, the
value is the container path.
- `windows_container` (bool) - If "true", tells Packer that you are building a
Windows container running on a windows host. This is necessary for building
Windows containers, because our normal docker bindings do not work for them.
- `container_dir` (string) - The directory inside container to mount temp
directory from host server for work [file
provisioner](/docs/provisioners/file.html). By default this is set to
@ -334,6 +341,33 @@ nearly-identical sequence definitions, as demonstrated by the example below:
<span id="amazon-ec2-container-registry"></span>
## Docker For Windows
You should be able to run docker builds against both linux and Windows
containers. Windows containers use a different communicator than linux
containers, because Windows containers cannot use `docker cp`.
If you are building a Windows container, you must set the template option
`"windows_container": true`. Please note that docker cannot export Windows
containers, so you must either commit or discard them.
The following is a fully functional template for building a Windows
container.
``` json
{
"builders": [
{
"type": "docker",
"image": "microsoft/windowsservercore:1709",
"container_dir": "c:/app",
"windows_container": true,
"commit": true
}
]
}
```
## Amazon EC2 Container Registry
Packer can tag and push images for use in [Amazon EC2 Container