@@ -24,7 +24,9 @@ import (
2424 "fmt"
2525 "io"
2626 "io/ioutil"
27+ "os"
2728 "os/exec"
29+ "path/filepath"
2830 "runtime"
2931 "strings"
3032 "sync"
@@ -258,6 +260,213 @@ func TestDaemonRestart(t *testing.T) {
258260 <- statusC
259261}
260262
263+ func TestShimDoesNotLeakPipes (t * testing.T ) {
264+ containerdPid := ctrd .cmd .Process .Pid
265+ initialPipes , err := numPipes (containerdPid )
266+ if err != nil {
267+ t .Fatal (err )
268+ }
269+
270+ client , err := newClient (t , address )
271+ if err != nil {
272+ t .Fatal (err )
273+ }
274+ defer client .Close ()
275+
276+ var (
277+ image Image
278+ ctx , cancel = testContext ()
279+ id = t .Name ()
280+ )
281+ defer cancel ()
282+
283+ image , err = client .GetImage (ctx , testImage )
284+ if err != nil {
285+ t .Fatal (err )
286+ }
287+
288+ container , err := client .NewContainer (ctx , id , WithNewSnapshot (id , image ), WithNewSpec (oci .WithImageConfig (image ), withProcessArgs ("sleep" , "30" )))
289+ if err != nil {
290+ t .Fatal (err )
291+ }
292+
293+ task , err := container .NewTask (ctx , empty ())
294+ if err != nil {
295+ t .Fatal (err )
296+ }
297+
298+ exitChannel , err := task .Wait (ctx )
299+ if err != nil {
300+ t .Fatal (err )
301+ }
302+
303+ if err := task .Start (ctx ); err != nil {
304+ t .Fatal (err )
305+ }
306+
307+ if err := task .Kill (ctx , syscall .SIGKILL ); err != nil {
308+ t .Fatal (err )
309+ }
310+
311+ <- exitChannel
312+
313+ if _ , err := task .Delete (ctx ); err != nil {
314+ t .Fatal (err )
315+ }
316+
317+ if err := container .Delete (ctx , WithSnapshotCleanup ); err != nil {
318+ t .Fatal (err )
319+ }
320+
321+ currentPipes , err := numPipes (containerdPid )
322+ if err != nil {
323+ t .Fatal (err )
324+ }
325+
326+ if initialPipes != currentPipes {
327+ t .Errorf ("Pipes have leaked after container has been deleted. Initially there were %d pipes, after container deletion there were %d pipes" , initialPipes , currentPipes )
328+ }
329+ }
330+
331+ func numPipes (pid int ) (int , error ) {
332+ cmd := exec .Command ("sh" , "-c" , fmt .Sprintf ("lsof -p %d | grep FIFO" , pid ))
333+
334+ var stdout bytes.Buffer
335+ cmd .Stdout = & stdout
336+ if err := cmd .Run (); err != nil {
337+ return 0 , err
338+ }
339+ return strings .Count (stdout .String (), "\n " ), nil
340+ }
341+
342+ func TestDaemonReconnectsToShimIOPipesOnRestart (t * testing.T ) {
343+ client , err := newClient (t , address )
344+ if err != nil {
345+ t .Fatal (err )
346+ }
347+ defer client .Close ()
348+
349+ var (
350+ image Image
351+ ctx , cancel = testContext ()
352+ id = t .Name ()
353+ )
354+ defer cancel ()
355+
356+ image , err = client .GetImage (ctx , testImage )
357+ if err != nil {
358+ t .Fatal (err )
359+ }
360+
361+ container , err := client .NewContainer (ctx , id , WithNewSnapshot (id , image ), WithNewSpec (oci .WithImageConfig (image ), withProcessArgs ("sleep" , "30" )))
362+ if err != nil {
363+ t .Fatal (err )
364+ }
365+ defer container .Delete (ctx , WithSnapshotCleanup )
366+
367+ task , err := container .NewTask (ctx , empty ())
368+ if err != nil {
369+ t .Fatal (err )
370+ }
371+ defer task .Delete (ctx )
372+
373+ _ , err = task .Wait (ctx )
374+ if err != nil {
375+ t .Fatal (err )
376+ }
377+
378+ if err := task .Start (ctx ); err != nil {
379+ t .Fatal (err )
380+ }
381+
382+ if err := ctrd .Restart (nil ); err != nil {
383+ t .Fatal (err )
384+ }
385+
386+ waitCtx , waitCancel := context .WithTimeout (ctx , 2 * time .Second )
387+ serving , err := client .IsServing (waitCtx )
388+ waitCancel ()
389+ if ! serving {
390+ t .Fatalf ("containerd did not start within 2s: %v" , err )
391+ }
392+
393+ // After we restared containerd we write some messages to the log pipes, simulating shim writing stuff there.
394+ // Then we make sure that these messages are available on the containerd log thus proving that the server reconnected to the log pipes
395+ runtimeVersion := getRuntimeVersion ()
396+ logDirPath := getLogDirPath (runtimeVersion , id )
397+
398+ switch runtimeVersion {
399+ case "v1" :
400+ writeToFile (t , filepath .Join (logDirPath , "shim.stdout.log" ), fmt .Sprintf ("%s writing to stdout\n " , id ))
401+ writeToFile (t , filepath .Join (logDirPath , "shim.stderr.log" ), fmt .Sprintf ("%s writing to stderr\n " , id ))
402+ case "v2" :
403+ writeToFile (t , filepath .Join (logDirPath , "log" ), fmt .Sprintf ("%s writing to log\n " , id ))
404+ }
405+
406+ statusC , err := task .Wait (ctx )
407+ if err != nil {
408+ t .Fatal (err )
409+ }
410+
411+ if err := task .Kill (ctx , syscall .SIGKILL ); err != nil {
412+ t .Fatal (err )
413+ }
414+
415+ <- statusC
416+
417+ stdioContents , err := ioutil .ReadFile (ctrdStdioFilePath )
418+ if err != nil {
419+ t .Fatal (err )
420+ }
421+
422+ switch runtimeVersion {
423+ case "v1" :
424+ if ! strings .Contains (string (stdioContents ), fmt .Sprintf ("%s writing to stdout" , id )) {
425+ t .Fatal ("containerd did not connect to the shim stdout pipe" )
426+ }
427+ if ! strings .Contains (string (stdioContents ), fmt .Sprintf ("%s writing to stderr" , id )) {
428+ t .Fatal ("containerd did not connect to the shim stderr pipe" )
429+ }
430+ case "v2" :
431+ if ! strings .Contains (string (stdioContents ), fmt .Sprintf ("%s writing to log" , id )) {
432+ t .Fatal ("containerd did not connect to the shim log pipe" )
433+ }
434+ }
435+ }
436+
437+ func writeToFile (t * testing.T , filePath , message string ) {
438+ writer , err := os .OpenFile (filePath , os .O_WRONLY , 0600 )
439+ if err != nil {
440+ t .Fatal (err )
441+ }
442+ if _ , err := writer .WriteString (message ); err != nil {
443+ t .Fatal (err )
444+ }
445+ if err := writer .Close (); err != nil {
446+ t .Fatal (err )
447+ }
448+ }
449+
450+ func getLogDirPath (runtimeVersion , id string ) string {
451+ switch runtimeVersion {
452+ case "v1" :
453+ return filepath .Join (defaultRoot , "io.containerd.runtime.v1.linux" , testNamespace , id )
454+ case "v2" :
455+ return filepath .Join (defaultState , "io.containerd.runtime.v2.task" , testNamespace , id )
456+ default :
457+ panic (fmt .Errorf ("Unsupported runtime version %s" , runtimeVersion ))
458+ }
459+ }
460+
461+ func getRuntimeVersion () string {
462+ switch rt := os .Getenv ("TEST_RUNTIME" ); rt {
463+ case "io.containerd.runc.v1" :
464+ return "v2"
465+ default :
466+ return "v1"
467+ }
468+ }
469+
261470func TestContainerPTY (t * testing.T ) {
262471 t .Parallel ()
263472
0 commit comments