Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit 5ddcc84

Browse files
authored
Merge pull request #1263 from docker/exitCodeFrom
2 parents 1562af9 + 8b90814 commit 5ddcc84

File tree

22 files changed

+374
-89
lines changed

22 files changed

+374
-89
lines changed

aci/compose.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (cs *aciComposeService) Create(ctx context.Context, project *types.Project,
6060
return errdefs.ErrNotImplemented
6161
}
6262

63-
func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
63+
func (cs *aciComposeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error {
6464
return errdefs.ErrNotImplemented
6565
}
6666

api/client/compose.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func (c *composeService) Create(ctx context.Context, project *types.Project, opt
4444
return errdefs.ErrNotImplemented
4545
}
4646

47-
func (c *composeService) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
47+
func (c *composeService) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error {
4848
return errdefs.ErrNotImplemented
4949
}
5050

api/compose/api.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type Service interface {
3434
// Create executes the equivalent to a `compose create`
3535
Create(ctx context.Context, project *types.Project, opts CreateOptions) error
3636
// Start executes the equivalent to a `compose start`
37-
Start(ctx context.Context, project *types.Project, consumer LogConsumer) error
37+
Start(ctx context.Context, project *types.Project, options StartOptions) error
3838
// Stop executes the equivalent to a `compose stop`
3939
Stop(ctx context.Context, project *types.Project) error
4040
// Up executes the equivalent to a `compose up`
@@ -63,6 +63,12 @@ type CreateOptions struct {
6363
Recreate string
6464
}
6565

66+
// StartOptions group options of the Start API
67+
type StartOptions struct {
68+
// Attach will attach to service containers and send container logs and events
69+
Attach ContainerEventListener
70+
}
71+
6672
// UpOptions group options of the Up API
6773
type UpOptions struct {
6874
// Detach will create services and return immediately
@@ -177,4 +183,24 @@ type Stack struct {
177183
// LogConsumer is a callback to process log messages from services
178184
type LogConsumer interface {
179185
Log(service, container, message string)
186+
Status(service, container, msg string)
187+
}
188+
189+
// ContainerEventListener is a callback to process ContainerEvent from services
190+
type ContainerEventListener func(event ContainerEvent)
191+
192+
// ContainerEvent notify an event has been collected on Source container implementing Service
193+
type ContainerEvent struct {
194+
Type int
195+
Source string
196+
Service string
197+
Line string
198+
ExitCode int
180199
}
200+
201+
const (
202+
// ContainerEventLog is a ContainerEvent of type log. Line is set
203+
ContainerEventLog = iota
204+
// ContainerEventExit is a ContainerEvent of type exit. ExitCode is set
205+
ContainerEventExit
206+
)

cli/cmd/compose/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func startDependencies(ctx context.Context, c *client.Client, project *types.Pro
102102
if err := c.ComposeService().Create(ctx, project, compose.CreateOptions{}); err != nil {
103103
return err
104104
}
105-
if err := c.ComposeService().Start(ctx, project, nil); err != nil {
105+
if err := c.ComposeService().Start(ctx, project, compose.StartOptions{}); err != nil {
106106
return err
107107
}
108108
return nil

cli/cmd/compose/start.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@ package compose
1818

1919
import (
2020
"context"
21-
"os"
22-
23-
"github.com/spf13/cobra"
2421

2522
"github.com/docker/compose-cli/api/client"
23+
"github.com/docker/compose-cli/api/compose"
2624
"github.com/docker/compose-cli/api/progress"
27-
"github.com/docker/compose-cli/cli/formatter"
25+
26+
"github.com/spf13/cobra"
2827
)
2928

3029
type startOptions struct {
@@ -61,10 +60,30 @@ func runStart(ctx context.Context, opts startOptions, services []string) error {
6160

6261
if opts.Detach {
6362
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
64-
return "", c.ComposeService().Start(ctx, project, nil)
63+
return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
6564
})
6665
return err
6766
}
6867

69-
return c.ComposeService().Start(ctx, project, formatter.NewLogConsumer(ctx, os.Stdout))
68+
queue := make(chan compose.ContainerEvent)
69+
printer := printer{
70+
queue: queue,
71+
}
72+
err = c.ComposeService().Start(ctx, project, compose.StartOptions{
73+
Attach: func(event compose.ContainerEvent) {
74+
queue <- event
75+
},
76+
})
77+
if err != nil {
78+
return err
79+
}
80+
81+
_, err = printer.run(ctx, false, "", func() error {
82+
ctx := context.Background()
83+
_, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
84+
return "", c.ComposeService().Stop(ctx, project)
85+
})
86+
return err
87+
})
88+
return err
7089
}

cli/cmd/compose/up.go

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@ package compose
1818

1919
import (
2020
"context"
21-
"errors"
2221
"fmt"
2322
"os"
23+
"os/signal"
2424
"path/filepath"
25+
"syscall"
2526

2627
"github.com/docker/compose-cli/api/client"
2728
"github.com/docker/compose-cli/api/compose"
2829
"github.com/docker/compose-cli/api/context/store"
2930
"github.com/docker/compose-cli/api/progress"
31+
"github.com/docker/compose-cli/cli/cmd"
3032
"github.com/docker/compose-cli/cli/formatter"
3133

3234
"github.com/compose-spec/compose-go/types"
35+
"github.com/sirupsen/logrus"
3336
"github.com/spf13/cobra"
3437
)
3538

@@ -49,6 +52,8 @@ type upOptions struct {
4952
forceRecreate bool
5053
noRecreate bool
5154
noStart bool
55+
cascadeStop bool
56+
exitCodeFrom string
5257
}
5358

5459
func (o upOptions) recreateStrategy() string {
@@ -73,6 +78,12 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
7378
RunE: func(cmd *cobra.Command, args []string) error {
7479
switch contextType {
7580
case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
81+
if opts.exitCodeFrom != "" {
82+
opts.cascadeStop = true
83+
}
84+
if opts.cascadeStop && opts.Detach {
85+
return fmt.Errorf("--abort-on-container-exit and --detach are incompatible")
86+
}
7687
if opts.forceRecreate && opts.noRecreate {
7788
return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
7889
}
@@ -95,6 +106,8 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
95106
flags.BoolVar(&opts.forceRecreate, "force-recreate", false, "Recreate containers even if their configuration and image haven't changed.")
96107
flags.BoolVar(&opts.noRecreate, "no-recreate", false, "If containers already exist, don't recreate them. Incompatible with --force-recreate.")
97108
flags.BoolVar(&opts.noStart, "no-start", false, "Don't start the services after creating them.")
109+
flags.BoolVar(&opts.cascadeStop, "abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d")
110+
flags.StringVar(&opts.exitCodeFrom, "exit-code-from", "", "Return the exit code of the selected service container. Implies --abort-on-container-exit")
98111
}
99112

100113
return upCmd
@@ -120,6 +133,13 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
120133
return err
121134
}
122135

136+
if opts.exitCodeFrom != "" {
137+
_, err := project.GetService(opts.exitCodeFrom)
138+
if err != nil {
139+
return err
140+
}
141+
}
142+
123143
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
124144
err := c.ComposeService().Create(ctx, project, compose.CreateOptions{
125145
RemoveOrphans: opts.removeOrphans,
@@ -129,7 +149,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
129149
return "", err
130150
}
131151
if opts.Detach {
132-
err = c.ComposeService().Start(ctx, project, nil)
152+
err = c.ComposeService().Start(ctx, project, compose.StartOptions{})
133153
}
134154
return "", err
135155
})
@@ -145,13 +165,38 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
145165
return nil
146166
}
147167

148-
err = c.ComposeService().Start(ctx, project, formatter.NewLogConsumer(ctx, os.Stdout))
149-
if errors.Is(ctx.Err(), context.Canceled) {
150-
fmt.Println("Gracefully stopping...")
151-
ctx = context.Background()
152-
_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
168+
queue := make(chan compose.ContainerEvent)
169+
printer := printer{
170+
queue: queue,
171+
}
172+
173+
stopFunc := func() error {
174+
ctx := context.Background()
175+
_, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
153176
return "", c.ComposeService().Stop(ctx, project)
154177
})
178+
return err
179+
}
180+
signalChan := make(chan os.Signal, 1)
181+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
182+
go func() {
183+
<-signalChan
184+
fmt.Println("Gracefully stopping...")
185+
stopFunc() // nolint:errcheck
186+
}()
187+
188+
err = c.ComposeService().Start(ctx, project, compose.StartOptions{
189+
Attach: func(event compose.ContainerEvent) {
190+
queue <- event
191+
},
192+
})
193+
if err != nil {
194+
return err
195+
}
196+
197+
exitCode, err := printer.run(ctx, opts.cascadeStop, opts.exitCodeFrom, stopFunc)
198+
if exitCode != 0 {
199+
return cmd.ExitCodeError{ExitCode: exitCode}
155200
}
156201
return err
157202
}
@@ -196,3 +241,37 @@ func setup(ctx context.Context, opts composeOptions, services []string) (*client
196241

197242
return c, project, nil
198243
}
244+
245+
type printer struct {
246+
queue chan compose.ContainerEvent
247+
}
248+
249+
func (p printer) run(ctx context.Context, cascadeStop bool, exitCodeFrom string, stopFn func() error) (int, error) { //nolint:unparam
250+
consumer := formatter.NewLogConsumer(ctx, os.Stdout)
251+
var aborting bool
252+
for {
253+
event := <-p.queue
254+
switch event.Type {
255+
case compose.ContainerEventExit:
256+
if !aborting {
257+
consumer.Status(event.Service, event.Source, fmt.Sprintf("exited with code %d", event.ExitCode))
258+
}
259+
if cascadeStop && !aborting {
260+
aborting = true
261+
fmt.Println("Aborting on container exit...")
262+
err := stopFn()
263+
if err != nil {
264+
return 0, err
265+
}
266+
}
267+
if exitCodeFrom == "" || exitCodeFrom == event.Service {
268+
logrus.Error(event.ExitCode)
269+
return event.ExitCode, nil
270+
}
271+
case compose.ContainerEventLog:
272+
if !aborting {
273+
consumer.Log(event.Service, event.Source, event.Line)
274+
}
275+
}
276+
}
277+
}

