diff --git a/cli/command/service/create.go b/cli/command/service/create.go index ad6dcb3c8f..28cd696985 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -64,6 +64,8 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation(flagInit, "version", []string{"1.37"}) flags.Var(&opts.sysctls, flagSysCtl, "Sysctl options") flags.SetAnnotation(flagSysCtl, "version", []string{"1.40"}) + flags.Var(&opts.ulimits, flagUlimit, "Ulimit options") + flags.SetAnnotation(flagUlimit, "version", []string{"1.41"}) flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources") flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index 57d0d08766..5e99acc845 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -111,6 +111,11 @@ SysCtls: {{- range $k, $v := .ContainerSysCtls }} {{ $k }}{{if $v }}: {{ $v }}{{ end }} {{- end }}{{ end }} +{{- if .ContainerUlimits }} +Ulimits: +{{- range $k, $v := .ContainerUlimits }} + {{ $k }}: {{ $v }} +{{- end }}{{ end }} {{- if .ContainerMounts }} Mounts: {{- end }} @@ -467,6 +472,20 @@ func (ctx *serviceInspectContext) HasContainerSysCtls() bool { return len(ctx.Service.Spec.TaskTemplate.ContainerSpec.Sysctls) > 0 } +func (ctx *serviceInspectContext) ContainerUlimits() map[string]string { + ulimits := map[string]string{} + + for _, u := range ctx.Service.Spec.TaskTemplate.ContainerSpec.Ulimits { + ulimits[u.Name] = fmt.Sprintf("%d:%d", u.Soft, u.Hard) + } + + return ulimits +} + +func (ctx *serviceInspectContext) HasContainerUlimits() bool { + return len(ctx.Service.Spec.TaskTemplate.ContainerSpec.Ulimits) > 0 +} + func (ctx *serviceInspectContext) HasResources() bool { return ctx.Service.Spec.TaskTemplate.Resources != nil } diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index 053a76ea5e..b8b6873a0b 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -508,6 +508,7 @@ type serviceOptions struct { sysctls opts.ListOpts capAdd opts.ListOpts capDrop opts.ListOpts + ulimits opts.UlimitOpt resources resourceOptions stopGrace opts.DurationOpt @@ -553,6 +554,7 @@ func newServiceOptions() *serviceOptions { sysctls: opts.NewListOpts(nil), capAdd: opts.NewListOpts(nil), capDrop: opts.NewListOpts(nil), + ulimits: *opts.NewUlimitOpt(nil), } } @@ -724,6 +726,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N Sysctls: opts.ConvertKVStringsToMap(options.sysctls.GetAll()), CapabilityAdd: capAdd, CapabilityDrop: capDrop, + Ulimits: options.ulimits.GetList(), }, Networks: networks, Resources: resources, @@ -1015,6 +1018,9 @@ const ( flagIsolation = "isolation" flagCapAdd = "cap-add" flagCapDrop = "cap-drop" + flagUlimit = "ulimit" + flagUlimitAdd = "ulimit-add" + flagUlimitRemove = "ulimit-rm" ) func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error { diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 8adcdfa8f2..76fb6c744e 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" + units "github.com/docker/go-units" "github.com/docker/swarmkit/api/defaults" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -100,6 +101,10 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation(flagSysCtlAdd, "version", []string{"1.40"}) flags.Var(newListOptsVar(), flagSysCtlRemove, "Remove a Sysctl option") flags.SetAnnotation(flagSysCtlRemove, "version", []string{"1.40"}) + flags.Var(&options.ulimits, flagUlimitAdd, "Add or update a ulimit option") + flags.SetAnnotation(flagUlimitAdd, "version", []string{"1.41"}) + flags.Var(newListOptsVar(), flagUlimitRemove, "Remove a ulimit option") + flags.SetAnnotation(flagUlimitRemove, "version", []string{"1.41"}) // Add needs parsing, Remove only needs the key flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource") @@ -344,6 +349,7 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags } updateSysCtls(flags, &task.ContainerSpec.Sysctls) + task.ContainerSpec.Ulimits = updateUlimits(flags, task.ContainerSpec.Ulimits) if anyChanged(flags, flagLimitCPU, flagLimitMemory, flagLimitPids) { taskResources().Limits = spec.TaskTemplate.Resources.Limits @@ -700,6 +706,35 @@ func updateSysCtls(flags *pflag.FlagSet, field *map[string]string) { } } +func updateUlimits(flags *pflag.FlagSet, ulimits []*units.Ulimit) []*units.Ulimit { + newUlimits := make(map[string]*units.Ulimit) + + for _, ulimit := range ulimits { + newUlimits[ulimit.Name] = ulimit + } + if flags.Changed(flagUlimitRemove) { + values := flags.Lookup(flagUlimitRemove).Value.(*opts.ListOpts).GetAll() + for key := range opts.ConvertKVStringsToMap(values) { + delete(newUlimits, key) + } + } + + if flags.Changed(flagUlimitAdd) { + for _, ulimit := range flags.Lookup(flagUlimitAdd).Value.(*opts.UlimitOpt).GetList() { + newUlimits[ulimit.Name] = ulimit + } + } + + var limits []*units.Ulimit + for _, ulimit := range newUlimits { + limits = append(limits, ulimit) + } + sort.SliceStable(limits, func(i, j int) bool { + return limits[i].Name < limits[j].Name + }) + return limits +} + func updateEnvironment(flags *pflag.FlagSet, field *[]string) { toRemove := buildToRemoveSet(flags, flagEnvRemove) *field = removeItems(*field, toRemove, envKey) diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index cbae2ddd11..e96f52812b 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" + "github.com/docker/go-units" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -1541,3 +1542,120 @@ func TestUpdateCaps(t *testing.T) { }) } } + +func TestUpdateUlimits(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + spec []*units.Ulimit + rm []string + add []string + expected []*units.Ulimit + }{ + { + name: "from scratch", + add: []string{"nofile=512:1024", "core=1024:1024"}, + expected: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + { + name: "append new", + spec: []*units.Ulimit{ + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + add: []string{"core=1024:1024"}, + expected: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + { + name: "remove and append new should append", + spec: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + rm: []string{"nofile=512:1024"}, + add: []string{"nofile=512:1024"}, + expected: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + { + name: "update existing", + spec: []*units.Ulimit{ + {Name: "nofile", Hard: 2048, Soft: 1024}, + }, + add: []string{"nofile=512:1024"}, + expected: []*units.Ulimit{ + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + { + name: "update existing twice", + spec: []*units.Ulimit{ + {Name: "nofile", Hard: 2048, Soft: 1024}, + }, + add: []string{"nofile=256:512", "nofile=512:1024"}, + expected: []*units.Ulimit{ + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + { + name: "remove all", + spec: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + rm: []string{"nofile=512:1024", "core=1024:1024"}, + expected: nil, + }, + { + name: "remove by key", + spec: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + rm: []string{"core"}, + expected: []*units.Ulimit{ + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + { + name: "remove by key and different value", + spec: []*units.Ulimit{ + {Name: "core", Hard: 1024, Soft: 1024}, + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + rm: []string{"core=1234:5678"}, + expected: []*units.Ulimit{ + {Name: "nofile", Hard: 1024, Soft: 512}, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + svc := swarm.ServiceSpec{ + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: &swarm.ContainerSpec{Ulimits: tc.spec}, + }, + } + flags := newUpdateCommand(nil).Flags() + for _, v := range tc.add { + assert.NilError(t, flags.Set(flagUlimitAdd, v)) + } + for _, v := range tc.rm { + assert.NilError(t, flags.Set(flagUlimitRemove, v)) + } + err := updateService(ctx, &fakeClient{}, flags, &svc) + assert.NilError(t, err) + assert.DeepEqual(t, svc.TaskTemplate.ContainerSpec.Ulimits, tc.expected) + }) + } +} diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index f55c302f49..5598dcd53b 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -3754,6 +3754,7 @@ _docker_service_update_and_create() { --publish -p --secret --sysctl + --ulimit " case "$prev" in @@ -3806,6 +3807,8 @@ _docker_service_update_and_create() { --secret-rm --sysctl-add --sysctl-rm + --ulimit-add + --ulimit-rm " boolean_options="$boolean_options diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index 13e40d50d3..800c9ae393 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -74,6 +74,7 @@ Options: --stop-signal string Signal to stop the container --sysctl list Sysctl options -t, --tty Allocate a pseudo-TTY + --ulimit ulimit Ulimit options (default []) --update-delay duration Delay between updates (ns|us|ms|s|m|h) (default 0s) --update-failure-action string Action on update failure ("pause"|"continue"|"rollback") (default "pause") --update-max-failure-ratio float Failure rate to tolerate during an update (default 0) diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index fc5bd424cd..68626395b1 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -91,6 +91,8 @@ Options: --sysctl-add list Add or update a Sysctl option --sysctl-rm list Remove a Sysctl option -t, --tty Allocate a pseudo-TTY + --ulimit-add ulimit Add or update a ulimit option (default []) + --ulimit-rm list Remove a ulimit option --update-delay duration Delay between updates (ns|us|ms|s|m|h) --update-failure-action string Action on update failure ("pause"|"continue"|"rollback") --update-max-failure-ratio float Failure rate to tolerate during an update