mirror of https://github.com/docker/cli.git
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:
parent
c6ec4e081e
commit
190c64b415
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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: "
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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