diff --git a/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go index a70248860208f..f984328322280 100644 --- a/api/server/router/swarm/cluster_routes.go +++ b/api/server/router/swarm/cluster_routes.go @@ -182,8 +182,17 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter, encodedAuth := r.Header.Get("X-Registry-Auth") cliVersion := r.Header.Get("version") queryRegistry := false - if cliVersion != "" && versions.LessThan(cliVersion, "1.30") { - queryRegistry = true + if cliVersion != "" { + if versions.LessThan(cliVersion, "1.30") { + queryRegistry = true + } + if versions.LessThan(cliVersion, "1.39") { + if service.TaskTemplate.ContainerSpec != nil { + // Sysctls for docker swarm services weren't supported before + // API version 1.39 + service.TaskTemplate.ContainerSpec.Sysctls = nil + } + } } resp, err := sr.backend.CreateService(service, encodedAuth, queryRegistry) @@ -216,8 +225,17 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, flags.Rollback = r.URL.Query().Get("rollback") cliVersion := r.Header.Get("version") queryRegistry := false - if cliVersion != "" && versions.LessThan(cliVersion, "1.30") { - queryRegistry = true + if cliVersion != "" { + if versions.LessThan(cliVersion, "1.30") { + queryRegistry = true + } + if versions.LessThan(cliVersion, "1.39") { + if service.TaskTemplate.ContainerSpec != nil { + // Sysctls for docker swarm services weren't supported before + // API version 1.39 + service.TaskTemplate.ContainerSpec.Sysctls = nil + } + } } resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry) diff --git a/api/swagger.yaml b/api/swagger.yaml index fd4f2f59efae5..7267432285c5d 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2750,6 +2750,18 @@ 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 + Sysctls: + description: | + Set kernel namedspaced parameters (sysctls) in the container. + The Sysctls option on services accepts the same sysctls as the + are supported on containers. Note that while the same sysctls are + supported, no guarantees or checks are made about their + suitability for a clustered environment, and it's up to the user + to determine whether a given sysctl will work properly in a + Service. + type: "object" + additionalProperties: + type: "string" NetworkAttachmentSpec: description: | Read-only spec type for non-swarm containers attached to swarm overlay diff --git a/api/types/swarm/container.go b/api/types/swarm/container.go index 151211ff5a496..e12f09837fce8 100644 --- a/api/types/swarm/container.go +++ b/api/types/swarm/container.go @@ -71,4 +71,5 @@ type ContainerSpec struct { Secrets []*SecretReference `json:",omitempty"` Configs []*ConfigReference `json:",omitempty"` Isolation container.Isolation `json:",omitempty"` + Sysctls map[string]string `json:",omitempty"` } diff --git a/daemon/cluster/convert/container.go b/daemon/cluster/convert/container.go index d889b4004ccc7..effd362a12992 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), + Sysctls: c.Sysctls, } if c.DNSConfig != nil { @@ -251,6 +252,7 @@ func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) { Configs: configReferencesToGRPC(c.Configs), Isolation: isolationToGRPC(c.Isolation), Init: initToGRPC(c.Init), + Sysctls: c.Sysctls, } if c.DNSConfig != nil { diff --git a/daemon/cluster/executor/container/container.go b/daemon/cluster/executor/container/container.go index 77d21d2c1f32b..98de56d68f170 100644 --- a/daemon/cluster/executor/container/container.go +++ b/daemon/cluster/executor/container/container.go @@ -364,6 +364,7 @@ func (c *containerConfig) hostConfig() *enginecontainer.HostConfig { ReadonlyRootfs: c.spec().ReadOnly, Isolation: c.isolation(), Init: c.init(), + Sysctls: c.spec().Sysctls, } if c.spec().DNSConfig != nil { diff --git a/docs/api/version-history.md b/docs/api/version-history.md index b91e5954f56e2..9c9d2f954a11a 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -30,6 +30,12 @@ keywords: "API, Docker, rcli, REST, documentation" on the node.label. The format of the label filter is `node.label=`/`node.label==` to return those with the specified labels, or `node.label!=`/`node.label!==` to return those without the specified labels. +* `GET /services` now returns `Sysctls` as part of the `ContainerSpec`. +* `GET /services/{id}` now returns `Sysctls` as part of the `ContainerSpec`. +* `POST /services/create` now accepts `Sysctls` as part of the `ContainerSpec`. +* `POST /services/{id}/update` now accepts `Sysctls` as part of the `ContainerSpec`. +* `GET /tasks` now returns `Sysctls` as part of the `ContainerSpec`. +* `GET /tasks/{id}` now returns `Sysctls` as part of the `ContainerSpec`. ## V1.38 API changes diff --git a/integration/internal/swarm/service.go b/integration/internal/swarm/service.go index d8b16224fb61e..47c9abbea912e 100644 --- a/integration/internal/swarm/service.go +++ b/integration/internal/swarm/service.go @@ -159,6 +159,14 @@ func ServiceWithEndpoint(endpoint *swarmtypes.EndpointSpec) ServiceSpecOpt { } } +// ServiceWithSysctls sets the Sysctls option of the service's ContainerSpec. +func ServiceWithSysctls(sysctls map[string]string) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensureContainerSpec(spec) + spec.TaskTemplate.ContainerSpec.Sysctls = sysctls + } +} + // GetRunningTasks gets the list of running tasks for a service func GetRunningTasks(t *testing.T, d *daemon.Daemon, serviceID string) []swarmtypes.Task { t.Helper() diff --git a/integration/service/create_test.go b/integration/service/create_test.go index 384ca335e4039..a8f2538a9b34d 100644 --- a/integration/service/create_test.go +++ b/integration/service/create_test.go @@ -10,6 +10,7 @@ import ( "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" @@ -17,6 +18,7 @@ import ( "gotest.tools/assert" is "gotest.tools/assert/cmp" "gotest.tools/poll" + "gotest.tools/skip" ) func TestServiceCreateInit(t *testing.T) { @@ -309,6 +311,101 @@ func TestCreateServiceConfigFileMode(t *testing.T) { assert.NilError(t, err) } +// TestServiceCreateSysctls tests that a service created with sysctl options in +// the ContainerSpec correctly applies those options. +// +// To test this, we're going to create a service with the sysctl option +// +// {"net.ipv4.ip_nonlocal_bind": "0"} +// +// We'll get the service's tasks to get the container ID, and then we'll +// inspect the container. If the output of the container inspect contains the +// sysctl option with the correct value, we can assume that the sysctl has been +// plumbed correctly. +// +// Next, we'll remove that service and create a new service with that option +// set to 1. This means that no matter what the default is, we can be confident +// that the sysctl option is applying as intended. +// +// Additionally, we'll do service and task inspects to verify that the inspect +// output includes the desired sysctl option. +// +// We're using net.ipv4.ip_nonlocal_bind because it's something that I'm fairly +// confident won't be modified by the container runtime, and won't blow +// anything up in the test environment +func TestCreateServiceSysctls(t *testing.T) { + skip.If( + t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.39"), + "setting service sysctls is unsupported before api v1.39", + ) + + defer setupTest(t)() + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + client := d.NewClientT(t) + defer client.Close() + + ctx := context.Background() + + // run thie block twice, so that no matter what the default value of + // net.ipv4.ip_nonlocal_bind is, we can verify that setting the sysctl + // options works + for _, expected := range []string{"0", "1"} { + + // store the map we're going to be using everywhere. + expectedSysctls := map[string]string{"net.ipv4.ip_nonlocal_bind": expected} + + // Create the service with the sysctl options + var instances uint64 = 1 + serviceID := swarm.CreateService(t, d, + swarm.ServiceWithSysctls(expectedSysctls), + ) + + // wait for the service to converge to 1 running task as expected + poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, instances)) + + // we're going to check 3 things: + // + // 1. Does the container, when inspected, have the sysctl option set? + // 2. Does the task have the sysctl in the spec? + // 3. Does the service have the sysctl in the spec? + // + // if all 3 of these things are true, we know that the sysctl has been + // plumbed correctly through the engine. + // + // We don't actually have to get inside the container and check its + // logs or anything. If we see the sysctl set on the container inspect, + // we know that the sysctl is plumbed correctly. everything below that + // level has been tested elsewhere. (thanks @thaJeztah, because an + // earlier version of this test had to get container logs and was much + // more complex) + + // get all of the tasks of the service, so we can get the container + filter := filters.NewArgs() + filter.Add("service", serviceID) + tasks, err := client.TaskList(ctx, types.TaskListOptions{ + Filters: filter, + }) + assert.NilError(t, err) + assert.Check(t, is.Equal(len(tasks), 1)) + + // verify that the container has the sysctl option set + ctnr, err := client.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID) + assert.NilError(t, err) + assert.DeepEqual(t, ctnr.HostConfig.Sysctls, expectedSysctls) + + // verify that the task has the sysctl option set in the task object + assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.Sysctls, expectedSysctls) + + // verify that the service also has the sysctl set in the spec. + service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{}) + assert.NilError(t, err) + assert.DeepEqual(t, + service.Spec.TaskTemplate.ContainerSpec.Sysctls, expectedSysctls, + ) + } +} + func serviceRunningTasksCount(client client.ServiceAPIClient, serviceID string, instances uint64) func(log poll.LogT) poll.Result { return func(log poll.LogT) poll.Result { filter := filters.NewArgs()