Browse code

Add support for sysctl options in services

Adds support for sysctl options in docker services.

* Adds API plumbing for creating services with sysctl options set.
* Adds swagger.yaml documentation for new API field.
* Updates the API version history document.
* Changes executor package to make use of the Sysctls field on objects
* Includes integration test to verify that new behavior works.

Essentially, everything needed to support the equivalent of docker run's
`--sysctl` option except the CLI.

Includes a vendoring of swarmkit for proto changes to support the new
behavior.

Signed-off-by: Drew Erny <drew.erny@docker.com>

Drew Erny authored on 2018/08/23 05:24:14
Showing 8 changed files
... ...
@@ -182,8 +182,17 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
182 182
 	encodedAuth := r.Header.Get("X-Registry-Auth")
183 183
 	cliVersion := r.Header.Get("version")
184 184
 	queryRegistry := false
185
-	if cliVersion != "" && versions.LessThan(cliVersion, "1.30") {
186
-		queryRegistry = true
185
+	if cliVersion != "" {
186
+		if versions.LessThan(cliVersion, "1.30") {
187
+			queryRegistry = true
188
+		}
189
+		if versions.LessThan(cliVersion, "1.39") {
190
+			if service.TaskTemplate.ContainerSpec != nil {
191
+				// Sysctls for docker swarm services weren't supported before
192
+				// API version 1.39
193
+				service.TaskTemplate.ContainerSpec.Sysctls = nil
194
+			}
195
+		}
187 196
 	}
188 197
 
189 198
 	resp, err := sr.backend.CreateService(service, encodedAuth, queryRegistry)
... ...
@@ -216,8 +225,17 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
216 216
 	flags.Rollback = r.URL.Query().Get("rollback")
217 217
 	cliVersion := r.Header.Get("version")
218 218
 	queryRegistry := false
219
-	if cliVersion != "" && versions.LessThan(cliVersion, "1.30") {
220
-		queryRegistry = true
219
+	if cliVersion != "" {
220
+		if versions.LessThan(cliVersion, "1.30") {
221
+			queryRegistry = true
222
+		}
223
+		if versions.LessThan(cliVersion, "1.39") {
224
+			if service.TaskTemplate.ContainerSpec != nil {
225
+				// Sysctls for docker swarm services weren't supported before
226
+				// API version 1.39
227
+				service.TaskTemplate.ContainerSpec.Sysctls = nil
228
+			}
229
+		}
221 230
 	}
222 231
 
223 232
 	resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry)
... ...
@@ -2750,6 +2750,18 @@ definitions:
2750 2750
             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."
2751 2751
             type: "boolean"
2752 2752
             x-nullable: true
2753
+          Sysctls:
2754
+            description: |
2755
+              Set kernel namedspaced parameters (sysctls) in the container.
2756
+              The Sysctls option on services accepts the same sysctls as the
2757
+              are supported on containers. Note that while the same sysctls are
2758
+              supported, no guarantees or checks are made about their
2759
+              suitability for a clustered environment, and it's up to the user
2760
+              to determine whether a given sysctl will work properly in a
2761
+              Service.
2762
+            type: "object"
2763
+            additionalProperties:
2764
+              type: "string"
2753 2765
       NetworkAttachmentSpec:
2754 2766
         description: |
2755 2767
           Read-only spec type for non-swarm containers attached to swarm overlay
... ...
@@ -71,4 +71,5 @@ type ContainerSpec struct {
71 71
 	Secrets   []*SecretReference  `json:",omitempty"`
72 72
 	Configs   []*ConfigReference  `json:",omitempty"`
73 73
 	Isolation container.Isolation `json:",omitempty"`
74
+	Sysctls   map[string]string   `json:",omitempty"`
74 75
 }
... ...
@@ -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
+		Sysctls:    c.Sysctls,
39 40
 	}
40 41
 
41 42
 	if c.DNSConfig != nil {
... ...
@@ -251,6 +252,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
251 251
 		Configs:    configReferencesToGRPC(c.Configs),
252 252
 		Isolation:  isolationToGRPC(c.Isolation),
253 253
 		Init:       initToGRPC(c.Init),
254
+		Sysctls:    c.Sysctls,
254 255
 	}
255 256
 
256 257
 	if c.DNSConfig != nil {
... ...
@@ -364,6 +364,7 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
364 364
 		ReadonlyRootfs: c.spec().ReadOnly,
365 365
 		Isolation:      c.isolation(),
366 366
 		Init:           c.init(),
367
+		Sysctls:        c.spec().Sysctls,
367 368
 	}
368 369
 
369 370
 	if c.spec().DNSConfig != nil {
... ...
@@ -30,6 +30,12 @@ keywords: "API, Docker, rcli, REST, documentation"
30 30
   on the node.label. The format of the label filter is `node.label=<key>`/`node.label=<key>=<value>`
31 31
   to return those with the specified labels, or `node.label!=<key>`/`node.label!=<key>=<value>`
32 32
   to return those without the specified labels.
33
+* `GET /services` now returns `Sysctls` as part of the `ContainerSpec`.
34
+* `GET /services/{id}` now returns `Sysctls` as part of the `ContainerSpec`.
35
+* `POST /services/create` now accepts `Sysctls` as part of the `ContainerSpec`.
36
+* `POST /services/{id}/update` now accepts `Sysctls` as part of the `ContainerSpec`.
37
+* `GET /tasks` now  returns `Sysctls` as part of the `ContainerSpec`.
38
+* `GET /tasks/{id}` now  returns `Sysctls` as part of the `ContainerSpec`.
33 39
 
34 40
 ## V1.38 API changes
35 41
 
... ...
@@ -159,6 +159,14 @@ func ServiceWithEndpoint(endpoint *swarmtypes.EndpointSpec) ServiceSpecOpt {
159 159
 	}
160 160
 }
