Service cap-add/cap-drop: improve handling of combinations and special "ALL" value

When creating and updating services, we need to avoid unneeded service churn.

The interaction of separate lists to "add" and "drop" capabilities, a special
("ALL") capability, as well as a "relaxed" format for accepted capabilities
(case-insensitive, `CAP_` prefix optional) make this rather involved.

This patch updates how we handle `--cap-add` / `--cap-drop` when  _creating_ as
well as _updating_, with the following rules/assumptions applied:

- both existing (service spec) and new (values passed through flags or in
  the compose-file) are normalized and de-duplicated before use.
- the special "ALL" capability is equivalent to "all capabilities" and taken
  into account when normalizing capabilities. Combining "ALL" capabilities
  and other capabilities is therefore equivalent to just specifying "ALL".
- adding capabilities takes precedence over dropping, which means that if
  a capability is both set to be "dropped" and to be "added", it is removed
  from the list to "drop".
- the final lists should be sorted and normalized to reduce service churn
- no validation of capabilities is handled by the client. Validation is
  delegated to the daemon/server.

When deploying a service using a docker-compose file, the docker-compose file
is *mostly* handled as being "declarative". However, many of the issues outlined
above also apply to compose-files, so similar handling is applied to compose
files as well to prevent service churn.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2020-08-25 13:03:06 +02:00
parent c6ec4e081e
commit 190c64b415
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
11 changed files with 466 additions and 70 deletions

View File

@ -689,6 +689,8 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
return service, err return service, err
} }
capAdd, capDrop := opts.EffectiveCapAddCapDrop(options.capAdd.GetAll(), options.capDrop.GetAll())
service = swarm.ServiceSpec{ service = swarm.ServiceSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Name: options.name, Name: options.name,
@ -720,8 +722,8 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
Healthcheck: healthConfig, Healthcheck: healthConfig,
Isolation: container.Isolation(options.isolation), Isolation: container.Isolation(options.isolation),
Sysctls: opts.ConvertKVStringsToMap(options.sysctls.GetAll()), Sysctls: opts.ConvertKVStringsToMap(options.sysctls.GetAll()),
CapabilityAdd: options.capAdd.GetAll(), CapabilityAdd: capAdd,
CapabilityDrop: options.capDrop.GetAll(), CapabilityDrop: capDrop,
}, },
Networks: networks, Networks: networks,
Resources: resources, Resources: resources,

View File

