Skip to content

Commit 14da20f

Browse files
committed
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>
1 parent d6a7c22 commit 14da20f

8 files changed

Lines changed: 149 additions & 4 deletions

File tree

api/server/router/swarm/cluster_routes.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,17 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
182182
encodedAuth := r.Header.Get("X-Registry-Auth")
183183
cliVersion := r.Header.Get("version")
184184
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+
}
187196
}
188197

189198
resp, err := sr.backend.CreateService(service, encodedAuth, queryRegistry)
@@ -216,8 +225,17 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
216225
flags.Rollback = r.URL.Query().Get("rollback")
217226
cliVersion := r.Header.Get("version")
218227
queryRegistry := false
219-
if cliVersion != "" && versions.LessThan(cliVersion, "1.30") {
220-
queryRegistry = true
228+
if cliVersion != "" {
229+
if versions.LessThan(cliVersion, "1.30") {
230+
queryRegistry = true
231+
}
232+
if versions.LessThan(cliVersion, "1.39") {
233+
if service.TaskTemplate.ContainerSpec != nil {
234+
// Sysctls for docker swarm services weren't supported before
235+
// API version 1.39
236+
service.TaskTemplate.ContainerSpec.Sysctls = nil
237+
}
238+
}
221239
}
222240

223241
resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry)

api/swagger.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2750,6 +2750,18 @@ definitions:
27502750
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."
27512751
type: "boolean"
27522752
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"
27532765
NetworkAttachmentSpec:
27542766
description: |
27552767
Read-only spec type for non-swarm containers attached to swarm overlay

api/types/swarm/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,5 @@ type ContainerSpec struct {
7171
Secrets []*SecretReference `json:",omitempty"`
7272
Configs []*ConfigReference `json:",omitempty"`
7373
Isolation container.Isolation `json:",omitempty"`
74+
Sysctls map[string]string `json:",omitempty"`
7475
}

daemon/cluster/convert/container.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
3636
Configs: configReferencesFromGRPC(c.Configs),
3737
Isolation: IsolationFromGRPC(c.Isolation),
3838
Init: initFromGRPC(c.Init),
39+
Sysctls: c.Sysctls,
3940
}
4041

4142
if c.DNSConfig != nil {
@@ -251,6 +252,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
251252
Configs: configReferencesToGRPC(c.Configs),
252253
Isolation: isolationToGRPC(c.Isolation),
253254
Init: initToGRPC(c.Init),
255+
Sysctls: c.Sysctls,
254256
}
255257

