Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions cli/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,27 @@ func ProjectPull(p *project.Project, c *cli.Context) {
}
}

// ProjectRun runs a given command within a service's container.
func ProjectRun(p *project.Project, c *cli.Context) {
if len(c.Args()) == 1 {
logrus.Fatal("No service specified")
}

serviceName := c.Args()[0]
commandParts := c.Args()[1:]

if _, ok := p.Configs[serviceName]; !ok {
logrus.Fatalf("%s is not defined in the template", serviceName)
}

exitCode, err := p.Run(serviceName, commandParts)
if err != nil {
logrus.Fatal(err)
}

os.Exit(exitCode)
}

// ProjectDelete delete services.
func ProjectDelete(p *project.Project, c *cli.Context) {
if !c.Bool("force") && len(c.Args()) == 0 {
Expand Down
10 changes: 10 additions & 0 deletions cli/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ func ScaleCommand(factory app.ProjectFactory) cli.Command {
}
}

// RunCommand defines the libcompose run subcommand.
func RunCommand(factory app.ProjectFactory) cli.Command {
return cli.Command{
Name: "run",
Usage: "Run a one-off command",
Action: app.WithProject(factory, app.ProjectRun),
Flags: []cli.Flag{},
}
}

// RmCommand defines the libcompose rm subcommand.
func RmCommand(factory app.ProjectFactory) cli.Command {
return cli.Command{
Expand Down
1 change: 1 addition & 0 deletions cli/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func main() {
command.RestartCommand(factory),
command.StopCommand(factory),
command.ScaleCommand(factory),
command.RunCommand(factory),
command.RmCommand(factory),
command.PullCommand(factory),
command.KillCommand(factory),
Expand Down
78 changes: 67 additions & 11 deletions docker/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package docker

import (
"fmt"
"io"
"math"
"os"
"strings"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/docker/docker/pkg/parsers"
"github.com/docker/docker/registry"
"github.com/docker/docker/utils"
"github.com/docker/libcompose/docker/pty"
"github.com/docker/libcompose/logger"
"github.com/docker/libcompose/project"
util "github.com/docker/libcompose/utils"
Expand Down Expand Up @@ -124,7 +126,7 @@ func (c *Container) Recreate(imageName string) (*dockerclient.APIContainers, err
return nil, err
}

newContainer, err := c.createContainer(imageName, info.ID)
newContainer, err := c.createContainer(imageName, info.ID, nil)
if err != nil {
return nil, err
}
Expand All @@ -145,13 +147,20 @@ func (c *Container) Recreate(imageName string) (*dockerclient.APIContainers, err
// to notify the container has been created. If the container already exists, does
// nothing.
func (c *Container) Create(imageName string) (*dockerclient.APIContainers, error) {
return c.CreateWithOverride(imageName, nil)
}

// CreateWithOverride create container and override parts of the config to
// allow special situations to override the config generated from the compose
// file
func (c *Container) CreateWithOverride(imageName string, configOverride *project.ServiceConfig) (*dockerclient.APIContainers, error) {
container, err := c.findExisting()
if err != nil {
return nil, err
}

if container == nil {
container, err = c.createContainer(imageName, "")
container, err = c.createContainer(imageName, "", configOverride)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -220,6 +229,35 @@ func (c *Container) Delete() error {
return c.client.RemoveContainer(dockerclient.RemoveContainerOptions{ID: container.ID, Force: true, RemoveVolumes: c.service.context.Volume})
}

// Run creates, start and attach to the container based on the image name,
// the specified configuration.
// It will always create a new container.
func (c *Container) Run(imageName string, configOverride *project.ServiceConfig) (int, error) {
var err error

container, err := c.createContainer(imageName, "", configOverride)
if err != nil {
return -1, err
}

info, err := c.client.InspectContainer(container.ID)
if err != nil {
return -1, err
}

// Fire up the console
if err = pty.Start(c.client, info, info.HostConfig); err != nil && err != io.ErrClosedPipe {
return -1, err
}

info, err = c.client.InspectContainer(container.ID)
if err != nil {
return -1, err
}

return info.State.ExitCode, nil
}

// Up creates and start the container based on the image name and send an event
// to notify the container has been created. If the container exists but is stopped
// it tries to start it.
Expand All @@ -243,17 +281,28 @@ func (c *Container) Up(imageName string) error {
}

if !info.State.Running {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
if err = c.client.StartContainer(container.ID, nil); err != nil {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
return err
}
c.Start(container, info.HostConfig)
}

c.service.context.Project.Notify(project.EventContainerStarted, c.service.Name(), map[string]string{
"name": c.Name(),
})
return nil
}

// Start the specified container with the specified host config
func (c *Container) Start(container *dockerclient.APIContainers, hostConfig *dockerclient.HostConfig) error {
logrus.Debugf("Starting container: %s: %#v", container.ID, hostConfig)
err := c.populateAdditionalHostConfig(hostConfig)
if err != nil {
return err
}

if err := c.client.StartContainer(container.ID, hostConfig); err != nil {
return err
}

c.service.context.Project.Notify(project.EventContainerStarted, c.service.Name(), map[string]string{
"name": c.Name(),
})

return nil
}

Expand Down Expand Up @@ -301,7 +350,14 @@ func volumeBinds(volumes map[string]struct{}, container *dockerclient.Container)
return result
}

func (c *Container) createContainer(imageName, oldContainer string) (*dockerclient.APIContainers, error) {
func (c *Container) createContainer(imageName, oldContainer string, configOverride *project.ServiceConfig) (*dockerclient.APIContainers, error) {
serviceConfig := c.service.serviceConfig
if configOverride != nil {
serviceConfig.Command = configOverride.Command
serviceConfig.Tty = configOverride.Tty
serviceConfig.StdinOpen = configOverride.StdinOpen
}

createOpts, err := ConvertToAPI(c.service, c.name)
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions docker/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ func Convert(c *project.ServiceConfig, ctx *Context) (*dockerclient.Config, *doc
WorkingDir: c.WorkingDir,
VolumeDriver: c.VolumeDriver,
Volumes: volumes(c, ctx),
AttachStdin: c.StdinOpen,
AttachStdout: c.Tty,
AttachStderr: c.Tty,
StdinOnce: c.StdinOpen,
}
hostConfig := &dockerclient.HostConfig{
VolumesFrom: utils.CopySlice(c.VolumesFrom),
Expand Down
128 changes: 128 additions & 0 deletions docker/pty/dockerpty.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package pty

import (
"errors"
"io"
"os"
gosignal "os/signal"
"time"

"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/fsouza/go-dockerclient"
)

// Start starts and attach a pty (Pseudo-TTY) to the specified container.
// It will take over the current process' TTY until the container's PTY is closed.
func Start(client *docker.Client, container *docker.Container, hostConfig *docker.HostConfig) (err error) {
var (
terminalFd uintptr
oldState *term.State
out io.Writer = os.Stdout
)

if file, ok := out.(*os.File); ok {
terminalFd = file.Fd()
} else {
return errors.New("Not a terminal!")
}

// Set up the pseudo terminal
oldState, err = term.SetRawTerminal(terminalFd)
if err != nil {
return
}

// Clean up after the container has exited
defer term.RestoreTerminal(terminalFd, oldState)

// Attach to the container on a separate thread
attachChan := make(chan error)
go attachToContainer(client, container.ID, attachChan)

// Start it
err = client.StartContainer(container.ID, hostConfig)
if err != nil {
return
}

// Make sure terminal resizes are passed on to the container
monitorTty(client, container.ID, terminalFd)

return <-attachChan
}

func attachToContainer(client *docker.Client, containerID string, errorChan chan error) {
r, w := io.Pipe()
go io.Copy(w, os.Stdin)
err := client.AttachToContainer(docker.AttachToContainerOptions{
Container: containerID,
InputStream: r,
OutputStream: os.Stdout,
ErrorStream: os.Stderr,
Stdin: true,
Stdout: true,
Stderr: true,
Stream: true,
RawTerminal: true,
})
errorChan <- err
}

// From https://github.com/docker/docker/blob/0d70706b4b6bf9d5a5daf46dd147ca71270d0ab7/api/client/utils.go#L222-L233
func monitorTty(client *docker.Client, containerID string, terminalFd uintptr) {
resizeTty(client, containerID, terminalFd)

sigchan := make(chan os.Signal, 1)
gosignal.Notify(sigchan, signal.SIGWINCH)
go func() {
for range sigchan {
resizeTty(client, containerID, terminalFd)
}
}()
}

// From https://github.com/docker/docker/blob/0d70706b4b6bf9d5a5daf46dd147ca71270d0ab7/api/client/utils.go#L222-L233
func monitorExecTty(client *docker.Client, execID string, terminalFd uintptr) {
// HACK: For some weird reason on Docker 1.4.1 this resize is being triggered
// before the Exec instance is running resulting in an error on the
// Docker server. So we wait a little bit before triggering this first
// resize
time.Sleep(50 * time.Millisecond)
resizeExecTty(client, execID, terminalFd)

sigchan := make(chan os.Signal, 1)
gosignal.Notify(sigchan, signal.SIGWINCH)
go func() {
for range sigchan {
resizeExecTty(client, execID, terminalFd)
}
}()
}

func resizeTty(client *docker.Client, containerID string, terminalFd uintptr) error {
height, width := getTtySize(terminalFd)
if height == 0 && width == 0 {
return nil
}
return client.ResizeContainerTTY(containerID, height, width)
}

func resizeExecTty(client *docker.Client, containerID string, terminalFd uintptr) error {
height, width := getTtySize(terminalFd)
if height == 0 && width == 0 {
return nil
}
return client.ResizeExecTTY(containerID, height, width)
}

// From https://github.com/docker/docker/blob/0d70706b4b6bf9d5a5daf46dd147ca71270d0ab7/api/client/utils.go#L235-L247
func getTtySize(terminalFd uintptr) (int, int) {
ws, err := term.GetWinsize(terminalFd)
if err != nil {
if ws == nil {
return 0, 0
}
}
return int(ws.Height), int(ws.Width)
}
20 changes: 20 additions & 0 deletions docker/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package docker

import (
"fmt"

"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/nat"
"github.com/docker/libcompose/project"
Expand Down Expand Up @@ -252,6 +253,25 @@ func (s *Service) Restart() error {
})
}

// Run implements Service.Run. It runs a one of command within the service container.
func (s *Service) Run(commandParts []string) (int, error) {
imageName, err := s.build()
if err != nil {
return -1, err
}

client := s.context.ClientFactory.Create(s)

namer := NewNamer(client, s.context.Project.Name, s.name+"_run")
defer namer.Close()

containerName := namer.Next()

c := NewContainer(client, containerName, s)

return c.Run(imageName, &project.ServiceConfig{Command: project.NewCommand(commandParts...), Tty: true, StdinOpen: true})
}

// Kill implements Service.Kill. It kills any containers related to the service.
func (s *Service) Kill() error {
return s.eachContainer(func(c *Container) error {
Expand Down
2 changes: 2 additions & 0 deletions integration/assets/run/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hello:
image: busybox
6 changes: 1 addition & 5 deletions integration/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,7 @@ func (s *RunSuite) FromText(c *C, projectName, command string, argsAndInput ...s
}

err := cmd.Run()
if err != nil {
logrus.Errorf("Failed to run %s %v: %v\n with input:\n%s", s.command, err, args, input)
}

c.Assert(err, IsNil)
c.Assert(err, IsNil, Commentf("Failed to run %s %v: %v\n with input:\n%s", s.command, err, args, input))

return projectName
}
Expand Down
Loading