drivers/docker: reuse capabilities plumbing in docker driver

This changeset does not introduce any functional change for the
docker driver, but rather cleans up the implementation around
computing configured capabilities by re-using code written for
the exec/java task drivers.
This commit is contained in:
Seth Hoenig 2021-05-15 14:48:01 -06:00
parent 2361a91938
commit 87c96eed11
8 changed files with 353 additions and 325 deletions

View file

@ -10,7 +10,6 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
@ -23,10 +22,9 @@ import (
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/drivers/docker/docklog"
"github.com/hashicorp/nomad/drivers/shared/capabilities"
"github.com/hashicorp/nomad/drivers/shared/eventer"
"github.com/hashicorp/nomad/drivers/shared/executor"
"github.com/hashicorp/nomad/drivers/shared/resolvconf"
"github.com/hashicorp/nomad/helper"
nstructs "github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/drivers"
@ -913,8 +911,9 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T
hostConfig.Privileged = driverConfig.Privileged
// set add/drop capabilities
hostConfig.CapAdd, hostConfig.CapDrop, err = d.getCaps(driverConfig)
if err != nil {
if hostConfig.CapAdd, hostConfig.CapDrop, err = capabilities.Delta(
capabilities.DockerDefaults(), d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop,
); err != nil {
return c, err
}
@ -1184,119 +1183,6 @@ func (d *Driver) createContainerConfig(task *drivers.TaskConfig, driverConfig *T
}, nil
}
// getCaps computes the capabilities to supply to the --add-cap and --drop-cap
// options to the docker driver, which override the default capabilities enabled
// by docker itself.
func (d *Driver) getCaps(taskConfig *TaskConfig) ([]string, []string, error) {
// capabilities allowable by client docker plugin configuration
allowCaps := expandAllowCaps(d.config.AllowCaps)
// capabilities the task docker config is asking for based on the default
// capabilities allowable by nomad
desiredCaps, err := tweakCapabilities(nomadDefaultCaps(), taskConfig.CapAdd, taskConfig.CapDrop)
if err != nil {
return nil, nil, err
}
// capabilities the task is requesting that are NOT allowed by the docker plugin
if missing := missingCaps(allowCaps, desiredCaps); len(missing) > 0 {
return nil, nil, fmt.Errorf("Docker driver does not have the following caps allow-listed on this Nomad agent: %s", missing)
}
// capabilities that should be dropped relative to the docker default capabilities
dropCaps := capDrops(taskConfig.CapDrop, allowCaps)
return taskConfig.CapAdd, dropCaps, nil
}
// capDrops will compute the total dropped capabilities set
//
// {task cap_drop} U ({docker defaults} \ {driver allow caps})
func capDrops(dropCaps []string, allowCaps []string) []string {
dropSet := make(map[string]struct{})
for _, c := range normalizeCaps(dropCaps) {
dropSet[c] = struct{}{}
}
// if dropCaps includes ALL, no need to iterate every capability
if _, exists := dropSet["ALL"]; exists {
return []string{"ALL"}
}
dockerDefaults := helper.SliceStringToSet(normalizeCaps(dockerDefaultCaps()))
allowedCaps := helper.SliceStringToSet(normalizeCaps(allowCaps))
// find the docker default caps not in allowed caps
for dCap := range dockerDefaults {
if _, exists := allowedCaps[dCap]; !exists {
dropSet[dCap] = struct{}{}
}
}
drops := make([]string, 0, len(dropSet))
for c := range dropSet {
drops = append(drops, c)
}
sort.Strings(drops)
return drops
}
// expandAllowCaps returns the normalized set of allowable capabilities set
// for the docker plugin configuration.
func expandAllowCaps(allowCaps []string) []string {
if len(allowCaps) == 0 {
return nil
}
set := make(map[string]struct{}, len(allowCaps))
for _, rawCap := range allowCaps {
capability := strings.ToUpper(rawCap)
if capability == "ALL" {
for _, defCap := range normalizeCaps(executor.SupportedCaps(true)) {
set[defCap] = struct{}{}
}
} else {
set[capability] = struct{}{}
}
}
result := make([]string, 0, len(set))
for capability := range set {
result = append(result, capability)
}
sort.Strings(result)
return result
}
// missingCaps returns the set of elements in desired that are not present in
// allowed. The elements in desired are first upper-cased before comparison.
// The elements in allowed are assumed to be upper-cased.
func missingCaps(allowed, desired []string) []string {
_, missing := helper.SliceStringIsSubset(allowed, normalizeCaps(desired))
sort.Strings(missing)
return missing
}
// normalizeCaps returns a copy of caps with duplicate elements removed and all
// elements upper-cased.
func normalizeCaps(caps []string) []string {
set := make(map[string]struct{}, len(caps))
for _, c := range caps {
normal := strings.TrimPrefix(strings.ToUpper(c), "CAP_")
set[strings.ToUpper(normal)] = struct{}{}
}
result := make([]string, 0, len(set))
for c := range set {
result = append(result, c)
}
sort.Strings(result)
return result
}
func (d *Driver) toDockerMount(m *DockerMount, task *drivers.TaskConfig) (*docker.HostMount, error) {
hm, err := m.toDockerHostMount()
if err != nil {

View file

@ -19,7 +19,6 @@ import (
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/nomad/client/taskenv"
"github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/drivers/shared/executor"
"github.com/hashicorp/nomad/helper/freeport"
"github.com/hashicorp/nomad/helper/pluginutils/hclspecutils"
"github.com/hashicorp/nomad/helper/pluginutils/hclutils"
@ -1387,44 +1386,44 @@ func TestDockerDriver_Capabilities(t *testing.T) {
{
Name: "default-allowlist-add-allowed",
CapAdd: []string{"fowner", "mknod"},
CapDrop: []string{"ALL"},
CapDrop: []string{"all"},
},
{
Name: "default-allowlist-add-forbidden",
CapAdd: []string{"net_admin"},
StartError: "NET_ADMIN",
StartError: "net_admin",
},
{
Name: "default-allowlist-drop-existing",
CapDrop: []string{"FOWNER", "MKNOD", "NET_RAW"},
CapDrop: []string{"fowner", "mknod", "net_raw"},
},
{
Name: "restrictive-allowlist-drop-all",
CapDrop: []string{"ALL"},
Allowlist: "FOWNER,MKNOD",
CapDrop: []string{"all"},
Allowlist: "fowner,mknod",
},
{
Name: "restrictive-allowlist-add-allowed",
CapAdd: []string{"fowner", "mknod"},
CapDrop: []string{"ALL"},
Allowlist: "fowner,mknod",
CapDrop: []string{"all"},
Allowlist: "mknod,fowner",
},
{
Name: "restrictive-allowlist-add-forbidden",
CapAdd: []string{"net_admin", "mknod"},
CapDrop: []string{"ALL"},
CapDrop: []string{"all"},
Allowlist: "fowner,mknod",
StartError: "NET_ADMIN",
StartError: "net_admin",
},
{
Name: "permissive-allowlist",
CapAdd: []string{"net_admin", "mknod"},
Allowlist: "ALL",
CapAdd: []string{"mknod", "net_admin"},
Allowlist: "all",
},
{
Name: "permissive-allowlist-add-all",
CapAdd: []string{"all"},
Allowlist: "ALL",
Allowlist: "all",
},
}
@ -3064,169 +3063,3 @@ func TestDockerDriver_StopSignal(t *testing.T) {
})
}
}
func TestDockerCaps_normalizeCaps(t *testing.T) {
t.Run("empty", func(t *testing.T) {
result := normalizeCaps(nil)
require.Len(t, result, 0)
})
t.Run("mixed", func(t *testing.T) {
result := normalizeCaps([]string{
"DAC_OVERRIDE", "sys_chroot", "kill", "KILL",
})
require.Equal(t, []string{
"DAC_OVERRIDE", "KILL", "SYS_CHROOT",
}, result)
})
}
func TestDockerCaps_missingCaps(t *testing.T) {
allowed := []string{
"DAC_OVERRIDE", "SYS_CHROOT", "KILL", "CHOWN",
}
t.Run("none missing", func(t *testing.T) {
result := missingCaps(allowed, []string{
"SYS_CHROOT", "chown", "KILL",
})
require.Equal(t, []string(nil), result)
})
t.Run("some missing", func(t *testing.T) {
result := missingCaps(allowed, []string{
"chown", "audit_write", "SETPCAP", "dac_override",
})
require.Equal(t, []string{"AUDIT_WRITE", "SETPCAP"}, result)
})
}
func TestDockerCaps_expandAllowCaps(t *testing.T) {
t.Run("empty", func(t *testing.T) {
result := expandAllowCaps(nil)
require.Empty(t, result)
})
t.Run("manual", func(t *testing.T) {
result := expandAllowCaps([]string{
"DAC_OVERRIDE", "SYS_CHROOT", "KILL", "CHOWN",
})
require.Equal(t, []string{
"CHOWN", "DAC_OVERRIDE", "KILL", "SYS_CHROOT",
}, result)
})
t.Run("all", func(t *testing.T) {
result := expandAllowCaps([]string{"all"})
exp := normalizeCaps(executor.SupportedCaps(true))
sort.Strings(exp)
require.Equal(t, exp, result)
})
}
func TestDockerCaps_capDrops(t *testing.T) {
// docker default caps is always the same, task configured drop_caps and
// plugin config allow_caps may be altered
// This is the 90% use case, where NET_RAW is dropped, as Nomad's default
// capability allow-list is a subset of the docker default cap list.
t.Run("defaults", func(t *testing.T) {
result := capDrops(nil, nomadDefaultCaps())
require.Equal(t, []string{"NET_RAW"}, result)
})
// Users want to use ICMP (ping).
t.Run("enable net_raw", func(t *testing.T) {
result := capDrops(nil, append(nomadDefaultCaps(), "net_raw"))
require.Empty(t, result)
})
// The plugin is reduced in ability.
t.Run("enable minimal", func(t *testing.T) {
allow := []string{"setgid", "setuid", "chown", "kill"}
exp := []string{"AUDIT_WRITE", "DAC_OVERRIDE", "FOWNER", "FSETID", "MKNOD", "NET_BIND_SERVICE", "NET_RAW", "SETFCAP", "SETPCAP", "SYS_CHROOT"}
result := capDrops(nil, allow)
require.Equal(t, exp, result)
})
// The task drops abilities.
t.Run("task drops", func(t *testing.T) {
drops := []string{"audit_write", "fowner", "kill", "chown"}
exp := []string{"AUDIT_WRITE", "CHOWN", "FOWNER", "KILL", "NET_RAW"}
result := capDrops(drops, nomadDefaultCaps())
require.Equal(t, exp, result)
})
// Drop all mixed with others.
t.Run("task drops mix", func(t *testing.T) {
drops := []string{"audit_write", "all", "chown"}
exp := []string{"ALL"} // minimized
result := capDrops(drops, nomadDefaultCaps())
require.Equal(t, exp, result)
})
}
func TestDockerCaps_getCaps(t *testing.T) {
testutil.ExecCompatible(t) // tests require linux
t.Run("defaults", func(t *testing.T) {
d := Driver{config: &DriverConfig{
AllowCaps: nomadDefaultCaps(),
}}
add, drop, err := d.getCaps(&TaskConfig{
CapAdd: nil, CapDrop: nil,
})
require.NoError(t, err)
require.Empty(t, add)
require.Equal(t, []string{"NET_RAW"}, drop)
})
t.Run("enable net_raw", func(t *testing.T) {
d := Driver{config: &DriverConfig{
AllowCaps: append(nomadDefaultCaps(), "net_raw"),
}}
add, drop, err := d.getCaps(&TaskConfig{
CapAdd: nil, CapDrop: nil,
})
require.NoError(t, err)
require.Empty(t, add)
require.Empty(t, drop)
})
t.Run("block sys_time", func(t *testing.T) {
d := Driver{config: &DriverConfig{
AllowCaps: nomadDefaultCaps(),
}}
_, _, err := d.getCaps(&TaskConfig{
CapAdd: []string{"SYS_TIME"},
CapDrop: nil,
})
require.EqualError(t, err, `Docker driver does not have the following caps allow-listed on this Nomad agent: [SYS_TIME]`)
})
t.Run("enable sys_time", func(t *testing.T) {
d := Driver{config: &DriverConfig{
AllowCaps: append(nomadDefaultCaps(), "sys_time"),
}}
add, drop, err := d.getCaps(&TaskConfig{
CapAdd: []string{"SYS_TIME"},
CapDrop: nil,
})
require.NoError(t, err)
require.Equal(t, []string{"SYS_TIME"}, add)
require.Equal(t, []string{"NET_RAW"}, drop)
})
t.Run("task drops chown", func(t *testing.T) {
d := Driver{config: &DriverConfig{
AllowCaps: nomadDefaultCaps(),
}}
add, drop, err := d.getCaps(&TaskConfig{
CapAdd: nil,
CapDrop: []string{"chown"},
})
require.NoError(t, err)
require.Empty(t, add)
require.Equal(t, []string{"CHOWN", "NET_RAW"}, drop)
})
}

View file

@ -473,7 +473,9 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
cfg.Mounts = append(cfg.Mounts, dnsMount)
}
caps, err := capabilities.Calculate(d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop)
caps, err := capabilities.Calculate(
capabilities.NomadDefaults(), d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop,
)
if err != nil {
return nil, nil, err
}

