Browse code

Merge pull request #39882 from thaJeztah/swarm_pids_limit

Add API support for PidsLimit on services

Sebastiaan van Stijn authored on 2020/04/17 04:02:30
Showing 9 changed files
... ...
@@ -97,9 +97,10 @@ func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) {
97 97
 	}
98 98
 	if versions.LessThan(cliVersion, "1.41") {
99 99
 		if service.TaskTemplate.ContainerSpec != nil {
100
-			// Capabilities for docker swarm services weren't supported before
101
-			// API version 1.41
100
+			// Capabilities and PidsLimit for docker swarm services weren't
101
+			// supported before API version 1.41
102 102
 			service.TaskTemplate.ContainerSpec.Capabilities = nil
103
+			service.TaskTemplate.ContainerSpec.PidsLimit = 0
103 104
 		}
104 105
 
105 106
 		// jobs were only introduced in API version 1.41. Nil out both Job
... ...
@@ -17,7 +17,8 @@ func TestAdjustForAPIVersion(t *testing.T) {
17 17
 	spec := &swarm.ServiceSpec{
18 18
 		TaskTemplate: swarm.TaskSpec{
19 19
 			ContainerSpec: &swarm.ContainerSpec{
20
-				Sysctls: expectedSysctls,
20
+				Sysctls:   expectedSysctls,
21
+				PidsLimit: 300,
21 22
 				Privileges: &swarm.Privileges{
22 23
 					CredentialSpec: &swarm.CredentialSpec{
23 24
 						Config: "someconfig",
... ...
@@ -49,11 +50,18 @@ func TestAdjustForAPIVersion(t *testing.T) {
49 49
 	// first, does calling this with a later version correctly NOT strip
50 50
 	// fields? do the later version first, so we can reuse this spec in the
51 51
 	// next test.
52
-	adjustForAPIVersion("1.40", spec)
52
+	adjustForAPIVersion("1.41", spec)
53 53
 	if !reflect.DeepEqual(spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls) {
54 54
 		t.Error("Sysctls was stripped from spec")
55 55
 	}
56 56
 
57
+	if spec.TaskTemplate.ContainerSpec.PidsLimit == 0 {
58
+		t.Error("PidsLimit was stripped from spec")
59
+	}
60
+	if spec.TaskTemplate.ContainerSpec.PidsLimit != 300 {
61
+		t.Error("PidsLimit did not preserve the value from spec")
62
+	}
63
+
57 64
 	if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "someconfig" {
58 65
 		t.Error("CredentialSpec.Config field was stripped from spec")
59 66
 	}
... ...
@@ -72,6 +80,10 @@ func TestAdjustForAPIVersion(t *testing.T) {
72 72
 		t.Error("Sysctls was not stripped from spec")
73 73
 	}
74 74
 
75
+	if spec.TaskTemplate.ContainerSpec.PidsLimit != 0 {
76
+		t.Error("PidsLimit was not stripped from spec")
77
+	}
78
+
75 79
 	if spec.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config != "" {
76 80
 		t.Error("CredentialSpec.Config field was not stripped from spec")
77 81
 	}
... ...
@@ -2967,6 +2967,13 @@ definitions:
2967 2967
             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."
2968 2968
             type: "boolean"
2969 2969
             x-nullable: true
2970
+          PidsLimit:
2971
+            description: |
2972
+              Tune a container's PIDs limit. Set `0` for unlimited.
2973
+            type: "integer"
2974
+            format: "int64"
2975
+            default: 0
2976
+            example: 100
2970 2977
           Sysctls:
2971 2978
             description: |
2972 2979
               Set kernel namedspaced parameters (sysctls) in the container.
... ...
@@ -72,6 +72,7 @@ type ContainerSpec struct {
72 72
 	Secrets      []*SecretReference  `json:",omitempty"`
73 73
 	Configs      []*ConfigReference  `json:",omitempty"`
74 74
 	Isolation    container.Isolation `json:",omitempty"`
75
+	PidsLimit    int64               `json:",omitempty"`
75 76
 	Sysctls      map[string]string   `json:",omitempty"`
76 77
 	Capabilities []string            `json:",omitempty"`
77 78
 }
... ...
@@ -36,6 +36,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
36 36
 		Configs:      configReferencesFromGRPC(c.Configs),
37 37
 		Isolation:    IsolationFromGRPC(c.Isolation),
38 38
 		Init:         initFromGRPC(c.Init),
39
+		PidsLimit:    c.PidsLimit,
39 40
 		Sysctls:      c.Sysctls,
40 41
 		Capabilities: c.Capabilities,
41 42
 	}
... ...
@@ -263,6 +264,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
263 263
 		Secrets:      secretReferencesToGRPC(c.Secrets),
264 264
 		Isolation:    isolationToGRPC(c.Isolation),
265 265
 		Init:         initToGRPC(c.Init),
266
+		PidsLimit:    c.PidsLimit,
266 267
 		Sysctls:      c.Sysctls,
267 268
 		Capabilities: c.Capabilities,
268 269
 	}
... ...
@@ -431,6 +431,12 @@ func (c *containerConfig) volumeCreateRequest(mount *api.Mount) *volumetypes.Vol
431 431
 func (c *containerConfig) resources() enginecontainer.Resources {
432 432
 	resources := enginecontainer.Resources{}
433 433
 
434
+	// set pids limit
435
+	pidsLimit := c.spec().PidsLimit
436
+	if pidsLimit > 0 {
437
+		resources.PidsLimit = &pidsLimit
438
+	}
439
+
434 440
 	// If no limits are specified let the engine use its defaults.
435 441
 	//
436 442
 	// TODO(aluzzardi): We might want to set some limits anyway otherwise
... ...
@@ -30,6 +30,12 @@ keywords: "API, Docker, rcli, REST, documentation"
30 30
 * `POST /services/{id}/update` now accepts `Capabilities` as part of the `ContainerSpec`.
31 31
 * `GET /tasks` now  returns `Capabilities` as part of the `ContainerSpec`.
32 32
 * `GET /tasks/{id}` now  returns `Capabilities` as part of the `ContainerSpec`.
33
+* `GET /services` now returns `PidsLimit` as part of the `ContainerSpec`.
34
+* `GET /services/{id}` now returns `PidsLimit` as part of the `ContainerSpec`.
35
+* `POST /services/create` now accepts `PidsLimit` as part of the `ContainerSpec`.
36
+* `POST /services/{id}/update` now accepts `PidsLimit` as part of the `ContainerSpec`.
37
+* `GET /tasks` now  returns `PidsLimit` as part of the `ContainerSpec`.
38
+* `GET /tasks/{id}` now  returns `PidsLimit` as part of the `ContainerSpec`.
33 39
 * `POST /containers/create` on Linux now accepts the `HostConfig.CgroupnsMode` property.
34 40
   Set the property to `host` to create the container in the daemon's cgroup namespace, or
35 41
   `private` to create the container in its own private cgroup namespace.  The per-daemon
... ...
@@ -196,6 +196,14 @@ func ServiceWithCapabilities(Capabilities []string) ServiceSpecOpt {
196 196
 	}
197 197
 }
198 198
 
199
+// ServiceWithPidsLimit sets the PidsLimit option of the service's ContainerSpec.
200
+func ServiceWithPidsLimit(limit int64) ServiceSpecOpt {
201
+	return func(spec *swarmtypes.ServiceSpec) {
202
+		ensureContainerSpec(spec)
203
+		spec.TaskTemplate.ContainerSpec.PidsLimit = limit
204
+	}
205
+}
206
+
199 207
 // GetRunningTasks gets the list of running tasks for a service
200 208
 func GetRunningTasks(t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task {
201 209
 	t.Helper()
... ...
@@ -5,7 +5,9 @@ import (
5 5
 	"testing"
6 6
 
7 7
 	"github.com/docker/docker/api/types"
8
+	"github.com/docker/docker/api/types/filters"
8 9
 	swarmtypes "github.com/docker/docker/api/types/swarm"
10
+	"github.com/docker/docker/api/types/versions"
9 11
 	"github.com/docker/docker/client"
10 12
 	"github.com/docker/docker/integration/internal/network"
11 13
 	"github.com/docker/docker/integration/internal/swarm"
... ...
@@ -248,6 +250,91 @@ func TestServiceUpdateNetwork(t *testing.T) {
248 248
 	assert.NilError(t, err)
249 249
 }
250 250
 
251
+// TestServiceUpdatePidsLimit tests creating and updating a service with PidsLimit
252
+func TestServiceUpdatePidsLimit(t *testing.T) {
253
+	skip.If(
254
+		t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.41"),
255
+		"setting pidslimit for services is not supported before api v1.41",
256
+	)
257
+	skip.If(t, testEnv.DaemonInfo.OSType != "linux")
258
+	tests := []struct {
259
+		name      string
260
+		pidsLimit int64
261
+		expected  int64
262
+	}{
263
+		{
264
+			name:      "create service with PidsLimit 300",
265
+			pidsLimit: 300,
266
+			expected:  300,
267
+		},
268
+		{
269
+			name:      "unset PidsLimit to 0",
270
+			pidsLimit: 0,
271
+			expected:  0,
272
+		},
273
+		{
274
+			name:      "update PidsLimit to 100",
275
+			pidsLimit: 100,
276
+			expected:  100,
277
+		},
278
+	}
279
+
280
+	defer setupTest(t)()
281
+	d := swarm.NewSwarm(t, testEnv)
282
+	defer d.Stop(t)
283
+	cli := d.NewClientT(t)
284
+	defer func() { _ = cli.Close() }()
285
+	ctx := context.Background()
286
+	var (
287
+		serviceID string
288
+		service   swarmtypes.Service
289
+	)
290
+	for i, tc := range tests {
291
+		t.Run(tc.name, func(t *testing.T) {
292
+			if i == 0 {
293
+				serviceID = swarm.CreateService(t, d, swarm.ServiceWithPidsLimit(tc.pidsLimit))
294
+			} else {
295
+				service = getService(t, cli, serviceID)
296
+				service.Spec.TaskTemplate.ContainerSpec.PidsLimit = tc.pidsLimit
297
+				_, err := cli.ServiceUpdate(ctx, serviceID, service.Version, service.Spec, types.ServiceUpdateOptions{})
298
+				assert.NilError(t, err)
299
+				poll.WaitOn(t, serviceIsUpdated(cli, serviceID), swarm.ServicePoll)
300
+			}
301
+
302
+			poll.WaitOn(t, swarm.RunningTasksCount(cli, serviceID, 1), swarm.ServicePoll)
303
+			service = getService(t, cli, serviceID)
304
+			container := getServiceTaskContainer(ctx, t, cli, serviceID)
305
+			assert.Equal(t, service.Spec.TaskTemplate.ContainerSpec.PidsLimit, tc.expected)
306
+			if tc.expected == 0 {
307
+				if container.HostConfig.Resources.PidsLimit != nil {
308
+					t.Fatalf("Expected container.HostConfig.Resources.PidsLimit to be nil")
309
+				}
310
+			} else {
311
+				assert.Assert(t, container.HostConfig.Resources.PidsLimit != nil)
312
+				assert.Equal(t, *container.HostConfig.Resources.PidsLimit, tc.expected)
313
+			}
314
+		})
315
+	}
316
+
317
+	err := cli.ServiceRemove(ctx, serviceID)
318
+	assert.NilError(t, err)
319
+}
320
+
321
+func getServiceTaskContainer(ctx context.Context, t *testing.T, cli client.APIClient, serviceID string) types.ContainerJSON {
322
+	t.Helper()
323
+	filter := filters.NewArgs()
324
+	filter.Add("service", serviceID)
325
+	filter.Add("desired-state", "running")
326
+	tasks, err := cli.TaskList(ctx, types.TaskListOptions{Filters: filter})
327
+	assert.NilError(t, err)
328
+	assert.Assert(t, len(tasks) > 0)
329
+
330
+	ctr, err := cli.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID)
331
+	assert.NilError(t, err)
332
+	assert.Equal(t, ctr.State.Running, true)
333
+	return ctr
334
+}
335
+
251 336
 func getService(t *testing.T, cli client.ServiceAPIClient, serviceID string) swarmtypes.Service {
252 337
 	t.Helper()
253 338
 	service, _, err := cli.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})