Add support for capabilities options in services
| ... | ... |
@@ -95,4 +95,11 @@ func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) {
|
| 95 | 95 |
service.TaskTemplate.Placement.MaxReplicas = 0 |
| 96 | 96 |
} |
| 97 | 97 |
} |
| 98 |
+ if versions.LessThan(cliVersion, "1.41") {
|
|
| 99 |
+ if service.TaskTemplate.ContainerSpec != nil {
|
|
| 100 |
+ // Capabilities for docker swarm services weren't supported before |
|
| 101 |
+ // API version 1.41 |
|
| 102 |
+ service.TaskTemplate.ContainerSpec.Capabilities = nil |
|
| 103 |
+ } |
|
| 104 |
+ } |
|
| 98 | 105 |
} |
| ... | ... |
@@ -2882,6 +2882,18 @@ definitions: |
| 2882 | 2882 |
type: "object" |
| 2883 | 2883 |
additionalProperties: |
| 2884 | 2884 |
type: "string" |
| 2885 |
+ # This option is not used by Windows containers |
|
| 2886 |
+ Capabilities: |
|
| 2887 |
+ type: "array" |
|
| 2888 |
+ description: | |
|
| 2889 |
+ A list of kernel capabilities to be available for container (this overrides the default set). |
|
| 2890 |
+ items: |
|
| 2891 |
+ type: "string" |
|
| 2892 |
+ example: |
|
| 2893 |
+ - "CAP_NET_RAW" |
|
| 2894 |
+ - "CAP_SYS_ADMIN" |
|
| 2895 |
+ - "CAP_SYS_CHROOT" |
|
| 2896 |
+ - "CAP_SYSLOG" |
|
| 2885 | 2897 |
NetworkAttachmentSpec: |
| 2886 | 2898 |
description: | |
| 2887 | 2899 |
Read-only spec type for non-swarm containers attached to swarm overlay |
| ... | ... |
@@ -67,10 +67,11 @@ type ContainerSpec struct {
|
| 67 | 67 |
// The format of extra hosts on swarmkit is specified in: |
| 68 | 68 |
// http://man7.org/linux/man-pages/man5/hosts.5.html |
| 69 | 69 |
// IP_address canonical_hostname [aliases...] |
| 70 |
- Hosts []string `json:",omitempty"` |
|
| 71 |
- DNSConfig *DNSConfig `json:",omitempty"` |
|
| 72 |
- Secrets []*SecretReference `json:",omitempty"` |
|
| 73 |
- Configs []*ConfigReference `json:",omitempty"` |
|
| 74 |
- Isolation container.Isolation `json:",omitempty"` |
|
| 75 |
- Sysctls map[string]string `json:",omitempty"` |
|
| 70 |
+ Hosts []string `json:",omitempty"` |
|
| 71 |
+ DNSConfig *DNSConfig `json:",omitempty"` |
|
| 72 |
+ Secrets []*SecretReference `json:",omitempty"` |
|
| 73 |
+ Configs []*ConfigReference `json:",omitempty"` |
|
| 74 |
+ Isolation container.Isolation `json:",omitempty"` |
|
| 75 |
+ Sysctls map[string]string `json:",omitempty"` |
|
| 76 |
+ Capabilities []string `json:",omitempty"` |
|
| 76 | 77 |
} |
| ... | ... |
@@ -18,25 +18,26 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) *types.ContainerSpec {
|
| 18 | 18 |
return nil |
| 19 | 19 |
} |
| 20 | 20 |
containerSpec := &types.ContainerSpec{
|
| 21 |
- Image: c.Image, |
|
| 22 |
- Labels: c.Labels, |
|
| 23 |
- Command: c.Command, |
|
| 24 |
- Args: c.Args, |
|
| 25 |
- Hostname: c.Hostname, |
|
| 26 |
- Env: c.Env, |
|
| 27 |
- Dir: c.Dir, |
|
| 28 |
- User: c.User, |
|
| 29 |
- Groups: c.Groups, |
|
| 30 |
- StopSignal: c.StopSignal, |
|
| 31 |
- TTY: c.TTY, |
|
| 32 |
- OpenStdin: c.OpenStdin, |
|
| 33 |
- ReadOnly: c.ReadOnly, |
|
| 34 |
- Hosts: c.Hosts, |
|
| 35 |
- Secrets: secretReferencesFromGRPC(c.Secrets), |
|
| 36 |
- Configs: configReferencesFromGRPC(c.Configs), |
|
| 37 |
- Isolation: IsolationFromGRPC(c.Isolation), |
|
| 38 |
- Init: initFromGRPC(c.Init), |
|
| 39 |
- Sysctls: c.Sysctls, |
|
| 21 |
+ Image: c.Image, |
|
| 22 |
+ Labels: c.Labels, |
|
| 23 |
+ Command: c.Command, |
|
| 24 |
+ Args: c.Args, |
|
| 25 |
+ Hostname: c.Hostname, |
|
| 26 |
+ Env: c.Env, |
|
| 27 |
+ Dir: c.Dir, |
|
| 28 |
+ User: c.User, |
|
| 29 |
+ Groups: c.Groups, |
|
| 30 |
+ StopSignal: c.StopSignal, |
|
| 31 |
+ TTY: c.TTY, |
|
| 32 |
+ OpenStdin: c.OpenStdin, |
|
| 33 |
+ ReadOnly: c.ReadOnly, |
|
| 34 |
+ Hosts: c.Hosts, |
|
| 35 |
+ Secrets: secretReferencesFromGRPC(c.Secrets), |
|
| 36 |
+ Configs: configReferencesFromGRPC(c.Configs), |
|
| 37 |
+ Isolation: IsolationFromGRPC(c.Isolation), |
|
| 38 |
+ Init: initFromGRPC(c.Init), |
|
| 39 |
+ Sysctls: c.Sysctls, |
|
| 40 |
+ Capabilities: c.Capabilities, |
|
| 40 | 41 |
} |
| 41 | 42 |
|
| 42 | 43 |
if c.DNSConfig != nil {
|
| ... | ... |
@@ -244,24 +245,25 @@ func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigRef |
| 244 | 244 |
|
| 245 | 245 |
func containerToGRPC(c *types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
|
| 246 | 246 |
containerSpec := &swarmapi.ContainerSpec{
|
| 247 |
- Image: c.Image, |
|
| 248 |
- Labels: c.Labels, |
|
| 249 |
- Command: c.Command, |
|
| 250 |
- Args: c.Args, |
|
| 251 |
- Hostname: c.Hostname, |
|
| 252 |
- Env: c.Env, |
|
| 253 |
- Dir: c.Dir, |
|
| 254 |
- User: c.User, |
|
| 255 |
- Groups: c.Groups, |
|
| 256 |
- StopSignal: c.StopSignal, |
|
| 257 |
- TTY: c.TTY, |
|
| 258 |
- OpenStdin: c.OpenStdin, |
|
| 259 |
- ReadOnly: c.ReadOnly, |
|
| 260 |
- Hosts: c.Hosts, |
|
| 261 |
- Secrets: secretReferencesToGRPC(c.Secrets), |
|
| 262 |
- Isolation: isolationToGRPC(c.Isolation), |
|
| 263 |
- Init: initToGRPC(c.Init), |
|
| 264 |
- Sysctls: c.Sysctls, |
|
| 247 |
+ Image: c.Image, |
|
| 248 |
+ Labels: c.Labels, |
|
| 249 |
+ Command: c.Command, |
|
| 250 |
+ Args: c.Args, |
|
| 251 |
+ Hostname: c.Hostname, |
|
| 252 |
+ Env: c.Env, |
|
| 253 |
+ Dir: c.Dir, |
|
| 254 |
+ User: c.User, |
|
| 255 |
+ Groups: c.Groups, |
|
| 256 |
+ StopSignal: c.StopSignal, |
|
| 257 |
+ TTY: c.TTY, |
|
| 258 |
+ OpenStdin: c.OpenStdin, |
|
| 259 |
+ ReadOnly: c.ReadOnly, |
|
| 260 |
+ Hosts: c.Hosts, |
|
| 261 |
+ Secrets: secretReferencesToGRPC(c.Secrets), |
|
| 262 |
+ Isolation: isolationToGRPC(c.Isolation), |
|
| 263 |
+ Init: initToGRPC(c.Init), |
|
| 264 |
+ Sysctls: c.Sysctls, |
|
| 265 |
+ Capabilities: c.Capabilities, |
|
| 265 | 266 |
} |
| 266 | 267 |
|
| 267 | 268 |
if c.DNSConfig != nil {
|
| ... | ... |
@@ -17,6 +17,12 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 17 | 17 |
|
| 18 | 18 |
[Docker Engine API v1.41](https://docs.docker.com/engine/api/v1.41/) documentation |
| 19 | 19 |
|
| 20 |
+* `GET /services` now returns `Capabilities` as part of the `ContainerSpec`. |
|
| 21 |
+* `GET /services/{id}` now returns `Capabilities` as part of the `ContainerSpec`.
|
|
| 22 |
+* `POST /services/create` now accepts `Capabilities` as part of the `ContainerSpec`. |
|
| 23 |
+* `POST /services/{id}/update` now accepts `Capabilities` as part of the `ContainerSpec`.
|
|
| 24 |
+* `GET /tasks` now returns `Capabilities` as part of the `ContainerSpec`. |
|
| 25 |
+* `GET /tasks/{id}` now returns `Capabilities` as part of the `ContainerSpec`.
|
|
| 20 | 26 |
* `POST /containers/create` on Linux now accepts the `HostConfig.CgroupnsMode` property. |
| 21 | 27 |
Set the property to `host` to create the container in the daemon's cgroup namespace, or |
| 22 | 28 |
`private` to create the container in its own private cgroup namespace. The per-daemon |
| ... | ... |
@@ -180,6 +180,14 @@ func ServiceWithSysctls(sysctls map[string]string) ServiceSpecOpt {
|
| 180 | 180 |
} |
| 181 | 181 |
} |
| 182 | 182 |
|
| 183 |
+// ServiceWithCapabilities sets the Capabilities option of the service's ContainerSpec. |
|
| 184 |
+func ServiceWithCapabilities(Capabilities []string) ServiceSpecOpt {
|
|
| 185 |
+ return func(spec *swarmtypes.ServiceSpec) {
|
|
| 186 |
+ ensureContainerSpec(spec) |
|
| 187 |
+ spec.TaskTemplate.ContainerSpec.Capabilities = Capabilities |
|
| 188 |
+ } |
|
| 189 |
+} |
|
| 190 |
+ |
|
| 183 | 191 |
// GetRunningTasks gets the list of running tasks for a service |
| 184 | 192 |
func GetRunningTasks(t *testing.T, c client.ServiceAPIClient, serviceID string) []swarmtypes.Task {
|
| 185 | 193 |
t.Helper() |
| ... | ... |
@@ -440,3 +440,79 @@ func TestCreateServiceSysctls(t *testing.T) {
|
| 440 | 440 |
) |
| 441 | 441 |
} |
| 442 | 442 |
} |
| 443 |
+ |
|
| 444 |
+// TestServiceCreateCapabilities tests that a service created with capabilities options in |
|
| 445 |
+// the ContainerSpec correctly applies those options. |
|
| 446 |
+// |
|
| 447 |
+// To test this, we're going to create a service with the capabilities option |
|
| 448 |
+// |
|
| 449 |
+// []string{"CAP_NET_RAW", "CAP_SYS_CHROOT"}
|
|
| 450 |
+// |
|
| 451 |
+// We'll get the service's tasks to get the container ID, and then we'll |
|
| 452 |
+// inspect the container. If the output of the container inspect contains the |
|
| 453 |
+// capabilities option with the correct value, we can assume that the capabilities has been |
|
| 454 |
+// plumbed correctly. |
|
| 455 |
+func TestCreateServiceCapabilities(t *testing.T) {
|
|
| 456 |
+ skip.If( |
|
| 457 |
+ t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.41"), |
|
| 458 |
+ "setting service capabilities is unsupported before api v1.41", |
|
| 459 |
+ ) |
|
| 460 |
+ |
|
| 461 |
+ defer setupTest(t)() |
|
| 462 |
+ d := swarm.NewSwarm(t, testEnv) |
|
| 463 |
+ defer d.Stop(t) |
|
| 464 |
+ client := d.NewClientT(t) |
|
| 465 |
+ defer client.Close() |
|
| 466 |
+ |
|
| 467 |
+ ctx := context.Background() |
|
| 468 |
+ |
|
| 469 |
+ // store the map we're going to be using everywhere. |
|
| 470 |
+ expectedCapabilities := []string{"CAP_NET_RAW", "CAP_SYS_CHROOT"}
|
|
| 471 |
+ |
|
| 472 |
+ // Create the service with the capabilities options |
|
| 473 |
+ var instances uint64 = 1 |
|
| 474 |
+ serviceID := swarm.CreateService(t, d, |
|
| 475 |
+ swarm.ServiceWithCapabilities(expectedCapabilities), |
|
| 476 |
+ ) |
|
| 477 |
+ |
|
| 478 |
+ // wait for the service to converge to 1 running task as expected |
|
| 479 |
+ poll.WaitOn(t, swarm.RunningTasksCount(client, serviceID, instances)) |
|
| 480 |
+ |
|
| 481 |
+ // we're going to check 3 things: |
|
| 482 |
+ // |
|
| 483 |
+ // 1. Does the container, when inspected, have the capabilities option set? |
|
| 484 |
+ // 2. Does the task have the capabilities in the spec? |
|
| 485 |
+ // 3. Does the service have the capabilities in the spec? |
|
| 486 |
+ // |
|
| 487 |
+ // if all 3 of these things are true, we know that the capabilities has been |
|
| 488 |
+ // plumbed correctly through the engine. |
|
| 489 |
+ // |
|
| 490 |
+ // We don't actually have to get inside the container and check its |
|
| 491 |
+ // logs or anything. If we see the capabilities set on the container inspect, |
|
| 492 |
+ // we know that the capabilities is plumbed correctly. everything below that |
|
| 493 |
+ // level has been tested elsewhere. |
|
| 494 |
+ |
|
| 495 |
+ // get all of the tasks of the service, so we can get the container |
|
| 496 |
+ filter := filters.NewArgs() |
|
| 497 |
+ filter.Add("service", serviceID)
|
|
| 498 |
+ tasks, err := client.TaskList(ctx, types.TaskListOptions{
|
|
| 499 |
+ Filters: filter, |
|
| 500 |
+ }) |
|
| 501 |
+ assert.NilError(t, err) |
|
| 502 |
+ assert.Check(t, is.Equal(len(tasks), 1)) |
|
| 503 |
+ |
|
| 504 |
+ // verify that the container has the capabilities option set |
|
| 505 |
+ ctnr, err := client.ContainerInspect(ctx, tasks[0].Status.ContainerStatus.ContainerID) |
|
| 506 |
+ assert.NilError(t, err) |
|
| 507 |
+ assert.DeepEqual(t, ctnr.HostConfig.Capabilities, expectedCapabilities) |
|
| 508 |
+ |
|
| 509 |
+ // verify that the task has the capabilities option set in the task object |
|
| 510 |
+ assert.DeepEqual(t, tasks[0].Spec.ContainerSpec.Capabilities, expectedCapabilities) |
|
| 511 |
+ |
|
| 512 |
+ // verify that the service also has the capabilities set in the spec. |
|
| 513 |
+ service, _, err := client.ServiceInspectWithRaw(ctx, serviceID, types.ServiceInspectOptions{})
|
|
| 514 |
+ assert.NilError(t, err) |
|
| 515 |
+ assert.DeepEqual(t, |
|
| 516 |
+ service.Spec.TaskTemplate.ContainerSpec.Capabilities, expectedCapabilities, |
|
| 517 |
+ ) |
|
| 518 |
+} |