@ -505,7 +505,10 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
} }
updateString(flagStopSignal, &cspec.StopSignal) updateString(flagStopSignal, &cspec.StopSignal)
if anyChanged(flags, flagCapAdd, flagCapDrop) {
updateCapabilities(flags, cspec) updateCapabilities(flags, cspec)
}
return nil return nil
} }
@ -1351,40 +1354,71 @@ func updateCredSpecConfig(flags *pflag.FlagSet, containerSpec *swarm.ContainerSp
} }
} }
// updateCapabilities calculates the list of capabilities to "drop" and to "add"
// after applying the capabilities passed through `--cap-add` and `--cap-drop`
// to the existing list of added/dropped capabilities in the service spec.
//
// Adding capabilities takes precedence over "dropping" the same capability, so
// if both `--cap-add` and `--cap-drop` are specifying the same capability, the
// `--cap-drop` is ignored.
//
// Capabilities to "drop" are removed from the existing list of "added"
// capabilities, and vice-versa (capabilities to "add" are removed from the existing
// list of capabilities to "drop").
//
// Capabilities are normalized, sorted, and duplicates are removed to prevent
// service tasks from being updated if no changes are made. If a list has the "ALL"
// capability set, then any other capability is removed from that list.
func updateCapabilities(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) { func updateCapabilities(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) {
var addToRemove, dropToRemove map[string]struct{} var (
capAdd := containerSpec.CapabilityAdd toAdd, toDrop map[string]bool
capDrop := containerSpec.CapabilityDrop
// First add the capabilities passed to --cap-add to the list of requested caps capDrop = opts.CapabilitiesMap(containerSpec.CapabilityDrop)
capAdd = opts.CapabilitiesMap(containerSpec.CapabilityAdd)
)
if flags.Changed(flagCapAdd) { if flags.Changed(flagCapAdd) {
caps := flags.Lookup(flagCapAdd).Value.(*opts.ListOpts).GetAll() toAdd = opts.CapabilitiesMap(flags.Lookup(flagCapAdd).Value.(*opts.ListOpts).GetAll())
capAdd = append(capAdd, caps...)
dropToRemove = buildToRemoveSet(flags, flagCapAdd)
} }
// And add the capabilities passed to --cap-drop to the list of dropped caps
if flags.Changed(flagCapDrop) { if flags.Changed(flagCapDrop) {
caps := flags.Lookup(flagCapDrop).Value.(*opts.ListOpts).GetAll() toDrop = opts.CapabilitiesMap(flags.Lookup(flagCapDrop).Value.(*opts.ListOpts).GetAll())
capDrop = append(capDrop, caps...)
addToRemove = buildToRemoveSet(flags, flagCapDrop)
} }
// Then take care of removing caps passed to --cap-drop from the list of requested caps // First remove the capabilities to "drop" from the service's exiting
containerSpec.CapabilityAdd = make([]string, 0, len(capAdd)) // list of capabilities to "add". If a capability is both added and dropped
for _, cap := range capAdd { // on update, then "adding" takes precedence.
if _, exists := addToRemove[cap]; !exists { for c := range toDrop {
containerSpec.CapabilityAdd = append(containerSpec.CapabilityAdd, cap) if !toAdd[c] {
delete(capAdd, c)
capDrop[c] = true
} }
} }
// And remove the caps passed to --cap-add from the list of caps to drop // And remove the capabilities we're "adding" from the service's existing
containerSpec.CapabilityDrop = make([]string, 0, len(capDrop)) // list of capabilities to "drop".
for _, cap := range capDrop { //
if _, exists := dropToRemove[cap]; !exists { // "Adding" capabilities takes precedence over "dropping" them, so if a
containerSpec.CapabilityDrop = append(containerSpec.CapabilityDrop, cap) // capability is set both as "add" and "drop", remove the capability from
// the service's list of dropped capabilities (if present).
for c := range toAdd {
delete(capDrop, c)
capAdd[c] = true
} }
// Now that the service's existing lists are updated, apply the new
// capabilities to add/drop to both lists. Sort the lists to prevent
// unneeded updates to service-tasks.
containerSpec.CapabilityDrop = capsList(capDrop)
containerSpec.CapabilityAdd = capsList(capAdd)
} }
func capsList(caps map[string]bool) []string {
if caps[opts.AllCapabilities] {
return []string{opts.AllCapabilities}
}
var out []string
for c := range caps {
out = append(out, c)
}
sort.Strings(out)
return out
} }

View File

