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 }}
|
{{- 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}}"
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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{
|
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,
|
||||||
|
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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