View file

@ -485,7 +485,9 @@ func (d *Driver) StartTask(cfg *drivers.TaskConfig) (*drivers.TaskHandle, *drive
cfg.Mounts = append(cfg.Mounts, dnsMount)
}
caps, err := capabilities.Calculate(d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop)
caps, err := capabilities.Calculate(
capabilities.NomadDefaults(), d.config.AllowCaps, driverConfig.CapAdd, driverConfig.CapDrop,
)
if err != nil {
return nil, nil, err
}

View file

@ -39,6 +39,10 @@ func DockerDefaults() *Set {
// Supported returns the set of capabilities supported by the operating system.
//
// This set will expand over time as new capabilities are introduced to the kernel
// and the capability library is updated (which tends to happen to keep up with
// run-container libraries).
//
// Defers to a library generated from
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/capability.h
func Supported() *Set {
@ -114,32 +118,92 @@ func LegacySupported() *Set {
})
}
// Calculate the reduced set of linux capabilities to enable for driver, taking
// into account the capabilities allowed by the driver and the capabilities
// explicitly requested / removed by the task configuration.
// Calculate the resulting set of linux capabilities to enable for a task, taking
// into account:
// - default capability basis
// - driver allowable capabilities
// - task capability drops
// - task capability adds
//
// capAdd if set indicates the minimal set of capabilities that should be enabled.
// capDrop if set indicates capabilities that should be dropped from the driver defaults
// Nomad establishes a standard set of enabled capabilities allowed by the task
// driver if allow_caps is not set. This is the same set that the task will be
// enabled with by default if allow_caps does not further reduce permissions,
// in which case the task capabilities will also be reduced accordingly.
//
// If the task requests a capability not allowed by the driver, an error is
// returned.
func Calculate(allowCaps, capAdd, capDrop []string) ([]string, error) {
driverAllowed := New(allowCaps)
// The task will drop any capabilities specified in cap_drop, and add back
// capabilities specified in cap_add. The task will not be allowed to add capabilities
// not set in the the allow_caps setting (which by default is the same as the basis).
//
// cap_add takes precedence over cap_drop, enabling the common pattern of dropping
// all capabilities, then adding back the desired smaller set. e.g.
// cap_drop = ["all"]
// cap_add = ["chown", "kill"]
//
// Note that the resulting capability names are upper-cased and prefixed with
// "CAP_", which is the expected input for the exec/java driver implementation.
func Calculate(basis *Set, allowCaps, capAdd, capDrop []string) ([]string, error) {
allow := New(allowCaps)
adds := New(capAdd)
// determine caps the task wants that are not allowed
taskCaps := New(capAdd)
missing := driverAllowed.Difference(taskCaps)
missing := allow.Difference(adds)
if !missing.Empty() {
return nil, fmt.Errorf("driver does not allow the following capabilities: %s", missing)
}
// if task did not specify allowed caps, use nomad defaults minus task drops
if len(capAdd) == 0 {
driverAllowed.Remove(capDrop)
return driverAllowed.Slice(true), nil
// the realized enabled capabilities starts with what is allowed both by driver
// config AND is a member of the basis (i.e. nomad defaults)
result := basis.Intersect(allow)
// then remove capabilities the task explicitly drops
result.Remove(capDrop)
// then add back capabilities the task explicitly adds
return result.Union(adds).Slice(true), nil
}
// Delta calculates the set of capabilities that must be added and dropped relative
// to a basis to achieve a desired result. The use case is that the docker driver
// assumes a default set (DockerDefault), and we must calculate what to pass into
// --cap-add and --cap-drop on container creation given the inputs of the docker
// plugin config for allow_caps, and the docker task configuration for cap_add and
// cap_drop. Note that the user provided cap_add and cap_drop settings are always
// included, even if they are redundant with the basis (maintaining existing
// behavior, working with existing tests).
//
// Note that the resulting capability names are lower-cased and not prefixed with
// "CAP_", which is the existing style used with the docker driver implementation.
func Delta(basis *Set, allowCaps, capAdd, capDrop []string) ([]string, []string, error) {
all := func(caps []string) bool {
for _, c := range caps {
if normalize(c) == "all" {
return true
}
}
return false
}
// otherwise task did specify allowed caps, enable exactly those
taskAdd := New(capAdd)
return taskAdd.Slice(true), nil
// set of caps allowed by driver
allow := New(allowCaps)
// determine caps the task wants that are not allowed
missing := allow.Difference(New(capAdd))
if !missing.Empty() {
return nil, nil, fmt.Errorf("driver does not allow the following capabilities: %s", missing)
}
// add what the task is asking for
add := New(capAdd).Slice(false)
if all(capAdd) {
add = []string{"all"}
}
// drop what the task removes plus whatever is in the basis that is not
// in the driver allow configuration
drop := New(allowCaps).Difference(basis).Union(New(capDrop)).Slice(false)
if all(capDrop) {
drop = []string{"all"}
}
return add, drop, nil
}

View file

@ -45,27 +45,59 @@ func TestCaps_Calculate(t *testing.T) {
err: nil,
},
{
name: "allow all",
name: "allow all no mods",
allowCaps: []string{"all"},
capAdd: nil,
capDrop: nil,
exp: Supported().Slice(true),
exp: NomadDefaults().Slice(true),
err: nil,
},
{
name: "allow selection",
name: "allow selection no mods",
allowCaps: []string{"cap_net_raw", "chown", "SYS_TIME"},
capAdd: nil,
capDrop: nil,
exp: []string{"CAP_CHOWN"},
err: nil,
},
{
name: "allow selection and add them",
allowCaps: []string{"cap_net_raw", "chown", "SYS_TIME"},
capAdd: []string{"net_raw", "sys_time"},
capDrop: nil,
exp: []string{"CAP_CHOWN", "CAP_NET_RAW", "CAP_SYS_TIME"},
err: nil,
},
{
name: "add allowed",
name: "allow defaults and add redundant",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"chown", "KILL"},
capDrop: nil,
exp: []string{"CAP_CHOWN", "CAP_KILL"},
exp: NomadDefaults().Slice(true),
err: nil,
},
{
name: "allow defaults and add all",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"all"},
capDrop: nil,
exp: nil,
err: errors.New("driver does not allow the following capabilities: audit_control, audit_read, block_suspend, bpf, dac_read_search, ipc_lock, ipc_owner, lease, linux_immutable, mac_admin, mac_override, net_admin, net_broadcast, net_raw, perfmon, sys_admin, sys_boot, sys_module, sys_nice, sys_pacct, sys_ptrace, sys_rawio, sys_resource, sys_time, sys_tty_config, syslog, wake_alarm"),
},
{
name: "allow defaults and drop all",
allowCaps: NomadDefaults().Slice(false),
capAdd: nil,
capDrop: []string{"all"},
exp: []string{},
err: nil,
},
{
name: "allow defaults and drop all and add back some",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"chown", "fowner"},
capDrop: []string{"all"},
exp: []string{"CAP_CHOWN", "CAP_FOWNER"},
err: nil,
},
{
@ -92,11 +124,145 @@ func TestCaps_Calculate(t *testing.T) {
exp: []string{},
err: nil,
},
{
name: "drop all and add back",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"chown", "mknod"},
capDrop: []string{"all"},
exp: []string{"CAP_CHOWN", "CAP_MKNOD"},
err: nil,
},
} {
t.Run(tc.name, func(t *testing.T) {
caps, err := Calculate(tc.allowCaps, tc.capAdd, tc.capDrop)
caps, err := Calculate(NomadDefaults(), tc.allowCaps, tc.capAdd, tc.capDrop)
require.Equal(t, tc.err, err)
require.Equal(t, tc.exp, caps)
})
}
}
func TestCaps_Delta(t *testing.T) {
for _, tc := range []struct {
name string
// input
allowCaps []string // driver config
capAdd []string // task config
capDrop []string // task config
// output
expAdd []string
expDrop []string
err error
}{
{
name: "the default setting",
allowCaps: NomadDefaults().Slice(false),
capAdd: nil,
capDrop: nil,
expAdd: []string{},
expDrop: []string{"net_raw"},
err: nil,
},
{
name: "allow all no mods",
allowCaps: []string{"all"},
capAdd: nil,
capDrop: nil,
expAdd: []string{},
expDrop: []string{},
err: nil,
},
{
name: "allow non-default no mods",
allowCaps: []string{"cap_net_raw", "chown", "SYS_TIME"},
capAdd: nil,
capDrop: nil,
expAdd: []string{},
expDrop: []string{
"audit_write", "dac_override", "fowner", "fsetid",
"kill", "mknod", "net_bind_service", "setfcap",
"setgid", "setpcap", "setuid", "sys_chroot"},
err: nil,
},
{
name: "allow default add from default",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"chown", "KILL"},
capDrop: nil,
expAdd: []string{"chown", "kill"},
expDrop: []string{"net_raw"},
err: nil,
},
{
name: "allow default add disallowed",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"chown", "net_raw"},
capDrop: nil,
expAdd: nil,
expDrop: nil,
err: errors.New("driver does not allow the following capabilities: net_raw"),
},
{
name: "allow default drop from default",
allowCaps: NomadDefaults().Slice(false),
capAdd: nil,
capDrop: []string{"chown", "fowner", "CAP_KILL", "SYS_CHROOT", "mknod", "dac_override"},
expAdd: []string{},
expDrop: []string{"chown", "dac_override", "fowner", "kill", "mknod", "net_raw", "sys_chroot"},
err: nil,
},
{
name: "allow default drop all",
allowCaps: NomadDefaults().Slice(false),
capAdd: nil,
capDrop: []string{"all"},
expAdd: []string{},
expDrop: []string{"all"},
err: nil,
},
{
name: "task drop all and add back",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"chown", "fowner"},
capDrop: []string{"all"},
expAdd: []string{"chown", "fowner"},
expDrop: []string{"all"},
err: nil,
},
{
name: "add atop allow all",
allowCaps: []string{"all"},
capAdd: []string{"chown", "fowner"},
capDrop: nil,
expAdd: []string{"chown", "fowner"},
expDrop: []string{},
err: nil,
},
{
name: "add all atop all",
allowCaps: []string{"all"},
capAdd: []string{"all"},
capDrop: nil,
expAdd: []string{"all"},
expDrop: []string{},
err: nil,
},
{
name: "add all atop defaults",
allowCaps: NomadDefaults().Slice(false),
capAdd: []string{"all"},
capDrop: nil,
expAdd: nil,
expDrop: nil,
err: errors.New("driver does not allow the following capabilities: audit_control, audit_read, block_suspend, bpf, dac_read_search, ipc_lock, ipc_owner, lease, linux_immutable, mac_admin, mac_override, net_admin, net_broadcast, net_raw, perfmon, sys_admin, sys_boot, sys_module, sys_nice, sys_pacct, sys_ptrace, sys_rawio, sys_resource, sys_time, sys_tty_config, syslog, wake_alarm"),
},
} {
t.Run(tc.name, func(t *testing.T) {
add, drop, err := Delta(DockerDefaults(), tc.allowCaps, tc.capAdd, tc.capDrop)
require.Equal(t, tc.err, err)
require.Equal(t, tc.expAdd, add)
require.Equal(t, tc.expDrop, drop)
})
}
}

