github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/docker/driver_unix_test.go (about)

     1  //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
     2  
     3  package docker
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	docker "github.com/fsouza/go-dockerclient"
    18  	"github.com/hashicorp/nomad/ci"
    19  	"github.com/hashicorp/nomad/client/allocdir"
    20  	"github.com/hashicorp/nomad/client/testutil"
    21  	"github.com/hashicorp/nomad/helper/freeport"
    22  	"github.com/hashicorp/nomad/helper/uuid"
    23  	"github.com/hashicorp/nomad/plugins/drivers"
    24  	dtestutil "github.com/hashicorp/nomad/plugins/drivers/testutils"
    25  	tu "github.com/hashicorp/nomad/testutil"
    26  	"github.com/stretchr/testify/assert"
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  func TestDockerDriver_User(t *testing.T) {
    31  	ci.Parallel(t)
    32  	testutil.DockerCompatible(t)
    33  
    34  	task, cfg, ports := dockerTask(t)
    35  	defer freeport.Return(ports)
    36  	task.User = "alice"
    37  	cfg.Command = "/bin/sleep"
    38  	cfg.Args = []string{"10000"}
    39  	require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
    40  
    41  	d := dockerDriverHarness(t, nil)
    42  	cleanup := d.MkAllocDir(task, true)
    43  	defer cleanup()
    44  	copyImage(t, task.TaskDir(), "busybox.tar")
    45  
    46  	_, _, err := d.StartTask(task)
    47  	if err == nil {
    48  		d.DestroyTask(task.ID, true)
    49  		t.Fatalf("Should've failed")
    50  	}
    51  
    52  	if !strings.Contains(err.Error(), "alice") {
    53  		t.Fatalf("Expected failure string not found, found %q instead", err.Error())
    54  	}
    55  }
    56  
    57  func TestDockerDriver_NetworkAliases_Bridge(t *testing.T) {
    58  	ci.Parallel(t)
    59  	testutil.DockerCompatible(t)
    60  
    61  	require := require.New(t)
    62  
    63  	// Because go-dockerclient doesn't provide api for query network aliases, just check that
    64  	// a container can be created with a 'network_aliases' property
    65  
    66  	// Create network, network-scoped alias is supported only for containers in user defined networks
    67  	client := newTestDockerClient(t)
    68  	networkOpts := docker.CreateNetworkOptions{Name: "foobar", Driver: "bridge"}
    69  	network, err := client.CreateNetwork(networkOpts)
    70  	require.NoError(err)
    71  	defer client.RemoveNetwork(network.ID)
    72  
    73  	expected := []string{"foobar"}
    74  	taskCfg := newTaskConfig("", busyboxLongRunningCmd)
    75  	taskCfg.NetworkMode = network.Name
    76  	taskCfg.NetworkAliases = expected
    77  	task := &drivers.TaskConfig{
    78  		ID:        uuid.Generate(),
    79  		Name:      "busybox",
    80  		Resources: basicResources,
    81  	}
    82  	require.NoError(task.EncodeConcreteDriverConfig(&taskCfg))
    83  
    84  	d := dockerDriverHarness(t, nil)
    85  	cleanup := d.MkAllocDir(task, true)
    86  	defer cleanup()
    87  	copyImage(t, task.TaskDir(), "busybox.tar")
    88  
    89  	_, _, err = d.StartTask(task)
    90  	require.NoError(err)
    91  	require.NoError(d.WaitUntilStarted(task.ID, 5*time.Second))
    92  
    93  	defer d.DestroyTask(task.ID, true)
    94  
    95  	dockerDriver, ok := d.Impl().(*Driver)
    96  	require.True(ok)
    97  
    98  	handle, ok := dockerDriver.tasks.Get(task.ID)
    99  	require.True(ok)
   100  
   101  	_, err = client.InspectContainer(handle.containerID)
   102  	require.NoError(err)
   103  }
   104  
   105  func TestDockerDriver_NetworkMode_Host(t *testing.T) {
   106  	ci.Parallel(t)
   107  	testutil.DockerCompatible(t)
   108  	expected := "host"
   109  
   110  	taskCfg := newTaskConfig("", busyboxLongRunningCmd)
   111  	taskCfg.NetworkMode = expected
   112  
   113  	task := &drivers.TaskConfig{
   114  		ID:        uuid.Generate(),
   115  		Name:      "busybox-demo",
   116  		Resources: basicResources,
   117  	}
   118  	require.NoError(t, task.EncodeConcreteDriverConfig(&taskCfg))
   119  
   120  	d := dockerDriverHarness(t, nil)
   121  	cleanup := d.MkAllocDir(task, true)
   122  	defer cleanup()
   123  	copyImage(t, task.TaskDir(), "busybox.tar")
   124  
   125  	_, _, err := d.StartTask(task)
   126  	require.NoError(t, err)
   127  
   128  	require.NoError(t, d.WaitUntilStarted(task.ID, 5*time.Second))
   129  
   130  	defer d.DestroyTask(task.ID, true)
   131  
   132  	dockerDriver, ok := d.Impl().(*Driver)
   133  	require.True(t, ok)
   134  
   135  	handle, ok := dockerDriver.tasks.Get(task.ID)
   136  	require.True(t, ok)
   137  
   138  	container, err := client.InspectContainer(handle.containerID)
   139  	if err != nil {
   140  		t.Fatalf("err: %v", err)
   141  	}
   142  
   143  	actual := container.HostConfig.NetworkMode
   144  	require.Equal(t, expected, actual)
   145  }
   146  
   147  func TestDockerDriver_CPUCFSPeriod(t *testing.T) {
   148  	ci.Parallel(t)
   149  	testutil.DockerCompatible(t)
   150  
   151  	task, cfg, ports := dockerTask(t)
   152  	defer freeport.Return(ports)
   153  	cfg.CPUHardLimit = true
   154  	cfg.CPUCFSPeriod = 1000000
   155  	require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   156  
   157  	client, _, handle, cleanup := dockerSetup(t, task, nil)
   158  	defer cleanup()
   159  
   160  	waitForExist(t, client, handle.containerID)
   161  
   162  	container, err := client.InspectContainer(handle.containerID)
   163  	require.NoError(t, err)
   164  
   165  	require.Equal(t, cfg.CPUCFSPeriod, container.HostConfig.CPUPeriod)
   166  }
   167  
   168  func TestDockerDriver_Sysctl_Ulimit(t *testing.T) {
   169  	ci.Parallel(t)
   170  	testutil.DockerCompatible(t)
   171  
   172  	task, cfg, ports := dockerTask(t)
   173  	defer freeport.Return(ports)
   174  	expectedUlimits := map[string]string{
   175  		"nproc":  "4242",
   176  		"nofile": "2048:4096",
   177  	}
   178  	cfg.Sysctl = map[string]string{
   179  		"net.core.somaxconn": "16384",
   180  	}
   181  	cfg.Ulimit = expectedUlimits
   182  	require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   183  
   184  	client, d, handle, cleanup := dockerSetup(t, task, nil)
   185  	defer cleanup()
   186  	require.NoError(t, d.WaitUntilStarted(task.ID, 5*time.Second))
   187  
   188  	container, err := client.InspectContainer(handle.containerID)
   189  	assert.Nil(t, err, "unexpected error: %v", err)
   190  
   191  	want := "16384"
   192  	got := container.HostConfig.Sysctls["net.core.somaxconn"]
   193  	assert.Equal(t, want, got, "Wrong net.core.somaxconn config for docker job. Expect: %s, got: %s", want, got)
   194  
   195  	expectedUlimitLen := 2
   196  	actualUlimitLen := len(container.HostConfig.Ulimits)
   197  	assert.Equal(t, want, got, "Wrong number of ulimit configs for docker job. Expect: %d, got: %d", expectedUlimitLen, actualUlimitLen)
   198  
   199  	for _, got := range container.HostConfig.Ulimits {
   200  		if expectedStr, ok := expectedUlimits[got.Name]; !ok {
   201  			t.Errorf("%s config unexpected for docker job.", got.Name)
   202  		} else {
   203  			if !strings.Contains(expectedStr, ":") {
   204  				expectedStr = expectedStr + ":" + expectedStr
   205  			}
   206  
   207  			splitted := strings.SplitN(expectedStr, ":", 2)
   208  			soft, _ := strconv.Atoi(splitted[0])
   209  			hard, _ := strconv.Atoi(splitted[1])
   210  			assert.Equal(t, int64(soft), got.Soft, "Wrong soft %s ulimit for docker job. Expect: %d, got: %d", got.Name, soft, got.Soft)
   211  			assert.Equal(t, int64(hard), got.Hard, "Wrong hard %s ulimit for docker job. Expect: %d, got: %d", got.Name, hard, got.Hard)
   212  
   213  		}
   214  	}
   215  }
   216  
   217  func TestDockerDriver_Sysctl_Ulimit_Errors(t *testing.T) {
   218  	ci.Parallel(t)
   219  	testutil.DockerCompatible(t)
   220  
   221  	brokenConfigs := []map[string]string{
   222  		{
   223  			"nofile": "",
   224  		},
   225  		{
   226  			"nofile": "abc:1234",
   227  		},
   228  		{
   229  			"nofile": "1234:abc",
   230  		},
   231  	}
   232  
   233  	testCases := []struct {
   234  		ulimitConfig map[string]string
   235  		err          error
   236  	}{
   237  		{brokenConfigs[0], fmt.Errorf("Malformed ulimit specification nofile: \"\", cannot be empty")},
   238  		{brokenConfigs[1], fmt.Errorf("Malformed soft ulimit nofile: abc:1234")},
   239  		{brokenConfigs[2], fmt.Errorf("Malformed hard ulimit nofile: 1234:abc")},
   240  	}
   241  
   242  	for _, tc := range testCases {
   243  		task, cfg, ports := dockerTask(t)
   244  		cfg.Ulimit = tc.ulimitConfig
   245  		require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   246  
   247  		d := dockerDriverHarness(t, nil)
   248  		cleanup := d.MkAllocDir(task, true)
   249  		defer cleanup()
   250  		copyImage(t, task.TaskDir(), "busybox.tar")
   251  
   252  		_, _, err := d.StartTask(task)
   253  		require.NotNil(t, err, "Expected non nil error")
   254  		require.Contains(t, err.Error(), tc.err.Error())
   255  		freeport.Return(ports)
   256  	}
   257  }
   258  
   259  // This test does not run on Windows due to stricter path validation in the
   260  // negative case for non existent mount paths. We should write a similar test
   261  // for windows.
   262  func TestDockerDriver_BindMountsHonorVolumesEnabledFlag(t *testing.T) {
   263  	ci.Parallel(t)
   264  	testutil.DockerCompatible(t)
   265  
   266  	allocDir := "/tmp/nomad/alloc-dir"
   267  
   268  	cases := []struct {
   269  		name            string
   270  		requiresVolumes bool
   271  
   272  		volumeDriver string
   273  		volumes      []string
   274  
   275  		expectedVolumes []string
   276  	}{
   277  		{
   278  			name:            "basic plugin",
   279  			requiresVolumes: true,
   280  			volumeDriver:    "nfs",
   281  			volumes:         []string{"test-path:/tmp/taskpath"},
   282  			expectedVolumes: []string{"test-path:/tmp/taskpath"},
   283  		},
   284  		{
   285  			name:            "absolute default driver",
   286  			requiresVolumes: true,
   287  			volumeDriver:    "",
   288  			volumes:         []string{"/abs/test-path:/tmp/taskpath"},
   289  			expectedVolumes: []string{"/abs/test-path:/tmp/taskpath"},
   290  		},
   291  		{
   292  			name:            "absolute local driver",
   293  			requiresVolumes: true,
   294  			volumeDriver:    "local",
   295  			volumes:         []string{"/abs/test-path:/tmp/taskpath"},
   296  			expectedVolumes: []string{"/abs/test-path:/tmp/taskpath"},
   297  		},
   298  		{
   299  			name:            "relative default driver",
   300  			requiresVolumes: false,
   301  			volumeDriver:    "",
   302  			volumes:         []string{"test-path:/tmp/taskpath"},
   303  			expectedVolumes: []string{"/tmp/nomad/alloc-dir/demo/test-path:/tmp/taskpath"},
   304  		},
   305  		{
   306  			name:            "named volume local driver",
   307  			requiresVolumes: true,
   308  			volumeDriver:    "local",
   309  			volumes:         []string{"test-path:/tmp/taskpath"},
   310  			expectedVolumes: []string{"test-path:/tmp/taskpath"},
   311  		},
   312  		{
   313  			name:            "relative outside task-dir default driver",
   314  			requiresVolumes: false,
   315  			volumeDriver:    "",
   316  			volumes:         []string{"../test-path:/tmp/taskpath"},
   317  			expectedVolumes: []string{"/tmp/nomad/alloc-dir/test-path:/tmp/taskpath"},
   318  		},
   319  		{
   320  			name:            "relative outside alloc-dir default driver",
   321  			requiresVolumes: true,
   322  			volumeDriver:    "",
   323  			volumes:         []string{"../../test-path:/tmp/taskpath"},
   324  			expectedVolumes: []string{"/tmp/nomad/test-path:/tmp/taskpath"},
   325  		},
   326  		{
   327  			name:            "clean path local driver",
   328  			requiresVolumes: true,
   329  			volumeDriver:    "local",
   330  			volumes:         []string{"/tmp/nomad/../test-path:/tmp/taskpath"},
   331  			expectedVolumes: []string{"/tmp/test-path:/tmp/taskpath"},
   332  		},
   333  	}
   334  
   335  	t.Run("with volumes enabled", func(t *testing.T) {
   336  		dh := dockerDriverHarness(t, nil)
   337  		driver := dh.Impl().(*Driver)
   338  		driver.config.Volumes.Enabled = true
   339  
   340  		for _, c := range cases {
   341  			t.Run(c.name, func(t *testing.T) {
   342  				task, cfg, ports := dockerTask(t)
   343  				defer freeport.Return(ports)
   344  				cfg.VolumeDriver = c.volumeDriver
   345  				cfg.Volumes = c.volumes
   346  
   347  				task.AllocDir = allocDir
   348  				task.Name = "demo"
   349  
   350  				require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   351  
   352  				cc, err := driver.createContainerConfig(task, cfg, "org/repo:0.1")
   353  				require.NoError(t, err)
   354  
   355  				for _, v := range c.expectedVolumes {
   356  					require.Contains(t, cc.HostConfig.Binds, v)
   357  				}
   358  			})
   359  		}
   360  	})
   361  
   362  	t.Run("with volumes disabled", func(t *testing.T) {
   363  		dh := dockerDriverHarness(t, nil)
   364  		driver := dh.Impl().(*Driver)
   365  		driver.config.Volumes.Enabled = false
   366  
   367  		for _, c := range cases {
   368  			t.Run(c.name, func(t *testing.T) {
   369  				task, cfg, ports := dockerTask(t)
   370  				defer freeport.Return(ports)
   371  				cfg.VolumeDriver = c.volumeDriver
   372  				cfg.Volumes = c.volumes
   373  
   374  				task.AllocDir = allocDir
   375  				task.Name = "demo"
   376  
   377  				require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   378  
   379  				cc, err := driver.createContainerConfig(task, cfg, "org/repo:0.1")
   380  				if c.requiresVolumes {
   381  					require.Error(t, err, "volumes are not enabled")
   382  				} else {
   383  					require.NoError(t, err)
   384  
   385  					for _, v := range c.expectedVolumes {
   386  						require.Contains(t, cc.HostConfig.Binds, v)
   387  					}
   388  				}
   389  			})
   390  		}
   391  	})
   392  }
   393  
   394  // This test does not run on windows due to differences in the definition of
   395  // an absolute path, changing path expansion behaviour. A similar test should
   396  // be written for windows.
   397  func TestDockerDriver_MountsSerialization(t *testing.T) {
   398  	ci.Parallel(t)
   399  	testutil.DockerCompatible(t)
   400  
   401  	allocDir := "/tmp/nomad/alloc-dir"
   402  
   403  	cases := []struct {
   404  		name            string
   405  		requiresVolumes bool
   406  		passedMounts    []DockerMount
   407  		expectedMounts  []docker.HostMount
   408  	}{
   409  		{
   410  			name:            "basic volume",
   411  			requiresVolumes: true,
   412  			passedMounts: []DockerMount{
   413  				{
   414  					Target:   "/nomad",
   415  					ReadOnly: true,
   416  					Source:   "test",
   417  				},
   418  			},
   419  			expectedMounts: []docker.HostMount{
   420  				{
   421  					Type:          "volume",
   422  					Target:        "/nomad",
   423  					Source:        "test",
   424  					ReadOnly:      true,
   425  					VolumeOptions: &docker.VolumeOptions{},
   426  				},
   427  			},
   428  		},
   429  		{
   430  			name: "basic bind",
   431  			passedMounts: []DockerMount{
   432  				{
   433  					Type:   "bind",
   434  					Target: "/nomad",
   435  					Source: "test",
   436  				},
   437  			},
   438  			expectedMounts: []docker.HostMount{
   439  				{
   440  					Type:        "bind",
   441  					Target:      "/nomad",
   442  					Source:      "/tmp/nomad/alloc-dir/demo/test",
   443  					BindOptions: &docker.BindOptions{},
   444  				},
   445  			},
   446  		},
   447  		{
   448  			name:            "basic absolute bind",
   449  			requiresVolumes: true,
   450  			passedMounts: []DockerMount{
   451  				{
   452  					Type:   "bind",
   453  					Target: "/nomad",
   454  					Source: "/tmp/test",
   455  				},
   456  			},
   457  			expectedMounts: []docker.HostMount{
   458  				{
   459  					Type:        "bind",
   460  					Target:      "/nomad",
   461  					Source:      "/tmp/test",
   462  					BindOptions: &docker.BindOptions{},
   463  				},
   464  			},
   465  		},
   466  		{
   467  			name:            "bind relative outside",
   468  			requiresVolumes: true,
   469  			passedMounts: []DockerMount{
   470  				{
   471  					Type:   "bind",
   472  					Target: "/nomad",
   473  					Source: "../../test",
   474  				},
   475  			},
   476  			expectedMounts: []docker.HostMount{
   477  				{
   478  					Type:        "bind",
   479  					Target:      "/nomad",
   480  					Source:      "/tmp/nomad/test",
   481  					BindOptions: &docker.BindOptions{},
   482  				},
   483  			},
   484  		},
   485  		{
   486  			name:            "basic tmpfs",
   487  			requiresVolumes: false,
   488  			passedMounts: []DockerMount{
   489  				{
   490  					Type:   "tmpfs",
   491  					Target: "/nomad",
   492  					TmpfsOptions: DockerTmpfsOptions{
   493  						SizeBytes: 321,
   494  						Mode:      0666,
   495  					},
   496  				},
   497  			},
   498  			expectedMounts: []docker.HostMount{
   499  				{
   500  					Type:   "tmpfs",
   501  					Target: "/nomad",
   502  					TempfsOptions: &docker.TempfsOptions{
   503  						SizeBytes: 321,
   504  						Mode:      0666,
   505  					},
   506  				},
   507  			},
   508  		},
   509  	}
   510  
   511  	t.Run("with volumes enabled", func(t *testing.T) {
   512  		dh := dockerDriverHarness(t, nil)
   513  		driver := dh.Impl().(*Driver)
   514  		driver.config.Volumes.Enabled = true
   515  
   516  		for _, c := range cases {
   517  			t.Run(c.name, func(t *testing.T) {
   518  				task, cfg, ports := dockerTask(t)
   519  				defer freeport.Return(ports)
   520  				cfg.Mounts = c.passedMounts
   521  
   522  				task.AllocDir = allocDir
   523  				task.Name = "demo"
   524  
   525  				require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   526  
   527  				cc, err := driver.createContainerConfig(task, cfg, "org/repo:0.1")
   528  				require.NoError(t, err)
   529  				require.EqualValues(t, c.expectedMounts, cc.HostConfig.Mounts)
   530  			})
   531  		}
   532  	})
   533  
   534  	t.Run("with volumes disabled", func(t *testing.T) {
   535  		dh := dockerDriverHarness(t, nil)
   536  		driver := dh.Impl().(*Driver)
   537  		driver.config.Volumes.Enabled = false
   538  
   539  		for _, c := range cases {
   540  			t.Run(c.name, func(t *testing.T) {
   541  				task, cfg, ports := dockerTask(t)
   542  				defer freeport.Return(ports)
   543  				cfg.Mounts = c.passedMounts
   544  
   545  				task.AllocDir = allocDir
   546  				task.Name = "demo"
   547  
   548  				require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   549  
   550  				cc, err := driver.createContainerConfig(task, cfg, "org/repo:0.1")
   551  				if c.requiresVolumes {
   552  					require.Error(t, err, "volumes are not enabled")
   553  				} else {
   554  					require.NoError(t, err)
   555  					require.EqualValues(t, c.expectedMounts, cc.HostConfig.Mounts)
   556  				}
   557  			})
   558  		}
   559  	})
   560  }
   561  
   562  // TestDockerDriver_CreateContainerConfig_MountsCombined asserts that
   563  // devices and mounts set by device managers/plugins are honored
   564  // and present in docker.CreateContainerOptions, and that it is appended
   565  // to any devices/mounts a user sets in the task config.
   566  func TestDockerDriver_CreateContainerConfig_MountsCombined(t *testing.T) {
   567  	ci.Parallel(t)
   568  	testutil.DockerCompatible(t)
   569  
   570  	task, cfg, ports := dockerTask(t)
   571  	defer freeport.Return(ports)
   572  
   573  	task.Devices = []*drivers.DeviceConfig{
   574  		{
   575  			HostPath:    "/dev/fuse",
   576  			TaskPath:    "/container/dev/task-fuse",
   577  			Permissions: "rw",
   578  		},
   579  	}
   580  	task.Mounts = []*drivers.MountConfig{
   581  		{
   582  			HostPath: "/tmp/task-mount",
   583  			TaskPath: "/container/tmp/task-mount",
   584  			Readonly: true,
   585  		},
   586  	}
   587  
   588  	cfg.Devices = []DockerDevice{
   589  		{
   590  			HostPath:          "/dev/stdout",
   591  			ContainerPath:     "/container/dev/cfg-stdout",
   592  			CgroupPermissions: "rwm",
   593  		},
   594  	}
   595  	cfg.Mounts = []DockerMount{
   596  		{
   597  			Type:     "bind",
   598  			Source:   "/tmp/cfg-mount",
   599  			Target:   "/container/tmp/cfg-mount",
   600  			ReadOnly: false,
   601  		},
   602  	}
   603  
   604  	require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   605  
   606  	dh := dockerDriverHarness(t, nil)
   607  	driver := dh.Impl().(*Driver)
   608  	driver.config.Volumes.Enabled = true
   609  
   610  	c, err := driver.createContainerConfig(task, cfg, "org/repo:0.1")
   611  	require.NoError(t, err)
   612  	expectedMounts := []docker.HostMount{
   613  		{
   614  			Type:     "bind",
   615  			Source:   "/tmp/cfg-mount",
   616  			Target:   "/container/tmp/cfg-mount",
   617  			ReadOnly: false,
   618  			BindOptions: &docker.BindOptions{
   619  				Propagation: "",
   620  			},
   621  		},
   622  		{
   623  			Type:     "bind",
   624  			Source:   "/tmp/task-mount",
   625  			Target:   "/container/tmp/task-mount",
   626  			ReadOnly: true,
   627  			BindOptions: &docker.BindOptions{
   628  				Propagation: "rprivate",
   629  			},
   630  		},
   631  	}
   632  
   633  	if runtime.GOOS != "linux" {
   634  		expectedMounts[0].BindOptions = &docker.BindOptions{}
   635  		expectedMounts[1].BindOptions = &docker.BindOptions{}
   636  	}
   637  
   638  	foundMounts := c.HostConfig.Mounts
   639  	sort.Slice(foundMounts, func(i, j int) bool {
   640  		return foundMounts[i].Target < foundMounts[j].Target
   641  	})
   642  	require.EqualValues(t, expectedMounts, foundMounts)
   643  
   644  	expectedDevices := []docker.Device{
   645  		{
   646  			PathOnHost:        "/dev/stdout",
   647  			PathInContainer:   "/container/dev/cfg-stdout",
   648  			CgroupPermissions: "rwm",
   649  		},
   650  		{
   651  			PathOnHost:        "/dev/fuse",
   652  			PathInContainer:   "/container/dev/task-fuse",
   653  			CgroupPermissions: "rw",
   654  		},
   655  	}
   656  
   657  	foundDevices := c.HostConfig.Devices
   658  	sort.Slice(foundDevices, func(i, j int) bool {
   659  		return foundDevices[i].PathInContainer < foundDevices[j].PathInContainer
   660  	})
   661  	require.EqualValues(t, expectedDevices, foundDevices)
   662  }
   663  
   664  // TestDockerDriver_Cleanup ensures Cleanup removes only downloaded images.
   665  // Doesn't run on windows because it requires an image variant
   666  func TestDockerDriver_Cleanup(t *testing.T) {
   667  	ci.Parallel(t)
   668  	testutil.DockerCompatible(t)
   669  
   670  	// using a small image and an specific point release to avoid accidental conflicts with other tasks
   671  	cfg := newTaskConfig("", []string{"sleep", "100"})
   672  	cfg.Image = "busybox:1.29.2"
   673  	cfg.LoadImage = ""
   674  	task := &drivers.TaskConfig{
   675  		ID:        uuid.Generate(),
   676  		Name:      "cleanup_test",
   677  		Resources: basicResources,
   678  	}
   679  
   680  	require.NoError(t, task.EncodeConcreteDriverConfig(cfg))
   681  
   682  	client, driver, handle, cleanup := dockerSetup(t, task, map[string]interface{}{
   683  		"gc": map[string]interface{}{
   684  			"image":       true,
   685  			"image_delay": "1ms",
   686  		},
   687  	})
   688  	defer cleanup()
   689  
   690  	require.NoError(t, driver.WaitUntilStarted(task.ID, 5*time.Second))
   691  	// Cleanup
   692  	require.NoError(t, driver.DestroyTask(task.ID, true))
   693  
   694  	// Ensure image was removed
   695  	tu.WaitForResult(func() (bool, error) {
   696  		if _, err := client.InspectImage(cfg.Image); err == nil {
   697  			return false, fmt.Errorf("image exists but should have been removed. Does another %v container exist?", cfg.Image)
   698  		}
   699  
   700  		return true, nil
   701  	}, func(err error) {
   702  		require.NoError(t, err)
   703  	})
   704  
   705  	// The image doesn't exist which shouldn't be an error when calling
   706  	// Cleanup, so call it again to make sure.
   707  	require.NoError(t, driver.Impl().(*Driver).cleanupImage(handle))
   708  }
   709  
   710  // Tests that images prefixed with "https://" are supported
   711  func TestDockerDriver_Start_Image_HTTPS(t *testing.T) {
   712  	ci.Parallel(t)
   713  	testutil.DockerCompatible(t)
   714  
   715  	taskCfg := TaskConfig{
   716  		Image:            "https://gcr.io/google_containers/pause:0.8.0",
   717  		ImagePullTimeout: "5m",
   718  	}
   719  	task := &drivers.TaskConfig{
   720  		ID:        uuid.Generate(),
   721  		Name:      "pause",
   722  		AllocID:   uuid.Generate(),
   723  		Resources: basicResources,
   724  	}
   725  	require.NoError(t, task.EncodeConcreteDriverConfig(&taskCfg))
   726  
   727  	harness := dockerDriverHarness(t, nil)
   728  	cleanup := harness.MkAllocDir(task, true)
   729  	defer cleanup()
   730  
   731  	_, _, err := harness.StartTask(task)
   732  	require.NoError(t, err)
   733  
   734  	err = harness.WaitUntilStarted(task.ID, 1*time.Minute)
   735  	require.NoError(t, err)
   736  
   737  	harness.DestroyTask(task.ID, true)
   738  }
   739  
   740  func newTaskConfig(variant string, command []string) TaskConfig {
   741  	// busyboxImageID is the ID stored in busybox.tar
   742  	busyboxImageID := "busybox:1.29.3"
   743  
   744  	image := busyboxImageID
   745  	loadImage := "busybox.tar"
   746  	if variant != "" {
   747  		image = fmt.Sprintf("%s-%s", busyboxImageID, variant)
   748  		loadImage = fmt.Sprintf("busybox_%s.tar", variant)
   749  	}
   750  
   751  	return TaskConfig{
   752  		Image:            image,
   753  		ImagePullTimeout: "5m",
   754  		LoadImage:        loadImage,
   755  		Command:          command[0],
   756  		Args:             command[1:],
   757  	}
   758  }
   759  
   760  func copyImage(t *testing.T, taskDir *allocdir.TaskDir, image string) {
   761  	dst := filepath.Join(taskDir.LocalDir, image)
   762  	copyFile(filepath.Join("./test-resources/docker", image), dst, t)
   763  }
   764  
   765  // copyFile moves an existing file to the destination
   766  func copyFile(src, dst string, t *testing.T) {
   767  	t.Helper()
   768  	in, err := os.Open(src)
   769  	if err != nil {
   770  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   771  	}
   772  	defer in.Close()
   773  	out, err := os.Create(dst)
   774  	require.NoError(t, err, "copying %v -> %v failed: %v", src, dst, err)
   775  
   776  	defer func() {
   777  		if err := out.Close(); err != nil {
   778  			t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   779  		}
   780  	}()
   781  	if _, err = io.Copy(out, in); err != nil {
   782  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   783  	}
   784  	if err := out.Sync(); err != nil {
   785  		t.Fatalf("copying %v -> %v failed: %v", src, dst, err)
   786  	}
   787  }
   788  
   789  func TestDocker_ExecTaskStreaming(t *testing.T) {
   790  	ci.Parallel(t)
   791  	testutil.DockerCompatible(t)
   792  
   793  	taskCfg := newTaskConfig("", []string{"/bin/sleep", "1000"})
   794  	task := &drivers.TaskConfig{
   795  		ID:        uuid.Generate(),
   796  		Name:      "nc-demo",
   797  		AllocID:   uuid.Generate(),
   798  		Resources: basicResources,
   799  	}
   800  	require.NoError(t, task.EncodeConcreteDriverConfig(&taskCfg))
   801  
   802  	harness := dockerDriverHarness(t, nil)
   803  	cleanup := harness.MkAllocDir(task, true)
   804  	defer cleanup()
   805  	copyImage(t, task.TaskDir(), "busybox.tar")
   806  
   807  	_, _, err := harness.StartTask(task)
   808  	require.NoError(t, err)
   809  
   810  	err = harness.WaitUntilStarted(task.ID, 1*time.Minute)
   811  	require.NoError(t, err)
   812  
   813  	defer harness.DestroyTask(task.ID, true)
   814  
   815  	dtestutil.ExecTaskStreamingConformanceTests(t, harness, task.ID)
   816  
   817  }
   818  
   819  // Tests that a given DNSConfig properly configures dns
   820  func Test_dnsConfig(t *testing.T) {
   821  	ci.Parallel(t)
   822  	testutil.DockerCompatible(t)
   823  
   824  	cases := []struct {
   825  		name string
   826  		cfg  *drivers.DNSConfig
   827  	}{
   828  		{
   829  			name: "nil",
   830  		},
   831  		{
   832  			name: "basic",
   833  			cfg: &drivers.DNSConfig{
   834  				Servers: []string{"1.1.1.1", "1.0.0.1"},
   835  			},
   836  		},
   837  		{
   838  			name: "full",
   839  			cfg: &drivers.DNSConfig{
   840  				Servers:  []string{"1.1.1.1", "1.0.0.1"},
   841  				Searches: []string{"local.test", "node.consul"},
   842  				Options:  []string{"ndots:2", "edns0"},
   843  			},
   844  		},
   845  	}
   846  
   847  	for _, c := range cases {
   848  		t.Run(c.name, func(t *testing.T) {
   849  			harness := dockerDriverHarness(t, nil)
   850  
   851  			taskCfg := newTaskConfig("", []string{"/bin/sleep", "1000"})
   852  			task := &drivers.TaskConfig{
   853  				ID:        uuid.Generate(),
   854  				Name:      "nc-demo",
   855  				AllocID:   uuid.Generate(),
   856  				Resources: basicResources,
   857  				DNS:       c.cfg,
   858  			}
   859  			require.NoError(t, task.EncodeConcreteDriverConfig(&taskCfg))
   860  
   861  			cleanup := harness.MkAllocDir(task, false)
   862  
   863  			_, _, err := harness.StartTask(task)
   864  			require.NoError(t, err)
   865  
   866  			err = harness.WaitUntilStarted(task.ID, 1*time.Minute)
   867  			require.NoError(t, err)
   868  
   869  			dtestutil.TestTaskDNSConfig(t, harness, task.ID, c.cfg)
   870  
   871  			// cleanup immediately before the next test case
   872  			require.NoError(t, harness.DestroyTask(task.ID, true))
   873  			cleanup()
   874  			harness.Kill()
   875  		})
   876  	}
   877  }