mirror of https://github.com/docker/cli.git
Merge pull request #2687 from thaJeztah/carry_service_caps
[carry 2663] Add capabilities support to stack/service commands
This commit is contained in:
commit
164802973e
|
@ -97,6 +97,15 @@ ContainerSpec:
|
|||
{{- if .ContainerUser }}
|
||||
User: {{ .ContainerUser }}
|
||||
{{- end }}
|
||||
{{- if .HasCapabilities }}
|
||||
Capabilities:
|
||||
{{- if .HasCapabilityAdd }}
|
||||
Add: {{ .CapabilityAdd }}
|
||||
{{- end }}
|
||||
{{- if .HasCapabilityDrop }}
|
||||
Drop: {{ .CapabilityDrop }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .ContainerSysCtls }}
|
||||
SysCtls:
|
||||
{{- range $k, $v := .ContainerSysCtls }}
|
||||
|
@ -532,6 +541,26 @@ func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
|
|||
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 (
|
||||
defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}"
|
||||
|
||||
|
|
|
@ -506,6 +506,8 @@ type serviceOptions struct {
|
|||
dnsOption opts.ListOpts
|
||||
hosts opts.ListOpts
|
||||
sysctls opts.ListOpts
|
||||
capAdd opts.ListOpts
|
||||
capDrop opts.ListOpts
|
||||
|
||||
resources resourceOptions
|
||||
stopGrace opts.DurationOpt
|
||||
|
@ -549,6 +551,8 @@ func newServiceOptions() *serviceOptions {
|
|||
dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch),
|
||||
hosts: opts.NewListOpts(opts.ValidateExtraHost),
|
||||
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
|
||||
}
|
||||
|
||||
capAdd, capDrop := opts.EffectiveCapAddCapDrop(options.capAdd.GetAll(), options.capDrop.GetAll())
|
||||
|
||||
service = swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
|
@ -716,6 +722,8 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
|||
Healthcheck: healthConfig,
|
||||
Isolation: container.Isolation(options.isolation),
|
||||
Sysctls: opts.ConvertKVStringsToMap(options.sysctls.GetAll()),
|
||||
CapabilityAdd: capAdd,
|
||||
CapabilityDrop: capDrop,
|
||||
},
|
||||
Networks: networks,
|
||||
Resources: resources,
|
||||
|
@ -818,6 +826,10 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu
|
|||
flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname")
|
||||
flags.SetAnnotation(flagHostname, "version", []string{"1.25"})
|
||||
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.limitMemBytes, flagLimitMemory, "Limit Memory")
|
||||
|
@ -1001,6 +1013,8 @@ const (
|
|||
flagConfigAdd = "config-add"
|
||||
flagConfigRemove = "config-rm"
|
||||
flagIsolation = "isolation"
|
||||
flagCapAdd = "cap-add"
|
||||
flagCapDrop = "cap-drop"
|
||||
)
|
||||
|
||||
func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {
|
||||
|
|
|
@ -506,6 +506,10 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
|
|||
|
||||
updateString(flagStopSignal, &cspec.StopSignal)
|
||||
|
||||
if anyChanged(flags, flagCapAdd, flagCapDrop) {
|
||||
updateCapabilities(flags, cspec)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1349,3 +1353,123 @@ func updateCredSpecConfig(flags *pflag.FlagSet, containerSpec *swarm.ContainerSp
|
|||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,6 +117,8 @@ func Service(
|
|||
}
|
||||
}
|
||||
|
||||
capAdd, capDrop := opts.EffectiveCapAddCapDrop(service.CapAdd, service.CapDrop)
|
||||
|
||||
serviceSpec := swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: name,
|
||||
|
@ -147,6 +149,8 @@ func Service(
|
|||
Isolation: container.Isolation(service.Isolation),
|
||||
Init: service.Init,
|
||||
Sysctls: service.Sysctls,
|
||||
CapabilityAdd: capAdd,
|
||||
CapabilityDrop: capDrop,
|
||||
},
|
||||
LogDriver: logDriver,
|
||||
Resources: resources,
|
||||
|
|
|
@ -623,3 +623,56 @@ func TestConvertUpdateConfigParallelism(t *testing.T) {
|
|||
})
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import (
|
|||
// UnsupportedProperties not yet supported by this implementation of the compose file
|
||||
var UnsupportedProperties = []string{
|
||||
"build",
|
||||
"cap_add",
|
||||
"cap_drop",
|
||||
"cgroupns_mode",
|
||||
"cgroup_parent",
|
||||
"devices",
|
||||
|
|
|
@ -3674,6 +3674,8 @@ _docker_service_update() {
|
|||
# and `docker service update`
|
||||
_docker_service_update_and_create() {
|
||||
local options_with_args="
|
||||
--cap-add
|
||||
--cap-drop
|
||||
--endpoint-mode
|
||||
--entrypoint
|
||||
--health-cmd
|
||||
|
@ -3832,6 +3834,14 @@ _docker_service_update_and_create() {
|
|||
esac
|
||||
|
||||
case "$prev" in
|
||||
--cap-add)
|
||||
__docker_complete_capabilities_addable
|
||||
return
|
||||
;;
|
||||
--cap-drop)
|
||||
__docker_complete_capabilities_droppable
|
||||
return
|
||||
;;
|
||||
--config|--config-add|--config-rm)
|
||||
__docker_complete_configs
|
||||
return
|
||||
|
|
|
@ -1961,6 +1961,8 @@ __docker_service_subcommand() {
|
|||
|
||||
opts_help=("(: -)--help[Print usage]")
|
||||
opts_create_update=(
|
||||
"($help)*--cap-add=[Add Linux capabilities]:capability: "
|
||||
"($help)*--cap-drop=[Drop Linux capabilities]:capability: "
|
||||
"($help)*--constraint=[Placement constraints]:constraint: "
|
||||
"($help)--endpoint-mode=[Placement constraints]:mode:(dnsrr vip)"
|
||||
"($help)*"{-e=,--env=}"[Set environment variables]:env: "
|
||||
|
|
|
@ -12,6 +12,8 @@ Usage: docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]
|
|||
Create a new service
|
||||
|
||||
Options:
|
||||
--cap-add list Add Linux capabilities
|
||||
--cap-drop list Drop Linux capabilities
|
||||
--config config Specify configurations to expose to the service
|
||||
--constraint list Placement constraints
|
||||
--container-label list Container labels
|
||||
|
|
|
@ -13,6 +13,8 @@ Update a service
|
|||
|
||||
Options:
|
||||
--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-rm list Remove a configuration file
|
||||
--constraint-add list Add or update a placement constraint
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue