@@ -20,68 +20,30 @@ import (
20
20
"context"
21
21
22
22
"github.com/docker/compose-cli/api/compose"
23
- convert "github.com/docker/compose-cli/local/moby"
24
23
"github.com/docker/compose-cli/utils"
25
24
26
25
"github.com/compose-spec/compose-go/types"
27
26
moby "github.com/docker/docker/api/types"
28
- "github.com/docker/docker/api/types/container "
27
+ "github.com/pkg/errors "
29
28
"golang.org/x/sync/errgroup"
30
29
)
31
30
32
31
func (s * composeService ) Start (ctx context.Context , project * types.Project , options compose.StartOptions ) error {
32
+ listener := options .Attach
33
33
if len (options .Services ) == 0 {
34
34
options .Services = project .ServiceNames ()
35
35
}
36
36
37
- var containers Containers
38
- if options . Attach != nil {
39
- attached , err := s .attach (ctx , project , options . Attach , options .Services )
37
+ eg , ctx := errgroup . WithContext ( ctx )
38
+ if listener != nil {
39
+ attached , err := s .attach (ctx , project , listener , options .Services )
40
40
if err != nil {
41
41
return err
42
42
}
43
- containers = attached
44
43
45
- // Watch events to capture container restart and re-attach
46
- go func () {
47
- watched := map [string ]struct {}{}
48
- for _ , c := range containers {
49
- watched [c .ID ] = struct {}{}
50
- }
51
- s .Events (ctx , project .Name , compose.EventsOptions { // nolint: errcheck
52
- Services : options .Services ,
53
- Consumer : func (event compose.Event ) error {
54
- if event .Status == "start" {
55
- inspect , err := s .apiClient .ContainerInspect (ctx , event .Container )
56
- if err != nil {
57
- return err
58
- }
59
-
60
- container := moby.Container {
61
- ID : event .Container ,
62
- Names : []string {inspect .Name },
63
- State : convert .ContainerRunning ,
64
- Labels : map [string ]string {
65
- projectLabel : project .Name ,
66
- serviceLabel : event .Service ,
67
- },
68
- }
69
-
70
- // Just ignore errors when reattaching to already crashed containers
71
- s .attachContainer (ctx , container , options .Attach , project ) // nolint: errcheck
72
-
73
- if _ , ok := watched [inspect .ID ]; ! ok {
74
- // a container has been added to the service, see --scale option
75
- watched [inspect .ID ] = struct {}{}
76
- go func () {
77
- s .waitContainer (container , options .Attach ) // nolint: errcheck
78
- }()
79
- }
80
- }
81
- return nil
82
- },
83
- })
84
- }()
44
+ eg .Go (func () error {
45
+ return s .watchContainers (project , options .Services , listener , attached )
46
+ })
85
47
}
86
48
87
49
err := InDependencyOrder (ctx , project , func (c context.Context , service types.ServiceConfig ) error {
@@ -93,34 +55,79 @@ func (s *composeService) Start(ctx context.Context, project *types.Project, opti
93
55
if err != nil {
94
56
return err
95
57
}
58
+ return eg .Wait ()
59
+ }
96
60
97
- if options .Attach == nil {
98
- return nil
99
- }
100
-
101
- eg , ctx := errgroup .WithContext (ctx )
61
+ // watchContainers uses engine events to capture container start/die and notify ContainerEventListener
62
+ func (s * composeService ) watchContainers (project * types.Project , services []string , listener compose.ContainerEventListener , containers Containers ) error {
63
+ watched := map [string ]int {}
102
64
for _ , c := range containers {
103
- c := c
104
- eg .Go (func () error {
105
- return s .waitContainer (c , options .Attach )
106
- })
65
+ watched [c .ID ] = 0
107
66
}
108
- return eg .Wait ()
109
- }
110
67
111
- func (s * composeService ) waitContainer (c moby.Container , listener compose.ContainerEventListener ) error {
112
- statusC , errC := s .apiClient .ContainerWait (context .Background (), c .ID , container .WaitConditionNotRunning )
113
- name := getContainerNameWithoutProject (c )
114
- select {
115
- case status := <- statusC :
116
- listener (compose.ContainerEvent {
117
- Type : compose .ContainerEventExit ,
118
- Container : name ,
119
- Service : c .Labels [serviceLabel ],
120
- ExitCode : int (status .StatusCode ),
121
- })
68
+ ctx , stop := context .WithCancel (context .Background ())
69
+ err := s .Events (ctx , project .Name , compose.EventsOptions {
70
+ Services : services ,
71
+ Consumer : func (event compose.Event ) error {
72
+ inspected , err := s .apiClient .ContainerInspect (ctx , event .Container )
73
+ if err != nil {
74
+ return err
75
+ }
76
+ container := moby.Container {
77
+ ID : inspected .ID ,
78
+ Names : []string {inspected .Name },
79
+ Labels : inspected .Config .Labels ,
80
+ }
81
+ name := getContainerNameWithoutProject (container )
82
+
83
+ if event .Status == "die" {
84
+ restarted := watched [container .ID ]
85
+ watched [container .ID ] = restarted + 1
86
+ // Container terminated.
87
+ willRestart := inspected .HostConfig .RestartPolicy .MaximumRetryCount > restarted
88
+
89
+ listener (compose.ContainerEvent {
90
+ Type : compose .ContainerEventExit ,
91
+ Container : name ,
92
+ Service : container .Labels [serviceLabel ],
93
+ ExitCode : inspected .State .ExitCode ,
94
+ Restarting : willRestart ,
95
+ })
96
+
97
+ if ! willRestart {
98
+ // we're done with this one
99
+ delete (watched , container .ID )
100
+ }
101
+
102
+ if len (watched ) == 0 {
103
+ // all project containers stopped, we're done
104
+ stop ()
105
+ }
106
+ return nil
107
+ }
108
+
109
+ if event .Status == "start" {
110
+ count , ok := watched [container .ID ]
111
+ mustAttach := ok && count > 0 // Container restarted, need to re-attach
112
+ if ! ok {
113
+ // A new container has just been added to service by scale
114
+ watched [container .ID ] = 0
115
+ mustAttach = true
116
+ }
117
+ if mustAttach {
118
+ // Container restarted, need to re-attach
119
+ err := s .attachContainer (ctx , container , listener , project )
120
+ if err != nil {
121
+ return err
122
+ }
123
+ }
124
+ }
125
+
126
+ return nil
127
+ },
128
+ })
129
+ if errors .Is (ctx .Err (), context .Canceled ) {
122
130
return nil
123
- case err := <- errC :
124
- return err
125
131
}
132
+ return err
126
133
}
0 commit comments