@ -1358,52 +1358,170 @@ func TestUpdateCaps(t *testing.T) {
// expectedDrop is the set of dropped caps the ContainerSpec should have once updated // expectedDrop is the set of dropped caps the ContainerSpec should have once updated
expectedDrop []string expectedDrop []string
}{ }{
{
// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
name: "Empty spec, no updates",
spec: &swarm.ContainerSpec{},
},
{
// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
name: "No updates",
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
CapabilityDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
},
expectedAdd: []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
},
{
// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
name: "Empty updates",
flagAdd: []string{},
flagDrop: []string{},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
CapabilityDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
},
expectedAdd: []string{"CAP_MOUNT", "CAP_NET_ADMIN"},
expectedDrop: []string{"CAP_CHOWN", "CAP_SYS_ADMIN"},
},
{
// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
name: "Normalize cap-add only",
flagAdd: []string{},
flagDrop: []string{},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"ALL", "CAP_MOUNT", "CAP_NET_ADMIN"},
},
expectedAdd: []string{"ALL"},
expectedDrop: nil,
},
{
// Note that this won't be run as updateCapabilities is gated by anyChanged(flags, flagCapAdd, flagCapDrop)
name: "Normalize cap-drop only",
spec: &swarm.ContainerSpec{
CapabilityDrop: []string{"ALL", "CAP_MOUNT", "CAP_NET_ADMIN"},
},
expectedDrop: []string{"ALL"},
},
{ {
name: "Add new caps", name: "Add new caps",
flagAdd: []string{"NET_ADMIN"}, flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{}, flagDrop: []string{},
spec: &swarm.ContainerSpec{}, spec: &swarm.ContainerSpec{},
expectedAdd: []string{"NET_ADMIN"}, expectedAdd: []string{"CAP_NET_ADMIN"},
expectedDrop: []string{}, expectedDrop: nil,
}, },
{ {
name: "Drop new caps", name: "Drop new caps",
flagAdd: []string{}, flagAdd: []string{},
flagDrop: []string{"CAP_MKNOD"}, flagDrop: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{}, spec: &swarm.ContainerSpec{},
expectedAdd: []string{}, expectedAdd: nil,
expectedDrop: []string{"CAP_MKNOD"}, expectedDrop: []string{"CAP_NET_ADMIN"},
}, },
{ {
name: "Add a previously dropped cap", name: "Add a previously dropped cap",
flagAdd: []string{"NET_ADMIN"}, flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{}, flagDrop: []string{},
spec: &swarm.ContainerSpec{ spec: &swarm.ContainerSpec{
CapabilityDrop: []string{"NET_ADMIN"}, CapabilityDrop: []string{"CAP_NET_ADMIN"},
}, },
expectedAdd: []string{"NET_ADMIN"}, expectedAdd: []string{"CAP_NET_ADMIN"},
expectedDrop: []string{}, expectedDrop: nil,
}, },
{ {
name: "Drop a previously requested cap", name: "Drop a previously requested cap, and add a new one",
flagAdd: []string{}, flagAdd: []string{"CAP_CHOWN"},
flagDrop: []string{"CAP_MKNOD"}, flagDrop: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{ spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_MKNOD"}, CapabilityAdd: []string{"CAP_NET_ADMIN"},
}, },
expectedAdd: []string{}, expectedDrop: []string{"CAP_NET_ADMIN"},
expectedDrop: []string{"CAP_MKNOD"}, expectedAdd: []string{"CAP_CHOWN"},
},
{
name: "Add caps to service that has ALL caps has no effect",
flagAdd: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"ALL"},
},
expectedAdd: []string{"ALL"},
expectedDrop: nil,
},
{
name: "Add takes precedence on empty spec",
flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{},
expectedAdd: []string{"CAP_NET_ADMIN"},
expectedDrop: nil,
},
{
name: "Add takes precedence on existing spec",
flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_NET_ADMIN"},
CapabilityDrop: []string{"CAP_NET_ADMIN"},
},
expectedAdd: []string{"CAP_NET_ADMIN"},
expectedDrop: nil,
},
{
name: "Drop all, and add new caps",
flagAdd: []string{"CAP_CHOWN"},
flagDrop: []string{"ALL"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_NET_ADMIN", "CAP_MOUNT"},
CapabilityDrop: []string{"CAP_NET_ADMIN", "CAP_MOUNT"},
},
expectedAdd: []string{"CAP_CHOWN", "CAP_MOUNT", "CAP_NET_ADMIN"},
expectedDrop: []string{"ALL"},
},
{
name: "Add all caps",
flagAdd: []string{"ALL"},
flagDrop: []string{"CAP_NET_ADMIN", "CAP_SYS_ADMIN"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_NET_ADMIN"},
CapabilityDrop: []string{"CAP_CHOWN"},
},
expectedAdd: []string{"ALL"},
expectedDrop: []string{"CAP_CHOWN", "CAP_NET_ADMIN", "CAP_SYS_ADMIN"},
},
{
name: "Drop all, and add all",
flagAdd: []string{"ALL"},
flagDrop: []string{"ALL"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_NET_ADMIN"},
CapabilityDrop: []string{"CAP_CHOWN"},
},
expectedAdd: []string{"ALL"},
expectedDrop: []string{"CAP_CHOWN"},
},
{
name: "Caps are normalized and sorted",
flagAdd: []string{"bbb", "aaa", "cAp_bBb", "cAp_aAa"},
flagDrop: []string{"zzz", "yyy", "cAp_yYy", "cAp_yYy"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"ccc", "CAP_DDD"},
CapabilityDrop: []string{"www", "CAP_XXX"},
},
expectedAdd: []string{"CAP_AAA", "CAP_BBB", "CAP_CCC", "CAP_DDD"},
expectedDrop: []string{"CAP_WWW", "CAP_XXX", "CAP_YYY", "CAP_ZZZ"},
}, },
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
flags := newUpdateCommand(nil).Flags() flags := newUpdateCommand(nil).Flags()
for _, cap := range tc.flagAdd { for _, c := range tc.flagAdd {
flags.Set(flagCapAdd, cap) _ = flags.Set(flagCapAdd, c)
} }
for _, cap := range tc.flagDrop { for _, c := range tc.flagDrop {
flags.Set(flagCapDrop, cap) _ = flags.Set(flagCapDrop, c)
} }
updateCapabilities(flags, tc.spec) updateCapabilities(flags, tc.spec)

