@@ -22,8 +22,9 @@ import (
22
22
"io"
23
23
24
24
"github.com/compose-spec/compose-go/types"
25
- apitypes "github.com/docker/docker/api/types"
25
+ moby "github.com/docker/docker/api/types"
26
26
"github.com/docker/docker/api/types/filters"
27
+ "github.com/docker/docker/pkg/stdcopy"
27
28
28
29
"github.com/docker/compose-cli/api/compose"
29
30
)
@@ -34,34 +35,14 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts
34
35
return 0 , err
35
36
}
36
37
37
- containers , err := s .apiClient .ContainerList (ctx , apitypes.ContainerListOptions {
38
- Filters : filters .NewArgs (
39
- projectFilter (project .Name ),
40
- serviceFilter (service .Name ),
41
- filters .Arg ("label" , fmt .Sprintf ("%s=%d" , containerNumberLabel , opts .Index )),
42
- ),
43
- })
38
+ container , err := s .getExecTarget (ctx , project , service , opts )
44
39
if err != nil {
45
40
return 0 , err
46
41
}
47
- if len (containers ) < 1 {
48
- return 0 , fmt .Errorf ("container %s not running" , getContainerName (project .Name , service , opts .Index ))
49
- }
50
- container := containers [0 ]
51
-
52
- var env []string
53
- for k , v := range service .Environment .OverrideBy (types .NewMappingWithEquals (opts .Environment )).
54
- Resolve (func (s string ) (string , bool ) {
55
- v , ok := project .Environment [s ]
56
- return v , ok
57
- }).
58
- RemoveEmpty () {
59
- env = append (env , fmt .Sprintf ("%s=%s" , k , * v ))
60
- }
61
42
62
- exec , err := s .apiClient .ContainerExecCreate (ctx , container .ID , apitypes .ExecConfig {
43
+ exec , err := s .apiClient .ContainerExecCreate (ctx , container .ID , moby .ExecConfig {
63
44
Cmd : opts .Command ,
64
- Env : env ,
45
+ Env : s . getExecEnvironment ( project , service , opts ) ,
65
46
User : opts .User ,
66
47
Privileged : opts .Privileged ,
67
48
Tty : opts .Tty ,
@@ -77,15 +58,14 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts
77
58
}
78
59
79
60
if opts .Detach {
80
- return 0 , s .apiClient .ContainerExecStart (ctx , exec .ID , apitypes .ExecStartCheck {
61
+ return 0 , s .apiClient .ContainerExecStart (ctx , exec .ID , moby .ExecStartCheck {
81
62
Detach : true ,
82
63
Tty : opts .Tty ,
83
64
})
84
65
}
85
66
86
- resp , err := s .apiClient .ContainerExecAttach (ctx , exec .ID , apitypes.ExecStartCheck {
87
- Detach : false ,
88
- Tty : opts .Tty ,
67
+ resp , err := s .apiClient .ContainerExecAttach (ctx , exec .ID , moby.ExecStartCheck {
68
+ Tty : opts .Tty ,
89
69
})
90
70
if err != nil {
91
71
return 0 , err
@@ -99,30 +79,78 @@ func (s *composeService) Exec(ctx context.Context, project *types.Project, opts
99
79
}
100
80
}
101
81
102
- readChannel := make (chan error )
103
- writeChannel := make (chan error )
82
+ err = s .interactiveExec (ctx , opts , resp )
83
+ if err != nil {
84
+ return 0 , err
85
+ }
86
+
87
+ return s .getExecExitStatus (ctx , exec .ID )
88
+ }
89
+
90
+ // inspired by https://github.com/docker/cli/blob/master/cli/command/container/exec.go#L116
91
+ func (s * composeService ) interactiveExec (ctx context.Context , opts compose.RunOptions , resp moby.HijackedResponse ) error {
92
+ outputDone := make (chan error )
93
+ inputDone := make (chan error )
104
94
105
95
go func () {
106
- _ , err := io .Copy (opts .Writer , resp .Reader )
107
- readChannel <- err
96
+ if opts .Tty {
97
+ _ , err := io .Copy (opts .Writer , resp .Reader )
98
+ outputDone <- err
99
+ } else {
100
+ _ , err := stdcopy .StdCopy (opts .Writer , opts .Writer , resp .Reader )
101
+ outputDone <- err
102
+ }
108
103
}()
109
104
110
105
go func () {
111
106
_ , err := io .Copy (resp .Conn , opts .Reader )
112
- writeChannel <- err
107
+ inputDone <- err
113
108
}()
114
109
115
- select {
116
- case err = <- readChannel :
117
- break
118
- case err = <- writeChannel :
119
- break
110
+ for {
111
+ select {
112
+ case err := <- outputDone :
113
+ return err
114
+ case err := <- inputDone :
115
+ if err != nil {
116
+ return err
117
+ }
118
+ // Wait for output to complete streaming
119
+ case <- ctx .Done ():
120
+ return ctx .Err ()
121
+ }
120
122
}
123
+ }
121
124
125
+ func (s * composeService ) getExecTarget (ctx context.Context , project * types.Project , service types.ServiceConfig , opts compose.RunOptions ) (moby.Container , error ) {
126
+ containers , err := s .apiClient .ContainerList (ctx , moby.ContainerListOptions {
127
+ Filters : filters .NewArgs (
128
+ projectFilter (project .Name ),
129
+ serviceFilter (service .Name ),
130
+ filters .Arg ("label" , fmt .Sprintf ("%s=%d" , containerNumberLabel , opts .Index )),
131
+ ),
132
+ })
122
133
if err != nil {
123
- return 0 , err
134
+ return moby. Container {} , err
124
135
}
125
- return s .getExecExitStatus (ctx , exec .ID )
136
+ if len (containers ) < 1 {
137
+ return moby.Container {}, fmt .Errorf ("container %s not running" , getContainerName (project .Name , service , opts .Index ))
138
+ }
139
+ container := containers [0 ]
140
+ return container , nil
141
+ }
142
+
143
+ func (s * composeService ) getExecEnvironment (project * types.Project , service types.ServiceConfig , opts compose.RunOptions ) []string {
144
+ var env []string
145
+ for k , v := range service .Environment .OverrideBy (types .NewMappingWithEquals (opts .Environment )).
146
+ Resolve (func (s string ) (string , bool ) {
147
+ v , ok := project .Environment [s ]
148
+ return v , ok
149
+ }).
150
+ RemoveEmpty () {
151
+ env = append (env , fmt .Sprintf ("%s=%s" , k , * v ))
152
+ }
153
+ return env
126
154
}
127
155
128
156
func (s * composeService ) getExecExitStatus (ctx context.Context , execID string ) (int , error ) {
0 commit comments