Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[carry 2663] Add capabilities support to stack/service commands #2687

Merged
merged 4 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cli/command/service/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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}}"

Expand Down
14 changes: 14 additions & 0 deletions cli/command/service/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
124 changes: 124 additions & 0 deletions cli/command/service/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Loading