View file

@ -14,7 +14,7 @@ var null = nothing{}
// operations, taking care of name normalization, and sentinel value expansions.
//
// Linux capabilities can be expressed in multiple ways when working with docker
// and/or libcontainer, along with Nomad.
// and/or executor, along with Nomad configuration.
//
// Capability names may be upper or lower case, and may or may not be prefixed
// with "CAP_" or "cap_". On top of that, Nomad interprets the special name "all"
@ -61,6 +61,18 @@ func (s *Set) Remove(caps []string) {
}
}
// Union returns of Set of elements of both s and b.
func (s *Set) Union(b *Set) *Set {
data := make(map[string]nothing)
for c := range s.data {
data[c] = null
}
for c := range b.data {
data[c] = null
}
return &Set{data: data}
}
// Difference returns the Set of elements of b not in s.
func (s *Set) Difference(b *Set) *Set {
data := make(map[string]nothing)
@ -72,6 +84,17 @@ func (s *Set) Difference(b *Set) *Set {
return &Set{data: data}
}
// Intersect returns the Set of elements in both s and b.
func (s *Set) Intersect(b *Set) *Set {
data := make(map[string]nothing)
for c := range s.data {
if _, exists := b.data[c]; exists {
data[c] = null
}
}
return &Set{data: data}
}
// Empty return true if no capabilities exist in s.
func (s *Set) Empty() bool {
return len(s.data) == 0
@ -84,7 +107,7 @@ func (s *Set) String() string {
// Slice returns a sorted slice of capabilities in s.
//
// big - indicates whether to uppercase and prefix capabilities with CAP_
// upper - indicates whether to uppercase and prefix capabilities with CAP_
func (s *Set) Slice(upper bool) []string {
caps := make([]string, 0, len(s.data))
for c := range s.data {

View file

@ -160,3 +160,55 @@ func TestSet_Difference(t *testing.T) {
require.Equal(t, "x, y", result.String())
})
}
func TestSet_Intersect(t *testing.T) {
t.Parallel()
t.Run("empty", func(t *testing.T) {
a := New(nil)
b := New([]string{"a", "b"})
result := a.Intersect(b)
require.True(t, result.Empty())
result2 := b.Intersect(a)
require.True(t, result2.Empty())
})
t.Run("intersect", func(t *testing.T) {
a := New([]string{"A", "b", "C", "d", "e", "f", "G"})
b := New([]string{"Z", "B", "E", "f", "y"})
result := a.Intersect(b)
require.Equal(t, "b, e, f", result.String())
result2 := b.Intersect(a)
require.Equal(t, "b, e, f", result2.String())
})
}
func TestSet_Union(t *testing.T) {
t.Parallel()
t.Run("empty", func(t *testing.T) {
a := New(nil)
b := New([]string{"a", "b"})
result := a.Union(b)
require.Equal(t, "a, b", result.String())
result2 := b.Union(a)
require.Equal(t, "a, b", result2.String())
})
t.Run("union", func(t *testing.T) {
a := New([]string{"A", "b", "C", "d", "e", "f", "G"})
b := New([]string{"Z", "B", "E", "f", "y"})
result := a.Union(b)
require.Equal(t, "a, b, c, d, e, f, g, y, z", result.String())
result2 := b.Union(a)
require.Equal(t, "a, b, c, d, e, f, g, y, z", result2.String())
})
}