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>
| ... | ... |
@@ -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 |
| ... | ... |
@@ -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 {
|
| ... | ... |
@@ -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() |