256258
if c.DNSConfig != nil {

daemon/cluster/executor/container/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig {
364364
ReadonlyRootfs: c.spec().ReadOnly,
365365
Isolation: c.isolation(),
366366
Init: c.init(),
367+
Sysctls: c.spec().Sysctls,
367368
}
368369

369370
if c.spec().DNSConfig != nil {

docs/api/version-history.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ keywords: "API, Docker, rcli, REST, documentation"
3030
on the node.label. The format of the label filter is `node.label=<key>`/`node.label=<key>=<value>`
3131
to return those with the specified labels, or `node.label!=<key>`/`node.label!=<key>=<value>`
3232
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`.
3339

3440
## V1.38 API changes
3541

integration/internal/swarm/service.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ func ServiceWithEndpoint(endpoint *swarmtypes.EndpointSpec) ServiceSpecOpt {
159159
}
160160
}
161161

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+
162170
// GetRunningTasks gets the list of running tasks for a service
163171
func GetRunningTasks(t *testing.T, d *daemon.Daemon, serviceID string) []swarmtypes.Task {
164172
t.Helper()

integration/service/create_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import (
1010
"github.com/docker/docker/api/types"
1111
"github.com/docker/docker/api/types/filters"
1212
swarmtypes "github.com/docker/docker/api/types/swarm"
13+
"github.com/docker/docker/api/types/versions"
1314
"github.com/docker/docker/client"
1415
"github.com/docker/docker/integration/internal/network"
1516
"github.com/docker/docker/integration/internal/swarm"
1617
"github.com/docker/docker/internal/test/daemon"
1718
"gotest.tools/assert"
1819
is "gotest.tools/assert/cmp"
1920
"gotest.tools/poll"
21+
"gotest.tools/skip"
2022
)
2123

2224
func TestServiceCreateInit(t *testing.T) {
@@ -309,6 +311,101 @@ func TestCreateServiceConfigFileMode(t *testing.T) {
309311
assert.NilError(t, err)
310312
}
311313

314+
// TestServiceCreateSysctls tests that a service created with sysctl options in
315+
// the ContainerSpec correctly applies those options.
316+
//
317+
// To test this, we're going to create a service with the sysctl option
318+
//
319+
// {"net.ipv4.ip_nonlocal_bind": "0"}
320+
//
321+
// We'll get the service's tasks to get the container ID, and then we'll
322+
// inspect the container. If the output of the container inspect contains the
323+
// sysctl option with the correct value, we can assume that the sysctl has been
324+
// plumbed correctly.
325+
//
326+
// Next, we'll remove that service and create a new service with that option
327+
// set to 1. This means that no matter what the default is, we can be confident
328+
// that the sysctl option is applying as intended.
329+
//
330+
// Additionally, we'll do service and task inspects to verify that the inspect
331+
// output includes the desired sysctl option.
332+
//
333+
// We're using net.ipv4.ip_nonlocal_bind because it's something that I'm fairly
334+
// confident won't be modified by the container runtime, and won't blow
335+
// anything up in the test environment
336+
func TestCreateServiceSysctls(t *testing.T) {
337+
skip.If(
338+
t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.39"),
339+
"setting service sysctls is unsupported before api v1.39",
340+
)
341+
342+
defer setupTest(t)()
343+
d := swarm.NewSwarm(t, testEnv)
344+
defer d.Stop(t)
345+
client := d.NewClientT(t)
346+
defer client.Close()
347+
348+
ctx := context.Background()
349+
350+
// run thie block twice, so that no matter what the default value of
351+
// net.ipv4.ip_nonlocal_bind is, we can verify that setting the sysctl
352+
// options works
353+
for _, expected := range []string{"0", "1"} {
354+
355+
// store the map we're going to be using everywhere.
356+
expectedSysctls := map[string]string{"net.ipv4.ip_nonlocal_bind": expected}
357+
358+
// Create the service with the sysctl options
359+
var instances uint64 = 1
360+
serviceID := swarm.CreateService(t, d,
361+
swarm.ServiceWithSysctls(expectedSysctls),
362+
)
363+
364+
// wait for the service to converge to 1 running task as expected
365+
poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances))
366+
367+
// we're going to check 3 things:
368+
//
369+
// 1. Does the container, when inspected, have the sysctl option set?
370+
// 2. Does the task have the sysctl in the spec?
371+
// 3. Does the service have the sysctl in the spec?
372+
//
373+
// if all 3 of these things are true, we know that the sysctl has been
374+
// plumbed correctly through the engine.
375+
//
376+
// We don't actually have to get inside the container and check its
377+
// logs or anything. If we see the sysctl set on the container inspect,
378+
// we know that the sysctl is plumbed correctly. everything below that
379+
// level has been tested elsewhere. (thanks @thaJeztah, because an
380+
// earlier version of this test had to get container logs and was much
381+
// more complex)
382+
383+
// get all of the tasks of the service, so we can get the container
384+
filter := filters.NewArgs()
385+
filter.Add("service", serviceID)
386+
tasks, err := client.TaskList(ctx, types.TaskListOptions{
387+
Filters: filter,
388+
})
389+
assert.NilError(t, err)
390+
assert.Check(t, is.Equal(len(tasks), 1))
391+
392+
// verify that the container has the sysctl option set
393+
ctnr, err := client.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID)
394+
assert.NilError(t, err)
395+
assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls)
396+
397+
// verify that the task has the sysctl option set in the task object
398+
assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls)
399+
400+
// verify that the service also has the sysctl set in the spec.
401+
service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
402+
assert.NilError(t, err)
403+
assert.DeepEqual(t,
404+
service.Spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls,
405+
)
406+
}
407+
}
408+
312409
func serviceRunningTasksCount(client client.ServiceAPIClient, serviceID string, instances uint64) func(log poll.LogT) poll.Result {
313410
return func(log poll.LogT) poll.Result {
314411
filter := filters.NewArgs()

0 commit comments

Comments
 (0)