cli/cmd/exit.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2020 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cmd
18+
19+
import "strconv"
20+
21+
// ExitCodeError reports an exit code set by command.
22+
type ExitCodeError struct {
23+
ExitCode int
24+
}
25+
26+
func (e ExitCodeError) Error() string {
27+
return strconv.Itoa(e.ExitCode)
28+
}

cli/formatter/logs.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,7 @@ func (l *logConsumer) Log(service, container, message string) {
4242
if l.ctx.Err() != nil {
4343
return
4444
}
45-
cf, ok := l.colors[service]
46-
if !ok {
47-
cf = <-loop
48-
l.colors[service] = cf
49-
l.computeWidth()
50-
}
45+
cf := l.getColorFunc(service)
5146
prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", container)
5247

5348
for _, line := range strings.Split(message, "\n") {
@@ -56,6 +51,22 @@ func (l *logConsumer) Log(service, container, message string) {
5651
}
5752
}
5853

54+
func (l *logConsumer) Status(service, container, msg string) {
55+
cf := l.getColorFunc(service)
56+
buf := bytes.NewBufferString(cf(fmt.Sprintf("%s %s\n", container, msg)))
57+
l.writer.Write(buf.Bytes()) // nolint:errcheck
58+
}
59+
60+
func (l *logConsumer) getColorFunc(service string) colorFunc {
61+
cf, ok := l.colors[service]
62+
if !ok {
63+
cf = <-loop
64+
l.colors[service] = cf
65+
l.computeWidth()
66+
}
67+
return cf
68+
}
69+
5970
func (l *logConsumer) computeWidth() {
6071
width := 0
6172
for n := range l.colors {

cli/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ func main() {
170170
fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", opts.LogLevel)
171171
os.Exit(1)
172172
}
173+
logrus.SetFormatter(&logrus.TextFormatter{
174+
DisableTimestamp: true,
175+
DisableLevelTruncation: true,
176+
})
173177
logrus.SetLevel(level)
174178
if opts.Debug {
175179
logrus.SetLevel(logrus.DebugLevel)
@@ -241,6 +245,11 @@ $ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype)
241245
}
242246

243247
func exit(ctx string, err error, ctype string) {
248+
if exit, ok := err.(cmd.ExitCodeError); ok {
249+
metrics.Track(ctype, os.Args[1:], metrics.SuccessStatus)
250+
os.Exit(exit.ExitCode)
251+
}
252+
244253
metrics.Track(ctype, os.Args[1:], metrics.FailureStatus)
245254

246255
if errors.Is(err, errdefs.ErrLoginRequired) {

ecs/local/compose.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ func (e ecsLocalSimulation) Create(ctx context.Context, project *types.Project,
5353
return e.compose.Create(ctx, enhanced, opts)
5454
}
5555

56-
func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, consumer compose.LogConsumer) error {
57-
return e.compose.Start(ctx, project, consumer)
56+
func (e ecsLocalSimulation) Start(ctx context.Context, project *types.Project, options compose.StartOptions) error {
57+
return e.compose.Start(ctx, project, options)
5858
}
5959

6060
func (e ecsLocalSimulation) Stop(ctx context.Context, project *types.Project) error {

0 commit comments

Comments
 (0)