View File

@ -117,6 +117,8 @@ func Service(
} }
} }
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
serviceSpec := swarm.ServiceSpec{ serviceSpec := swarm.ServiceSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Name: name, Name: name,
@ -147,8 +149,8 @@ func Service(
Isolation: container.Isolation(service.Isolation), Isolation: container.Isolation(service.Isolation),
Init: service.Init, Init: service.Init,
Sysctls: service.Sysctls, Sysctls: service.Sysctls,
CapabilityAdd: service.CapAdd, CapabilityAdd: capAdd,
CapabilityDrop: service.CapDrop, CapabilityDrop: capDrop,
}, },
LogDriver: logDriver, LogDriver: logDriver,
Resources: resources, Resources: resources,

View File

@ -625,27 +625,54 @@ func TestConvertUpdateConfigParallelism(t *testing.T) {
} }
func TestConvertServiceCapAddAndCapDrop(t *testing.T) { func TestConvertServiceCapAddAndCapDrop(t *testing.T) {
// test default behavior tests := []struct {
result, err := Service("1.41", Namespace{name: "foo"}, composetypes.ServiceConfig{}, nil, nil, nil, nil) title string
assert.NilError(t, err) in, out composetypes.ServiceConfig
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, []string(nil))) }{
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, []string(nil))) {
title: "default behavior",
// with some values },
service := composetypes.ServiceConfig{ {
CapAdd: []string{ title: "some values",
"SYS_NICE", in: composetypes.ServiceConfig{
"CAP_NET_ADMIN", CapAdd: []string{"SYS_NICE", "CAP_NET_ADMIN"},
CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
},
out: composetypes.ServiceConfig{
CapAdd: []string{"CAP_NET_ADMIN", "CAP_SYS_NICE"},
CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID"},
},
},
{
title: "adding ALL capabilities",
in: composetypes.ServiceConfig{
CapAdd: []string{"ALL", "CAP_NET_ADMIN"},
CapDrop: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
},
out: composetypes.ServiceConfig{
CapAdd: []string{"ALL"},
CapDrop: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
},
},
{
title: "dropping ALL capabilities",
in: composetypes.ServiceConfig{
CapAdd: []string{"CHOWN", "CAP_NET_ADMIN", "DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER"},
CapDrop: []string{"ALL", "CAP_NET_ADMIN", "CAP_FOO"},
},
out: composetypes.ServiceConfig{
CapAdd: []string{"CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FOWNER", "CAP_FSETID", "CAP_NET_ADMIN"},
CapDrop: []string{"ALL"},
}, },
CapDrop: []string{
"CHOWN",
"DAC_OVERRIDE",
"CAP_FSETID",
"CAP_FOWNER",
}, },
} }
result, err = Service("1.41", Namespace{name: "foo"}, service, nil, nil, nil, nil) for _, tc := range tests {
tc := tc
t.Run(tc.title, func(t *testing.T) {
result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, service.CapAdd)) assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd))
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, service.CapDrop)) assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop))
})
}
} }

