Browse Source

Add API support for PidsLimit on services

Support for PidsLimit was added to SwarmKit in docker/swarmkit/pull/2415,
but never exposed through the Docker remove API.

This patch exposes the feature in the repote API.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn 5 years ago
parent
commit
157c53c8e0

+ 3 - 2
api/server/router/swarm/helpers.go

@@ -97,9 +97,10 @@ func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) {
 	}
 	}
 	if versions.LessThan(cliVersion, "1.41") {
 	if versions.LessThan(cliVersion, "1.41") {
 		if service.TaskTemplate.ContainerSpec != nil {
 		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.Capabilities = nil
+			service.TaskTemplate.ContainerSpec.PidsLimit = 0
 		}
 		}
 
 
 		// jobs were only introduced in API version 1.41. Nil out both Job
 		// jobs were only introduced in API version 1.41. Nil out both Job

+ 14 - 2
api/server/router/swarm/helpers_test.go

@@ -17,7 +17,8 @@ func TestAdjustForAPIVersion(t *testing.T) {
 	spec := &swarm.ServiceSpec{
 	spec := &swarm.ServiceSpec{
 		TaskTemplate: swarm.TaskSpec{
 		TaskTemplate: swarm.TaskSpec{
 			ContainerSpec: &swarm.ContainerSpec{
 			ContainerSpec: &swarm.ContainerSpec{
-				Sysctls: expectedSysctls,
+				Sysctls:   expectedSysctls,
+				PidsLimit: 300,
 				Privileges: &swarm.Privileges{
 				Privileges: &swarm.Privileges{
 					CredentialSpec: &swarm.CredentialSpec{
 					CredentialSpec: &swarm.CredentialSpec{
 						Config: "someconfig",
 						Config: "someconfig",
@@ -49,11 +50,18 @@ func TestAdjustForAPIVersion(t *testing.T) {
 	// first, does calling this with a later version correctly NOT strip
 	// 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
 	// fields? do the later version first, so we can reuse this spec in the
 	// next test.
 	// next test.
-	adjustForAPIVersion("1.40", spec)
+	adjustForAPIVersion("1.41", spec)
 	if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls) {
 	if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls) {
 		t.Error("Sysctls was stripped from spec")
 		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" {
 	if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "someconfig" {
 		t.Error("CredentialSpec.Config field was stripped from spec")
 		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")
 		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 != "" {
 	if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "" {
 		t.Error("CredentialSpec.Config field was not stripped from spec")
 		t.Error("CredentialSpec.Config field was not stripped from spec")
 	}
 	}

+ 7 - 0
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."
             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"
             type: "boolean"
             x-nullable: true
             x-nullable: true
+          PidsLimit:
+            description: |
+              Tune a container's PIDs limit. Set `0` for unlimited.
+            type: "integer"
+            format: "int64"
+            default: 0
+            example: 100
           Sysctls:
           Sysctls:
             description: |
             description: |
               Set kernel namedspaced parameters (sysctls) in the container.
               Set kernel namedspaced parameters (sysctls) in the container.

+ 1 - 0
api/types/swarm/container.go

@@ -72,6 +72,7 @@ type ContainerSpec struct {
 	Secrets      []*SecretReference  `json:",omitempty"`
 	Secrets      []*SecretReference  `json:",omitempty"`
 	Configs      []*ConfigReference  `json:",omitempty"`
 	Configs      []*ConfigReference  `json:",omitempty"`
 	Isolation    container.Isolation `json:",omitempty"`
 	Isolation    container.Isolation `json:",omitempty"`
+	PidsLimit    int64               `json:",omitempty"`
 	Sysctls      map[string]string   `json:",omitempty"`
 	Sysctls      map[string]string   `json:",omitempty"`
 	Capabilities []string            `json:",omitempty"`
 	Capabilities []string            `json:",omitempty"`
 }
 }

+ 2 - 0
daemon/cluster/convert/container.go

@@ -36,6 +36,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
 		Configs:      configReferencesFromGRPC(c.Configs),
 		Configs:      configReferencesFromGRPC(c.Configs),
 		Isolation:    IsolationFromGRPC(c.Isolation),
 		Isolation:    IsolationFromGRPC(c.Isolation),
 		Init:         initFromGRPC(c.Init),
 		Init:         initFromGRPC(c.Init),
+		PidsLimit:    c.PidsLimit,
 		Sysctls:      c.Sysctls,
 		Sysctls:      c.Sysctls,
 		Capabilities: c.Capabilities,
 		Capabilities: c.Capabilities,
 	}
 	}
@@ -263,6 +264,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
 		Secrets:      secretReferencesToGRPC(c.Secrets),
 		Secrets:      secretReferencesToGRPC(c.Secrets),
 		Isolation:    isolationToGRPC(c.Isolation),
 		Isolation:    isolationToGRPC(c.Isolation),
 		Init:         initToGRPC(c.Init),
 		Init:         initToGRPC(c.Init),
+		PidsLimit:    c.PidsLimit,
 		Sysctls:      c.Sysctls,
 		Sysctls:      c.Sysctls,
 		Capabilities: c.Capabilities,
 		Capabilities: c.Capabilities,
 	}
 	}

+ 6 - 0
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 {
 func (c *containerConfig) resources() enginecontainer.Resources {
 	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.
 	// If no limits are specified let the engine use its defaults.
 	//
 	//
 	// TODO(aluzzardi): We might want to set some limits anyway otherwise
 	// TODO(aluzzardi): We might want to set some limits anyway otherwise

+ 6 - 0
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`.
 * `POST /services/{id}/update` now accepts `Capabilities` as part of the `ContainerSpec`.
 * `GET /tasks` now  returns `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 /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.
 * `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
   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
   `private` to create the container in its own private cgroup namespace.  The per-daemon

+ 8 - 0
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
 // GetRunningTasks gets the list of running tasks for a service
 func GetRunningTasks(t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task {
 func GetRunningTasks(t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task {
 	t.Helper()
 	t.Helper()

+ 87 - 0
integration/service/update_test.go

@@ -5,7 +5,9 @@ import (
 	"testing"
 	"testing"
 
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
 	swarmtypes "github.com/docker/docker/api/types/swarm"
 	swarmtypes "github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/client"
 	"github.com/docker/docker/integration/internal/network"
 	"github.com/docker/docker/integration/internal/network"
 	"github.com/docker/docker/integration/internal/swarm"
 	"github.com/docker/docker/integration/internal/swarm"
@@ -248,6 +250,91 @@ func TestServiceUpdateNetwork(t *testing.T) {
 	assert.NilError(t, err)
 	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 {
 func getService(t *testing.T, cli client.ServiceAPIClient, serviceID string) swarmtypes.Service {
 	t.Helper()
 	t.Helper()
 	service, _, err := cli.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
 	service, _, err := cli.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})