github.com/moby/docker@v26.1.3+incompatible/daemon/daemon_unix_test.go (about)

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