commit
196028a7ed
|
@ -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{},
|
||||
|
|
|
@ -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:]...)...)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue