package run import ( "bytes" "fmt" docker "github.com/fsouza/go-dockerclient" "github.com/golang/glog" "github.com/openshift/origin/pkg/bootstrap/docker/errors" ) type RunHelper struct { client *docker.Client } func NewRunHelper(client *docker.Client) *RunHelper { return &RunHelper{ client: client, } } func (h *RunHelper) New() *Runner { return &Runner{ client: h.client, config: &docker.Config{}, hostConfig: &docker.HostConfig{}, } } // Runner is a helper to run new containers on Docker type Runner struct { name string client *docker.Client config *docker.Config hostConfig *docker.HostConfig removeContainer bool } // Name sets the name of the container to create func (h *Runner) Name(name string) *Runner { h.name = name return h } // Image sets the image to run func (h *Runner) Image(image string) *Runner { h.config.Image = image return h } func (h *Runner) PortForward(local, remote int) *Runner { if h.hostConfig.PortBindings == nil { h.hostConfig.PortBindings = map[docker.Port][]docker.PortBinding{} } containerPort := docker.Port(fmt.Sprintf("%d/tcp", remote)) binding := docker.PortBinding{ HostPort: fmt.Sprintf("%d", local), } h.hostConfig.PortBindings[containerPort] = []docker.PortBinding{binding} if h.config.ExposedPorts == nil { h.config.ExposedPorts = map[docker.Port]struct{}{} } h.config.ExposedPorts[containerPort] = struct{}{} return h } // Entrypoint sets the entrypoint to use when running func (h *Runner) Entrypoint(cmd ...string) *Runner { h.config.Entrypoint = cmd return h } // Command sets the command to run func (h *Runner) Command(cmd ...string) *Runner { h.config.Cmd = cmd return h } // HostPid tells Docker to run using the host's pid namespace func (h *Runner) HostPid() *Runner { h.hostConfig.PidMode = "host" return h } // HostNetwork tells Docker to run using the host's Network namespace func (h *Runner) HostNetwork() *Runner { h.hostConfig.NetworkMode = "host" return h } // Bind tells Docker to bind host dirs to container dirs func (h *Runner) Bind(binds ...string) *Runner { h.hostConfig.Binds = append(h.hostConfig.Binds, binds...) return h } // Env tells Docker to add environment variables to the container getting started func (h *Runner) Env(env ...string) *Runner { h.config.Env = append(h.config.Env, env...) return h } // Privileged tells Docker to run the container as privileged func (h *Runner) Privileged() *Runner { h.hostConfig.Privileged = true return h } // DiscardContainer if true will cause the container to be removed when done executing. // Will be ignored in the case of Start func (h *Runner) DiscardContainer() *Runner { h.removeContainer = true return h } // Start starts the container as a daemon and returns func (h *Runner) Start() (string, error) { id, err := h.Create() if err != nil { return "", err } return id, h.startContainer(id) } // Output starts the container, waits for it to finish and returns its output func (h *Runner) Output() (string, string, int, error) { return h.runWithOutput() } // Run executes the container and waits until it completes func (h *Runner) Run() (int, error) { _, _, rc, err := h.runWithOutput() return rc, err } func (h *Runner) Create() (string, error) { glog.V(4).Infof("Creating container named %q\nconfig:\n%s\nhost config:\n%s\n", h.name, printConfig(h.config), printHostConfig(h.hostConfig)) container, err := h.client.CreateContainer(docker.CreateContainerOptions{ Name: h.name, Config: h.config, HostConfig: h.hostConfig, }) if err != nil { return "", errors.NewError("cannot create container using image %s", h.config.Image).WithCause(err) } glog.V(5).Infof("Container created with id %q", container.ID) glog.V(5).Infof("Container: %#v", container) return container.ID, nil } func (h *Runner) startContainer(id string) error { err := h.client.StartContainer(id, nil) if err != nil { return errors.NewError("cannot start container %s", id).WithCause(err) } return nil } func (h *Runner) runWithOutput() (string, string, int, error) { id, err := h.Create() if err != nil { return "", "", 0, err } if h.removeContainer { defer func() { glog.V(5).Infof("Deleting container %q", id) if err = h.client.RemoveContainer(docker.RemoveContainerOptions{ID: id}); err != nil { glog.V(1).Infof("Error deleting container %q: %v", id, err) } }() } glog.V(5).Infof("Starting container %q", id) err = h.startContainer(id) if err != nil { glog.V(2).Infof("Error occurred starting container %q: %v", id, err) return "", "", 0, err } glog.V(5).Infof("Waiting for container %q", id) rc, err := h.client.WaitContainer(id) if err != nil { glog.V(2).Infof("Error occurred waiting for container %q: %v", id, err) return "", "", 0, err } glog.V(5).Infof("Done waiting for container %q, rc=%d", id, rc) stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{} // changed to only reading logs after execution instead of streaming // stdout/stderr to avoid race condition in (at least) docker 1.10-1.14-dev: // https://github.com/docker/docker/issues/29285 glog.V(5).Infof("Reading logs from container %q", id) err = h.client.Logs(docker.LogsOptions{ Container: id, Stdout: true, Stderr: true, OutputStream: stdOut, ErrorStream: stdErr, }) if err != nil { glog.V(2).Infof("Error occurred while reading logs: %v", err) return "", "", 0, err } glog.V(5).Infof("Done reading logs from container %q", id) glog.V(5).Infof("Stdout:\n%s", stdOut.String()) glog.V(5).Infof("Stderr:\n%s", stdErr.String()) if rc != 0 || err != nil { return stdOut.String(), stdErr.String(), rc, newRunError(rc, err, stdOut.Bytes(), stdErr.Bytes(), h.config) } glog.V(4).Infof("Container run successful\n") return stdOut.String(), stdErr.String(), rc, nil } // printConfig prints out the relevant parts of a container's Docker config func printConfig(c *docker.Config) string { out := &bytes.Buffer{} fmt.Fprintf(out, " image: %s\n", c.Image) if len(c.Entrypoint) > 0 { fmt.Fprintf(out, " entry point:\n") for _, e := range c.Entrypoint { fmt.Fprintf(out, " %s\n", e) } } if len(c.Cmd) > 0 { fmt.Fprintf(out, " command:\n") for _, c := range c.Cmd { fmt.Fprintf(out, " %s\n", c) } } if len(c.Env) > 0 { fmt.Fprintf(out, " environment:\n") for _, e := range c.Env { fmt.Fprintf(out, " %s\n", e) } } return out.String() } func printHostConfig(c *docker.HostConfig) string { out := &bytes.Buffer{} fmt.Fprintf(out, " pid mode: %s\n", c.PidMode) fmt.Fprintf(out, " network mode: %s\n", c.NetworkMode) if len(c.Binds) > 0 { fmt.Fprintf(out, " volume binds:\n") for _, b := range c.Binds { fmt.Fprintf(out, " %s\n", b) } } return out.String() }