github.com/hpcng/singularity@v3.1.1+incompatible/cmd/singularity/actions_test.go (about) 1 // Copyright (c) 2018, Sylabs Inc. All rights reserved. 2 // This software is licensed under a 3-clause BSD license. Please consult the 3 // LICENSE.md file distributed with the sources of this project regarding your 4 // rights to use or distribute this software. 5 6 package main 7 8 import ( 9 "bytes" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "os/exec" 14 "strconv" 15 "strings" 16 "syscall" 17 "testing" 18 19 "github.com/sylabs/singularity/internal/pkg/test" 20 ) 21 22 //build base image for tests 23 const imagePath = "./container.sif" 24 const appsImage = "./appsImage.sif" 25 26 type opts struct { 27 binds []string 28 security []string 29 keepPrivs bool 30 dropCaps string 31 contain bool 32 noHome bool 33 home string 34 workdir string 35 pwd string 36 app string 37 overlay []string 38 userns bool 39 } 40 41 // imageExec can be used to run/exec/shell a Singularity image 42 // it return the exitCode and err of the execution 43 func imageExec(t *testing.T, action string, opts opts, imagePath string, command []string) (stdout string, stderr string, exitCode int, err error) { 44 // action can be run/exec/shell 45 argv := []string{action} 46 for _, bind := range opts.binds { 47 argv = append(argv, "--bind", bind) 48 } 49 for _, sec := range opts.security { 50 argv = append(argv, "--security", sec) 51 } 52 if opts.keepPrivs { 53 argv = append(argv, "--keep-privs") 54 } 55 if opts.dropCaps != "" { 56 argv = append(argv, "--drop-caps", opts.dropCaps) 57 } 58 if opts.contain { 59 argv = append(argv, "--contain") 60 } 61 if opts.noHome { 62 argv = append(argv, "--no-home") 63 } 64 if opts.home != "" { 65 argv = append(argv, "--home", opts.home) 66 } 67 for _, fs := range opts.overlay { 68 argv = append(argv, "--overlay", fs) 69 } 70 if opts.workdir != "" { 71 argv = append(argv, "--workdir", opts.workdir) 72 } 73 if opts.pwd != "" { 74 argv = append(argv, "--pwd", opts.pwd) 75 } 76 if opts.app != "" { 77 argv = append(argv, "--app", opts.app) 78 } 79 if opts.userns { 80 argv = append(argv, "-u") 81 } 82 argv = append(argv, imagePath) 83 argv = append(argv, command...) 84 85 var outbuf, errbuf bytes.Buffer 86 cmd := exec.Command(cmdPath, argv...) 87 88 cmd.Stdout = &outbuf 89 cmd.Stderr = &errbuf 90 91 if err := cmd.Start(); err != nil { 92 t.Fatalf("cmd.Start: %v", err) 93 } 94 95 // retrieve exit code 96 if err := cmd.Wait(); err != nil { 97 if _, ok := err.(*exec.ExitError); ok { 98 // The program has exited with an exit code != 0 99 exitCode = 1 100 } 101 } 102 103 stdout = outbuf.String() 104 stderr = errbuf.String() 105 106 return 107 } 108 109 // testSingularityRun tests min fuctionality for singularity run 110 func testSingularityRun(t *testing.T) { 111 tests := []struct { 112 name string 113 image string 114 action string 115 argv []string 116 opts 117 exit int 118 expectSuccess bool 119 }{ 120 {"NoCommand", imagePath, "run", []string{}, opts{}, 0, true}, 121 {"true", imagePath, "run", []string{"true"}, opts{}, 0, true}, 122 {"false", imagePath, "run", []string{"false"}, opts{}, 1, false}, 123 {"ScifTestAppGood", imagePath, "run", []string{}, opts{app: "testapp"}, 0, true}, 124 {"ScifTestAppBad", imagePath, "run", []string{}, opts{app: "fakeapp"}, 1, false}, 125 } 126 127 for _, tt := range tests { 128 t.Run(tt.name, test.WithoutPrivilege(func(t *testing.T) { 129 _, stderr, exitCode, err := imageExec(t, tt.action, tt.opts, tt.image, tt.argv) 130 if tt.expectSuccess && (exitCode != 0) { 131 t.Log(stderr) 132 t.Fatalf("unexpected failure running '%v': %v", strings.Join(tt.argv, " "), err) 133 } else if !tt.expectSuccess && (exitCode != 1) { 134 t.Log(stderr) 135 t.Fatalf("unexpected success running '%v'", strings.Join(tt.argv, " ")) 136 } 137 })) 138 } 139 } 140 141 // testSingularityExec tests min fuctionality for singularity exec 142 func testSingularityExec(t *testing.T) { 143 // Create a temp testfile 144 tmpfile, err := ioutil.TempFile("", "testSingularityExec.tmp") 145 if err != nil { 146 t.Fatal(err) 147 } 148 defer os.Remove(tmpfile.Name()) // clean up 149 150 testfile, err := tmpfile.Stat() 151 if err != nil { 152 t.Fatal(err) 153 } 154 155 pwd, err := os.Getwd() 156 if err != nil { 157 t.Fatal(err) 158 } 159 160 tests := []struct { 161 name string 162 image string 163 action string 164 argv []string 165 opts 166 exit int 167 expectSuccess bool 168 }{ 169 {"NoCommand", imagePath, "exec", []string{}, opts{}, 1, false}, 170 {"true", imagePath, "exec", []string{"true"}, opts{}, 0, true}, 171 {"trueAbsPAth", imagePath, "exec", []string{"/bin/true"}, opts{}, 0, true}, 172 {"false", imagePath, "exec", []string{"false"}, opts{}, 1, false}, 173 {"falseAbsPath", imagePath, "exec", []string{"/bin/false"}, opts{}, 1, false}, 174 // Scif apps tests 175 {"ScifTestAppGood", imagePath, "exec", []string{"testapp.sh"}, opts{app: "testapp"}, 0, true}, 176 {"ScifTestAppBad", imagePath, "exec", []string{"testapp.sh"}, opts{app: "fakeapp"}, 1, false}, 177 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif"}, opts{}, 0, true}, 178 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif/apps"}, opts{}, 0, true}, 179 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif/data"}, opts{}, 0, true}, 180 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif/apps/foo"}, opts{}, 0, true}, 181 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif/apps/bar"}, opts{}, 0, true}, 182 // blocked by issue [scif-apps] Files created at install step fall into an unexpected path #2404 183 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-f", "/scif/apps/foo/filefoo.exec"}, opts{}, 0, true}, 184 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-f", "/scif/apps/bar/filebar.exec"}, opts{}, 0, true}, 185 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif/data/foo/output"}, opts{}, 0, true}, 186 {"ScifTestfolderOrg", appsImage, "exec", []string{"test", "-d", "/scif/data/foo/input"}, opts{}, 0, true}, 187 {"WorkdirContain", imagePath, "exec", []string{"test", "-f", tmpfile.Name()}, opts{workdir: "testdata", contain: true}, 0, false}, 188 {"Workdir", imagePath, "exec", []string{"test", "-f", tmpfile.Name()}, opts{workdir: "testdata"}, 0, true}, 189 {"pwdGood", imagePath, "exec", []string{"true"}, opts{pwd: "/etc"}, 0, true}, 190 {"home", imagePath, "exec", []string{"test", "-f", tmpfile.Name()}, opts{home: pwd + "testdata"}, 0, true}, 191 {"homePath", imagePath, "exec", []string{"test", "-f", "/home/" + testfile.Name()}, opts{home: "/tmp:/home"}, 0, true}, 192 {"homeTmp", imagePath, "exec", []string{"true"}, opts{home: "/tmp"}, 0, true}, 193 {"homeTmpExplicit", imagePath, "exec", []string{"true"}, opts{home: "/tmp:/home"}, 0, true}, 194 {"ScifTestAppGood", imagePath, "exec", []string{"testapp.sh"}, opts{app: "testapp"}, 0, true}, 195 {"ScifTestAppBad", imagePath, "exec", []string{"testapp.sh"}, opts{app: "fakeapp"}, 1, false}, 196 // 197 {"userBind", imagePath, "exec", []string{"test", "-f", "/var/tmp/" + testfile.Name()}, opts{binds: []string{"/tmp:/var/tmp"}}, 0, true}, 198 } 199 200 for _, tt := range tests { 201 t.Run(tt.name, test.WithoutPrivilege(func(t *testing.T) { 202 _, stderr, exitCode, err := imageExec(t, tt.action, tt.opts, tt.image, tt.argv) 203 if tt.expectSuccess && (exitCode != 0) { 204 t.Log(stderr) 205 t.Fatalf("unexpected failure running '%v': %v", strings.Join(tt.argv, " "), err) 206 } else if !tt.expectSuccess && (exitCode != 1) { 207 t.Log(stderr) 208 t.Fatalf("unexpected success running '%v'", strings.Join(tt.argv, " ")) 209 } 210 })) 211 } 212 213 // test --no-home option 214 err = os.Chdir("/tmp") 215 if err != nil { 216 t.Fatal(err) 217 } 218 t.Run("noHome", test.WithoutPrivilege(func(t *testing.T) { 219 _, stderr, exitCode, err := imageExec(t, "exec", opts{noHome: true}, pwd+"/container.img", []string{"ls", "-ld", "$HOME"}) 220 if exitCode != 1 { 221 t.Log(stderr, err) 222 t.Fatalf("unexpected success running '%v'", strings.Join([]string{"ls", "-ld", "$HOME"}, " ")) 223 } 224 })) 225 // return to test SOURCEDIR 226 err = os.Chdir(pwd) 227 if err != nil { 228 t.Fatal(err) 229 } 230 } 231 232 // testSTDINPipe tests pipe stdin to singularity actions cmd 233 func testSTDINPipe(t *testing.T) { 234 tests := []struct { 235 binName string 236 name string 237 argv []string 238 exit int 239 }{ 240 {"sh", "trueSTDIN", []string{"-c", fmt.Sprintf("echo hi | singularity exec %s grep hi", imagePath)}, 0}, 241 {"sh", "falseSTDIN", []string{"-c", fmt.Sprintf("echo bye | singularity exec %s grep hi", imagePath)}, 1}, 242 // Checking permissions 243 {"sh", "permissions", []string{"-c", fmt.Sprintf("singularity exec %s id -u | grep `id -u`", imagePath)}, 0}, 244 // testing run command properly hands arguments 245 {"sh", "arguments", []string{"-c", fmt.Sprintf("singularity run %s foo | grep foo", imagePath)}, 0}, 246 // Stdin to URI based image 247 {"sh", "library", []string{"-c", "echo true | singularity shell library://busybox"}, 0}, 248 {"sh", "docker", []string{"-c", "echo true | singularity shell docker://busybox"}, 0}, 249 {"sh", "shub", []string{"-c", "echo true | singularity shell shub://singularityhub/busybox"}, 0}, 250 // Test apps 251 {"sh", "appsFoo", []string{"-c", fmt.Sprintf("singularity run --app foo %s | grep 'FOO'", appsImage)}, 0}, 252 // Test target pwd 253 {"sh", "pwdPath", []string{"-c", fmt.Sprintf("singularity exec --pwd /etc %s pwd | egrep '^/etc'", imagePath)}, 0}, 254 } 255 256 for _, tt := range tests { 257 t.Run(tt.name, test.WithoutPrivilege(func(t *testing.T) { 258 cmd := exec.Command(tt.binName, tt.argv...) 259 if err := cmd.Start(); err != nil { 260 t.Fatalf("cmd.Start: %v", err) 261 } 262 263 if err := cmd.Wait(); err != nil { 264 exiterr, _ := err.(*exec.ExitError) 265 status, _ := exiterr.Sys().(syscall.WaitStatus) 266 if status.ExitStatus() != tt.exit { 267 // The program has exited with an unexpected exit code 268 { 269 t.Fatalf("unexpected exit code '%v': for cmd %v", status.ExitStatus(), strings.Join(tt.argv, " ")) 270 } 271 } 272 } 273 })) 274 } 275 } 276 277 // testRunFromURI tests min fuctionality for singularity run/exec URI:// 278 func testRunFromURI(t *testing.T) { 279 runScript := "testdata/runscript.sh" 280 bind := fmt.Sprintf("%s:/.singularity.d/runscript", runScript) 281 282 runOpts := opts{ 283 binds: []string{bind}, 284 } 285 286 fi, err := os.Stat(runScript) 287 if err != nil { 288 t.Fatalf("can't find %s", runScript) 289 } 290 size := strconv.Itoa(int(fi.Size())) 291 292 tests := []struct { 293 name string 294 image string 295 action string 296 argv []string 297 opts 298 expectSuccess bool 299 }{ 300 // Run from supported URI's and check the runscript call works 301 {"RunFromDockerOK", "docker://busybox:latest", "run", []string{size}, runOpts, true}, 302 {"RunFromLibraryOK", "library://busybox:latest", "run", []string{size}, runOpts, true}, 303 {"RunFromShubOK", "shub://singularityhub/busybox", "run", []string{size}, runOpts, true}, 304 {"RunFromDockerKO", "docker://busybox:latest", "run", []string{"0"}, runOpts, false}, 305 {"RunFromLibraryKO", "library://busybox:latest", "run", []string{"0"}, runOpts, false}, 306 {"RunFromShubKO", "shub://singularityhub/busybox", "run", []string{"0"}, runOpts, false}, 307 // exec from a supported URI's and check the exit code 308 {"trueDocker", "docker://busybox:latest", "exec", []string{"true"}, opts{}, true}, 309 {"trueLibrary", "library://busybox:latest", "exec", []string{"true"}, opts{}, true}, 310 {"trueShub", "shub://singularityhub/busybox", "exec", []string{"true"}, opts{}, true}, 311 {"falseDocker", "docker://busybox:latest", "exec", []string{"false"}, opts{}, false}, 312 {"falselibrary", "library://busybox:latest", "exec", []string{"false"}, opts{}, false}, 313 {"falseShub", "shub://singularityhub/busybox", "exec", []string{"false"}, opts{}, false}, 314 // exec from URI with user namespace enabled 315 {"trueDockerUserns", "docker://busybox:latest", "exec", []string{"true"}, opts{userns: true}, true}, 316 {"trueLibraryUserns", "library://busybox:latest", "exec", []string{"true"}, opts{userns: true}, true}, 317 {"trueShubUserns", "shub://singularityhub/busybox", "exec", []string{"true"}, opts{userns: true}, true}, 318 {"falseDockerUserns", "docker://busybox:latest", "exec", []string{"false"}, opts{userns: true}, false}, 319 {"falselibraryUserns", "library://busybox:latest", "exec", []string{"false"}, opts{userns: true}, false}, 320 {"falseShubUserns", "shub://singularityhub/busybox", "exec", []string{"false"}, opts{userns: true}, false}, 321 } 322 323 for _, tt := range tests { 324 t.Run(tt.name, test.WithoutPrivilege(func(t *testing.T) { 325 _, stderr, exitCode, err := imageExec(t, tt.action, tt.opts, tt.image, tt.argv) 326 if tt.expectSuccess && (exitCode != 0) { 327 t.Log(stderr) 328 t.Fatalf("unexpected failure running '%v': %v", strings.Join(tt.argv, " "), err) 329 } else if !tt.expectSuccess && (exitCode != 1) { 330 t.Log(stderr) 331 t.Fatalf("unexpected success running '%v'", strings.Join(tt.argv, " ")) 332 } 333 })) 334 } 335 } 336 337 // testPersistentOverlay test the --overlay function 338 func testPersistentOverlay(t *testing.T) { 339 const squashfsImage = "squashfs.simg" 340 // Create the overlay dir 341 cwd, err := os.Getwd() 342 if err != nil { 343 t.Fatal(err) 344 } 345 dir, err := ioutil.TempDir(cwd, "overlay_test") 346 if err != nil { 347 t.Fatal(err) 348 } 349 defer os.RemoveAll(dir) 350 351 // Create dirfs for squashfs 352 squashDir, err := ioutil.TempDir(cwd, "overlay_test") 353 if err != nil { 354 t.Fatal(err) 355 } 356 defer os.RemoveAll(squashDir) 357 358 content := []byte("temporary file's content") 359 tmpfile, err := ioutil.TempFile(squashDir, "bogus") 360 if err != nil { 361 t.Fatal(err) 362 } 363 if _, err := tmpfile.Write(content); err != nil { 364 t.Fatal(err) 365 } 366 if err := tmpfile.Close(); err != nil { 367 t.Fatal(err) 368 } 369 defer os.Remove(tmpfile.Name()) 370 371 cmd := exec.Command("mksquashfs", squashDir, squashfsImage, "-noappend", "-all-root") 372 var out bytes.Buffer 373 cmd.Stdout = &out 374 err = cmd.Run() 375 if err != nil { 376 t.Fatal(err) 377 } 378 defer os.RemoveAll(squashfsImage) 379 380 // Create the overlay ext3 fs 381 cmd = exec.Command("dd", "if=/dev/zero", "of=ext3_fs.img", "bs=1M", "count=768", "status=none") 382 cmd.Stdout = &out 383 err = cmd.Run() 384 if err != nil { 385 t.Fatal(err) 386 } 387 cmd = exec.Command("mkfs.ext3", "-q", "-F", "ext3_fs.img") 388 cmd.Stdout = &out 389 err = cmd.Run() 390 if err != nil { 391 t.Fatal(err) 392 } 393 defer os.Remove("ext3_fs.img") 394 395 // create a file dir 396 t.Run("overlay_create", test.WithPrivilege(func(t *testing.T) { 397 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{dir}}, imagePath, []string{"touch", "/dir_overlay"}) 398 if exitCode != 0 { 399 t.Log(stderr, err) 400 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", "/dir_overlay"}, " ")) 401 } 402 })) 403 // look for the file dir 404 t.Run("overlay_find", test.WithPrivilege(func(t *testing.T) { 405 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{dir}}, imagePath, []string{"test", "-f", "/dir_overlay"}) 406 if exitCode != 0 { 407 t.Log(stderr, err) 408 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", "/dir_overlay"}, " ")) 409 } 410 })) 411 // create a file ext3 412 t.Run("overlay_ext3_create", test.WithPrivilege(func(t *testing.T) { 413 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{"ext3_fs.img"}}, imagePath, []string{"touch", "/ext3_overlay"}) 414 if exitCode != 0 { 415 t.Log(stderr, err) 416 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", "/ext3_overlay"}, " ")) 417 } 418 })) 419 // look for the file ext3 420 t.Run("overlay_ext3_find", test.WithPrivilege(func(t *testing.T) { 421 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{"ext3_fs.img"}}, imagePath, []string{"test", "-f", "/ext3_overlay"}) 422 if exitCode != 0 { 423 t.Log(stderr, err) 424 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", "/ext3_overlay"}, " ")) 425 } 426 })) 427 // look for the file squashFs 428 t.Run("overlay_squashFS_find", test.WithPrivilege(func(t *testing.T) { 429 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{squashfsImage}}, imagePath, []string{"test", "-f", fmt.Sprintf("/%s", tmpfile.Name())}) 430 if exitCode != 0 { 431 t.Log(stderr, err) 432 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", fmt.Sprintf("/%s", tmpfile.Name())}, " ")) 433 } 434 })) 435 // create a file multiple overlays 436 t.Run("overlay_multiple_create", test.WithPrivilege(func(t *testing.T) { 437 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{"ext3_fs.img", squashfsImage}}, imagePath, []string{"touch", "/multiple_overlay_fs"}) 438 if exitCode != 0 { 439 t.Log(stderr, err) 440 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"touch", "/multiple_overlay_fs"}, " ")) 441 } 442 })) 443 // look for the file with multiple overlays 444 t.Run("overlay_multiple_find_ext3", test.WithPrivilege(func(t *testing.T) { 445 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{"ext3_fs.img", squashfsImage}}, imagePath, []string{"test", "-f", "/multiple_overlay_fs"}) 446 if exitCode != 0 { 447 t.Log(stderr, err) 448 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", "multiple_overlay_fs"}, " ")) 449 } 450 })) 451 t.Run("overlay_multiple_find_squashfs", test.WithPrivilege(func(t *testing.T) { 452 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{"ext3_fs.img", squashfsImage}}, imagePath, []string{"test", "-f", fmt.Sprintf("/%s", tmpfile.Name())}) 453 if exitCode != 0 { 454 t.Log(stderr, err) 455 t.Fatalf("unexpected failure running '%v'", strings.Join([]string{"test", "-f", fmt.Sprintf("/%s", tmpfile.Name())}, " ")) 456 } 457 })) 458 // look for the file without root privs 459 t.Run("overlay_noroot", test.WithoutPrivilege(func(t *testing.T) { 460 _, stderr, exitCode, err := imageExec(t, "exec", opts{overlay: []string{dir}}, imagePath, []string{"test", "-f", "/foo_overlay"}) 461 if exitCode != 1 { 462 t.Log(stderr, err) 463 t.Fatalf("unexpected success running '%v'", strings.Join([]string{"test", "-f", "/foo_overlay"}, " ")) 464 } 465 })) 466 // look for the file without --overlay 467 t.Run("overlay_noflag", test.WithPrivilege(func(t *testing.T) { 468 _, stderr, exitCode, err := imageExec(t, "exec", opts{}, imagePath, []string{"test", "-f", "/foo_overlay"}) 469 if exitCode != 1 { 470 t.Log(stderr, err) 471 t.Fatalf("unexpected success running '%v'", strings.Join([]string{"test", "-f", "/foo_overlay"}, " ")) 472 } 473 })) 474 } 475 476 func TestSingularityActions(t *testing.T) { 477 test.EnsurePrivilege(t) 478 opts := buildOpts{ 479 force: true, 480 sandbox: false, 481 } 482 if b, err := imageBuild(opts, imagePath, "../../examples/busybox/Singularity"); err != nil { 483 t.Log(string(b)) 484 t.Fatalf("unexpected failure: %v", err) 485 } 486 defer os.Remove(imagePath) 487 if b, err := imageBuild(opts, appsImage, "../../examples/apps/Singularity"); err != nil { 488 t.Log(string(b)) 489 t.Fatalf("unexpected failure: %v", err) 490 } 491 defer os.Remove(appsImage) 492 493 // singularity run 494 t.Run("run", testSingularityRun) 495 // singularity exec 496 t.Run("exec", testSingularityExec) 497 // stdin pipe 498 t.Run("STDIN", testSTDINPipe) 499 // action_URI 500 t.Run("action_URI", testRunFromURI) 501 // Persistent Overlay 502 t.Run("Persistent_Overlay", testPersistentOverlay) 503 }