3eebf4d162
- Split these options to a separate struct, so that we can handle them in isolation. - Change some tests to use subtests, and improve coverage Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
439 lines
13 KiB
Go
439 lines
13 KiB
Go
//go:build !windows
|
|
// +build !windows
|
|
|
|
package daemon // import "github.com/docker/docker/daemon"
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/docker/docker/api/types/blkiodev"
|
|
containertypes "github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/container"
|
|
"github.com/docker/docker/daemon/config"
|
|
"github.com/docker/docker/pkg/sysinfo"
|
|
"github.com/opencontainers/selinux/go-selinux"
|
|
"golang.org/x/sys/unix"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
type fakeContainerGetter struct {
|
|
containers map[string]*container.Container
|
|
}
|
|
|
|
func (f *fakeContainerGetter) GetContainer(cid string) (*container.Container, error) {
|
|
ctr, ok := f.containers[cid]
|
|
if !ok {
|
|
return nil, errors.New("container not found")
|
|
}
|
|
return ctr, nil
|
|
}
|
|
|
|
// Unix test as uses settings which are not available on Windows
|
|
func TestAdjustSharedNamespaceContainerName(t *testing.T) {
|
|
fakeID := "abcdef1234567890"
|
|
hostConfig := &containertypes.HostConfig{
|
|
IpcMode: containertypes.IpcMode("container:base"),
|
|
PidMode: containertypes.PidMode("container:base"),
|
|
NetworkMode: containertypes.NetworkMode("container:base"),
|
|
}
|
|
containerStore := &fakeContainerGetter{}
|
|
containerStore.containers = make(map[string]*container.Container)
|
|
containerStore.containers["base"] = &container.Container{
|
|
ID: fakeID,
|
|
}
|
|
|
|
adaptSharedNamespaceContainer(containerStore, hostConfig)
|
|
if hostConfig.IpcMode != containertypes.IpcMode("container:"+fakeID) {
|
|
t.Errorf("Expected IpcMode to be container:%s", fakeID)
|
|
}
|
|
if hostConfig.PidMode != containertypes.PidMode("container:"+fakeID) {
|
|
t.Errorf("Expected PidMode to be container:%s", fakeID)
|
|
}
|
|
if hostConfig.NetworkMode != containertypes.NetworkMode("container:"+fakeID) {
|
|
t.Errorf("Expected NetworkMode to be container:%s", fakeID)
|
|
}
|
|
}
|
|
|
|
// Unix test as uses settings which are not available on Windows
|
|
func TestAdjustCPUShares(t *testing.T) {
|
|
tmp, err := os.MkdirTemp("", "docker-daemon-unix-test-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
daemon := &Daemon{
|
|
repository: tmp,
|
|
root: tmp,
|
|
}
|
|
muteLogs()
|
|
|
|
hostConfig := &containertypes.HostConfig{
|
|
Resources: containertypes.Resources{CPUShares: linuxMinCPUShares - 1},
|
|
}
|
|
daemon.adaptContainerSettings(hostConfig, true)
|
|
if hostConfig.CPUShares != linuxMinCPUShares {
|
|
t.Errorf("Expected CPUShares to be %d", linuxMinCPUShares)
|
|
}
|
|
|
|
hostConfig.CPUShares = linuxMaxCPUShares + 1
|
|
daemon.adaptContainerSettings(hostConfig, true)
|
|
if hostConfig.CPUShares != linuxMaxCPUShares {
|
|
t.Errorf("Expected CPUShares to be %d", linuxMaxCPUShares)
|
|
}
|
|
|
|
hostConfig.CPUShares = 0
|
|
daemon.adaptContainerSettings(hostConfig, true)
|
|
if hostConfig.CPUShares != 0 {
|
|
t.Error("Expected CPUShares to be unchanged")
|
|
}
|
|
|
|
hostConfig.CPUShares = 1024
|
|
daemon.adaptContainerSettings(hostConfig, true)
|
|
if hostConfig.CPUShares != 1024 {
|
|
t.Error("Expected CPUShares to be unchanged")
|
|
}
|
|
}
|
|
|
|
// Unix test as uses settings which are not available on Windows
|
|
func TestAdjustCPUSharesNoAdjustment(t *testing.T) {
|
|
tmp, err := os.MkdirTemp("", "docker-daemon-unix-test-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tmp)
|
|
daemon := &Daemon{
|
|
repository: tmp,
|
|
root: tmp,
|
|
}
|
|
|
|
hostConfig := &containertypes.HostConfig{
|
|
Resources: containertypes.Resources{CPUShares: linuxMinCPUShares - 1},
|
|
}
|
|
daemon.adaptContainerSettings(hostConfig, false)
|
|
if hostConfig.CPUShares != linuxMinCPUShares-1 {
|
|
t.Errorf("Expected CPUShares to be %d", linuxMinCPUShares-1)
|
|
}
|
|
|
|
hostConfig.CPUShares = linuxMaxCPUShares + 1
|
|
daemon.adaptContainerSettings(hostConfig, false)
|
|
if hostConfig.CPUShares != linuxMaxCPUShares+1 {
|
|
t.Errorf("Expected CPUShares to be %d", linuxMaxCPUShares+1)
|
|
}
|
|
|
|
hostConfig.CPUShares = 0
|
|
daemon.adaptContainerSettings(hostConfig, false)
|
|
if hostConfig.CPUShares != 0 {
|
|
t.Error("Expected CPUShares to be unchanged")
|
|
}
|
|
|
|
hostConfig.CPUShares = 1024
|
|
daemon.adaptContainerSettings(hostConfig, false)
|
|
if hostConfig.CPUShares != 1024 {
|
|
t.Error("Expected CPUShares to be unchanged")
|
|
}
|
|
}
|
|
|
|
// Unix test as uses settings which are not available on Windows
|
|
func TestParseSecurityOptWithDeprecatedColon(t *testing.T) {
|
|
opts := &container.SecurityOptions{}
|
|
cfg := &containertypes.HostConfig{}
|
|
|
|
// test apparmor
|
|
cfg.SecurityOpt = []string{"apparmor=test_profile"}
|
|
if err := parseSecurityOpt(opts, cfg); err != nil {
|
|
t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
|
|
}
|
|
if opts.AppArmorProfile != "test_profile" {
|
|
t.Fatalf("Unexpected AppArmorProfile, expected: \"test_profile\", got %q", opts.AppArmorProfile)
|
|
}
|
|
|
|
// test seccomp
|
|
sp := "/path/to/seccomp_test.json"
|
|
cfg.SecurityOpt = []string{"seccomp=" + sp}
|
|
if err := parseSecurityOpt(opts, cfg); err != nil {
|
|
t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
|
|
}
|
|
if opts.SeccompProfile != sp {
|
|
t.Fatalf("Unexpected AppArmorProfile, expected: %q, got %q", sp, opts.SeccompProfile)
|
|
}
|
|
|
|
// test valid label
|
|
cfg.SecurityOpt = []string{"label=user:USER"}
|
|
if err := parseSecurityOpt(opts, cfg); err != nil {
|
|
t.Fatalf("Unexpected parseSecurityOpt error: %v", err)
|
|
}
|
|
|
|
// test invalid label
|
|
cfg.SecurityOpt = []string{"label"}
|
|
if err := parseSecurityOpt(opts, cfg); err == nil {
|
|
t.Fatal("Expected parseSecurityOpt error, got nil")
|
|
}
|
|
|
|
// test invalid opt
|
|
cfg.SecurityOpt = []string{"test"}
|
|
if err := parseSecurityOpt(opts, cfg); err == nil {
|
|
t.Fatal("Expected parseSecurityOpt error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseSecurityOpt(t *testing.T) {
|
|
t.Run("apparmor", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"apparmor=test_profile"},
|
|
})
|
|
assert.Check(t, err)
|
|
assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
|
|
})
|
|
t.Run("apparmor using legacy separator", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"apparmor:test_profile"},
|
|
})
|
|
assert.Check(t, err)
|
|
assert.Equal(t, secOpts.AppArmorProfile, "test_profile")
|
|
})
|
|
t.Run("seccomp", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"seccomp=/path/to/seccomp_test.json"},
|
|
})
|
|
assert.Check(t, err)
|
|
assert.Equal(t, secOpts.SeccompProfile, "/path/to/seccomp_test.json")
|
|
})
|
|
t.Run("valid label", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"label=user:USER"},
|
|
})
|
|
assert.Check(t, err)
|
|
if selinux.GetEnabled() {
|
|
// TODO(thaJeztah): set expected labels here (or "partial" if depends on host)
|
|
// assert.Check(t, is.Equal(secOpts.MountLabel, ""))
|
|
// assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
|
|
} else {
|
|
assert.Check(t, is.Equal(secOpts.MountLabel, ""))
|
|
assert.Check(t, is.Equal(secOpts.ProcessLabel, ""))
|
|
}
|
|
})
|
|
t.Run("invalid label", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"label"},
|
|
})
|
|
assert.Error(t, err, `invalid --security-opt 1: "label"`)
|
|
})
|
|
t.Run("invalid option (no value)", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"unknown"},
|
|
})
|
|
assert.Error(t, err, `invalid --security-opt 1: "unknown"`)
|
|
})
|
|
t.Run("unknown option", func(t *testing.T) {
|
|
secOpts := &container.SecurityOptions{}
|
|
err := parseSecurityOpt(secOpts, &containertypes.HostConfig{
|
|
SecurityOpt: []string{"unknown=something"},
|
|
})
|
|
assert.Error(t, err, `invalid --security-opt 2: "unknown=something"`)
|
|
})
|
|
}
|
|
|
|
func TestParseNNPSecurityOptions(t *testing.T) {
|
|
daemon := &Daemon{
|
|
configStore: &config.Config{NoNewPrivileges: true},
|
|
}
|
|
opts := &container.SecurityOptions{}
|
|
cfg := &containertypes.HostConfig{}
|
|
|
|
// test NNP when "daemon:true" and "no-new-privileges=false""
|
|
cfg.SecurityOpt = []string{"no-new-privileges=false"}
|
|
|
|
if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
|
|
t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
|
|
}
|
|
if opts.NoNewPrivileges {
|
|
t.Fatalf("container.NoNewPrivileges should be FALSE: %v", opts.NoNewPrivileges)
|
|
}
|
|
|
|
// test NNP when "daemon:false" and "no-new-privileges=true""
|
|
daemon.configStore.NoNewPrivileges = false
|
|
cfg.SecurityOpt = []string{"no-new-privileges=true"}
|
|
|
|
if err := daemon.parseSecurityOpt(opts, cfg); err != nil {
|
|
t.Fatalf("Unexpected daemon.parseSecurityOpt error: %v", err)
|
|
}
|
|
if !opts.NoNewPrivileges {
|
|
t.Fatalf("container.NoNewPrivileges should be TRUE: %v", opts.NoNewPrivileges)
|
|
}
|
|
}
|
|
|
|
func TestVerifyPlatformContainerResources(t *testing.T) {
|
|
t.Parallel()
|
|
var (
|
|
no = false
|
|
yes = true
|
|
)
|
|
|
|
withMemoryLimit := func(si *sysinfo.SysInfo) {
|
|
si.MemoryLimit = true
|
|
}
|
|
withSwapLimit := func(si *sysinfo.SysInfo) {
|
|
si.SwapLimit = true
|
|
}
|
|
withOomKillDisable := func(si *sysinfo.SysInfo) {
|
|
si.OomKillDisable = true
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
resources containertypes.Resources
|
|
sysInfo sysinfo.SysInfo
|
|
update bool
|
|
expectedWarnings []string
|
|
}{
|
|
{
|
|
name: "no-oom-kill-disable",
|
|
resources: containertypes.Resources{},
|
|
sysInfo: sysInfo(t, withMemoryLimit),
|
|
expectedWarnings: []string{},
|
|
},
|
|
{
|
|
name: "oom-kill-disable-disabled",
|
|
resources: containertypes.Resources{
|
|
OomKillDisable: &no,
|
|
},
|
|
sysInfo: sysInfo(t, withMemoryLimit),
|
|
expectedWarnings: []string{},
|
|
},
|
|
{
|
|
name: "oom-kill-disable-not-supported",
|
|
resources: containertypes.Resources{
|
|
OomKillDisable: &yes,
|
|
},
|
|
sysInfo: sysInfo(t, withMemoryLimit),
|
|
expectedWarnings: []string{
|
|
"Your kernel does not support OomKillDisable. OomKillDisable discarded.",
|
|
},
|
|
},
|
|
{
|
|
name: "oom-kill-disable-without-memory-constraints",
|
|
resources: containertypes.Resources{
|
|
OomKillDisable: &yes,
|
|
Memory: 0,
|
|
},
|
|
sysInfo: sysInfo(t, withMemoryLimit, withOomKillDisable, withSwapLimit),
|
|
expectedWarnings: []string{
|
|
"OOM killer is disabled for the container, but no memory limit is set, this can result in the system running out of resources.",
|
|
},
|
|
},
|
|
{
|
|
name: "oom-kill-disable-with-memory-constraints-but-no-memory-limit-support",
|
|
resources: containertypes.Resources{
|
|
OomKillDisable: &yes,
|
|
Memory: linuxMinMemory,
|
|
},
|
|
sysInfo: sysInfo(t, withOomKillDisable),
|
|
expectedWarnings: []string{
|
|
"Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded.",
|
|
"OOM killer is disabled for the container, but no memory limit is set, this can result in the system running out of resources.",
|
|
},
|
|
},
|
|
{
|
|
name: "oom-kill-disable-with-memory-constraints",
|
|
resources: containertypes.Resources{
|
|
OomKillDisable: &yes,
|
|
Memory: linuxMinMemory,
|
|
},
|
|
sysInfo: sysInfo(t, withMemoryLimit, withOomKillDisable, withSwapLimit),
|
|
expectedWarnings: []string{},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
warnings, err := verifyPlatformContainerResources(&tc.resources, &tc.sysInfo, tc.update)
|
|
assert.NilError(t, err)
|
|
for _, w := range tc.expectedWarnings {
|
|
assert.Assert(t, is.Contains(warnings, w))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func sysInfo(t *testing.T, opts ...func(*sysinfo.SysInfo)) sysinfo.SysInfo {
|
|
t.Helper()
|
|
si := sysinfo.SysInfo{}
|
|
|
|
for _, opt := range opts {
|
|
opt(&si)
|
|
}
|
|
|
|
if si.OomKillDisable {
|
|
t.Log(t.Name(), "OOM disable supported")
|
|
}
|
|
return si
|
|
}
|
|
|
|
const (
|
|
// prepare major 0x1FD(509 in decimal) and minor 0x130(304)
|
|
DEVNO = 0x11FD30
|
|
MAJOR = 509
|
|
MINOR = 304
|
|
WEIGHT = 1024
|
|
)
|
|
|
|
func deviceTypeMock(t *testing.T, testAndCheck func(string)) {
|
|
if os.Getuid() != 0 {
|
|
t.Skip("root required") // for mknod
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
tempDir, err := os.MkdirTemp("", "tempDevDir"+t.Name())
|
|
assert.NilError(t, err, "create temp file")
|
|
tempFile := filepath.Join(tempDir, "dev")
|
|
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
if err = unix.Mknod(tempFile, unix.S_IFCHR, DEVNO); err != nil {
|
|
t.Fatalf("mknod error %s(%x): %v", tempFile, DEVNO, err)
|
|
}
|
|
|
|
testAndCheck(tempFile)
|
|
}
|
|
|
|
func TestGetBlkioWeightDevices(t *testing.T) {
|
|
deviceTypeMock(t, func(tempFile string) {
|
|
mockResource := containertypes.Resources{
|
|
BlkioWeightDevice: []*blkiodev.WeightDevice{{Path: tempFile, Weight: WEIGHT}},
|
|
}
|
|
|
|
weightDevs, err := getBlkioWeightDevices(mockResource)
|
|
|
|
assert.NilError(t, err, "getBlkioWeightDevices")
|
|
assert.Check(t, is.Len(weightDevs, 1), "getBlkioWeightDevices")
|
|
assert.Check(t, weightDevs[0].Major == MAJOR, "get major device type")
|
|
assert.Check(t, weightDevs[0].Minor == MINOR, "get minor device type")
|
|
assert.Check(t, *weightDevs[0].Weight == WEIGHT, "get device weight")
|
|
})
|
|
}
|
|
|
|
func TestGetBlkioThrottleDevices(t *testing.T) {
|
|
deviceTypeMock(t, func(tempFile string) {
|
|
mockDevs := []*blkiodev.ThrottleDevice{{Path: tempFile, Rate: WEIGHT}}
|
|
|
|
retDevs, err := getBlkioThrottleDevices(mockDevs)
|
|
|
|
assert.NilError(t, err, "getBlkioThrottleDevices")
|
|
assert.Check(t, is.Len(retDevs, 1), "getBlkioThrottleDevices")
|
|
assert.Check(t, retDevs[0].Major == MAJOR, "get major device type")
|
|
assert.Check(t, retDevs[0].Minor == MINOR, "get minor device type")
|
|
assert.Check(t, retDevs[0].Rate == WEIGHT, "get device rate")
|
|
})
|
|
}
|