Merge pull request #2687 from thaJeztah/carry_service_caps

[carry 2663] Add capabilities support to stack/service commands
This commit is contained in:
Brian Goff 2020-09-08 12:06:27 -07:00 committed by GitHub
commit 164802973e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 636 additions and 2 deletions

View File

@ -97,6 +97,15 @@ ContainerSpec:
{{- if .ContainerUser }} {{- if .ContainerUser }}
User: {{ .ContainerUser }} User: {{ .ContainerUser }}
{{- end }} {{- end }}
{{- if .HasCapabilities }}
Capabilities:
{{- if .HasCapabilityAdd }}
Add: {{ .CapabilityAdd }}
{{- end }}
{{- if .HasCapabilityDrop }}
Drop: {{ .CapabilityDrop }}
{{- end }}
{{- end }}
{{- if .ContainerSysCtls }} {{- if .ContainerSysCtls }}
SysCtls: SysCtls:
{{- range $k, $v := .ContainerSysCtls }} {{- range $k, $v := .ContainerSysCtls }}
@ -532,6 +541,26 @@ func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
return ctx.Service.Endpoint.Ports return ctx.Service.Endpoint.Ports
} }
func (ctx *serviceInspectContext) HasCapabilities() bool {
return len(ctx.Service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd) > 0 || len(ctx.Service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop) > 0
}
func (ctx *serviceInspectContext) HasCapabilityAdd() bool {
return len(ctx.Service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd) > 0
}
func (ctx *serviceInspectContext) HasCapabilityDrop() bool {
return len(ctx.Service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop) > 0
}
func (ctx *serviceInspectContext) CapabilityAdd() string {
return strings.Join(ctx.Service.Spec.TaskTemplate.ContainerSpec.CapabilityAdd, ", ")
}
func (ctx *serviceInspectContext) CapabilityDrop() string {
return strings.Join(ctx.Service.Spec.TaskTemplate.ContainerSpec.CapabilityDrop, ", ")
}
const ( const (
defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}" defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}"

View File

@ -506,6 +506,8 @@ type serviceOptions struct {
dnsOption opts.ListOpts dnsOption opts.ListOpts
hosts opts.ListOpts hosts opts.ListOpts
sysctls opts.ListOpts sysctls opts.ListOpts
capAdd opts.ListOpts
capDrop opts.ListOpts
resources resourceOptions resources resourceOptions
stopGrace opts.DurationOpt stopGrace opts.DurationOpt
@ -549,6 +551,8 @@ func newServiceOptions() *serviceOptions {
dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
hosts: opts.NewListOpts(opts.ValidateExtraHost), hosts: opts.NewListOpts(opts.ValidateExtraHost),
sysctls: opts.NewListOpts(nil), sysctls: opts.NewListOpts(nil),
capAdd: opts.NewListOpts(nil),
capDrop: opts.NewListOpts(nil),
} }
} }
@ -685,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,
@ -716,6 +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: capAdd,
CapabilityDrop: capDrop,
}, },
Networks: networks, Networks: networks,
Resources: resources, Resources: resources,
@ -818,6 +826,10 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu
flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname")
flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) flags.SetAnnotation(flagHostname, "version", []string{"1.25"})
flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image") flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image")
flags.Var(&opts.capAdd, flagCapAdd, "Add Linux capabilities")
flags.SetAnnotation(flagCapAdd, "version", []string{"1.41"})
flags.Var(&opts.capDrop, flagCapDrop, "Drop Linux capabilities")
flags.SetAnnotation(flagCapDrop, "version", []string{"1.41"})
flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs")
flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory")
@ -1001,6 +1013,8 @@ const (
flagConfigAdd = "config-add" flagConfigAdd = "config-add"
flagConfigRemove = "config-rm" flagConfigRemove = "config-rm"
flagIsolation = "isolation" flagIsolation = "isolation"
flagCapAdd = "cap-add"
flagCapDrop = "cap-drop"
) )
func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error { func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {

View File

@ -506,6 +506,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)
}
return nil return nil
} }
@ -1349,3 +1353,123 @@ func updateCredSpecConfig(flags *pflag.FlagSet, containerSpec *swarm.ContainerSp
containerSpec.Privileges.CredentialSpec = credSpec containerSpec.Privileges.CredentialSpec = credSpec
} }
} }
// 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.
//
// Adding/removing capabilities when updating a service is handled as a tri-state;
//
// - if the capability was previously "dropped", then remove it from "CapabilityDrop",
// but NOT added to "CapabilityAdd". However, if the capability was not yet in
// the service's "CapabilityDrop", then it's simply added to the service's "CapabilityAdd"
// - likewise, if the capability was previously "added", then it's removed from
// "CapabilityAdd", but NOT added to "CapabilityDrop". If the capability was
// not yet in the service's "CapabilityAdd", then simply add it to the service's
// "CapabilityDrop".
//
// In other words, given a service with the following:
//
// | CapDrop | CapAdd |
// | -------------- | ------------- |
// | CAP_SOME_CAP | |
//
// When updating the service, and applying `--cap-add CAP_SOME_CAP`, the previously
// dropped capability is removed:
//
// | CapDrop | CapAdd |
// | -------------- | ------------- |
// | | |
//
// After updating the service a second time, applying `--cap-add CAP_SOME_CAP`,
// capability is now added:
//
// | CapDrop | CapAdd |
// | -------------- | ------------- |
// | | CAP_SOME_CAP |
//
func updateCapabilities(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) {
var (
toAdd, toDrop map[string]bool
capDrop = opts.CapabilitiesMap(containerSpec.CapabilityDrop)
capAdd = opts.CapabilitiesMap(containerSpec.CapabilityAdd)
)
if flags.Changed(flagCapAdd) {
toAdd = opts.CapabilitiesMap(flags.Lookup(flagCapAdd).Value.(*opts.ListOpts).GetAll())
}
if flags.Changed(flagCapDrop) {
toDrop = opts.CapabilitiesMap(flags.Lookup(flagCapDrop).Value.(*opts.ListOpts).GetAll())
}
// First remove the capabilities to "drop" from the service's exiting
// list of capabilities to "add". If a capability is both added and dropped
// on update, then "adding" takes precedence.
//
// Dropping a capability when updating a service is considered a tri-state;
//
// - if the capability was previously "added", then remove it from
// "CapabilityAdd", and do NOT add it to "CapabilityDrop"
// - if the capability was not yet in the service's "CapabilityAdd",
// then simply add it to the service's "CapabilityDrop"
for c := range toDrop {
if !toAdd[c] {
if capAdd[c] {
delete(capAdd, c)
} else {
capDrop[c] = true
}
}
}
// And remove the capabilities we're "adding" from the service's existing
// list of capabilities to "drop".
//
// "Adding" capabilities takes precedence over "dropping" them, so if a
// capability is set both as "add" and "drop", remove the capability from
// the service's list of dropped capabilities (if present).
//
// Adding a capability when updating a service is considered a tri-state;
//
// - if the capability was previously "dropped", then remove it from
// "CapabilityDrop", and do NOT add it to "CapabilityAdd"
// - if the capability was not yet in the service's "CapabilityDrop",
// then simply add it to the service's "CapabilityAdd"
for c := range toAdd {
if capDrop[c] {
delete(capDrop, c)
} else {
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

@ -1342,3 +1342,202 @@ func TestUpdateCredSpec(t *testing.T) {
}) })
} }
} }
func TestUpdateCaps(t *testing.T) {
tests := []struct {
// name is the name of the testcase
name string
// flagAdd is the value passed to --cap-add
flagAdd []string
// flagDrop is the value passed to --cap-drop
flagDrop []string
// spec is the original ContainerSpec, before being updated
spec *swarm.ContainerSpec
// expectedAdd is the set of requested caps the ContainerSpec should have once updated
expectedAdd []string
// expectedDrop is the set of dropped caps the ContainerSpec should have once updated
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",
flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{},
spec: &swarm.ContainerSpec{},
expectedAdd: []string{"CAP_NET_ADMIN"},
expectedDrop: nil,
},
{
name: "Drop new caps",
flagAdd: []string{},
flagDrop: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{},
expectedAdd: nil,
expectedDrop: []string{"CAP_NET_ADMIN"},
},
{
name: "Add a previously dropped cap",
flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{},
spec: &swarm.ContainerSpec{
CapabilityDrop: []string{"CAP_NET_ADMIN"},
},
expectedAdd: nil,
expectedDrop: nil,
},
{
name: "Drop a previously requested cap, and add a new one",
flagAdd: []string{"CAP_CHOWN"},
flagDrop: []string{"CAP_NET_ADMIN"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"CAP_NET_ADMIN"},
},
expectedAdd: []string{"CAP_CHOWN"},
expectedDrop: nil,
},
{
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: "Drop ALL caps, then add new caps to service that has ALL caps",
flagAdd: []string{"CAP_NET_ADMIN"},
flagDrop: []string{"ALL"},
spec: &swarm.ContainerSpec{
CapabilityAdd: []string{"ALL"},
},
expectedAdd: []string{"CAP_NET_ADMIN"},
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_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 {
t.Run(tc.name, func(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
for _, c := range tc.flagAdd {
_ = flags.Set(flagCapAdd, c)
}
for _, c := range tc.flagDrop {
_ = flags.Set(flagCapDrop, c)
}
updateCapabilities(flags, tc.spec)
assert.DeepEqual(t, tc.spec.CapabilityAdd, tc.expectedAdd)
assert.DeepEqual(t, tc.spec.CapabilityDrop, tc.expectedDrop)
})
}
}

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,6 +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: capAdd,
CapabilityDrop: capDrop,
}, },
LogDriver: logDriver, LogDriver: logDriver,
Resources: resources, Resources: resources,

View File

@ -623,3 +623,56 @@ func TestConvertUpdateConfigParallelism(t *testing.T) {
}) })
assert.Check(t, is.Equal(parallel, updateConfig.Parallelism)) assert.Check(t, is.Equal(parallel, updateConfig.Parallelism))
} }
func TestConvertServiceCapAddAndCapDrop(t *testing.T) {
tests := []struct {
title string
in, out composetypes.ServiceConfig
}{
{
title: "default behavior",
},
{
title: "some values",
in: composetypes.ServiceConfig{
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"},
},
},
}
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.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd))
assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop))
})
}
}

View File

@ -9,8 +9,6 @@ import (
// UnsupportedProperties not yet supported by this implementation of the compose file // UnsupportedProperties not yet supported by this implementation of the compose file
var UnsupportedProperties = []string{ var UnsupportedProperties = []string{
"build", "build",
"cap_add",
"cap_drop",
"cgroupns_mode", "cgroupns_mode",
"cgroup_parent", "cgroup_parent",
"devices", "devices",

View File

@ -3674,6 +3674,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
@ -3832,6 +3834,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)
})
}
}