github.com/tri-adam/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  }