Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit c43ded0

Browse files
committed
Merge pull request #154 from vdemeester/run-command
Run command
2 parents 59c8bba + 250e8f7 commit c43ded0

38 files changed

+832
-27
lines changed

cli/app/app.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,27 @@ func ProjectUp(p *project.Project, c *cli.Context) {
138138
}
139139
}
140140

141+
// ProjectRun runs a given command within a service's container.
142+
func ProjectRun(p *project.Project, c *cli.Context) {
143+
if len(c.Args()) == 1 {
144+
logrus.Fatal("No service specified")
145+
}
146+
147+
serviceName := c.Args()[0]
148+
commandParts := c.Args()[1:]
149+
150+
if _, ok := p.Configs[serviceName]; !ok {
151+
logrus.Fatalf("%s is not defined in the template", serviceName)
152+
}
153+
154+
exitCode, err := p.Run(serviceName, commandParts)
155+
if err != nil {
156+
logrus.Fatal(err)
157+
}
158+
159+
os.Exit(exitCode)
160+
}
161+
141162
// ProjectStart starts services.
142163
func ProjectStart(p *project.Project, c *cli.Context) {
143164
err := p.Start(c.Args()...)

cli/command/command.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ func StartCommand(factory app.ProjectFactory) cli.Command {
124124
}
125125
}
126126

