Files
nomad/drivers/docker/config_test.go
Tim Gross bf7b82b52b drivers: make internal DisableLogCollection capability public (#17196)
The `DisableLogCollection` capability was introduced as an experimental
interface for the Docker driver in 0.10.4. The interface has been stable and
allowing third-party task drivers the same capability would be useful for those
drivers that don't need the additional overhead of logmon.

This PR only makes the capability public. It doesn't yet add it to the
configuration options for the other internal drivers.

Fixes: #14636 #15686
2023-05-16 09:16:03 -04:00

779 lines
18 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package docker
import (
"testing"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
"github.com/hashicorp/nomad/plugins/drivers"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require"
)
func TestConfig_ParseHCL(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
input string
expected *TaskConfig
}{
{
"basic image",
`config {
image = "redis:7"
}`,
&TaskConfig{
Image: "redis:7",
Devices: []DockerDevice{},
Mounts: []DockerMount{},
MountsList: []DockerMount{},
CPUCFSPeriod: 100000,
ImagePullTimeout: "5m",
},
},
}
parser := hclutils.NewConfigParser(taskConfigSpec)
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
var tc *TaskConfig
parser.ParseHCL(t, c.input, &tc)
require.EqualValues(t, c.expected, tc)
})
}
}
func TestConfig_ParseJSON(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
input string
expected TaskConfig
}{
{
name: "nil values for blocks are safe",
input: `{"Config": {"image": "bash:3", "mounts": null}}`,
expected: TaskConfig{
Image: "bash:3",
Mounts: []DockerMount{},
MountsList: []DockerMount{},
Devices: []DockerDevice{},
CPUCFSPeriod: 100000,
ImagePullTimeout: "5m",
},
},
{
name: "nil values for 'volumes' field are safe",
input: `{"Config": {"image": "bash:3", "volumes": null}}`,
expected: TaskConfig{
Image: "bash:3",
Mounts: []DockerMount{},
MountsList: []DockerMount{},
Devices: []DockerDevice{},
CPUCFSPeriod: 100000,
ImagePullTimeout: "5m",
},
},
{
name: "nil values for 'args' field are safe",
input: `{"Config": {"image": "bash:3", "args": null}}`,
expected: TaskConfig{
Image: "bash:3",
Mounts: []DockerMount{},
MountsList: []DockerMount{},
Devices: []DockerDevice{},
CPUCFSPeriod: 100000,
ImagePullTimeout: "5m",
},
},
{
name: "nil values for string fields are safe",
input: `{"Config": {"image": "bash:3", "command": null}}`,
expected: TaskConfig{
Image: "bash:3",
Mounts: []DockerMount{},
MountsList: []DockerMount{},
Devices: []DockerDevice{},
CPUCFSPeriod: 100000,
ImagePullTimeout: "5m",
},
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
var tc TaskConfig
hclutils.NewConfigParser(taskConfigSpec).ParseJson(t, c.input, &tc)
require.Equal(t, c.expected, tc)
})
}
}
func TestConfig_PortMap_Deserialization(t *testing.T) {
ci.Parallel(t)
parser := hclutils.NewConfigParser(taskConfigSpec)
expectedMap := map[string]int{
"ssh": 25,
"http": 80,
"https": 443,
}
t.Run("parsing hcl block case", func(t *testing.T) {
validHCL := `
config {
image = "redis"
port_map {
ssh = 25
http = 80
https = 443
}
}`
var tc *TaskConfig
parser.ParseHCL(t, validHCL, &tc)
require.EqualValues(t, expectedMap, tc.PortMap)
})
t.Run("parsing hcl assignment case", func(t *testing.T) {
validHCL := `
config {
image = "redis"
port_map = {
ssh = 25
http = 80
https = 443
}
}`
var tc *TaskConfig
parser.ParseHCL(t, validHCL, &tc)
require.EqualValues(t, expectedMap, tc.PortMap)
})
validJsons := []struct {
name string
json string
}{
{
"single map in an array",
`{"Config": {"image": "redis", "port_map": [{"ssh": 25, "http": 80, "https": 443}]}}`,
},
{
"array of single map entries",
`{"Config": {"image": "redis", "port_map": [{"ssh": 25}, {"http": 80}, {"https": 443}]}}`,
},
{
"array of maps",
`{"Config": {"image": "redis", "port_map": [{"ssh": 25, "http": 80}, {"https": 443}]}}`,
},
}
for _, c := range validJsons {
t.Run("json:"+c.name, func(t *testing.T) {
var tc *TaskConfig
parser.ParseJson(t, c.json, &tc)
require.EqualValues(t, expectedMap, tc.PortMap)
})
}
}
func TestConfig_ParseAllHCL(t *testing.T) {
ci.Parallel(t)
cfgStr := `
config {
image = "redis:7"
image_pull_timeout = "15m"
advertise_ipv6_address = true
args = ["command_arg1", "command_arg2"]
auth {
username = "myusername"
password = "mypassword"
email = "myemail@example.com"
server_address = "https://example.com"
}
auth_soft_fail = true
cap_add = ["CAP_SYS_NICE"]
cap_drop = ["CAP_SYS_ADMIN", "CAP_SYS_TIME"]
command = "/bin/bash"
cpu_hard_limit = true
cpu_cfs_period = 20
devices = [
{"host_path"="/dev/null", "container_path"="/tmp/container-null", cgroup_permissions="rwm"},
{"host_path"="/dev/random", "container_path"="/tmp/container-random"},
{"host_path"="/dev/bus/usb"},
]
dns_search_domains = ["sub.example.com", "sub2.example.com"]
dns_options = ["debug", "attempts:10"]
dns_servers = ["8.8.8.8", "1.1.1.1"]
entrypoint = ["/bin/bash", "-c"]
extra_hosts = ["127.0.0.1 localhost.example.com"]
force_pull = true
healthchecks {
disable = true
}
hostname = "self.example.com"
interactive = true
ipc_mode = "host"
ipv4_address = "10.0.2.1"
ipv6_address = "2601:184:407f:b37c:d834:412e:1f86:7699"
labels {
owner = "hashicorp-nomad"
key = "val"
}
load = "/tmp/image.tar.gz"
logging {
driver = "json-file-driver"
type = "json-file"
config {
"max-file" = "3"
"max-size" = "10m"
}
}
mac_address = "02:42:ac:11:00:02"
memory_hard_limit = 512
mount {
type = "bind"
target ="/mount-bind-target"
source = "/bind-source-mount"
readonly = true
bind_options {
propagation = "rshared"
}
}
mount {
type = "tmpfs"
target ="/mount-tmpfs-target"
readonly = true
tmpfs_options {
size = 30000
mode = 0777
}
}
mounts = [
{
type = "bind"
target = "/bind-target",
source = "/bind-source"
readonly = true
bind_options {
propagation = "rshared"
}
},
{
type = "tmpfs"
target = "/tmpfs-target",
readonly = true
tmpfs_options {
size = 30000
mode = 0777
}
},
{
type = "volume"
target = "/volume-target"
source = "/volume-source"
readonly = true
volume_options {
no_copy = true
labels {
label_key = "label_value"
}
driver_config {
name = "nfs"
options {
option_key = "option_value"
}
}
}
},
]
network_aliases = ["redis"]
network_mode = "host"
pids_limit = 2000
pid_mode = "host"
ports = ["http", "https"]
port_map {
http = 80
redis = 6379
}
privileged = true
readonly_rootfs = true
runtime = "runc"
security_opt = [
"credentialspec=file://gmsaUser.json"
],
shm_size = 30000
storage_opt {
dm.thinpooldev = "dev/mapper/thin-pool"
dm.use_deferred_deletion = "true"
dm.use_deferred_removal = "true"
}
sysctl {
net.core.somaxconn = "16384"
}
tty = true
ulimit {
nproc = "4242"
nofile = "2048:4096"
}
uts_mode = "host"
userns_mode = "host"
volumes = [
"/host-path:/container-path:rw",
]
volume_driver = "host"
work_dir = "/tmp/workdir"
}`
expected := &TaskConfig{
Image: "redis:7",
ImagePullTimeout: "15m",
AdvertiseIPv6Addr: true,
Args: []string{"command_arg1", "command_arg2"},
Auth: DockerAuth{
Username: "myusername",
Password: "mypassword",
Email: "myemail@example.com",
ServerAddr: "https://example.com",
},
AuthSoftFail: true,
CapAdd: []string{"CAP_SYS_NICE"},
CapDrop: []string{"CAP_SYS_ADMIN", "CAP_SYS_TIME"},
Command: "/bin/bash",
CPUHardLimit: true,
CPUCFSPeriod: 20,
Devices: []DockerDevice{
{
HostPath: "/dev/null",
ContainerPath: "/tmp/container-null",
CgroupPermissions: "rwm",
},
{
HostPath: "/dev/random",
ContainerPath: "/tmp/container-random",
CgroupPermissions: "",
},
{
HostPath: "/dev/bus/usb",
ContainerPath: "",
CgroupPermissions: "",
},
},
DNSSearchDomains: []string{"sub.example.com", "sub2.example.com"},
DNSOptions: []string{"debug", "attempts:10"},
DNSServers: []string{"8.8.8.8", "1.1.1.1"},
Entrypoint: []string{"/bin/bash", "-c"},
ExtraHosts: []string{"127.0.0.1 localhost.example.com"},
ForcePull: true,
Healthchecks: DockerHealthchecks{Disable: true},
Hostname: "self.example.com",
Interactive: true,
IPCMode: "host",
IPv4Address: "10.0.2.1",
IPv6Address: "2601:184:407f:b37c:d834:412e:1f86:7699",
Labels: map[string]string{
"owner": "hashicorp-nomad",
"key": "val",
},
LoadImage: "/tmp/image.tar.gz",
Logging: DockerLogging{
Driver: "json-file-driver",
Type: "json-file",
Config: map[string]string{
"max-file": "3",
"max-size": "10m",
}},
MacAddress: "02:42:ac:11:00:02",
MemoryHardLimit: 512,
Mounts: []DockerMount{
{
Type: "bind",
Target: "/mount-bind-target",
Source: "/bind-source-mount",
ReadOnly: true,
BindOptions: DockerBindOptions{
Propagation: "rshared",
},
},
{
Type: "tmpfs",
Target: "/mount-tmpfs-target",
Source: "",
ReadOnly: true,
TmpfsOptions: DockerTmpfsOptions{
SizeBytes: 30000,
Mode: 511,
},
},
},
MountsList: []DockerMount{
{
Type: "bind",
Target: "/bind-target",
Source: "/bind-source",
ReadOnly: true,
BindOptions: DockerBindOptions{
Propagation: "rshared",
},
},
{
Type: "tmpfs",
Target: "/tmpfs-target",
Source: "",
ReadOnly: true,
TmpfsOptions: DockerTmpfsOptions{
SizeBytes: 30000,
Mode: 511,
},
},
{
Type: "volume",
Target: "/volume-target",
Source: "/volume-source",
ReadOnly: true,
VolumeOptions: DockerVolumeOptions{
NoCopy: true,
Labels: map[string]string{
"label_key": "label_value",
},
DriverConfig: DockerVolumeDriverConfig{
Name: "nfs",
Options: map[string]string{
"option_key": "option_value",
},
},
},
},
},
NetworkAliases: []string{"redis"},
NetworkMode: "host",
PidsLimit: 2000,
PidMode: "host",
Ports: []string{"http", "https"},
PortMap: map[string]int{
"http": 80,
"redis": 6379,
},
Privileged: true,
ReadonlyRootfs: true,
Runtime: "runc",
SecurityOpt: []string{
"credentialspec=file://gmsaUser.json",
},
ShmSize: 30000,
StorageOpt: map[string]string{
"dm.thinpooldev": "dev/mapper/thin-pool",
"dm.use_deferred_deletion": "true",
"dm.use_deferred_removal": "true",
},
Sysctl: map[string]string{
"net.core.somaxconn": "16384",
},
TTY: true,
Ulimit: map[string]string{
"nofile": "2048:4096",
"nproc": "4242",
},
UTSMode: "host",
UsernsMode: "host",
Volumes: []string{
"/host-path:/container-path:rw",
},
VolumeDriver: "host",
WorkDir: "/tmp/workdir",
}
var tc *TaskConfig
hclutils.NewConfigParser(taskConfigSpec).ParseHCL(t, cfgStr, &tc)
require.EqualValues(t, expected, tc)
}
// TestConfig_DriverConfig_GC asserts that gc is parsed
// and populated with defaults as expected
func TestConfig_DriverConfig_GC(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
config string
expected GCConfig
}{
{
name: "pure default",
config: `{}`,
expected: GCConfig{
Image: true, ImageDelay: "3m", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: true, PeriodStr: "5m", CreationGraceStr: "5m"},
},
},
{
name: "partial gc",
config: `{ gc { } }`,
expected: GCConfig{
Image: true, ImageDelay: "3m", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: true, PeriodStr: "5m", CreationGraceStr: "5m"},
},
},
{
name: "partial gc",
config: `{ gc { dangling_containers { } } }`,
expected: GCConfig{
Image: true, ImageDelay: "3m", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: true, PeriodStr: "5m", CreationGraceStr: "5m"},
},
},
{
name: "partial image",
config: `{ gc { image = false } }`,
expected: GCConfig{
Image: false, ImageDelay: "3m", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: true, PeriodStr: "5m", CreationGraceStr: "5m"},
},
},
{
name: "partial image_delay",
config: `{ gc { image_delay = "1d"} }`,
expected: GCConfig{
Image: true, ImageDelay: "1d", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: true, PeriodStr: "5m", CreationGraceStr: "5m"},
},
},
{
name: "partial dangling_containers",
config: `{ gc { dangling_containers { enabled = false } } }`,
expected: GCConfig{
Image: true, ImageDelay: "3m", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: false, PeriodStr: "5m", CreationGraceStr: "5m"},
},
},
{
name: "incomplete dangling_containers 2",
config: `{ gc { dangling_containers { period = "10m" } } }`,
expected: GCConfig{
Image: true, ImageDelay: "3m", Container: true,
DanglingContainers: ContainerGCConfig{
Enabled: true, PeriodStr: "10m", CreationGraceStr: "5m"},
},
},
{
name: "full default",
config: `{ gc {
image = false
image_delay = "5m"
container = false
dangling_containers {
enabled = false
dry_run = true
period = "10m"
creation_grace = "20m"
}}}`,
expected: GCConfig{
Image: false,
ImageDelay: "5m",
Container: false,
DanglingContainers: ContainerGCConfig{
Enabled: false,
DryRun: true,
PeriodStr: "10m",
CreationGraceStr: "20m",
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var tc DriverConfig
hclutils.NewConfigParser(configSpec).ParseHCL(t, "config "+c.config, &tc)
require.EqualValues(t, c.expected, tc.GC)
})
}
}
func TestConfig_Capabilities(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
config string
expected *drivers.Capabilities
}{
{
name: "pure default",
config: `{}`,
expected: &drivers.Capabilities{
SendSignals: true,
Exec: true,
FSIsolation: "image",
NetIsolationModes: []drivers.NetIsolationMode{"host", "group", "task"},
MustInitiateNetwork: true,
MountConfigs: 0,
RemoteTasks: false,
DisableLogCollection: false,
},
},
{
name: "disabled",
config: `{ disable_log_collection = true }`,
expected: &drivers.Capabilities{
SendSignals: true,
Exec: true,
FSIsolation: "image",
NetIsolationModes: []drivers.NetIsolationMode{"host", "group", "task"},
MustInitiateNetwork: true,
MountConfigs: 0,
RemoteTasks: false,
DisableLogCollection: true,
},
},
{
name: "enabled explicitly",
config: `{ disable_log_collection = false }`,
expected: &drivers.Capabilities{
SendSignals: true,
Exec: true,
FSIsolation: "image",
NetIsolationModes: []drivers.NetIsolationMode{"host", "group", "task"},
MustInitiateNetwork: true,
MountConfigs: 0,
RemoteTasks: false,
DisableLogCollection: false,
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var tc DriverConfig
hclutils.NewConfigParser(configSpec).ParseHCL(t, "config "+c.config, &tc)
d := &Driver{config: &tc}
caps, err := d.Capabilities()
must.NoError(t, err)
must.Eq(t, c.expected, caps)
})
}
}
func TestConfig_DriverConfig_InfraImagePullTimeout(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
config string
expected string
}{
{
name: "default",
config: `{}`,
expected: "5m",
},
{
name: "set explicitly",
config: `{ infra_image_pull_timeout = "1m" }`,
expected: "1m",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var tc DriverConfig
hclutils.NewConfigParser(configSpec).ParseHCL(t, "config "+c.config, &tc)
require.Equal(t, c.expected, tc.InfraImagePullTimeout)
})
}
}
func TestConfig_DriverConfig_PullActivityTimeout(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
config string
expected string
}{
{
name: "default",
config: `{}`,
expected: "2m",
},
{
name: "set explicitly",
config: `{ pull_activity_timeout = "5m" }`,
expected: "5m",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var tc DriverConfig
hclutils.NewConfigParser(configSpec).ParseHCL(t, "config "+c.config, &tc)
require.Equal(t, c.expected, tc.PullActivityTimeout)
})
}
}
func TestConfig_DriverConfig_AllowRuntimes(t *testing.T) {
ci.Parallel(t)
cases := []struct {
name string
config string
expected map[string]struct{}
}{
{
name: "pure default",
config: `{}`,
expected: map[string]struct{}{"runc": {}, "nvidia": {}},
},
{
name: "custom",
config: `{ allow_runtimes = ["runc", "firecracker"]}`,
expected: map[string]struct{}{"runc": {}, "firecracker": {}},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
var tc map[string]interface{}
hclutils.NewConfigParser(configSpec).ParseHCL(t, "config "+c.config, &tc)
dh := dockerDriverHarness(t, tc)
d := dh.Impl().(*Driver)
require.Equal(t, c.expected, d.config.allowRuntimes)
})
}
}