diff --git a/api/server/router/swarm/helpers.go b/api/server/router/swarm/helpers.go index cda883044a..96e6f02f3a 100644 --- a/api/server/router/swarm/helpers.go +++ b/api/server/router/swarm/helpers.go @@ -97,9 +97,10 @@ func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) { } if versions.LessThan(cliVersion, "1.41") { if service.TaskTemplate.ContainerSpec != nil { - // Capabilities for docker swarm services weren't supported before - // API version 1.41 + // Capabilities and PidsLimit for docker swarm services weren't + // supported before API version 1.41 service.TaskTemplate.ContainerSpec.Capabilities = nil + service.TaskTemplate.ContainerSpec.PidsLimit = 0 } // jobs were only introduced in API version 1.41. Nil out both Job diff --git a/api/server/router/swarm/helpers_test.go b/api/server/router/swarm/helpers_test.go index 08764e5a17..881a6d8334 100644 --- a/api/server/router/swarm/helpers_test.go +++ b/api/server/router/swarm/helpers_test.go @@ -17,7 +17,8 @@ func TestAdjustForAPIVersion(t *testing.T) { spec := &swarm.ServiceSpec{ TaskTemplate: swarm.TaskSpec{ ContainerSpec: &swarm.ContainerSpec{ - Sysctls: expectedSysctls, + Sysctls: expectedSysctls, + PidsLimit: 300, Privileges: &swarm.Privileges{ CredentialSpec: &swarm.CredentialSpec{ Config: "someconfig", @@ -49,11 +50,18 @@ func TestAdjustForAPIVersion(t *testing.T) { // first, does calling this with a later version correctly NOT strip // fields? do the later version first, so we can reuse this spec in the // next test. - adjustForAPIVersion("1.40", spec) + adjustForAPIVersion("1.41", spec) if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls) { t.Error("Sysctls was stripped from spec") } + if spec.TaskTemplate.ContainerSpec.PidsLimit == 0 { + t.Error("PidsLimit was stripped from spec") + } + if spec.TaskTemplate.ContainerSpec.PidsLimit != 300 { + t.Error("PidsLimit did not preserve the value from spec") + } + if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "someconfig" { t.Error("CredentialSpec.Config field was stripped from spec") } @@ -72,6 +80,10 @@ func TestAdjustForAPIVersion(t *testing.T) { t.Error("Sysctls was not stripped from spec") } + if spec.TaskTemplate.ContainerSpec.PidsLimit != 0 { + t.Error("PidsLimit was not stripped from spec") + } + if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "" { t.Error("CredentialSpec.Config field was not stripped from spec") } diff --git a/api/swagger.yaml b/api/swagger.yaml index 99832e9f86..da30227383 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2967,6 +2967,13 @@ definitions: description: "Run an init inside the container that forwards signals and reaps processes. This field is omitted if empty, and the default (as configured on the daemon) is used." type: "boolean" x-nullable: true + PidsLimit: + description: | + Tune a container's PIDs limit. Set `0` for unlimited. + type: "integer" + format: "int64" + default: 0 + example: 100 Sysctls: description: | Set kernel namedspaced parameters (sysctls) in the container. diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index 5bbedfcf68..2eeee9f7f5 100644 --- a/api/types/swarm/container.go +++ b/api/types/swarm/container.go @@ -72,6 +72,7 @@ type ContainerSpec struct { Secrets []*SecretReference `json:",omitempty"` Configs []*ConfigReference `json:",omitempty"` Isolation container.Isolation `json:",omitempty"` + PidsLimit int64 `json:",omitempty"` Sysctls map[string]string `json:",omitempty"` Capabilities []string `json:",omitempty"` } diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index 7f1e0af22c..c7742c4e01 100644 --- a/daemon/cluster/convert/container.go +++ b/daemon/cluster/convert/container.go @@ -36,6 +36,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec { Configs: configReferencesFromGRPC(c.Configs), Isolation: IsolationFromGRPC(c.Isolation), Init: initFromGRPC(c.Init), + PidsLimit: c.PidsLimit, Sysctls: c.Sysctls, Capabilities: c.Capabilities, } @@ -263,6 +264,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) { Secrets: secretReferencesToGRPC(c.Secrets), Isolation: isolationToGRPC(c.Isolation), Init: initToGRPC(c.Init), + PidsLimit: c.PidsLimit, Sysctls: c.Sysctls, Capabilities: c.Capabilities, } diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 48d6aaa83c..4369b3e6eb 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -431,6 +431,12 @@ func (c *containerConfig) volumeCreateRequest(mount *api.Mount) *volumetypes.Vol func (c *containerConfig) resources() enginecontainer.Resources { resources := enginecontainer.Resources{} + // set pids limit + pidsLimit := c.spec().PidsLimit + if pidsLimit > 0 { + resources.PidsLimit = &pidsLimit + } + // If no limits are specified let the engine use its defaults. // // TODO(aluzzardi): We might want to set some limits anyway otherwise diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 5035added8..8ca3c36007 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -30,6 +30,12 @@ keywords: "API, Docker, rcli, REST, documentation" * `POST /services/{id}/update` now accepts `Capabilities` as part of the `ContainerSpec`. * `GET /tasks` now returns `Capabilities` as part of the `ContainerSpec`. * `GET /tasks/{id}` now returns `Capabilities` as part of the `ContainerSpec`. +* `GET /services` now returns `PidsLimit` as part of the `ContainerSpec`. +* `GET /services/{id}` now returns `PidsLimit` as part of the `ContainerSpec`. +* `POST /services/create` now accepts `PidsLimit` as part of the `ContainerSpec`. +* `POST /services/{id}/update` now accepts `PidsLimit` as part of the `ContainerSpec`. +* `GET /tasks` now returns `PidsLimit` as part of the `ContainerSpec`. +* `GET /tasks/{id}` now returns `PidsLimit` as part of the `ContainerSpec`. * `POST /containers/create` on Linux now accepts the `HostConfig.CgroupnsMode` property. Set the property to `host` to create the container in the daemon's cgroup namespace, or `private` to create the container in its own private cgroup namespace. The per-daemon diff --git a/integration/internal/swarm/service.go b/integration/internal/swarm/service.go index efd52609bc..1ee5004860 100644 --- a/integration/internal/swarm/service.go +++ b/integration/internal/swarm/service.go @@ -196,6 +196,14 @@ func ServiceWithCapabilities(Capabilities []string) ServiceSpecOpt { } } +// ServiceWithPidsLimit sets the PidsLimit option of the service's ContainerSpec. +func ServiceWithPidsLimit(limit int64) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.PidsLimit = limit + } +} + // GetRunningTasks gets the list of running tasks for a service func GetRunningTasks(t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task { t.Helper() diff --git a/integration/service/update_test.go b/integration/service/update_test.go index 3ba03a3f70..66f81689e1 100644 --- a/integration/service/update_test.go +++ b/integration/service/update_test.go @@ -5,7 +5,9 @@ import ( "testing" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" "github.com/docker/docker/integration/internal/network" "github.com/docker/docker/integration/internal/swarm" @@ -248,6 +250,91 @@ func TestServiceUpdateNetwork(t *testing.T) { assert.NilError(t, err) } +// TestServiceUpdatePidsLimit tests creating and updating a service with PidsLimit +func TestServiceUpdatePidsLimit(t *testing.T) { + skip.If( + t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.41"), + "setting pidslimit for services is not supported before api v1.41", + ) + skip.If(t, testEnv.DaemonInfo.OSType != "linux") + tests := []struct { + name string + pidsLimit int64 + expected int64 + }{ + { + name: "create service with PidsLimit 300", + pidsLimit: 300, + expected: 300, + }, + { + name: "unset PidsLimit to 0", + pidsLimit: 0, + expected: 0, + }, + { + name: "update PidsLimit to 100", + pidsLimit: 100, + expected: 100, + }, + } + + defer setupTest(t)() + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + cli := d.NewClientT(t) + defer func() { _ = cli.Close() }() + ctx := context.Background() + var ( + serviceID string + service swarmtypes.Service + ) + for i, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if i == 0 { + serviceID = swarm.CreateService(t, d, swarm.ServiceWithPidsLimit(tc.pidsLimit)) + } else { + service = getService(t, cli, serviceID) + service.Spec.TaskTemplate.ContainerSpec.PidsLimit = tc.pidsLimit + _, err := cli.ServiceUpdate(ctx, serviceID, service.Version, service.Spec, types.ServiceUpdateOptions{}) + assert.NilError(t, err) + poll.WaitOn(t, serviceIsUpdated(cli, serviceID), swarm.ServicePoll) + } + + poll.WaitOn(t, swarm.RunningTasksCount(cli, serviceID, 1), swarm.ServicePoll) + service = getService(t, cli, serviceID) + container := getServiceTaskContainer(ctx, t, cli, serviceID) + assert.Equal(t, service.Spec.TaskTemplate.ContainerSpec.PidsLimit, tc.expected) + if tc.expected == 0 { + if container.HostConfig.Resources.PidsLimit != nil { + t.Fatalf("Expected container.HostConfig.Resources.PidsLimit to be nil") + } + } else { + assert.Assert(t, container.HostConfig.Resources.PidsLimit != nil) + assert.Equal(t, *container.HostConfig.Resources.PidsLimit, tc.expected) + } + }) + } + + err := cli.ServiceRemove(ctx, serviceID) + assert.NilError(t, err) +} + +func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APIClient, serviceID string) types.ContainerJSON { + t.Helper() + filter := filters.NewArgs() + filter.Add("service", serviceID) + filter.Add("desired-state", "running") + tasks, err := cli.TaskList(ctx, types.TaskListOptions{Filters: filter}) + assert.NilError(t, err) + assert.Assert(t, len(tasks) > 0) + + ctr, err := cli.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID) + assert.NilError(t, err) + assert.Equal(t, ctr.State.Running, true) + return ctr +} + func getService(t *testing.T, cli client.ServiceAPIClient, serviceID string) swarmtypes.Service { t.Helper() service, _, err := cli.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})