127+
// RunCommand defines the libcompose run subcommand.
128+
func RunCommand(factory app.ProjectFactory) cli.Command {
129+
return cli.Command{
130+
Name: "run",
131+
Usage: "Run a one-off command",
132+
Action: app.WithProject(factory, app.ProjectRun),
133+
Flags: []cli.Flag{},
134+
}
135+
}
136+
127137
// PullCommand defines the libcompose pull subcommand.
128138
func PullCommand(factory app.ProjectFactory) cli.Command {
129139
return cli.Command{

cli/main/main.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,23 @@ func main() {
2424
app.Commands = []cli.Command{
2525
command.BuildCommand(factory),
2626
command.CreateCommand(factory),
27-
command.UpCommand(factory),
28-
command.StartCommand(factory),
29-
command.LogsCommand(factory),
30-
command.RestartCommand(factory),
31-
command.StopCommand(factory),
3227
command.DownCommand(factory),
33-
command.ScaleCommand(factory),
34-
command.RmCommand(factory),
35-
command.PullCommand(factory),
3628
command.KillCommand(factory),
29+
command.LogsCommand(factory),
30+
command.PauseCommand(factory),
3731
command.PortCommand(factory),
3832
command.PsCommand(factory),
39-
command.PauseCommand(factory),
33+
command.PullCommand(factory),
34+
command.RestartCommand(factory),
35+
command.RmCommand(factory),
36+
command.RunCommand(factory),
37+
command.ScaleCommand(factory),
38+
command.StartCommand(factory),
39+
command.StopCommand(factory),
4040
command.UnpauseCommand(factory),
41+
command.UpCommand(factory),
4142
}
4243

4344
app.Run(os.Args)
45+
4446
}

docker/container.go

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/Sirupsen/logrus"
1616
"github.com/docker/docker/pkg/jsonmessage"
17+
"github.com/docker/docker/pkg/promise"
1718
"github.com/docker/docker/pkg/stdcopy"
1819
"github.com/docker/docker/pkg/term"
1920
"github.com/docker/docker/reference"
@@ -145,7 +146,7 @@ func (c *Container) Recreate(imageName string) (*types.Container, error) {
145146
return nil, err
146147
}
147148

148-
newContainer, err := c.createContainer(imageName, info.ID)
149+
newContainer, err := c.createContainer(imageName, info.ID, nil)
149150
if err != nil {
150151
return nil, err
151152
}
@@ -168,13 +169,20 @@ func (c *Container) Recreate(imageName string) (*types.Container, error) {
168169
// to notify the container has been created. If the container already exists, does
169170
// nothing.
170171
func (c *Container) Create(imageName string) (*types.Container, error) {
172+
return c.CreateWithOverride(imageName, nil)
173+
}
174+
175+
// CreateWithOverride create container and override parts of the config to
176+
// allow special situations to override the config generated from the compose
177+
// file
178+
func (c *Container) CreateWithOverride(imageName string, configOverride *project.ServiceConfig) (*types.Container, error) {
171179
container, err := c.findExisting()
172180
if err != nil {
173181
return nil, err
174182
}
175183

176184
if container == nil {
177-
container, err = c.createContainer(imageName, "")
185+
container, err = c.createContainer(imageName, "", configOverride)
178186
if err != nil {
179187
return nil, err
180188
}
@@ -274,6 +282,126 @@ func (c *Container) IsRunning() (bool, error) {
274282
return info.State.Running, nil
275283
}
276284

285+
// Run creates, start and attach to the container based on the image name,
286+
// the specified configuration.
287+
// It will always create a new container.
288+
func (c *Container) Run(imageName string, configOverride *project.ServiceConfig) (int, error) {
289+
var (
290+
errCh chan error
291+
out, stderr io.Writer
292+
in io.ReadCloser
293+
)
294+
295+
container, err := c.createContainer(imageName, "", configOverride)
296+
if err != nil {
297+
return -1, err
298+
}
299+
300+
info, err := c.client.ContainerInspect(context.Background(), container.ID)
301+
if err != nil {
302+
return -1, err
303+
}
304+
305+
if configOverride.StdinOpen {
306+
in = os.Stdin
307+
}
308+
if configOverride.Tty {
309+
out = os.Stdout
310+
}
311+
if configOverride.Tty {
312+
stderr = os.Stderr
313+
}
314+
315+
options := types.ContainerAttachOptions{
316+
ContainerID: container.ID,
317+
Stream: true,
318+
Stdin: configOverride.StdinOpen,
319+
Stdout: configOverride.Tty,
320+
Stderr: configOverride.Tty,
321+
}
322+
323+
resp, err := c.client.ContainerAttach(context.Background(), options)
324+
if err != nil {
325+
return -1, err
326+
}
327+
328+
// set raw terminal
329+
inFd, _ := term.GetFdInfo(in)
330+
state, err := term.SetRawTerminal(inFd)
331+
if err != nil {
332+
return -1, err
333+
}
334+
// restore raw terminal
335+
defer term.RestoreTerminal(inFd, state)
336+
// holdHijackedConnection (in goroutine)
337+
errCh = promise.Go(func() error {
338+
return holdHijackedConnection(configOverride.Tty, in, out, stderr, resp)
339+
})
340+
341+
if err := c.client.ContainerStart(context.Background(), container.ID); err != nil {
342+
return -1, err
343+
}
344+
345+
if err := <-errCh; err != nil {
346+
logrus.Debugf("Error hijack: %s", err)
347+
return -1, err
348+
}
349+
350+
info, err = c.client.ContainerInspect(context.Background(), container.ID)
351+
if err != nil {
352+
return -1, err
353+
}
354+
355+
return info.State.ExitCode, nil
356+
}
357+
358+
func holdHijackedConnection(tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
359+
var err error
360+
receiveStdout := make(chan error, 1)
361+
if outputStream != nil || errorStream != nil {
362+
go func() {
363+
// When TTY is ON, use regular copy
364+
if tty && outputStream != nil {
365+
_, err = io.Copy(outputStream, resp.Reader)
366+
} else {
367+
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
368+
}
369+
logrus.Debugf("[hijack] End of stdout")
370+
receiveStdout <- err
371+
}()
372+
}
373+
374+
stdinDone := make(chan struct{})
375+
go func() {
376+
if inputStream != nil {
377+
io.Copy(resp.Conn, inputStream)
378+
logrus.Debugf("[hijack] End of stdin")
379+
}
380+
381+
if err := resp.CloseWrite(); err != nil {
382+
logrus.Debugf("Couldn't send EOF: %s", err)
383+
}
384+
close(stdinDone)
385+
}()
386+
387+
select {
388+
case err := <-receiveStdout:
389+
if err != nil {
390+
logrus.Debugf("Error receiveStdout: %s", err)
391+
return err
392+
}
393+
case <-stdinDone:
394+
if outputStream != nil || errorStream != nil {
395+
if err := <-receiveStdout; err != nil {
396+
logrus.Debugf("Error receiveStdout: %s", err)
397+
return err
398+
}
399+
}
400+
}
401+
402+
return nil
403+
}
404+
277405
// Up creates and start the container based on the image name and send an event
278406
// to notify the container has been created. If the container exists but is stopped
279407
// it tries to start it.
@@ -297,20 +425,25 @@ func (c *Container) Up(imageName string) error {
297425
}
298426

299427
if !info.State.Running {
300-
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
301-
if err = c.client.ContainerStart(context.Background(), container.ID); err != nil {
302-
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
303-
return err
304-
}
305-
306-
c.service.context.Project.Notify(project.EventContainerStarted, c.service.Name(), map[string]string{
307-
"name": c.Name(),
308-
})
428+
c.Start(container)
309429
}
310430

311431
return nil
312432
}
313433

434+
// Start the specified container with the specified host config
435+
func (c *Container) Start(container *types.Container) error {
436+
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
437+
if err := c.client.ContainerStart(context.Background(), container.ID); err != nil {
438+
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
439+
return err
440+
}
441+
c.service.context.Project.Notify(project.EventContainerStarted, c.service.Name(), map[string]string{
442+
"name": c.Name(),
443+
})
444+
return nil
445+
}
446+
314447
// OutOfSync checks if the container is out of sync with the service definition.
315448
// It looks if the the service hash container label is the same as the computed one.
316449
func (c *Container) OutOfSync(imageName string) (bool, error) {
@@ -356,7 +489,13 @@ func volumeBinds(volumes map[string]struct{}, container *types.ContainerJSON) []
356489
return result
357490
}
358491

359-
func (c *Container) createContainer(imageName, oldContainer string) (*types.Container, error) {
492+
func (c *Container) createContainer(imageName, oldContainer string, configOverride *project.ServiceConfig) (*types.Container, error) {
493+
serviceConfig := c.service.serviceConfig
494+
if configOverride != nil {
495+
serviceConfig.Command = configOverride.Command
496+
serviceConfig.Tty = configOverride.Tty
497+
serviceConfig.StdinOpen = configOverride.StdinOpen
498+
}
360499
configWrapper, err := ConvertToAPI(c.service)
361500
if err != nil {
362501
return nil, err

docker/service.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,25 @@ func (s *Service) Up() error {
205205
return s.up(imageName, true)
206206
}
207207

208+
// Run implements Service.Run. It runs a one of command within the service container.
209+
func (s *Service) Run(commandParts []string) (int, error) {
210+
imageName, err := s.ensureImageExists()
211+
if err != nil {
212+
return -1, err
213+
}
214+
215+
client := s.context.ClientFactory.Create(s)
216+
217+
namer := NewNamer(client, s.context.Project.Name, s.name+"_run")
218+
defer namer.Close()
219+
220+
containerName := namer.Next()
221+
222+
c := NewContainer(client, containerName, s)
223+
224+
return c.Run(imageName, &project.ServiceConfig{Command: project.NewCommand(commandParts...), Tty: true, StdinOpen: true})
225+
}
226+
208227
// Info implements Service.Info. It returns an project.InfoSet with the containers
209228
// related to this service (can be multiple if using the scale command).
210229
func (s *Service) Info(qFlag bool) (project.InfoSet, error) {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
hello:
2+
image: busybox

integration/common_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,7 @@ func (s *RunSuite) FromText(c *C, projectName, command string, argsAndInput ...s
127127
}
128128

129129
err := cmd.Run()
130-
if err != nil {
131-
logrus.Errorf("Failed to run %s %v: %v\n with input:\n%s", s.command, err, args, input)
132-
}
133-
134-
c.Assert(err, IsNil)
130+
c.Assert(err, IsNil, Commentf("Failed to run %s %v: %v\n with input:\n%s", s.command, err, args, input))
135131

136132
return projectName
137133
}

integration/run_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package integration
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"os"
9+
"os/exec"
10+
"strings"
11+
"syscall"
12+
13+
. "gopkg.in/check.v1"
14+
15+
"github.com/kr/pty"
16+
)
17+
18+
// FIXME find out why it fails with "inappropriate ioctl for device"
19+
func (s *RunSuite) TestRun(c *C) {
20+
p := s.RandomProject()
21+
cmd := exec.Command(s.command, "-f", "./assets/run/docker-compose.yml", "-p", p, "run", "hello", "echo", "test")
22+
var b bytes.Buffer
23+
wbuf := bufio.NewWriter(&b)
24+
25+
tty, err := pty.Start(cmd)
26+
27+
_, err = io.Copy(wbuf, tty)
28+
if e, ok := err.(*os.PathError); ok && e.Err == syscall.EIO {
29+
// We can safely ignore this error, because it's just
30+
// the PTY telling us that it closed successfully.
31+
// See:
32+
// https://github.com/buildkite/agent/pull/34#issuecomment-46080419
33+
err = nil
34+
}
35+
c.Assert(cmd.Wait(), IsNil)
36+
output := string(b.Bytes())
37+
38+
c.Assert(err, IsNil, Commentf("%s", output))
39+
40+
name := fmt.Sprintf("%s_%s_run_1", p, "hello")
41+
cn := s.GetContainerByName(c, name)
42+
c.Assert(cn, NotNil)
43+
44+
lines := strings.Split(output, "\r\n")
45+
lastLine := lines[len(lines)-2 : len(lines)-1][0]
46+
47+
c.Assert(cn.State.Running, Equals, false)
48+
c.Assert(lastLine, Equals, "test")
49+
}

0 commit comments

Comments
 (0)