github.com/apptainer/singularity@v3.1.1+incompatible/cmd/singularity/instance_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 "bufio" 10 "bytes" 11 "encoding/json" 12 "fmt" 13 "io/ioutil" 14 "net" 15 "os" 16 "os/exec" 17 "path/filepath" 18 "strconv" 19 "testing" 20 21 "github.com/sylabs/singularity/internal/pkg/test" 22 ) 23 24 const ( 25 instanceStartPort = 11372 26 instanceDefinition = "../../examples/instances/Singularity" 27 instanceImagePath = "./instance_tests.sif" 28 ) 29 30 type startOpts struct { 31 addCaps string 32 allowSetuid bool 33 applyCgroups string 34 binds []string 35 boot bool 36 cleanenv bool 37 contain bool 38 containall bool 39 dns string 40 dropCaps string 41 home string 42 hostname string 43 keepPrivs bool 44 net bool 45 network string 46 networkArgs string 47 noHome bool 48 noPrivs bool 49 nv bool 50 overlay string 51 scratch string 52 security string 53 userns bool 54 uts bool 55 workdir string 56 writable bool 57 writableTmpfs bool 58 } 59 60 type listOpts struct { 61 json bool 62 user string 63 container string 64 } 65 66 type stopOpts struct { 67 all bool 68 force bool 69 signal string 70 timeout string 71 user string 72 instance string 73 } 74 75 type instance struct { 76 Instance string `json:"instance"` 77 Pid int `json:"pid"` 78 Image string `json:"img"` 79 } 80 81 type instanceList struct { 82 Instances []instance `json:"instances"` 83 } 84 85 func startInstance(image string, instance string, portOffset int, opts startOpts) ([]byte, error) { 86 args := []string{"instance", "start"} 87 if opts.addCaps != "" { 88 args = append(args, "--add-caps", opts.addCaps) 89 } 90 if opts.allowSetuid { 91 args = append(args, "--allow-setuid") 92 } 93 if opts.applyCgroups != "" { 94 args = append(args, "--apply-cgroups", opts.applyCgroups) 95 } 96 for _, bind := range opts.binds { 97 args = append(args, "--bind", bind) 98 } 99 if opts.boot { 100 args = append(args, "--boot") 101 } 102 if opts.cleanenv { 103 args = append(args, "--cleanenv") 104 } 105 if opts.contain { 106 args = append(args, "--contain") 107 } 108 if opts.containall { 109 args = append(args, "--containall") 110 } 111 if opts.dns != "" { 112 args = append(args, "--dns", opts.dns) 113 } 114 if opts.dropCaps != "" { 115 args = append(args, "--drop-caps", opts.dropCaps) 116 } 117 if opts.home != "" { 118 args = append(args, "--home", opts.home) 119 } 120 if opts.hostname != "" { 121 args = append(args, "--hostname", opts.hostname) 122 } 123 if opts.keepPrivs { 124 args = append(args, "--keep-privs") 125 } 126 if opts.net { 127 args = append(args, "--net") 128 } 129 if opts.network != "" { 130 args = append(args, "--network", opts.network) 131 } 132 if opts.networkArgs != "" { 133 args = append(args, "--network-args", opts.networkArgs) 134 } 135 if opts.noHome { 136 args = append(args, "--no-home") 137 } 138 if opts.noPrivs { 139 args = append(args, "--no-privs") 140 } 141 if opts.nv { 142 args = append(args, "--nv") 143 } 144 if opts.overlay != "" { 145 args = append(args, "--overlay", opts.overlay) 146 } 147 if opts.scratch != "" { 148 args = append(args, "--scratch", opts.scratch) 149 } 150 if opts.security != "" { 151 args = append(args, "--security", opts.security) 152 } 153 if opts.userns { 154 args = append(args, "--userns") 155 } 156 if opts.uts { 157 args = append(args, "--uts") 158 } 159 if opts.workdir != "" { 160 args = append(args, "--workdir", opts.workdir) 161 } 162 if opts.writable { 163 args = append(args, "--writable") 164 } 165 if opts.writableTmpfs { 166 args = append(args, "--writable-tmpfs") 167 } 168 args = append(args, image, instance, strconv.Itoa(instanceStartPort+portOffset)) 169 cmd := exec.Command(cmdPath, args...) 170 return cmd.CombinedOutput() 171 } 172 173 func listInstance(opts listOpts) ([]byte, error) { 174 args := []string{"instance", "list"} 175 if opts.json { 176 args = append(args, "--json") 177 } 178 if opts.user != "" { 179 args = append(args, "--user", opts.user) 180 } 181 if opts.container != "" { 182 args = append(args, opts.container) 183 } 184 cmd := exec.Command(cmdPath, args...) 185 return cmd.CombinedOutput() 186 } 187 188 func stopInstance(opts stopOpts) ([]byte, error) { 189 args := []string{"instance", "stop"} 190 if opts.all { 191 args = append(args, "--all") 192 } 193 if opts.force { 194 args = append(args, "--force") 195 } 196 if opts.signal != "" { 197 args = append(args, "--signal", opts.signal) 198 } 199 if opts.timeout != "" { 200 args = append(args, "--timeout", opts.timeout) 201 } 202 if opts.user != "" { 203 args = append(args, "--user", opts.user) 204 } 205 if opts.instance != "" { 206 args = append(args, opts.instance) 207 } 208 cmd := exec.Command(cmdPath, args...) 209 return cmd.CombinedOutput() 210 } 211 212 func execInstance(instance string, execCmd ...string) ([]byte, error) { 213 args := []string{"exec", "instance://" + instance} 214 args = append(args, execCmd...) 215 cmd := exec.Command(cmdPath, args...) 216 return cmd.CombinedOutput() 217 } 218 219 // Sends a deterministic message to an echo server and expects the same message 220 // in response. 221 func echo(t *testing.T, port int) { 222 const message = "b40cbeaaea293f7e8bd40fb61f389cfca9823467\n" 223 sock, sockErr := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port)) 224 if sockErr != nil { 225 t.Fatalf("Failed to dial echo server: %v", sockErr) 226 } 227 fmt.Fprintf(sock, message) 228 response, responseErr := bufio.NewReader(sock).ReadString('\n') 229 if responseErr != nil || response != message { 230 t.Fatalf("Bad response: err = %v, response = %v", responseErr, response) 231 } 232 } 233 234 // Return the number of currently running instances. 235 func getNumberOfInstances(t *testing.T) int { 236 output, err := listInstance(listOpts{json: true}) 237 if err != nil { 238 t.Fatalf("Error listing instances: %v. Output:\n%s", err, string(output)) 239 } 240 var instances instanceList 241 if err = json.Unmarshal(output, &instances); err != nil { 242 t.Fatalf("Error decoding JSON from listInstance: %v", err) 243 } 244 return len(instances.Instances) 245 } 246 247 // Test that no instances are running. 248 func testNoInstances(t *testing.T) { 249 if n := getNumberOfInstances(t); n != 0 { 250 t.Fatalf("There are %d instances running, but there should be 0.\n", n) 251 } 252 } 253 254 // Test that a basic echo server instance can be started, communicated with, 255 // and stopped. 256 func testBasicEchoServer(t *testing.T) { 257 const instanceName = "echo1" 258 // Start the instance. 259 _, err := startInstance(instanceImagePath, instanceName, 0, startOpts{}) 260 if err != nil { 261 t.Fatalf("Failed to start instance %s: %v", instanceName, err) 262 } 263 // Try to contact the instance. 264 echo(t, instanceStartPort) 265 // Stop the instance. 266 _, err = stopInstance(stopOpts{instance: instanceName}) 267 if err != nil { 268 t.Fatalf("Failed to stop instance %s: %v", instanceName, err) 269 } 270 } 271 272 // Test creating many instances, but don't stop them. 273 func testCreateManyInstances(t *testing.T) { 274 const n = 10 275 // Start n instances. 276 for i := 0; i < n; i++ { 277 instanceName := "echo" + strconv.Itoa(i+1) 278 _, err := startInstance(instanceImagePath, instanceName, i, startOpts{}) 279 if err != nil { 280 t.Fatalf("Failed to start instance %s: %v", instanceName, err) 281 } 282 } 283 // Verify all instances started. 284 if numStarted := getNumberOfInstances(t); numStarted != n { 285 t.Fatalf("Expected %d instances, but see %d.", n, numStarted) 286 } 287 // Echo all n instances. 288 for i := 0; i < n; i++ { 289 echo(t, instanceStartPort+i) 290 } 291 } 292 293 // Test stopping all running instances. 294 func testStopAll(t *testing.T) { 295 _, err := stopInstance(stopOpts{all: true}) 296 if err != nil { 297 t.Fatalf("Failed to stop all instances: %v", err) 298 } 299 } 300 301 // Test basic options like mounting a custom home directory, changing the 302 // hostname, etc. 303 func testBasicOptions(t *testing.T) { 304 const fileName = "hello" 305 const instanceName = "testbasic" 306 const testHostname = "echoserver99" 307 fileContents := []byte("world") 308 309 // Create a temporary directory to serve as a home directory. 310 dir, err := ioutil.TempDir("", "TestInstance") 311 if err != nil { 312 t.Fatalf("Failed to create temporary directory: %v", err) 313 } 314 defer os.RemoveAll(dir) 315 // Create and populate a temporary file. 316 tempFile := filepath.Join(dir, fileName) 317 err = ioutil.WriteFile(tempFile, fileContents, 0644) 318 if err != nil { 319 t.Fatalf("Failed to create file %s: %v", tempFile, err) 320 } 321 instanceOpts := startOpts{ 322 home: dir + ":/home/temp", 323 hostname: testHostname, 324 cleanenv: true, 325 } 326 // Start an instance with the temporary directory as the home directory. 327 _, err = startInstance(instanceImagePath, instanceName, 0, instanceOpts) 328 if err != nil { 329 t.Fatalf("Failed to start instance %s: %v", instanceName, err) 330 } 331 // Verify we can see the file's contents from within the container. 332 output, err := execInstance(instanceName, "cat", "/home/temp/"+fileName) 333 if err != nil { 334 t.Fatalf("Error executing command on instance %s: %v", instanceName, err) 335 } 336 if !bytes.Equal(fileContents, output) { 337 t.Fatalf("File contents were %s, but expected %s", output, fileContents) 338 } 339 // Verify that the hostname has been set correctly. 340 output, err = execInstance(instanceName, "hostname") 341 if err != nil { 342 t.Fatalf("Error executing command on instance %s: %v", instanceName, err) 343 } 344 if !bytes.Equal([]byte(testHostname+"\n"), output) { 345 t.Fatalf("Hostname is %s, but expected %s", output, testHostname) 346 } 347 // Stop the container. 348 _, err = stopInstance(stopOpts{instance: instanceName}) 349 if err != nil { 350 t.Fatalf("Failed to stop instance %s: %v", instanceName, err) 351 } 352 } 353 354 // Test that contain works. 355 func testContain(t *testing.T) { 356 const instanceName = "testcontain" 357 const fileName = "thegreattestfile" 358 // Create a temporary directory to serve as a contain directory. 359 dir, err := ioutil.TempDir("", "TestInstance") 360 if err != nil { 361 t.Fatalf("Failed to create temporary directory: %v", err) 362 } 363 defer os.RemoveAll(dir) 364 instanceOpts := startOpts{ 365 contain: true, 366 workdir: dir, 367 } 368 // Start an instance with the temporary directory as the home directory. 369 _, err = startInstance(instanceImagePath, instanceName, 0, instanceOpts) 370 if err != nil { 371 t.Fatalf("Failed to start instance %s: %v", instanceName, err) 372 } 373 // Touch a file within /tmp. 374 _, err = execInstance(instanceName, "touch", "/tmp/"+fileName) 375 if err != nil { 376 t.Fatalf("Failed to touch a file: %v", err) 377 } 378 // Stop the container. 379 _, err = stopInstance(stopOpts{instance: instanceName}) 380 if err != nil { 381 t.Fatalf("Failed to stop instance %s: %v", instanceName, err) 382 } 383 // Verify that the touched file exists outside the container. 384 if _, err = os.Stat(filepath.Join(dir, "tmp", fileName)); os.IsNotExist(err) { 385 t.Fatal("The temp file doesn't exist.") 386 } 387 } 388 389 // Test by running directly from URI 390 func testInstanceFromURI(t *testing.T) { 391 instances := []struct { 392 name string 393 uri string 394 }{ 395 { 396 name: "test_from_docker", 397 uri: "docker://busybox", 398 }, 399 { 400 name: "test_from_library", 401 uri: "library://busybox", 402 }, 403 { 404 name: "test_from_shub", 405 uri: "shub://singularityhub/busybox", 406 }, 407 } 408 409 for _, i := range instances { 410 // Start an instance with the temporary directory as the home directory. 411 _, err := startInstance(i.uri, i.name, 0, startOpts{}) 412 if err != nil { 413 t.Fatalf("Failed to start instance %s: %v", i.name, err) 414 } 415 // Exec id command. 416 _, err = execInstance(i.name, "id") 417 if err != nil { 418 t.Fatalf("Failed to run id command: %v", err) 419 } 420 // Stop the container. 421 _, err = stopInstance(stopOpts{instance: i.name}) 422 if err != nil { 423 t.Fatalf("Failed to stop instance %s: %v", i.name, err) 424 } 425 } 426 } 427 428 // Bootstrap to run all instance tests. 429 func TestInstance(t *testing.T) { 430 // Build a basic Singularity image to test instances. 431 if b, err := imageBuild(buildOpts{force: true, sandbox: false}, instanceImagePath, instanceDefinition); err != nil { 432 t.Log(string(b)) 433 t.Fatalf("unexpected failure: %v", err) 434 } 435 imageVerify(t, instanceImagePath, true) 436 defer os.RemoveAll(instanceImagePath) 437 // Define and loop through tests. 438 tests := []struct { 439 name string 440 function func(*testing.T) 441 privileged bool 442 }{ 443 {"InitialNoInstances", testNoInstances, false}, 444 {"BasicEchoServer", testBasicEchoServer, false}, 445 {"BasicOptions", testBasicOptions, false}, 446 {"Contain", testContain, false}, 447 {"InstanceFromURI", testInstanceFromURI, false}, 448 {"CreateManyInstances", testCreateManyInstances, false}, 449 {"StopAll", testStopAll, false}, 450 {"FinalNoInstances", testNoInstances, false}, 451 } 452 for _, tt := range tests { 453 var wrappedFn func(*testing.T) 454 if tt.privileged { 455 wrappedFn = test.WithPrivilege(tt.function) 456 } else { 457 wrappedFn = test.WithoutPrivilege(tt.function) 458 } 459 t.Run(tt.name, wrappedFn) 460 } 461 }