161 161
 
162
+// ServiceWithSysctls sets the Sysctls option of the service's ContainerSpec.
163
+func ServiceWithSysctls(sysctls map[string]string) ServiceSpecOpt {
164
+	return func(spec *swarmtypes.ServiceSpec) {
165
+		ensureContainerSpec(spec)
166
+		spec.TaskTemplate.ContainerSpec.Sysctls = sysctls
167
+	}
168
+}
169
+
162 170
 // GetRunningTasks gets the list of running tasks for a service
163 171
 func GetRunningTasks(t *testing.T, d *daemon.Daemon, serviceID string) []swarmtypes.Task {
164 172
 	t.Helper()
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"github.com/docker/docker/api/types"
11 11
 	"github.com/docker/docker/api/types/filters"
12 12
 	swarmtypes "github.com/docker/docker/api/types/swarm"
13
+	"github.com/docker/docker/api/types/versions"
13 14
 	"github.com/docker/docker/client"
14 15
 	"github.com/docker/docker/integration/internal/network"
15 16
 	"github.com/docker/docker/integration/internal/swarm"
... ...
@@ -17,6 +18,7 @@ import (
17 17
 	"gotest.tools/assert"
18 18
 	is "gotest.tools/assert/cmp"
19 19
 	"gotest.tools/poll"
20
+	"gotest.tools/skip"
20 21
 )
21 22
 
22 23
 func TestServiceCreateInit(t *testing.T) {
... ...
@@ -309,6 +311,101 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
309 309
 	assert.NilError(t, err)
310 310
 }
311 311
 
312
+// TestServiceCreateSysctls tests that a service created with sysctl options in
313
+// the ContainerSpec correctly applies those options.
314
+//
315
+// To test this, we're going to create a service with the sysctl option
316
+//
317
+//   {"net.ipv4.ip_nonlocal_bind": "0"}
318
+//
319
+// We'll get the service's tasks to get the container ID, and then we'll
320
+// inspect the container. If the output of the container inspect contains the
321
+// sysctl option with the correct value, we can assume that the sysctl has been
322
+// plumbed correctly.
323
+//
324
+// Next, we'll remove that service and create a new service with that option
325
+// set to 1. This means that no matter what the default is, we can be confident
326
+// that the sysctl option is applying as intended.
327
+//
328
+// Additionally, we'll do service and task inspects to verify that the inspect
329
+// output includes the desired sysctl option.
330
+//
331
+// We're using net.ipv4.ip_nonlocal_bind because it's something that I'm fairly
332
+// confident won't be modified by the container runtime, and won't blow
333
+// anything up in the test environment
334
+func TestCreateServiceSysctls(t *testing.T) {
335
+	skip.If(
336
+		t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.39"),
337
+		"setting service sysctls is unsupported before api v1.39",
338
+	)
339
+
340
+	defer setupTest(t)()
341
+	d := swarm.NewSwarm(t, testEnv)
342
+	defer d.Stop(t)
343
+	client := d.NewClientT(t)
344
+	defer client.Close()
345
+
346
+	ctx := context.Background()
347
+
348
+	// run thie block twice, so that no matter what the default value of
349
+	// net.ipv4.ip_nonlocal_bind is, we can verify that setting the sysctl
350
+	// options works
351
+	for _, expected := range []string{"0", "1"} {
352
+
353
+		// store the map we're going to be using everywhere.
354
+		expectedSysctls := map[string]string{"net.ipv4.ip_nonlocal_bind": expected}
355
+
356
+		// Create the service with the sysctl options
357
+		var instances uint64 = 1
358
+		serviceID := swarm.CreateService(t, d,
359
+			swarm.ServiceWithSysctls(expectedSysctls),
360
+		)
361
+
362
+		// wait for the service to converge to 1 running task as expected
363
+		poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances))
364
+
365
+		// we're going to check 3 things:
366
+		//
367
+		//   1. Does the container, when inspected, have the sysctl option set?
368
+		//   2. Does the task have the sysctl in the spec?
369
+		//   3. Does the service have the sysctl in the spec?
370
+		//
371
+		// if all 3 of these things are true, we know that the sysctl has been
372
+		// plumbed correctly through the engine.
373
+		//
374
+		// We don't actually have to get inside the container and check its
375
+		// logs or anything. If we see the sysctl set on the container inspect,
376
+		// we know that the sysctl is plumbed correctly. everything below that
377
+		// level has been tested elsewhere. (thanks @thaJeztah, because an
378
+		// earlier version of this test had to get container logs and was much
379
+		// more complex)
380
+
381
+		// get all of the tasks of the service, so we can get the container
382
+		filter := filters.NewArgs()
383
+		filter.Add("service", serviceID)
384
+		tasks, err := client.TaskList(ctx, types.TaskListOptions{
385
+			Filters: filter,
386
+		})
387
+		assert.NilError(t, err)
388
+		assert.Check(t, is.Equal(len(tasks), 1))
389
+
390
+		// verify that the container has the sysctl option set
391
+		ctnr, err := client.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID)
392
+		assert.NilError(t, err)
393
+		assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls)
394
+
395
+		// verify that the task has the sysctl option set in the task object
396
+		assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls)
397
+
398
+		// verify that the service also has the sysctl set in the spec.
399
+		service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
400
+		assert.NilError(t, err)
401
+		assert.DeepEqual(t,
402
+			service.Spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls,
403
+		)
404
+	}
405
+}
406
+
312 407
 func serviceRunningTasksCount(client client.ServiceAPIClient, serviceID string, instances uint64) func(log poll.LogT) poll.Result {
313 408
 	return func(log poll.LogT) poll.Result {
314 409
 		filter := filters.NewArgs()