github.com/rawahars/moby@v24.0.4+incompatible/daemon/daemon_unix_test.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package daemon // import "github.com/docker/docker/daemon"
     5  
     6  import (
     7  	"errors"
     8  	"os"
     9  	"path/filepath"
    10  	"testing"
    11  
    12  	"github.com/docker/docker/api/types/blkiodev"
    13  	containertypes "github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/container"
    15  	"github.com/docker/docker/daemon/config"
    16  	"github.com/docker/docker/pkg/sysinfo"
    17  	"github.com/opencontainers/selinux/go-selinux"
    18  	"golang.org/x/sys/unix"
    19  	"gotest.tools/v3/assert"
    20  	is "gotest.tools/v3/assert/cmp"
    21  )
    22  
    23  type fakeContainerGetter struct {
    24  	containers map[string]*container.Container
    25  }
    26  
    27  func (f *fakeContainerGetter) GetContainer(cid string) (*container.Container, error) {
    28  	ctr, ok := f.containers[cid]
    29  	if !ok {
    30  		return nil, errors.New("container not found")
    31  	}
    32  	return ctr, nil
    33  }
    34  
    35  // Unix test as uses settings which are not available on Windows
    36  func TestAdjustSharedNamespaceContainerName(t *testing.T) {
    37  	fakeID := "abcdef1234567890"
    38  	hostConfig := &containertypes.HostConfig{
    39  		IpcMode:     containertypes.IpcMode("container:base"),
    40  		PidMode:     containertypes.PidMode("container:base"),
    41  		NetworkMode: containertypes.NetworkMode("container:base"),
    42  	}
    43  	containerStore := &fakeContainerGetter{}
    44  	containerStore.containers = make(map[string]*container.Container)
    45  	containerStore.containers["base"] = &container.Container{
    46  		ID: fakeID,
    47  	}
    48  
    49  	adaptSharedNamespaceContainer(containerStore, hostConfig)
    50  	if hostConfig.IpcMode != containertypes.IpcMode("container:"+fakeID) {
    51  		t.Errorf("Expected IpcMode to be container:%s", fakeID)
    52  	}
    53  	if hostConfig.PidMode != containertypes.PidMode("container:"+fakeID) {
    54  		t.Errorf("Expected PidMode to be container:%s", fakeID)
    55  	}
    56  	if hostConfig.NetworkMode != containertypes.NetworkMode("container:"+fakeID) {
    57  		t.Errorf("Expected NetworkMode to be container:%s", fakeID)
    58  	}
    59  }
    60  
    61  // Unix test as uses settings which are not available on Windows
    62  func TestAdjustCPUShares(t *testing.T) {
    63  	tmp, err := os.MkdirTemp("", "docker-daemon-unix-test-")
    64  	if err != nil {
    65  		t.Fatal(err)
    66  	}
    67  	defer os.RemoveAll(tmp)
    68  	daemon := &Daemon{
    69  		repository: tmp,
    70  		root:       tmp,
    71  	}
    72  	muteLogs()
    73  
    74  	hostConfig := &containertypes.HostConfig{
    75  		Resources: containertypes.Resources{CPUShares: linuxMinCPUShares - 1},
    76  	}
    77  	daemon.adaptContainerSettings(hostConfig, true)
    78  	if hostConfig.CPUShares != linuxMinCPUShares {
    79  		t.Errorf("Expected CPUShares to be %d", linuxMinCPUShares)
    80  	}
    81  
    82  	hostConfig.CPUShares = linuxMaxCPUShares + 1
    83  	daemon.adaptContainerSettings(hostConfig, true)
    84  	if hostConfig.CPUShares != linuxMaxCPUShares {
    85  		t.Errorf("Expected CPUShares to be %d", linuxMaxCPUShares)
    86  	}
    87  
    88  	hostConfig.CPUShares = 0
    89  	daemon.adaptContainerSettings(hostConfig, true)
    90  	if hostConfig.CPUShares != 0 {
    91  		t.Error("Expected CPUShares to be unchanged")
    92  	}
    93  
    94  	hostConfig.CPUShares = 1024
    95  	daemon.adaptContainerSettings(hostConfig, true)
    96  	if hostConfig.CPUShares != 1024 {
    97  		t.Error("Expected CPUShares to be unchanged")
    98  	}
    99  }
   100  
   101  // Unix test as uses settings which are not available on Windows
   102  func TestAdjustCPUSharesNoAdjustment(t *testing.T) {
   103  	tmp, err := os.MkdirTemp("", "docker-daemon-unix-test-")
   104  	if err != nil {
   105  		t.Fatal(err)
   106  	}
   107  	defer os.RemoveAll(tmp)
   108  	daemon := &Daemon{
   109  		repository: tmp,
   110  		root:       tmp,
   111  	}
   112  
   113  	hostConfig := &containertypes.HostConfig{
   114  		Resources: containertypes.Resources{CPUShares: linuxMinCPUShares - 1},
   115  	}
   116  	daemon.adaptContainerSettings(hostConfig, false)
   117  	if hostConfig.CPUShares != linuxMinCPUShares-1 {
   118  		t.Errorf("Expected CPUShares to be %d", linuxMinCPUShares-1)
   119  	}
   120  
   121  	hostConfig.CPUShares = linuxMaxCPUShares + 1
   122  	daemon.adaptContainerSettings(hostConfig, false)
   123  	if hostConfig.CPUShares != linuxMaxCPUShares+1 {
   124  		t.Errorf("Expected CPUShares to be %d", linuxMaxCPUShares+1)
   125  	}
   126  
   127  	hostConfig.CPUShares = 0
   128  	daemon.adaptContainerSettings(hostConfig, false)
   129  	if hostConfig.CPUShares != 0 {
   130  		t.Error("Expected CPUShares to be unchanged")
   131  	}
   132  
   133  	hostConfig.CPUShares = 1024
   134  	daemon.adaptContainerSettings(hostConfig, false)
   135  	if hostConfig.CPUShares != 1024 {
   136  		t.Error("Expected CPUShares to be unchanged")
   137  	}
   138  }
   139  
   140  // Unix test as uses settings which are not available on Windows
   141  func TestParseSecurityOptWithDeprecatedColon(t *testing.T) {
   142  	opts := &container.SecurityOptions{}
   143  	cfg := &containertypes.HostConfig{}
   144  
   145  	// test apparmor
   146  	cfg.SecurityOpt = []string{"apparmor=test_profile"}
   147  	if err := parseSecurityOpt(opts, cfg); err != nil {
   148  		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
   149  	}
   150  	if opts.AppArmorProfile != "test_profile" {
   151  		t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", opts.AppArmorProfile)
   152  	}
   153  
   154  	// test seccomp
   155  	sp := "/path/to/seccomp_test.json"
   156  	cfg.SecurityOpt = []string{"seccomp=" + sp}
   157  	if err := parseSecurityOpt(opts, cfg); err != nil {
   158  		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
   159  	}
   160  	if opts.SeccompProfile != sp {
   161  		t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, opts.SeccompProfile)
   162  	}
   163  
   164  	// test valid label
   165  	cfg.SecurityOpt = []string{"label=user:USER"}
   166  	if err := parseSecurityOpt(opts, cfg); err != nil {
   167  		t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
   168  	}
   169  
   170  	// test invalid label
   171  	cfg.SecurityOpt = []string{"label"}
   172  	if err := parseSecurityOpt(opts, cfg); err == nil {
   173  		t.Fatal("Expected parseSecurityOpt error, got nil")
   174  	}
   175  
   176  	// test invalid opt
   177  	cfg.SecurityOpt = []string{"test"}
   178  	if err := parseSecurityOpt(opts, cfg); err == nil {
   179  		t.Fatal("Expected parseSecurityOpt error, got nil")
   180  	}
   181  }
   182  
   183  func TestParseSecurityOpt(t *testing.T) {
   184  	t.Run("apparmor", func(t *testing.T) {
   185  		secOpts := &container.SecurityOptions{}
   186  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   187  			SecurityOpt: []string{"apparmor=test_profile"},
   188  		})
   189  		assert.Check(t, err)
   190  		assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
   191  	})
   192  	t.Run("apparmor using legacy separator", func(t *testing.T) {
   193  		secOpts := &container.SecurityOptions{}
   194  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   195  			SecurityOpt: []string{"apparmor:test_profile"},
   196  		})
   197  		assert.Check(t, err)
   198  		assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
   199  	})
   200  	t.Run("seccomp", func(t *testing.T) {
   201  		secOpts := &container.SecurityOptions{}
   202  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   203  			SecurityOpt: []string{"seccomp=/path/to/seccomp_test.json"},
   204  		})
   205  		assert.Check(t, err)
   206  		assert.Equal(t, secOpts.SeccompProfile, "/path/to/seccomp_test.json")
   207  	})
   208  	t.Run("valid label", func(t *testing.T) {
   209  		secOpts := &container.SecurityOptions{}
   210  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   211  			SecurityOpt: []string{"label=user:USER"},
   212  		})
   213  		assert.Check(t, err)
   214  		if selinux.GetEnabled() {
   215  			// TODO(thaJeztah): set expected labels here (or "partial" if depends on host)
   216  			// assert.Check(t, is.Equal(secOpts.MountLabel, ""))
   217  			// assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
   218  		} else {
   219  			assert.Check(t, is.Equal(secOpts.MountLabel, ""))
   220  			assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
   221  		}
   222  	})
   223  	t.Run("invalid label", func(t *testing.T) {
   224  		secOpts := &container.SecurityOptions{}
   225  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   226  			SecurityOpt: []string{"label"},
   227  		})
   228  		assert.Error(t, err, `invalid --security-opt 1: "label"`)
   229  	})
   230  	t.Run("invalid option (no value)", func(t *testing.T) {
   231  		secOpts := &container.SecurityOptions{}
   232  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   233  			SecurityOpt: []string{"unknown"},
   234  		})
   235  		assert.Error(t, err, `invalid --security-opt 1: "unknown"`)
   236  	})
   237  	t.Run("unknown option", func(t *testing.T) {
   238  		secOpts := &container.SecurityOptions{}
   239  		err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
   240  			SecurityOpt: []string{"unknown=something"},
   241  		})
   242  		assert.Error(t, err, `invalid --security-opt 2: "unknown=something"`)
   243  	})
   244  }
   245  
   246  func TestParseNNPSecurityOptions(t *testing.T) {
   247  	daemon := &Daemon{
   248  		configStore: &config.Config{NoNewPrivileges: true},
   249  	}
   250  	opts := &container.SecurityOptions{}
   251  	cfg := &containertypes.HostConfig{}
   252  
   253  	// test NNP when "daemon:true" and "no-new-privileges=false""
   254  	cfg.SecurityOpt = []string{"no-new-privileges=false"}
   255  
   256  	if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
   257  		t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
   258  	}
   259  	if opts.NoNewPrivileges {
   260  		t.Fatalf("container.NoNewPrivileges should be FALSE: %v", opts.NoNewPrivileges)
   261  	}
   262  
   263  	// test NNP when "daemon:false" and "no-new-privileges=true""
   264  	daemon.configStore.NoNewPrivileges = false
   265  	cfg.SecurityOpt = []string{"no-new-privileges=true"}
   266  
   267  	if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
   268  		t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
   269  	}
   270  	if !opts.NoNewPrivileges {
   271  		t.Fatalf("container.NoNewPrivileges should be TRUE: %v", opts.NoNewPrivileges)
   272  	}
   273  }
   274  
   275  func TestVerifyPlatformContainerResources(t *testing.T) {
   276  	t.Parallel()
   277  	var (
   278  		no  = false
   279  		yes = true
   280  	)
   281  
   282  	withMemoryLimit := func(si *sysinfo.SysInfo) {
   283  		si.MemoryLimit = true
   284  	}
   285  	withSwapLimit := func(si *sysinfo.SysInfo) {
   286  		si.SwapLimit = true
   287  	}
   288  	withOomKillDisable := func(si *sysinfo.SysInfo) {
   289  		si.OomKillDisable = true
   290  	}
   291  
   292  	tests := []struct {
   293  		name             string
   294  		resources        containertypes.Resources
   295  		sysInfo          sysinfo.SysInfo
   296  		update           bool
   297  		expectedWarnings []string
   298  	}{
   299  		{
   300  			name:             "no-oom-kill-disable",
   301  			resources:        containertypes.Resources{},
   302  			sysInfo:          sysInfo(t, withMemoryLimit),
   303  			expectedWarnings: []string{},
   304  		},
   305  		{
   306  			name: "oom-kill-disable-disabled",
   307  			resources: containertypes.Resources{
   308  				OomKillDisable: &no,
   309  			},
   310  			sysInfo:          sysInfo(t, withMemoryLimit),
   311  			expectedWarnings: []string{},
   312  		},
   313  		{
   314  			name: "oom-kill-disable-not-supported",
   315  			resources: containertypes.Resources{
   316  				OomKillDisable: &yes,
   317  			},
   318  			sysInfo: sysInfo(t, withMemoryLimit),
   319  			expectedWarnings: []string{
   320  				"Your kernel does not support OomKillDisable. OomKillDisable discarded.",
   321  			},
   322  		},
   323  		{
   324  			name: "oom-kill-disable-without-memory-constraints",
   325  			resources: containertypes.Resources{
   326  				OomKillDisable: &yes,
   327  				Memory:         0,
   328  			},
   329  			sysInfo: sysInfo(t, withMemoryLimit, withOomKillDisable, withSwapLimit),
   330  			expectedWarnings: []string{
   331  				"OOM killer is disabled for the container, but no memory limit is set, this can result in the system running out of resources.",
   332  			},
   333  		},
   334  		{
   335  			name: "oom-kill-disable-with-memory-constraints-but-no-memory-limit-support",
   336  			resources: containertypes.Resources{
   337  				OomKillDisable: &yes,
   338  				Memory:         linuxMinMemory,
   339  			},
   340  			sysInfo: sysInfo(t, withOomKillDisable),
   341  			expectedWarnings: []string{
   342  				"Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded.",
   343  				"OOM killer is disabled for the container, but no memory limit is set, this can result in the system running out of resources.",
   344  			},
   345  		},
   346  		{
   347  			name: "oom-kill-disable-with-memory-constraints",
   348  			resources: containertypes.Resources{
   349  				OomKillDisable: &yes,
   350  				Memory:         linuxMinMemory,
   351  			},
   352  			sysInfo:          sysInfo(t, withMemoryLimit, withOomKillDisable, withSwapLimit),
   353  			expectedWarnings: []string{},
   354  		},
   355  	}
   356  	for _, tc := range tests {
   357  		tc := tc
   358  		t.Run(tc.name, func(t *testing.T) {
   359  			t.Parallel()
   360  			warnings, err := verifyPlatformContainerResources(&tc.resources, &tc.sysInfo, tc.update)
   361  			assert.NilError(t, err)
   362  			for _, w := range tc.expectedWarnings {
   363  				assert.Assert(t, is.Contains(warnings, w))
   364  			}
   365  		})
   366  	}
   367  }
   368  
   369  func sysInfo(t *testing.T, opts ...func(*sysinfo.SysInfo)) sysinfo.SysInfo {
   370  	t.Helper()
   371  	si := sysinfo.SysInfo{}
   372  
   373  	for _, opt := range opts {
   374  		opt(&si)
   375  	}
   376  
   377  	if si.OomKillDisable {
   378  		t.Log(t.Name(), "OOM disable supported")
   379  	}
   380  	return si
   381  }
   382  
   383  const (
   384  	// prepare major 0x1FD(509 in decimal) and minor 0x130(304)
   385  	DEVNO  = 0x11FD30
   386  	MAJOR  = 509
   387  	MINOR  = 304
   388  	WEIGHT = 1024
   389  )
   390  
   391  func deviceTypeMock(t *testing.T, testAndCheck func(string)) {
   392  	if os.Getuid() != 0 {
   393  		t.Skip("root required") // for mknod
   394  	}
   395  
   396  	t.Parallel()
   397  
   398  	tempDir, err := os.MkdirTemp("", "tempDevDir"+t.Name())
   399  	assert.NilError(t, err, "create temp file")
   400  	tempFile := filepath.Join(tempDir, "dev")
   401  
   402  	defer os.RemoveAll(tempDir)
   403  
   404  	if err = unix.Mknod(tempFile, unix.S_IFCHR, DEVNO); err != nil {
   405  		t.Fatalf("mknod error %s(%x): %v", tempFile, DEVNO, err)
   406  	}
   407  
   408  	testAndCheck(tempFile)
   409  }
   410  
   411  func TestGetBlkioWeightDevices(t *testing.T) {
   412  	deviceTypeMock(t, func(tempFile string) {
   413  		mockResource := containertypes.Resources{
   414  			BlkioWeightDevice: []*blkiodev.WeightDevice{{Path: tempFile, Weight: WEIGHT}},
   415  		}
   416  
   417  		weightDevs, err := getBlkioWeightDevices(mockResource)
   418  
   419  		assert.NilError(t, err, "getBlkioWeightDevices")
   420  		assert.Check(t, is.Len(weightDevs, 1), "getBlkioWeightDevices")
   421  		assert.Check(t, weightDevs[0].Major == MAJOR, "get major device type")
   422  		assert.Check(t, weightDevs[0].Minor == MINOR, "get minor device type")
   423  		assert.Check(t, *weightDevs[0].Weight == WEIGHT, "get device weight")
   424  	})
   425  }
   426  
   427  func TestGetBlkioThrottleDevices(t *testing.T) {
   428  	deviceTypeMock(t, func(tempFile string) {
   429  		mockDevs := []*blkiodev.ThrottleDevice{{Path: tempFile, Rate: WEIGHT}}
   430  
   431  		retDevs, err := getBlkioThrottleDevices(mockDevs)
   432  
   433  		assert.NilError(t, err, "getBlkioThrottleDevices")
   434  		assert.Check(t, is.Len(retDevs, 1), "getBlkioThrottleDevices")
   435  		assert.Check(t, retDevs[0].Major == MAJOR, "get major device type")
   436  		assert.Check(t, retDevs[0].Minor == MINOR, "get minor device type")
   437  		assert.Check(t, retDevs[0].Rate == WEIGHT, "get device rate")
   438  	})
   439  }