Skip to content

Commit

Permalink
Add support for sysctl options in services
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
dperny committed Sep 20, 2018
1 parent d6a7c22 commit 14da20f
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 4 deletions.
26 changes: 22 additions & 4 deletions api/server/router/swarm/cluster_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions api/types/swarm/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
2 changes: 2 additions & 0 deletions daemon/cluster/convert/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions daemon/cluster/executor/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions docs/api/version-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ keywords: "API, Docker, rcli, REST, documentation"
on the node.label. The format of the label filter is `node.label=<key>`/`node.label=<key>=<value>`
to return those with the specified labels, or `node.label!=<key>`/`node.label!=<key>=<value>`
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

Expand Down
8 changes: 8 additions & 0 deletions integration/internal/swarm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
97 changes: 97 additions & 0 deletions integration/service/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ 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"
"github.com/docker/docker/internal/test/daemon"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/poll"
"gotest.tools/skip"
)

func TestServiceCreateInit(t *testing.T) {
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 14da20f

Please sign in to comment.