1 package remotecmd
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/golang/mock/gomock"
13 "github.com/google/go-cmp/cmp"
14
15 "github.com/redhat-developer/odo/pkg/exec"
16 "github.com/redhat-developer/odo/pkg/kclient"
17 )
18
19 const (
20 _podName = "my-pod"
21 _containerName = "my-container"
22 statFile = `1 (tail) S 0 1 1 0 -1 1077952768 943 0 0 0 1 1 0 0 20 0 1 0 171838 5050368 338 18446744073709551615 94133334573056 94133335487553 140737112090992 0 0 0 0 0 0 0 0 0 17 1 0 0 0 0 0 94133335803888 94133335849476 94133343424512 140737112095206 140737112095282 140737112095282 140737112096746 0
23 118 (bash) S 0 118 118 34816 128 4210944 1144 454 0 0 0 1 0 0 20 0 1 0 185395 4554752 926 18446744073709551615 93924054794240 93924055688405 140724979077904 0 0 0 65536 3686404 1266761467 0 0 0 17 1 0 0 0 0 0 93924055927824 93924055975568 93924085239808 140724979079714 140724979079719 140724979079719 140724979081194 0
24 81 (sh) (param) S 0 81 81 0 -1 4210944 693 0 0 0 0 0 0 0 20 0 1 0 172021 4284416 760 18446744073709551615 94666717065216 94666717959381 140728008896192 0 0 0 65536 4 65538 0 0 0 17 0 0 0 0 0 0 94666718198800 94666718246544 94666730864640 140728008903100 140728008903254 140728008903254 140728008904688 0
25 87 (main) S 81 81 81 0 -1 4210688 541 0 0 0 0 0 0 0 20 0 5 0 172022 1032048640 1892 18446744073709551615 4194304 6405776 140730311069152 0 0 0 0 0 2143420159 0 0 0 17 1 0 0 0 0 0 8507392 8757920 34906112 140730311072280 140730311072287 140730311072287 140730311073777 0
26 128 (cat) R 118 128 118 34816 128 4210688 152 0 0 0 0 0 0 0 20 0 1 0 193754 5185536 625 18446744073709551615 94301628837888 94301629752385 140721174235312 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 94301630068720 94301630114308 94301634404352 140721174243694 140721174243865 140721174243865 140721174245355 0
27 128 (cat) R 118 128 118 34816 128 4210688 152 0 0 0 0 0 0 0 20 0 1 0 193754 5185536 625 18446744073709551615 94301628837888 94301629752385 140721174235312 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 94301630068720 94301630114308 94301634404352 140721174243694 140721174243865 140721174243865 140721174245355 0
28 222 (my-cmd) S 87 81 81 0 -1 4210688 541 0 0 0 0 0 0 0 20 0 5 0 172022 1032048640 1892 18446744073709551615 4194304 6405776 140730311069152 0 0 0 0 0 2143420159 0 0 0 17 1 0 0 0 0 0 8507392 8757920 34906112 140730311072280 140730311072287 140730311072287 140730311073777 0
29 223 (my-cmd) S 87 81 81 0 -1 4210688 541 0 0 0 0 0 0 0 20 0 5 0 172022 1032048640 1892 18446744073709551615 4194304 6405776 140730311069152 0 0 0 0 0 2143420159 0 0 0 17 1 0 0 0 0 0 8507392 8757920 34906112 140730311072280 140730311072287 140730311072287 140730311073777 0
30 333 (my-cmd) S 222 81 81 0 -1 4210688 541 0 0 0 0 0 0 0 20 0 5 0 172022 1032048640 1892 18446744073709551615 4194304 6405776 140730311069152 0 0 0 0 0 2143420159 0 0 0 17 1 0 0 0 0 0 8507392 8757920 34906112 140730311072280 140730311072287 140730311072287 140730311073777 0
31 334 (my-cmd) S 222 81 81 0 -1 4210688 541 0 0 0 0 0 0 0 20 0 5 0 172022 1032048640 1892 18446744073709551615 4194304 6405776 140730311069152 0 0 0 0 0 2143420159 0 0 0 17 1 0 0 0 0 0 8507392 8757920 34906112 140730311072280 140730311072287 140730311072287 140730311073777 0`
32 )
33
34 func Test_kubeExecProcessHandler_GetProcessInfoForCommand(t *testing.T) {
35 cmdDef := CommandDefinition{Id: "my-run"}
36 kill0CmdProvider := func(p int) []string {
37 return []string{ShellExecutable, "-c", fmt.Sprintf("kill -0 %d; echo $?", p)}
38 }
39 for _, tt := range []struct {
40 name string
41 kubeClientCustomizer func(*kclient.MockClientInterface)
42 pid int
43 want RemoteProcessInfo
44 wantErr bool
45 }{
46 {
47 name: "error returned when checking pid file",
48 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
49 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
50 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
51 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
52 Return(errors.New("an error"))
53 },
54 wantErr: true,
55 },
56 {
57 name: "stopped status if PID file missing",
58 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
59 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
60 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
61 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
62 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
63 _, err := stderr.Write([]byte("no such file or directory"))
64 return err
65 })
66 },
67 want: RemoteProcessInfo{
68 Pid: 0,
69 Status: Stopped,
70 },
71 },
72 {
73 name: "unknown status if negative value stored in PID file",
74 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
75 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
76 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
77 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
78 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
79 _, err := stdout.Write([]byte("-1"))
80 return err
81 })
82 },
83 want: RemoteProcessInfo{
84 Pid: -1,
85 Status: Unknown,
86 },
87 wantErr: true,
88 },
89 {
90 name: "stopped status if kill -0 command exit status is non-zero",
91 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
92 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
93 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
94 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
95 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
96 _, err := stdout.Write([]byte("123"))
97 return err
98 })
99 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
100 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
101 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
102 _, err := stdout.Write([]byte("1"))
103 return err
104 })
105 },
106 want: RemoteProcessInfo{
107 Pid: 123,
108 Status: Stopped,
109 },
110 },
111 {
112 name: "error status if kill -0 command exit status is non-zero and process exit code recorded as failing",
113 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
114 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
115 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
116 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
117 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
118 _, err := stdout.Write([]byte("123\n1"))
119 return err
120 })
121 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
122 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
123 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
124 _, err := stdout.Write([]byte("1"))
125 return err
126 })
127 },
128 want: RemoteProcessInfo{
129 Pid: 123,
130 Status: Errored,
131 },
132 },
133 {
134 name: "running status if kill -0 command exit status is zero",
135 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
136 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
137 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
138 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
139 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
140 _, err := stdout.Write([]byte("123"))
141 return err
142 })
143 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
144 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
145 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
146 _, err := stdout.Write([]byte("0"))
147 return err
148 })
149 },
150 want: RemoteProcessInfo{
151 Pid: 123,
152 Status: Running,
153 },
154 },
155 } {
156 t.Run(tt.name, func(t *testing.T) {
157 ctrl := gomock.NewController(t)
158 kubeClient := kclient.NewMockClientInterface(ctrl)
159 if tt.kubeClientCustomizer != nil {
160 tt.kubeClientCustomizer(kubeClient)
161 }
162
163 execClient := exec.NewExecClient(kubeClient)
164 k := NewKubeExecProcessHandler(execClient)
165 got, err := k.GetProcessInfoForCommand(context.Background(), cmdDef, _podName, _containerName)
166
167 if tt.wantErr != (err != nil) {
168 t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
169 }
170 if diff := cmp.Diff(tt.want, got); diff != "" {
171 t.Errorf("kubeExecProcessHandler.GetProcessInfoForCommand() mismatch (-want +got):\n%s", diff)
172 }
173 })
174 }
175 }
176
177 func Test_kubeExecProcessHandler_StartProcessForCommand(t *testing.T) {
178 kill0CmdProvider := func(p int) []string {
179 return []string{ShellExecutable, "-c", fmt.Sprintf("kill -0 %d; echo $?", p)}
180 }
181
182 execCmdWithoutWorkingDir := CommandDefinition{
183 Id: "my-exec-cmd",
184 CmdLine: "echo Hello; sleep 300",
185 }
186 fullExecCmd := CommandDefinition{
187 Id: "my-exec-cmd",
188 CmdLine: "tail -f /path/to/a/file",
189 WorkingDir: "/path/to/working/dir",
190 EnvVars: []CommandEnvVar{
191 {
192 Key: "ENV_VAR1",
193 Value: "value1",
194 },
195 {
196 Key: "ENV_VAR2",
197 Value: "value2",
198 },
199 },
200 }
201 for _, tt := range []struct {
202 name string
203 cmdDef CommandDefinition
204 kubeClientCustomizer func(*kclient.MockClientInterface)
205 isCmdExpectedToRun bool
206 wantErr bool
207 expectedStatuses []RemoteProcessStatus
208 }{
209 {
210 name: "command execution returned no error",
211 cmdDef: execCmdWithoutWorkingDir,
212 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
213 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
214 gomock.Eq([]string{ShellExecutable, "-c",
215 fmt.Sprintf("echo $$ > %[1]s && (%s) 1>>/proc/1/fd/1 2>>/proc/1/fd/2; echo $? >> %[1]s",
216 getPidFileForCommand(execCmdWithoutWorkingDir), execCmdWithoutWorkingDir.CmdLine)}),
217 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
218 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
219 _, err := stdout.Write([]byte("Hello"))
220 return err
221 })
222 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
223 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(execCmdWithoutWorkingDir))}),
224 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
225 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
226 _, err := stdout.Write([]byte("123"))
227 return err
228 })
229 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
230 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
231 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
232 _, err := stdout.Write([]byte("1"))
233 return err
234 })
235 },
236 isCmdExpectedToRun: true,
237 expectedStatuses: []RemoteProcessStatus{Starting, Running, Stopped},
238 },
239 {
240 name: "command with all fields returned an error",
241 cmdDef: fullExecCmd,
242 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
243 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
244 gomock.Eq([]string{ShellExecutable, "-c",
245 fmt.Sprintf("echo $$ > %[1]s && cd %s && export ENV_VAR1='value1' ENV_VAR2='value2' && (%s) 1>>/proc/1/fd/1 2>>/proc/1/fd/2; echo $? >> %[1]s",
246 getPidFileForCommand(fullExecCmd), fullExecCmd.WorkingDir, fullExecCmd.CmdLine)}),
247 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
248 Return(errors.New("error while running command"))
249 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
250 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(fullExecCmd))}),
251 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
252 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
253 _, err := stdout.Write([]byte("123\n1"))
254 return err
255 })
256 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
257 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
258 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
259 _, err := stdout.Write([]byte("1"))
260 return err
261 })
262 },
263 isCmdExpectedToRun: true,
264 expectedStatuses: []RemoteProcessStatus{Starting, Running, Errored},
265 },
266 } {
267 t.Run(tt.name, func(t *testing.T) {
268 ctrl := gomock.NewController(t)
269 kubeClient := kclient.NewMockClientInterface(ctrl)
270 if tt.kubeClientCustomizer != nil {
271 tt.kubeClientCustomizer(kubeClient)
272 }
273
274 execClient := exec.NewExecClient(kubeClient)
275 k := NewKubeExecProcessHandler(execClient)
276
277 var wg sync.WaitGroup
278 wg.Add(len(tt.expectedStatuses))
279 var statusesReported []RemoteProcessStatus
280 err := k.StartProcessForCommand(context.Background(), tt.cmdDef, _podName, _containerName, func(status RemoteProcessStatus, stdout []string, stderr []string, err error) {
281 defer wg.Done()
282 statusesReported = append(statusesReported, status)
283 })
284
285 if tt.wantErr != (err != nil) {
286 t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
287 }
288
289 if tt.isCmdExpectedToRun && waitTimeout(&wg, 10*time.Second) {
290 t.Errorf("timeout waiting for output handler to get called")
291 return
292 }
293
294 if diff := cmp.Diff(tt.expectedStatuses, statusesReported); diff != "" {
295 t.Errorf("kubeExecProcessHandler.StartProcessForCommand() expectedStatuses mismatch (-want +got):\n%s", diff)
296 }
297 })
298 }
299 }
300
301 func Test_kubeExecProcessHandler_StopProcessForCommand(t *testing.T) {
302 cmdDef := CommandDefinition{Id: "my-run"}
303 retrieveChildrenCmdProvider := func() []string {
304 return []string{ShellExecutable, "-c", "cat /proc/*/stat || true"}
305 }
306 killCmdProvider := func(p int) []string {
307 return []string{ShellExecutable, "-c", fmt.Sprintf("kill %d || true", p)}
308 }
309 kill0CmdProvider := func(p int) []string {
310 return []string{ShellExecutable, "-c", fmt.Sprintf("kill -0 %d; echo $?", p)}
311 }
312
313 for _, tt := range []struct {
314 name string
315 kubeClientCustomizer func(*kclient.MockClientInterface)
316 pid int
317 wantErr bool
318 }{
319 {
320 name: "error returned when checking pid file",
321 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
322 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
323 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
324 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
325 Return(errors.New("an error"))
326 },
327 wantErr: true,
328 },
329 {
330 name: "nothing to do if PID file missing",
331 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
332 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
333 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
334 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
335 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
336 _, err := stderr.Write([]byte("no such file or directory"))
337 return err
338 })
339 },
340 },
341 {
342 name: "error while determining process children",
343 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
344 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
345 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
346 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
347 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
348 _, err := stdout.Write([]byte("123"))
349 return err
350 })
351 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(retrieveChildrenCmdProvider()),
352 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
353 Return(errors.New("an error"))
354
355 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(killCmdProvider(123)),
356 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
357 Return(nil)
358 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
359 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
360 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
361 _, _ = stderr.Write([]byte("no such process"))
362 _, err := stdout.Write([]byte("1"))
363 return err
364 })
365 },
366 wantErr: true,
367 },
368 {
369 name: "no process children killed if no children file found",
370 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
371 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
372 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("rm -f %s", getPidFileForCommand(cmdDef))}),
373 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
374 Return(errors.New("an error which should be ignored"))
375 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
376 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
377 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
378 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
379 _, err := stdout.Write([]byte("123"))
380 return err
381 })
382 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(retrieveChildrenCmdProvider()),
383 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
384 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
385 _, err := stderr.Write([]byte("no such file or directory"))
386 return err
387 })
388 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(killCmdProvider(123)),
389 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
390 Return(nil)
391 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(123)),
392 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
393 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
394 _, _ = stderr.Write([]byte("no such process"))
395 _, err := stdout.Write([]byte("1"))
396 return err
397 })
398 },
399 },
400 {
401 name: "process children should get killed",
402 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
403 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
404 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("rm -f %s", getPidFileForCommand(cmdDef))}),
405 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
406 Return(errors.New("an error which should be ignored"))
407 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
408 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
409 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
410 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
411 _, err := stdout.Write([]byte("81"))
412 return err
413 })
414 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(retrieveChildrenCmdProvider()),
415 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
416 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
417 _, err := stdout.Write([]byte(statFile))
418 return err
419 })
420 for _, p := range []int{333, 334, 222, 223, 87, 81} {
421 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(killCmdProvider(p)),
422 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
423 Return(nil)
424 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(p)),
425 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
426 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
427 _, _ = stderr.Write([]byte("no such process"))
428 _, err := stdout.Write([]byte("1"))
429 return err
430 })
431 }
432 },
433 },
434 {
435 name: "error if any child process could not be killed",
436 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
437 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
438 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("rm -f %s", getPidFileForCommand(cmdDef))}),
439 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
440 Return(errors.New("an error which should be ignored"))
441 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
442 gomock.Eq([]string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}),
443 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
444 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
445 _, err := stdout.Write([]byte("81"))
446 return err
447 })
448 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(retrieveChildrenCmdProvider()),
449 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
450 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
451 _, err := stdout.Write([]byte(statFile))
452 return err
453 })
454 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(killCmdProvider(333)),
455 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
456 Return(errors.New("error killing process 333"))
457
458 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(killCmdProvider(81)),
459 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
460 Return(nil)
461 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(kill0CmdProvider(81)),
462 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
463 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
464 _, _ = stderr.Write([]byte("no such process"))
465 _, err := stdout.Write([]byte("1"))
466 return err
467 })
468 },
469 wantErr: true,
470 },
471 } {
472 t.Run(tt.name, func(t *testing.T) {
473 ctrl := gomock.NewController(t)
474 kubeClient := kclient.NewMockClientInterface(ctrl)
475 if tt.kubeClientCustomizer != nil {
476 tt.kubeClientCustomizer(kubeClient)
477 }
478
479 execClient := exec.NewExecClient(kubeClient)
480 k := NewKubeExecProcessHandler(execClient)
481 err := k.StopProcessForCommand(context.Background(), cmdDef, _podName, _containerName)
482
483 if tt.wantErr != (err != nil) {
484 t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
485 }
486 })
487 }
488 }
489
490 func Test_kubeExecProcessHandler_getProcessInfoFromPid(t *testing.T) {
491 cmdProvider := func(p int) []string {
492 return []string{ShellExecutable, "-c", fmt.Sprintf("kill -0 %d; echo $?", p)}
493 }
494 for _, tt := range []struct {
495 name string
496 kubeClientCustomizer func(*kclient.MockClientInterface)
497 pid int
498 lastKnownExitStatus int
499 want RemoteProcessInfo
500 wantErr bool
501 }{
502 {
503 name: "pid < 0",
504 pid: -1,
505 wantErr: true,
506 want: RemoteProcessInfo{
507 Pid: -1,
508 Status: Unknown,
509 },
510 },
511 {
512 name: "pid == 0",
513 want: RemoteProcessInfo{
514 Status: Stopped,
515 },
516 },
517 {
518 name: "error when checking process status",
519 pid: 123,
520 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
521 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmdProvider(123)),
522 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
523 Return(errors.New("an error"))
524 },
525 wantErr: true,
526 want: RemoteProcessInfo{
527 Pid: 123,
528 Status: Unknown,
529 },
530 },
531 {
532 name: "non-integer content returned by kill command output",
533 pid: 123,
534 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
535 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmdProvider(123)),
536 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
537 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
538 _, err := stdout.Write([]byte("should-not-happen"))
539 return err
540 })
541 },
542 wantErr: true,
543 want: RemoteProcessInfo{
544 Pid: 123,
545 Status: Unknown,
546 },
547 },
548 {
549 name: "kill command returned non-zero exit status code",
550 pid: 123,
551 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
552 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmdProvider(123)),
553 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
554 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
555 _, err := stdout.Write([]byte("1"))
556 return err
557 })
558 },
559 want: RemoteProcessInfo{
560 Pid: 123,
561 Status: Stopped,
562 },
563 },
564 {
565 name: "kill command returned 0 as exit status code",
566 pid: 123,
567 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
568 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmdProvider(123)),
569 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
570 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
571 _, err := stdout.Write([]byte("0"))
572 return err
573 })
574 },
575 want: RemoteProcessInfo{
576 Pid: 123,
577 Status: Running,
578 },
579 },
580 } {
581 t.Run(tt.name, func(t *testing.T) {
582 ctrl := gomock.NewController(t)
583 kubeClient := kclient.NewMockClientInterface(ctrl)
584 if tt.kubeClientCustomizer != nil {
585 tt.kubeClientCustomizer(kubeClient)
586 }
587
588 execClient := exec.NewExecClient(kubeClient)
589 k := NewKubeExecProcessHandler(execClient)
590 got, err := k.getProcessInfoFromPid(context.Background(), tt.pid, tt.lastKnownExitStatus, _podName, _containerName)
591
592 if tt.wantErr != (err != nil) {
593 t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
594 }
595 if diff := cmp.Diff(tt.want, got); diff != "" {
596 t.Errorf("kubeExecProcessHandler.getProcessInfoFromPid() mismatch (-want +got):\n%s", diff)
597 }
598 })
599 }
600 }
601
602 func Test_kubeExecProcessHandler_getRemoteProcessPID(t *testing.T) {
603 cmdDef := CommandDefinition{Id: "my-run"}
604 cmd := []string{ShellExecutable, "-c", fmt.Sprintf("cat %s || true", getPidFileForCommand(cmdDef))}
605 for _, tt := range []struct {
606 name string
607 kubeClientCustomizer func(*kclient.MockClientInterface)
608 wantPid int
609 wantLastKnownExitCode int
610 wantErr bool
611 }{
612 {
613 name: "error returned at command execution",
614 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
615 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
616 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
617 Return(errors.New("an error"))
618 },
619 wantErr: true,
620 },
621 {
622 name: "missing pid file",
623 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
624 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
625 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
626 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
627 _, err := stderr.Write([]byte("no such file or directory"))
628 return err
629 })
630 },
631 },
632 {
633 name: "unexpected number of lines in pid file",
634 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
635 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
636 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
637 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
638 _, err := stdout.Write([]byte("123\n234\n345"))
639 return err
640 })
641 },
642 wantErr: true,
643 },
644 {
645 name: "invalid content in pid file",
646 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
647 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
648 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
649 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
650 _, err := stdout.Write([]byte("invalid-pid"))
651 return err
652 })
653 },
654 wantErr: true,
655 },
656 {
657 name: "valid content in pid file with trailing spaces",
658 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
659 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
660 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
661 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
662 _, err := stdout.Write([]byte(" 123 "))
663 return err
664 })
665 },
666 wantPid: 123,
667 },
668 {
669 name: "valid content in pid file",
670 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
671 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
672 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
673 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
674 _, err := stdout.Write([]byte("123"))
675 return err
676 })
677 },
678 wantPid: 123,
679 },
680 {
681 name: "negative value in pid file",
682 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
683 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
684 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
685 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
686 _, err := stdout.Write([]byte("-1"))
687 return err
688 })
689 },
690 wantPid: -1,
691 },
692 {
693 name: "valid content with zero exit status code in pid file",
694 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
695 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
696 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
697 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
698 _, err := stdout.Write([]byte("123\n0"))
699 return err
700 })
701 },
702 wantPid: 123,
703 wantLastKnownExitCode: 0,
704 },
705 {
706 name: "valid content with non-zero exit status code in pid file",
707 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
708 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
709 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
710 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
711 _, err := stdout.Write([]byte("123\n1"))
712 return err
713 })
714 },
715 wantPid: 123,
716 wantLastKnownExitCode: 1,
717 },
718 {
719 name: "error returned content if non-number recorded in pid file as process last-known exit code",
720 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
721 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
722 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
723 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
724 _, err := stdout.Write([]byte("123\nNAN"))
725 return err
726 })
727 },
728 wantErr: true,
729 wantPid: 123,
730 },
731 } {
732 t.Run(tt.name, func(t *testing.T) {
733 ctrl := gomock.NewController(t)
734 kubeClient := kclient.NewMockClientInterface(ctrl)
735 if tt.kubeClientCustomizer != nil {
736 tt.kubeClientCustomizer(kubeClient)
737 }
738
739 execClient := exec.NewExecClient(kubeClient)
740 kubeExecClient := NewKubeExecProcessHandler(execClient)
741 got, lastKnownExitStatus, err := kubeExecClient.getRemoteProcessPID(context.Background(), cmdDef, _podName, _containerName)
742 if tt.wantErr != (err != nil) {
743 t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
744 }
745 if diff := cmp.Diff(tt.wantPid, got); diff != "" {
746 t.Errorf("kubeExecProcessHandler.getRemoteProcessPID() wantPid mismatch (-want +got):\n%s", diff)
747 }
748 if diff := cmp.Diff(tt.wantLastKnownExitCode, lastKnownExitStatus); diff != "" {
749 t.Errorf("kubeExecProcessHandler.getRemoteProcessPID() wantLastKnownExitCode mismatch (-want +got):\n%s", diff)
750 }
751 })
752 }
753 }
754
755 func Test_kubeExecProcessHandler_getProcessChildren(t *testing.T) {
756 const ppid = 123
757 cmdProvider := func() []string {
758 return []string{ShellExecutable, "-c", "cat /proc/*/stat || true"}
759 }
760
761 for _, tt := range []struct {
762 name string
763 ppid int
764 kubeClientCustomizer func(*kclient.MockClientInterface)
765 want []int
766 wantErr bool
767 }{
768 {
769 name: "pid < 0",
770 ppid: -1,
771 wantErr: true,
772 },
773 {
774 name: "pid = 0",
775 wantErr: true,
776 },
777 {
778 name: "error returned at command execution",
779 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
780 cmd := cmdProvider()
781 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
782 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
783 Return(errors.New("an error"))
784 },
785 ppid: ppid,
786 wantErr: true,
787 },
788 {
789 name: "missing stat file",
790 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
791 cmd := cmdProvider()
792 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
793 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
794 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
795 _, err := stderr.Write([]byte("no such file or directory"))
796 return err
797 })
798 },
799 ppid: ppid,
800 want: nil,
801 },
802 {
803 name: "empty stat file",
804 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
805 cmd := cmdProvider()
806 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName), gomock.Eq(cmd),
807 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
808 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
809 _, err := stdout.Write([]byte(""))
810 return err
811 })
812 },
813 ppid: ppid,
814 want: nil,
815 },
816 {
817 name: "stat file with children at several levels",
818 kubeClientCustomizer: func(kclient *kclient.MockClientInterface) {
819 cmd := cmdProvider()
820 kclient.EXPECT().ExecCMDInContainer(gomock.Any(), gomock.Eq(_containerName), gomock.Eq(_podName),
821 gomock.Eq(cmd),
822 gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
823 DoAndReturn(func(ctx context.Context, containerName, podName string, cmd []string, stdout io.Writer, stderr io.Writer, stdin io.Reader, tty bool) error {
824 _, err := stdout.Write([]byte(statFile))
825 return err
826 })
827 },
828 ppid: 81,
829 want: []int{333, 334, 222, 223, 87},
830 },
831 } {
832 t.Run(tt.name, func(t *testing.T) {
833 ctrl := gomock.NewController(t)
834 kubeClient := kclient.NewMockClientInterface(ctrl)
835 if tt.kubeClientCustomizer != nil {
836 tt.kubeClientCustomizer(kubeClient)
837 }
838
839 execClient := exec.NewExecClient(kubeClient)
840 kubeExecClient := NewKubeExecProcessHandler(execClient)
841 got, err := kubeExecClient.getProcessChildren(context.Background(), tt.ppid, _podName, _containerName)
842 if tt.wantErr != (err != nil) {
843 t.Errorf("unexpected error %v, wantErr %v", err, tt.wantErr)
844 }
845 if diff := cmp.Diff(tt.want, got); diff != "" {
846 t.Errorf("kubeExecProcessHandler.getProcessChildren() mismatch (-want +got):\n%s", diff)
847 }
848 })
849 }
850 }
851
852
853
854 func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
855 c := make(chan struct{})
856 go func() {
857 defer close(c)
858 wg.Wait()
859 }()
860 select {
861 case <-c:
862 return false
863 case <-time.After(timeout):
864 return true
865 }
866 }
867
View as plain text