View File

@ -3672,6 +3672,8 @@ _docker_service_update() {
# and `docker service update` # and `docker service update`
_docker_service_update_and_create() { _docker_service_update_and_create() {
local options_with_args=" local options_with_args="
--cap-add
--cap-drop
--endpoint-mode --endpoint-mode
--entrypoint --entrypoint
--health-cmd --health-cmd
@ -3830,6 +3832,14 @@ _docker_service_update_and_create() {
esac esac
case "$prev" in case "$prev" in
--cap-add)
__docker_complete_capabilities_addable
return
;;
--cap-drop)
__docker_complete_capabilities_droppable
return
;;
--config|--config-add|--config-rm) --config|--config-add|--config-rm)
__docker_complete_configs __docker_complete_configs
return return

View File

@ -1961,6 +1961,8 @@ __docker_service_subcommand() {
opts_help=("(: -)--help[Print usage]") opts_help=("(: -)--help[Print usage]")
opts_create_update=( opts_create_update=(
"($help)*--cap-add=[Add Linux capabilities]:capability: "
"($help)*--cap-drop=[Drop Linux capabilities]:capability: "
"($help)*--constraint=[Placement constraints]:constraint: " "($help)*--constraint=[Placement constraints]:constraint: "
"($help)--endpoint-mode=[Placement constraints]:mode:(dnsrr vip)" "($help)--endpoint-mode=[Placement constraints]:mode:(dnsrr vip)"
"($help)*"{-e=,--env=}"[Set environment variables]:env: " "($help)*"{-e=,--env=}"[Set environment variables]:env: "

View File

@ -12,6 +12,8 @@ Usage: docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]
Create a new service Create a new service
Options: Options:
--cap-add list Add Linux capabilities
--cap-drop list Drop Linux capabilities
--config config Specify configurations to expose to the service --config config Specify configurations to expose to the service
--constraint list Placement constraints --constraint list Placement constraints
--container-label list Container labels --container-label list Container labels

View File

@ -13,6 +13,8 @@ Update a service
Options: Options:
--args command Service command args --args command Service command args
--cap-add list Add Linux capabilities
--cap-drop list Drop Linux capabilities
--config-add config Add or update a config file on a service --config-add config Add or update a config file on a service
--config-rm list Remove a configuration file --config-rm list Remove a configuration file
--constraint-add list Add or update a placement constraint --constraint-add list Add or update a placement constraint

78
opts/capabilities.go Normal file
View File

@ -0,0 +1,78 @@
package opts
import (
"sort"
"strings"
)
const (
// AllCapabilities is a special value to add or drop all capabilities
AllCapabilities = "ALL"
)
// NormalizeCapability normalizes a capability by upper-casing, trimming white space
// and adding a CAP_ prefix (if not yet present). This function also accepts the
// "ALL" magic-value, as used by CapAdd/CapDrop.
//
// This function only handles rudimentary formatting; no validation is performed,
// as the list of available capabilities can be updated over time, thus should be
// handled by the daemon.
func NormalizeCapability(cap string) string {
cap = strings.ToUpper(strings.TrimSpace(cap))
if cap == AllCapabilities {
return cap
}
if !strings.HasPrefix(cap, "CAP_") {
cap = "CAP_" + cap
}
return cap
}
// CapabilitiesMap normalizes the given capabilities and converts them to a map.
func CapabilitiesMap(caps []string) map[string]bool {
normalized := make(map[string]bool)
for _, c := range caps {
normalized[NormalizeCapability(c)] = true
}
return normalized
}
// EffectiveCapAddCapDrop normalizes and sorts capabilities to "add" and "drop",
// and returns the effective capabilities to include in both.
//
// "CapAdd" takes precedence over "CapDrop", so capabilities included in both
// lists are removed from the list of capabilities to drop. The special "ALL"
// capability is also taken into account.
//
// Duplicates are removed, and the resulting lists are sorted.
func EffectiveCapAddCapDrop(add, drop []string) (capAdd, capDrop []string) {
var (
addCaps = CapabilitiesMap(add)
dropCaps = CapabilitiesMap(drop)
)
if addCaps[AllCapabilities] {
// Special case: "ALL capabilities" trumps any other capability added.
addCaps = map[string]bool{AllCapabilities: true}
}
if dropCaps[AllCapabilities] {
// Special case: "ALL capabilities" trumps any other capability added.
dropCaps = map[string]bool{AllCapabilities: true}
}
for c := range dropCaps {
if addCaps[c] {
// Adding a capability takes precedence, so skip dropping
continue
}
capDrop = append(capDrop, c)
}
for c := range addCaps {
capAdd = append(capAdd, c)
}
sort.Strings(capAdd)
sort.Strings(capDrop)
return capAdd, capDrop
}

119
opts/capabilities_test.go Normal file
View File

@ -0,0 +1,119 @@
package opts
import (
"strconv"
"testing"
"gotest.tools/v3/assert"
)
func TestNormalizeCapability(t *testing.T) {
tests := []struct{ in, out string }{
{in: "ALL", out: "ALL"},
{in: "FOO", out: "CAP_FOO"},
{in: "CAP_FOO", out: "CAP_FOO"},
{in: "CAPFOO", out: "CAP_CAPFOO"},
// case-insensitive handling
{in: "aLl", out: "ALL"},
{in: "foO", out: "CAP_FOO"},
{in: "cAp_foO", out: "CAP_FOO"},
// white space handling. strictly, these could be considered "invalid",
// but are a likely situation, so handling these for now.
{in: " ALL ", out: "ALL"},
{in: " FOO ", out: "CAP_FOO"},
{in: " CAP_FOO ", out: "CAP_FOO"},
{in: " ALL ", out: "ALL"},
{in: " FOO ", out: "CAP_FOO"},
{in: " CAP_FOO ", out: "CAP_FOO"},
// weird values: no validation takes place currently, so these
// are handled same as values above; we could consider not accepting
// these in future
{in: "SOME CAP", out: "CAP_SOME CAP"},
{in: "_FOO", out: "CAP__FOO"},
}
for _, tc := range tests {
tc := tc
t.Run(tc.in, func(t *testing.T) {
assert.Equal(t, NormalizeCapability(tc.in), tc.out)
})
}
}
func TestEffectiveCapAddCapDrop(t *testing.T) {
type caps struct {
add, drop []string
}
tests := []struct {
in, out caps
}{
{
in: caps{
add: []string{"one", "two"},
drop: []string{"one", "two"},
},
out: caps{
add: []string{"CAP_ONE", "CAP_TWO"},
},
},
{
in: caps{
add: []string{"CAP_ONE", "cap_one", "CAP_TWO"},
drop: []string{"one", "cap_two"},
},
out: caps{
add: []string{"CAP_ONE", "CAP_TWO"},
},
},
{
in: caps{
add: []string{"CAP_ONE", "CAP_TWO"},
drop: []string{"CAP_ONE", "CAP_THREE"},
},
out: caps{
add: []string{"CAP_ONE", "CAP_TWO"},
drop: []string{"CAP_THREE"},
},
},
{
in: caps{
add: []string{"ALL"},
drop: []string{"CAP_ONE", "CAP_TWO"},
},
out: caps{
add: []string{"ALL"},
drop: []string{"CAP_ONE", "CAP_TWO"},
},
},
{
in: caps{
add: []string{"ALL", "CAP_ONE"},
},
out: caps{
add: []string{"ALL"},
},
},
{
in: caps{
drop: []string{"ALL", "CAP_ONE"},
},
out: caps{
drop: []string{"ALL"},
},
},
}
for i, tc := range tests {
tc := tc
t.Run(strconv.Itoa(i), func(t *testing.T) {
add, drop := EffectiveCapAddCapDrop(tc.in.add, tc.in.drop)
assert.DeepEqual(t, add, tc.out.add)
assert.DeepEqual(t, drop, tc.out.drop)
})
}
}