This adds both a daemon-wide flag and a container creation property:
- Set the `CgroupnsMode: "host|private"` HostConfig property at
container creation time to control what cgroup namespace the container
is created in
- Set the `--default-cgroupns-mode=host|private` daemon flag to control
what cgroup namespace containers are created in by default
- Set the default if the daemon flag is unset to "host", for backward
compatibility
- Default to CgroupnsMode: "host" for client versions < 1.40
Signed-off-by: Rob Gulewich <rgulewich@netflix.com>
| ... | ... |
@@ -489,6 +489,11 @@ func (s *containerRouter) postContainersCreate(ctx context.Context, w http.Respo |
| 489 | 489 |
if hostConfig.IpcMode.IsEmpty() {
|
| 490 | 490 |
hostConfig.IpcMode = container.IpcMode("shareable")
|
| 491 | 491 |
} |
| 492 |
+ |
|
| 493 |
+ // Older clients expect the default to be "host" |
|
| 494 |
+ if hostConfig.CgroupnsMode.IsEmpty() {
|
|
| 495 |
+ hostConfig.CgroupnsMode = container.CgroupnsMode("host")
|
|
| 496 |
+ } |
|
| 492 | 497 |
} |
| 493 | 498 |
|
| 494 | 499 |
if hostConfig != nil && hostConfig.PidsLimit != nil && *hostConfig.PidsLimit <= 0 {
|
| ... | ... |
@@ -707,6 +707,19 @@ definitions: |
| 707 | 707 |
description: "A list of kernel capabilities to drop from the container. Conflicts with option 'Capabilities'" |
| 708 | 708 |
items: |
| 709 | 709 |
type: "string" |
| 710 |
+ CgroupnsMode: |
|
| 711 |
+ type: "string" |
|
| 712 |
+ enum: |
|
| 713 |
+ - "private" |
|
| 714 |
+ - "host" |
|
| 715 |
+ description: | |
|
| 716 |
+ cgroup namespace mode for the container. Possible values are: |
|
| 717 |
+ |
|
| 718 |
+ - `"private"`: the container runs in its own private cgroup namespace |
|
| 719 |
+ - `"host"`: use the host system's cgroup namespace |
|
| 720 |
+ |
|
| 721 |
+ If not specified, the daemon default is used, which can either be `"private"` |
|
| 722 |
+ or `"host"`, depending on daemon version, kernel support and configuration. |
|
| 710 | 723 |
Dns: |
| 711 | 724 |
type: "array" |
| 712 | 725 |
description: "A list of DNS servers for the container to use." |
| ... | ... |
@@ -10,6 +10,29 @@ import ( |
| 10 | 10 |
"github.com/docker/go-units" |
| 11 | 11 |
) |
| 12 | 12 |
|
| 13 |
+// CgroupnsMode represents the cgroup namespace mode of the container |
|
| 14 |
+type CgroupnsMode string |
|
| 15 |
+ |
|
| 16 |
+// IsPrivate indicates whether the container uses its own private cgroup namespace |
|
| 17 |
+func (c CgroupnsMode) IsPrivate() bool {
|
|
| 18 |
+ return c == "private" |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 21 |
+// IsHost indicates whether the container shares the host's cgroup namespace |
|
| 22 |
+func (c CgroupnsMode) IsHost() bool {
|
|
| 23 |
+ return c == "host" |
|
| 24 |
+} |
|
| 25 |
+ |
|
| 26 |
+// IsEmpty indicates whether the container cgroup namespace mode is unset |
|
| 27 |
+func (c CgroupnsMode) IsEmpty() bool {
|
|
| 28 |
+ return c == "" |
|
| 29 |
+} |
|
| 30 |
+ |
|
| 31 |
+// Valid indicates whether the cgroup namespace mode is valid |
|
| 32 |
+func (c CgroupnsMode) Valid() bool {
|
|
| 33 |
+ return c.IsEmpty() || c.IsPrivate() || c.IsHost() |
|
| 34 |
+} |
|
| 35 |
+ |
|
| 13 | 36 |
// Isolation represents the isolation technology of a container. The supported |
| 14 | 37 |
// values are platform specific |
| 15 | 38 |
type Isolation string |
| ... | ... |
@@ -382,9 +405,10 @@ type HostConfig struct {
|
| 382 | 382 |
CapAdd strslice.StrSlice // List of kernel capabilities to add to the container |
| 383 | 383 |
CapDrop strslice.StrSlice // List of kernel capabilities to remove from the container |
| 384 | 384 |
Capabilities []string `json:"Capabilities"` // List of kernel capabilities to be available for container (this overrides the default set) |
| 385 |
- DNS []string `json:"Dns"` // List of DNS server to lookup |
|
| 386 |
- DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for |
|
| 387 |
- DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for |
|
| 385 |
+ CgroupnsMode CgroupnsMode // Cgroup namespace mode to use for the container |
|
| 386 |
+ DNS []string `json:"Dns"` // List of DNS server to lookup |
|
| 387 |
+ DNSOptions []string `json:"DnsOptions"` // List of DNSOption to look for |
|
| 388 |
+ DNSSearch []string `json:"DnsSearch"` // List of DNSSearch to look for |
|
| 388 | 389 |
ExtraHosts []string // List of extra hosts |
| 389 | 390 |
GroupAdd []string // List of additional groups that the container process will run as |
| 390 | 391 |
IpcMode IpcMode // IPC namespace to use for the container |
| ... | ... |
@@ -64,5 +64,6 @@ func installConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
|
| 64 | 64 |
// rootless needs to be explicitly specified for running "rootful" dockerd in rootless dockerd (#38702) |
| 65 | 65 |
// Note that defaultUserlandProxyPath and honorXDG are configured according to the value of rootless.RunningWithRootlessKit, not the value of --rootless. |
| 66 | 66 |
flags.BoolVar(&conf.Rootless, "rootless", rootless.RunningWithRootlessKit(), "Enable rootless mode; typically used with RootlessKit (experimental)") |
| 67 |
+ flags.StringVar(&conf.CgroupNamespaceMode, "default-cgroupns-mode", config.DefaultCgroupNamespaceMode, `Default mode for containers cgroup namespace ("host" | "private")`)
|
|
| 67 | 68 |
return nil |
| 68 | 69 |
} |
| ... | ... |
@@ -11,6 +11,8 @@ import ( |
| 11 | 11 |
) |
| 12 | 12 |
|
| 13 | 13 |
const ( |
| 14 |
+ // DefaultCgroupNamespaceMode is the default for a container's CgroupnsMode, if not set otherwise |
|
| 15 |
+ DefaultCgroupNamespaceMode = "host" // TODO: change to private |
|
| 14 | 16 |
// DefaultIpcMode is default for container's IpcMode, if not set otherwise |
| 15 | 17 |
DefaultIpcMode = "private" |
| 16 | 18 |
) |
| ... | ... |
@@ -37,6 +39,7 @@ type Config struct {
|
| 37 | 37 |
ShmSize opts.MemBytes `json:"default-shm-size,omitempty"` |
| 38 | 38 |
NoNewPrivileges bool `json:"no-new-privileges,omitempty"` |
| 39 | 39 |
IpcMode string `json:"default-ipc-mode,omitempty"` |
| 40 |
+ CgroupNamespaceMode string `json:"default-cgroupns-mode,omitempty"` |
|
| 40 | 41 |
// ResolvConf is the path to the configuration of the host resolver |
| 41 | 42 |
ResolvConf string `json:"resolv-conf,omitempty"` |
| 42 | 43 |
Rootless bool `json:"rootless,omitempty"` |
| ... | ... |
@@ -84,9 +87,22 @@ func verifyDefaultIpcMode(mode string) error {
|
| 84 | 84 |
return nil |
| 85 | 85 |
} |
| 86 | 86 |
|
| 87 |
+func verifyDefaultCgroupNsMode(mode string) error {
|
|
| 88 |
+ cm := containertypes.CgroupnsMode(mode) |
|
| 89 |
+ if !cm.Valid() {
|
|
| 90 |
+ return fmt.Errorf("Default cgroup namespace mode (%v) is invalid. Use \"host\" or \"private\".", cm) // nolint: golint
|
|
| 91 |
+ } |
|
| 92 |
+ |
|
| 93 |
+ return nil |
|
| 94 |
+} |
|
| 95 |
+ |
|
| 87 | 96 |
// ValidatePlatformConfig checks if any platform-specific configuration settings are invalid. |
| 88 | 97 |
func (conf *Config) ValidatePlatformConfig() error {
|
| 89 |
- return verifyDefaultIpcMode(conf.IpcMode) |
|
| 98 |
+ if err := verifyDefaultIpcMode(conf.IpcMode); err != nil {
|
|
| 99 |
+ return err |
|
| 100 |
+ } |
|
| 101 |
+ |
|
| 102 |
+ return verifyDefaultCgroupNsMode(conf.CgroupNamespaceMode) |
|
| 90 | 103 |
} |
| 91 | 104 |
|
| 92 | 105 |
// IsRootless returns conf.Rootless |
| ... | ... |
@@ -81,27 +81,26 @@ var ( |
| 81 | 81 |
|
| 82 | 82 |
// Daemon holds information about the Docker daemon. |
| 83 | 83 |
type Daemon struct {
|
| 84 |
- ID string |
|
| 85 |
- repository string |
|
| 86 |
- containers container.Store |
|
| 87 |
- containersReplica container.ViewDB |
|
| 88 |
- execCommands *exec.Store |
|
| 89 |
- imageService *images.ImageService |
|
| 90 |
- idIndex *truncindex.TruncIndex |
|
| 91 |
- configStore *config.Config |
|
| 92 |
- statsCollector *stats.Collector |
|
| 93 |
- defaultLogConfig containertypes.LogConfig |
|
| 94 |
- RegistryService registry.Service |
|
| 95 |
- EventsService *events.Events |
|
| 96 |
- netController libnetwork.NetworkController |
|
| 97 |
- volumes *volumesservice.VolumesService |
|
| 98 |
- discoveryWatcher discovery.Reloader |
|
| 99 |
- root string |
|
| 100 |
- seccompEnabled bool |
|
| 101 |
- apparmorEnabled bool |
|
| 102 |
- cgroupNamespacesEnabled bool |
|
| 103 |
- shutdown bool |
|
| 104 |
- idMapping *idtools.IdentityMapping |
|
| 84 |
+ ID string |
|
| 85 |
+ repository string |
|
| 86 |
+ containers container.Store |
|
| 87 |
+ containersReplica container.ViewDB |
|
| 88 |
+ execCommands *exec.Store |
|
| 89 |
+ imageService *images.ImageService |
|
| 90 |
+ idIndex *truncindex.TruncIndex |
|
| 91 |
+ configStore *config.Config |
|
| 92 |
+ statsCollector *stats.Collector |
|
| 93 |
+ defaultLogConfig containertypes.LogConfig |
|
| 94 |
+ RegistryService registry.Service |
|
| 95 |
+ EventsService *events.Events |
|
| 96 |
+ netController libnetwork.NetworkController |
|
| 97 |
+ volumes *volumesservice.VolumesService |
|
| 98 |
+ discoveryWatcher discovery.Reloader |
|
| 99 |
+ root string |
|
| 100 |
+ seccompEnabled bool |
|
| 101 |
+ apparmorEnabled bool |
|
| 102 |
+ shutdown bool |
|
| 103 |
+ idMapping *idtools.IdentityMapping |
|
| 105 | 104 |
// TODO: move graphDrivers field to an InfoService |
| 106 | 105 |
graphDrivers map[string]string // By operating system |
| 107 | 106 |
|
| ... | ... |
@@ -1021,7 +1020,6 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S |
| 1021 | 1021 |
d.idMapping = idMapping |
| 1022 | 1022 |
d.seccompEnabled = sysInfo.Seccomp |
| 1023 | 1023 |
d.apparmorEnabled = sysInfo.AppArmor |
| 1024 |
- d.cgroupNamespacesEnabled = sysInfo.CgroupNamespaces |
|
| 1025 | 1024 |
|
| 1026 | 1025 |
d.linkIndex = newLinkIndex() |
| 1027 | 1026 |
|
| ... | ... |
@@ -356,6 +356,15 @@ func (daemon *Daemon) adaptContainerSettings(hostConfig *containertypes.HostConf |
| 356 | 356 |
hostConfig.IpcMode = containertypes.IpcMode(m) |
| 357 | 357 |
} |
| 358 | 358 |
|
| 359 |
+ // Set default cgroup namespace mode, if unset for container |
|
| 360 |
+ if hostConfig.CgroupnsMode.IsEmpty() {
|
|
| 361 |
+ m := config.DefaultCgroupNamespaceMode |
|
| 362 |
+ if daemon.configStore != nil {
|
|
| 363 |
+ m = daemon.configStore.CgroupNamespaceMode |
|
| 364 |
+ } |
|
| 365 |
+ hostConfig.CgroupnsMode = containertypes.CgroupnsMode(m) |
|
| 366 |
+ } |
|
| 367 |
+ |
|
| 359 | 368 |
adaptSharedNamespaceContainer(daemon, hostConfig) |
| 360 | 369 |
|
| 361 | 370 |
var err error |
| ... | ... |
@@ -675,6 +684,19 @@ func verifyPlatformContainerSettings(daemon *Daemon, hostConfig *containertypes. |
| 675 | 675 |
} |
| 676 | 676 |
} |
| 677 | 677 |
|
| 678 |
+ if !hostConfig.CgroupnsMode.Valid() {
|
|
| 679 |
+ return warnings, fmt.Errorf("invalid cgroup namespace mode: %v", hostConfig.CgroupnsMode)
|
|
| 680 |
+ } |
|
| 681 |
+ if hostConfig.CgroupnsMode.IsPrivate() {
|
|
| 682 |
+ if !sysInfo.CgroupNamespaces {
|
|
| 683 |
+ warnings = append(warnings, "Your kernel does not support cgroup namespaces. Cgroup namespace setting discarded.") |
|
| 684 |
+ } |
|
| 685 |
+ |
|
| 686 |
+ if hostConfig.Privileged {
|
|
| 687 |
+ return warnings, fmt.Errorf("privileged mode is incompatible with private cgroup namespaces. You must run the container in the host cgroup namespace when running privileged mode")
|
|
| 688 |
+ } |
|
| 689 |
+ } |
|
| 690 |
+ |
|
| 678 | 691 |
return warnings, nil |
| 679 | 692 |
} |
| 680 | 693 |
|
| ... | ... |
@@ -178,6 +178,10 @@ func (daemon *Daemon) fillSecurityOptions(v *types.Info, sysInfo *sysinfo.SysInf |
| 178 | 178 |
if daemon.Rootless() {
|
| 179 | 179 |
securityOptions = append(securityOptions, "name=rootless") |
| 180 | 180 |
} |
| 181 |
+ if daemon.cgroupNamespacesEnabled(sysInfo) {
|
|
| 182 |
+ securityOptions = append(securityOptions, "name=cgroupns") |
|
| 183 |
+ } |
|
| 184 |
+ |
|
| 181 | 185 |
v.SecurityOptions = securityOptions |
| 182 | 186 |
} |
| 183 | 187 |
|
| ... | ... |
@@ -10,6 +10,7 @@ import ( |
| 10 | 10 |
"strings" |
| 11 | 11 |
|
| 12 | 12 |
"github.com/docker/docker/api/types" |
| 13 |
+ containertypes "github.com/docker/docker/api/types/container" |
|
| 13 | 14 |
"github.com/docker/docker/dockerversion" |
| 14 | 15 |
"github.com/docker/docker/pkg/sysinfo" |
| 15 | 16 |
"github.com/pkg/errors" |
| ... | ... |
@@ -247,6 +248,10 @@ func parseRuncVersion(v string) (version string, commit string, err error) {
|
| 247 | 247 |
return version, commit, err |
| 248 | 248 |
} |
| 249 | 249 |
|
| 250 |
+func (daemon *Daemon) cgroupNamespacesEnabled(sysInfo *sysinfo.SysInfo) bool {
|
|
| 251 |
+ return sysInfo.CgroupNamespaces && containertypes.CgroupnsMode(daemon.configStore.CgroupNamespaceMode).IsPrivate() |
|
| 252 |
+} |
|
| 253 |
+ |
|
| 250 | 254 |
// Rootless returns true if daemon is running in rootless mode |
| 251 | 255 |
func (daemon *Daemon) Rootless() bool {
|
| 252 | 256 |
return daemon.configStore.Rootless |
| ... | ... |
@@ -14,6 +14,10 @@ func (daemon *Daemon) fillPlatformVersion(v *types.Version) {}
|
| 14 | 14 |
func fillDriverWarnings(v *types.Info) {
|
| 15 | 15 |
} |
| 16 | 16 |
|
| 17 |
+func (daemon *Daemon) cgroupNamespacesEnabled(sysInfo *sysinfo.SysInfo) bool {
|
|
| 18 |
+ return false |
|
| 19 |
+} |
|
| 20 |
+ |
|
| 17 | 21 |
// Rootless returns true if daemon is running in rootless mode |
| 18 | 22 |
func (daemon *Daemon) Rootless() bool {
|
| 19 | 23 |
return false |
| ... | ... |
@@ -307,13 +307,21 @@ func WithNamespaces(daemon *Daemon, c *container.Container) coci.SpecOpts {
|
| 307 | 307 |
s.Hostname = "" |
| 308 | 308 |
} |
| 309 | 309 |
|
| 310 |
- // cgroup |
|
| 311 |
- if daemon.cgroupNamespacesEnabled && !c.HostConfig.Privileged {
|
|
| 312 |
- nsCgroup := specs.LinuxNamespace{Type: "cgroup"}
|
|
| 313 |
- setNamespace(s, nsCgroup) |
|
| 314 |
- } |
|
| 310 |
+ // cgroup |
|
| 311 |
+ if !c.HostConfig.CgroupnsMode.IsEmpty() {
|
|
| 312 |
+ cgroupNsMode := c.HostConfig.CgroupnsMode |
|
| 313 |
+ if !cgroupNsMode.Valid() {
|
|
| 314 |
+ return fmt.Errorf("invalid cgroup namespace mode: %v", cgroupNsMode)
|
|
| 315 |
+ } |
|
| 315 | 316 |
|
| 316 |
- return nil |
|
| 317 |
+ if cgroupNsMode.IsPrivate() && !c.HostConfig.Privileged {
|
|
| 318 |
+ nsCgroup := specs.LinuxNamespace{Type: "cgroup"}
|
|
| 319 |
+ setNamespace(s, nsCgroup) |
|
| 320 |
+ } |
|
| 321 |
+ } |
|
| 322 |
+ |
|
| 323 |
+ return nil |
|
| 324 |
+ } |
|
| 317 | 325 |
} |
| 318 | 326 |
|
| 319 | 327 |
func specMapping(s []idtools.IDMap) []specs.LinuxIDMapping {
|
| ... | ... |
@@ -34,6 +34,10 @@ func (daemon *Daemon) reloadPlatform(conf *config.Config, attributes map[string] |
| 34 | 34 |
daemon.configStore.ShmSize = conf.ShmSize |
| 35 | 35 |
} |
| 36 | 36 |
|
| 37 |
+ if conf.CgroupNamespaceMode != "" {
|
|
| 38 |
+ daemon.configStore.CgroupNamespaceMode = conf.CgroupNamespaceMode |
|
| 39 |
+ } |
|
| 40 |
+ |
|
| 37 | 41 |
if conf.IpcMode != "" {
|
| 38 | 42 |
daemon.configStore.IpcMode = conf.IpcMode |
| 39 | 43 |
} |
| ... | ... |
@@ -51,6 +55,7 @@ func (daemon *Daemon) reloadPlatform(conf *config.Config, attributes map[string] |
| 51 | 51 |
attributes["default-runtime"] = daemon.configStore.DefaultRuntime |
| 52 | 52 |
attributes["default-shm-size"] = fmt.Sprintf("%d", daemon.configStore.ShmSize)
|
| 53 | 53 |
attributes["default-ipc-mode"] = daemon.configStore.IpcMode |
| 54 |
+ attributes["default-cgroupns-mode"] = daemon.configStore.CgroupNamespaceMode |
|
| 54 | 55 |
|
| 55 | 56 |
return nil |
| 56 | 57 |
} |
| ... | ... |
@@ -68,6 +68,11 @@ keywords: "API, Docker, rcli, REST, documentation" |
| 68 | 68 |
* `POST /containers/{id}/update` now accepts a `PidsLimit` field to tune a container's
|
| 69 | 69 |
PID limit. Set `0` or `-1` for unlimited. Leave `null` to not change the current value. |
| 70 | 70 |
* `POST /build` now accepts `outputs` key for configuring build outputs when using BuildKit mode. |
| 71 |
+* `POST /containers/create` on Linux now accepts the `HostConfig.CgroupnsMode` property. |
|
| 72 |
+ Set the property to `host` to create the container in the daemon's cgroup namespace, or |
|
| 73 |
+ `private` to create the container in its own private cgroup namespace. The per-daemon |
|
| 74 |
+ default is `host`, and can be changed by using the`CgroupNamespaceMode` daemon configuration |
|
| 75 |
+ parameter. |
|
| 71 | 76 |
|
| 72 | 77 |
## V1.39 API changes |
| 73 | 78 |
|
| ... | ... |
@@ -3984,40 +3984,18 @@ func (s *DockerSuite) TestBuildContainerWithCgroupParent(c *check.C) {
|
| 3984 | 3984 |
if !found {
|
| 3985 | 3985 |
c.Fatalf("unable to find self memory cgroup path. CgroupsPath: %v", selfCgroupPaths)
|
| 3986 | 3986 |
} |
| 3987 |
- |
|
| 3988 |
- doneCh := make(chan string) |
|
| 3989 |
- |
|
| 3990 |
- // If cgroup namespaces are enabled, then processes running inside the container won't |
|
| 3991 |
- // be able to see the parent namespace. Check that they have the correct parents from |
|
| 3992 |
- // the host, which has the non-namespaced view of the hierarchy. |
|
| 3993 |
- |
|
| 3994 |
- go func() {
|
|
| 3995 |
- result := buildImage("buildcgroupparent",
|
|
| 3996 |
- cli.WithFlags("--cgroup-parent", cgroupParent),
|
|
| 3997 |
- build.WithDockerfile(` |
|
| 3987 |
+ result := buildImage("buildcgroupparent",
|
|
| 3988 |
+ cli.WithFlags("--cgroup-parent", cgroupParent),
|
|
| 3989 |
+ build.WithDockerfile(` |
|
| 3998 | 3990 |
FROM busybox |
| 3999 |
-RUN sleep 10 |
|
| 4000 |
- `)) |
|
| 4001 |
- result.Assert(c, icmd.Success) |
|
| 4002 |
- doneCh <- "done" |
|
| 4003 |
- }() |
|
| 4004 |
- |
|
| 4005 |
- // Wait until the build is well into the sleep |
|
| 4006 |
- time.Sleep(3 * time.Second) |
|
| 4007 |
- out, _, err := dockerCmdWithError("ps", "-q", "-l")
|
|
| 4008 |
- c.Assert(err, check.IsNil) |
|
| 4009 |
- cID := strings.TrimSpace(out) |
|
| 4010 |
- |
|
| 4011 |
- pid := inspectField(c, cID, "State.Pid") |
|
| 4012 |
- paths := ReadCgroupPathsForPid(c, pid) |
|
| 4013 |
- m, err := regexp.MatchString(fmt.Sprintf("memory:.*/%s/.*", cgroupParent), paths)
|
|
| 4014 |
- c.Assert(err, check.IsNil) |
|
| 3991 |
+RUN cat /proc/self/cgroup |
|
| 3992 |
+`)) |
|
| 3993 |
+ result.Assert(c, icmd.Success) |
|
| 3994 |
+ m, err := regexp.MatchString(fmt.Sprintf("memory:.*/%s/.*", cgroupParent), result.Combined())
|
|
| 3995 |
+ assert.NilError(c, err) |
|
| 4015 | 3996 |
if !m {
|
| 4016 |
- c.Fatalf("There is no expected memory cgroup with parent /%s/: %s", cgroupParent, paths)
|
|
| 3997 |
+ c.Fatalf("There is no expected memory cgroup with parent /%s/: %s", cgroupParent, result.Combined())
|
|
| 4017 | 3998 |
} |
| 4018 |
- |
|
| 4019 |
- // Wait for the build to complete, otherwise it will exit with an error |
|
| 4020 |
- <-doneCh |
|
| 4021 | 3999 |
} |
| 4022 | 4000 |
|
| 4023 | 4001 |
// FIXME(vdemeester) could be a unit test |
| ... | ... |
@@ -1787,8 +1787,7 @@ func (s *DockerDaemonSuite) TestDaemonRestartContainerLinksRestart(c *check.C) {
|
| 1787 | 1787 |
} |
| 1788 | 1788 |
|
| 1789 | 1789 |
func (s *DockerDaemonSuite) TestDaemonCgroupParent(c *check.C) {
|
| 1790 |
- // Test requires local filesystem access on a Linux host |
|
| 1791 |
- testRequires(c, DaemonIsLinux, testEnv.IsLocalDaemon) |
|
| 1790 |
+ testRequires(c, DaemonIsLinux) |
|
| 1792 | 1791 |
|
| 1793 | 1792 |
cgroupParent := "test" |
| 1794 | 1793 |
name := "cgroup-test" |
| ... | ... |
@@ -1796,20 +1795,10 @@ func (s *DockerDaemonSuite) TestDaemonCgroupParent(c *check.C) {
|
| 1796 | 1796 |
s.d.StartWithBusybox(c, "--cgroup-parent", cgroupParent) |
| 1797 | 1797 |
defer s.d.Restart(c) |
| 1798 | 1798 |
|
| 1799 |
- out, err := s.d.Cmd("run", "--name", name, "-d", "busybox", "top")
|
|
| 1800 |
- c.Assert(err, checker.IsNil) |
|
| 1801 |
- |
|
| 1802 |
- // If cgroup namespaces are enabled, then processes running inside the container won't |
|
| 1803 |
- // be able to see the parent namespace. Check that they have the correct parents from |
|
| 1804 |
- // the host, which has the non-namespaced view of the hierarchy. |
|
| 1805 |
- |
|
| 1806 |
- pid, err := s.d.Cmd("inspect", "-f", "{{.State.Pid}}", name)
|
|
| 1807 |
- c.Assert(err, checker.IsNil) |
|
| 1808 |
- pid = strings.TrimSpace(string(pid)) |
|
| 1809 |
- paths := ReadCgroupPathsForPid(c, pid) |
|
| 1810 |
- cgroupPaths := ParseCgroupPaths(paths) |
|
| 1811 |
- c.Assert(len(cgroupPaths), checker.Not(checker.Equals), 0, check.Commentf("unexpected output - %q", paths))
|
|
| 1812 |
- |
|
| 1799 |
+ out, err := s.d.Cmd("run", "--name", name, "busybox", "cat", "/proc/self/cgroup")
|
|
| 1800 |
+ assert.NilError(c, err) |
|
| 1801 |
+ cgroupPaths := ParseCgroupPaths(string(out)) |
|
| 1802 |
+ c.Assert(len(cgroupPaths), checker.Not(checker.Equals), 0, check.Commentf("unexpected output - %q", string(out)))
|
|
| 1813 | 1803 |
out, err = s.d.Cmd("inspect", "-f", "{{.Id}}", name)
|
| 1814 | 1804 |
assert.NilError(c, err) |
| 1815 | 1805 |
id := strings.TrimSpace(string(out)) |
| ... | ... |
@@ -3241,8 +3241,8 @@ func (s *DockerSuite) TestRunWithUlimits(c *check.C) {
|
| 3241 | 3241 |
} |
| 3242 | 3242 |
|
| 3243 | 3243 |
func (s *DockerSuite) TestRunContainerWithCgroupParent(c *check.C) {
|
| 3244 |
- // Test requires local filesystem access on a Linux host |
|
| 3245 |
- testRequires(c, DaemonIsLinux, testEnv.IsLocalDaemon) |
|
| 3244 |
+ // Not applicable on Windows as uses Unix specific functionality |
|
| 3245 |
+ testRequires(c, DaemonIsLinux) |
|
| 3246 | 3246 |
|
| 3247 | 3247 |
// cgroup-parent relative path |
| 3248 | 3248 |
testRunContainerWithCgroupParent(c, "test", "cgroup-test") |
| ... | ... |
@@ -3252,23 +3252,14 @@ func (s *DockerSuite) TestRunContainerWithCgroupParent(c *check.C) {
|
| 3252 | 3252 |
} |
| 3253 | 3253 |
|
| 3254 | 3254 |
func testRunContainerWithCgroupParent(c *check.C, cgroupParent, name string) {
|
| 3255 |
- out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "-d", "busybox", "top")
|
|
| 3255 |
+ out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "busybox", "cat", "/proc/self/cgroup")
|
|
| 3256 | 3256 |
if err != nil {
|
| 3257 | 3257 |
c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err)
|
| 3258 | 3258 |
} |
| 3259 |
- cID := strings.TrimSpace(out) |
|
| 3260 |
- |
|
| 3261 |
- // If cgroup namespaces are enabled, then processes running inside the container won't |
|
| 3262 |
- // be able to see the parent namespace. Check that they have the correct parents from |
|
| 3263 |
- // the host, which has the non-namespaced view of the hierarchy. |
|
| 3264 |
- |
|
| 3265 |
- pid := inspectField(c, cID, "State.Pid") |
|
| 3266 |
- paths := ReadCgroupPathsForPid(c, pid) |
|
| 3267 |
- cgroupPaths := ParseCgroupPaths(paths) |
|
| 3259 |
+ cgroupPaths := ParseCgroupPaths(string(out)) |
|
| 3268 | 3260 |
if len(cgroupPaths) == 0 {
|
| 3269 |
- c.Fatalf("unexpected output - %q", string(paths))
|
|
| 3261 |
+ c.Fatalf("unexpected output - %q", string(out))
|
|
| 3270 | 3262 |
} |
| 3271 |
- |
|
| 3272 | 3263 |
id := getIDByName(c, name) |
| 3273 | 3264 |
expectedCgroup := path.Join(cgroupParent, id) |
| 3274 | 3265 |
found := false |
| ... | ... |
@@ -3294,29 +3285,21 @@ func (s *DockerSuite) TestRunInvalidCgroupParent(c *check.C) {
|
| 3294 | 3294 |
} |
| 3295 | 3295 |
|
| 3296 | 3296 |
func testRunInvalidCgroupParent(c *check.C, cgroupParent, cleanCgroupParent, name string) {
|
| 3297 |
- out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "-d", "busybox", "top")
|
|
| 3297 |
+ out, _, err := dockerCmdWithError("run", "--cgroup-parent", cgroupParent, "--name", name, "busybox", "cat", "/proc/self/cgroup")
|
|
| 3298 | 3298 |
if err != nil {
|
| 3299 | 3299 |
// XXX: This may include a daemon crash. |
| 3300 | 3300 |
c.Fatalf("unexpected failure when running container with --cgroup-parent option - %s\n%v", string(out), err)
|
| 3301 | 3301 |
} |
| 3302 |
- cID := strings.TrimSpace(out) |
|
| 3303 | 3302 |
|
| 3304 | 3303 |
// We expect "/SHOULD_NOT_EXIST" to not exist. If not, we have a security issue. |
| 3305 | 3304 |
if _, err := os.Stat("/SHOULD_NOT_EXIST"); err == nil || !os.IsNotExist(err) {
|
| 3306 | 3305 |
c.Fatalf("SECURITY: --cgroup-parent with ../../ relative paths cause files to be created in the host (this is bad) !!")
|
| 3307 | 3306 |
} |
| 3308 | 3307 |
|
| 3309 |
- // If cgroup namespaces are enabled, then processes running inside the container won't |
|
| 3310 |
- // be able to see the parent namespace. Check that they have the correct parents from |
|
| 3311 |
- // the host, which has the non-namespaced view of the hierarchy. |
|
| 3312 |
- |
|
| 3313 |
- pid := inspectField(c, cID, "State.Pid") |
|
| 3314 |
- paths := ReadCgroupPathsForPid(c, pid) |
|
| 3315 |
- cgroupPaths := ParseCgroupPaths(paths) |
|
| 3308 |
+ cgroupPaths := ParseCgroupPaths(string(out)) |
|
| 3316 | 3309 |
if len(cgroupPaths) == 0 {
|
| 3317 |
- c.Fatalf("unexpected output - %q", string(paths))
|
|
| 3310 |
+ c.Fatalf("unexpected output - %q", string(out))
|
|
| 3318 | 3311 |
} |
| 3319 |
- |
|
| 3320 | 3312 |
id := getIDByName(c, name) |
| 3321 | 3313 |
expectedCgroup := path.Join(cleanCgroupParent, id) |
| 3322 | 3314 |
found := false |
| ... | ... |
@@ -2,7 +2,6 @@ package main |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
- "io/ioutil" |
|
| 6 | 5 |
"os" |
| 7 | 6 |
"os/exec" |
| 8 | 7 |
"path/filepath" |
| ... | ... |
@@ -39,17 +38,6 @@ func transformCmd(execCmd *exec.Cmd) icmd.Cmd {
|
| 39 | 39 |
} |
| 40 | 40 |
} |
| 41 | 41 |
|
| 42 |
-// ReadCgroupPathsForPid reads the cgroup path file for a pid in '/proc/<pid>/cgroup' |
|
| 43 |
-func ReadCgroupPathsForPid(c *check.C, pid string) string {
|
|
| 44 |
- cgroupFile := fmt.Sprintf("/proc/%s/cgroup", pid)
|
|
| 45 |
- out, err := ioutil.ReadFile(cgroupFile) |
|
| 46 |
- if err != nil {
|
|
| 47 |
- c.Fatalf("unexpected failure when reading cgroup file %s\n%v", cgroupFile, err)
|
|
| 48 |
- } |
|
| 49 |
- |
|
| 50 |
- return string(out) |
|
| 51 |
-} |
|
| 52 |
- |
|
| 53 | 42 |
// ParseCgroupPaths parses 'procCgroupData', which is output of '/proc/<pid>/cgroup', and returns |
| 54 | 43 |
// a map which cgroup name as key and path as value. |
| 55 | 44 |
func ParseCgroupPaths(procCgroupData string) map[string]string {
|
| 56 | 45 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,92 @@ |
| 0 |
+package build // import "github.com/docker/docker/integration/build" |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "io" |
|
| 6 |
+ "strings" |
|
| 7 |
+ "testing" |
|
| 8 |
+ |
|
| 9 |
+ "github.com/docker/docker/api/types" |
|
| 10 |
+ "github.com/docker/docker/integration/internal/requirement" |
|
| 11 |
+ "github.com/docker/docker/internal/test/daemon" |
|
| 12 |
+ "github.com/docker/docker/internal/test/fakecontext" |
|
| 13 |
+ "github.com/docker/docker/pkg/jsonmessage" |
|
| 14 |
+ "gotest.tools/assert" |
|
| 15 |
+ "gotest.tools/skip" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+// Finds the output of `readlink /proc/<pid>/ns/cgroup` in build output |
|
| 19 |
+func getCgroupFromBuildOutput(buildOutput io.Reader) (string, error) {
|
|
| 20 |
+ const prefix = "cgroup:" |
|
| 21 |
+ |
|
| 22 |
+ dec := json.NewDecoder(buildOutput) |
|
| 23 |
+ for {
|
|
| 24 |
+ m := jsonmessage.JSONMessage{}
|
|
| 25 |
+ err := dec.Decode(&m) |
|
| 26 |
+ if err == io.EOF {
|
|
| 27 |
+ return "", nil |
|
| 28 |
+ } |
|
| 29 |
+ if err != nil {
|
|
| 30 |
+ return "", err |
|
| 31 |
+ } |
|
| 32 |
+ if ix := strings.Index(m.Stream, prefix); ix == 0 {
|
|
| 33 |
+ return strings.TrimSpace(m.Stream), nil |
|
| 34 |
+ } |
|
| 35 |
+ } |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+// Runs a docker build against a daemon with the given cgroup namespace default value. |
|
| 39 |
+// Returns the container cgroup and daemon cgroup. |
|
| 40 |
+func testBuildWithCgroupNs(t *testing.T, daemonNsMode string) (string, string) {
|
|
| 41 |
+ d := daemon.New(t, daemon.WithDefaultCgroupNamespaceMode(daemonNsMode)) |
|
| 42 |
+ d.StartWithBusybox(t) |
|
| 43 |
+ defer d.Stop(t) |
|
| 44 |
+ |
|
| 45 |
+ dockerfile := ` |
|
| 46 |
+ FROM busybox |
|
| 47 |
+ RUN readlink /proc/self/ns/cgroup |
|
| 48 |
+ ` |
|
| 49 |
+ ctx := context.Background() |
|
| 50 |
+ source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)) |
|
| 51 |
+ defer source.Close() |
|
| 52 |
+ |
|
| 53 |
+ client := d.NewClientT(t) |
|
| 54 |
+ resp, err := client.ImageBuild(ctx, |
|
| 55 |
+ source.AsTarReader(t), |
|
| 56 |
+ types.ImageBuildOptions{
|
|
| 57 |
+ Remove: true, |
|
| 58 |
+ ForceRemove: true, |
|
| 59 |
+ Tags: []string{"buildcgroupns"},
|
|
| 60 |
+ }) |
|
| 61 |
+ assert.NilError(t, err) |
|
| 62 |
+ defer resp.Body.Close() |
|
| 63 |
+ |
|
| 64 |
+ containerCgroup, err := getCgroupFromBuildOutput(resp.Body) |
|
| 65 |
+ assert.NilError(t, err) |
|
| 66 |
+ daemonCgroup := d.CgroupNamespace(t) |
|
| 67 |
+ |
|
| 68 |
+ return containerCgroup, daemonCgroup |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+func TestCgroupNamespacesBuild(t *testing.T) {
|
|
| 72 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 73 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 74 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 75 |
+ |
|
| 76 |
+ // When the daemon defaults to private cgroup namespaces, containers launched |
|
| 77 |
+ // should be in their own private cgroup namespace by default |
|
| 78 |
+ containerCgroup, daemonCgroup := testBuildWithCgroupNs(t, "private") |
|
| 79 |
+ assert.Assert(t, daemonCgroup != containerCgroup) |
|
| 80 |
+} |
|
| 81 |
+ |
|
| 82 |
+func TestCgroupNamespacesBuildDaemonHostMode(t *testing.T) {
|
|
| 83 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 84 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 85 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 86 |
+ |
|
| 87 |
+ // When the daemon defaults to host cgroup namespaces, containers |
|
| 88 |
+ // launched should not be inside their own cgroup namespaces |
|
| 89 |
+ containerCgroup, daemonCgroup := testBuildWithCgroupNs(t, "host") |
|
| 90 |
+ assert.Assert(t, daemonCgroup == containerCgroup) |
|
| 91 |
+} |
| 0 | 92 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,152 @@ |
| 0 |
+package container // import "github.com/docker/docker/integration/container" |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+ "time" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/client" |
|
| 9 |
+ "github.com/docker/docker/integration/internal/container" |
|
| 10 |
+ "github.com/docker/docker/integration/internal/requirement" |
|
| 11 |
+ "github.com/docker/docker/internal/test/daemon" |
|
| 12 |
+ "gotest.tools/assert" |
|
| 13 |
+ is "gotest.tools/assert/cmp" |
|
| 14 |
+ "gotest.tools/poll" |
|
| 15 |
+ "gotest.tools/skip" |
|
| 16 |
+) |
|
| 17 |
+ |
|
| 18 |
+// Gets the value of the cgroup namespace for pid 1 of a container |
|
| 19 |
+func containerCgroupNamespace(ctx context.Context, t *testing.T, client *client.Client, cID string) string {
|
|
| 20 |
+ res, err := container.Exec(ctx, client, cID, []string{"readlink", "/proc/1/ns/cgroup"})
|
|
| 21 |
+ assert.NilError(t, err) |
|
| 22 |
+ assert.Assert(t, is.Len(res.Stderr(), 0)) |
|
| 23 |
+ assert.Equal(t, 0, res.ExitCode) |
|
| 24 |
+ return strings.TrimSpace(res.Stdout()) |
|
| 25 |
+} |
|
| 26 |
+ |
|
| 27 |
+// Bring up a daemon with the specified default cgroup namespace mode, and then create a container with the container options |
|
| 28 |
+func testRunWithCgroupNs(t *testing.T, daemonNsMode string, containerOpts ...func(*container.TestContainerConfig)) (string, string) {
|
|
| 29 |
+ d := daemon.New(t, daemon.WithDefaultCgroupNamespaceMode(daemonNsMode)) |
|
| 30 |
+ client := d.NewClientT(t) |
|
| 31 |
+ ctx := context.Background() |
|
| 32 |
+ |
|
| 33 |
+ d.StartWithBusybox(t) |
|
| 34 |
+ defer d.Stop(t) |
|
| 35 |
+ |
|
| 36 |
+ cID := container.Run(t, ctx, client, containerOpts...) |
|
| 37 |
+ poll.WaitOn(t, container.IsInState(ctx, client, cID, "running"), poll.WithDelay(100*time.Millisecond)) |
|
| 38 |
+ |
|
| 39 |
+ daemonCgroup := d.CgroupNamespace(t) |
|
| 40 |
+ containerCgroup := containerCgroupNamespace(ctx, t, client, cID) |
|
| 41 |
+ return containerCgroup, daemonCgroup |
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+// Bring up a daemon with the specified default cgroup namespace mode. Create a container with the container options, |
|
| 45 |
+// expecting an error with the specified string |
|
| 46 |
+func testCreateFailureWithCgroupNs(t *testing.T, daemonNsMode string, errStr string, containerOpts ...func(*container.TestContainerConfig)) {
|
|
| 47 |
+ d := daemon.New(t, daemon.WithDefaultCgroupNamespaceMode(daemonNsMode)) |
|
| 48 |
+ client := d.NewClientT(t) |
|
| 49 |
+ ctx := context.Background() |
|
| 50 |
+ |
|
| 51 |
+ d.StartWithBusybox(t) |
|
| 52 |
+ defer d.Stop(t) |
|
| 53 |
+ container.CreateExpectingErr(t, ctx, client, errStr, containerOpts...) |
|
| 54 |
+} |
|
| 55 |
+ |
|
| 56 |
+func TestCgroupNamespacesRun(t *testing.T) {
|
|
| 57 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 58 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 59 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 60 |
+ |
|
| 61 |
+ // When the daemon defaults to private cgroup namespaces, containers launched |
|
| 62 |
+ // should be in their own private cgroup namespace by default |
|
| 63 |
+ containerCgroup, daemonCgroup := testRunWithCgroupNs(t, "private") |
|
| 64 |
+ assert.Assert(t, daemonCgroup != containerCgroup) |
|
| 65 |
+} |
|
| 66 |
+ |
|
| 67 |
+func TestCgroupNamespacesRunPrivileged(t *testing.T) {
|
|
| 68 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 69 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 70 |
+ skip.If(t, requirement.CgroupNamespacesEnabled()) |
|
| 71 |
+ |
|
| 72 |
+ // When the daemon defaults to private cgroup namespaces, privileged containers |
|
| 73 |
+ // launched should not be inside their own cgroup namespaces |
|
| 74 |
+ containerCgroup, daemonCgroup := testRunWithCgroupNs(t, "private", container.WithPrivileged(true)) |
|
| 75 |
+ assert.Assert(t, daemonCgroup == containerCgroup) |
|
| 76 |
+} |
|
| 77 |
+ |
|
| 78 |
+func TestCgroupNamespacesRunDaemonHostMode(t *testing.T) {
|
|
| 79 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 80 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 81 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 82 |
+ |
|
| 83 |
+ // When the daemon defaults to host cgroup namespaces, containers |
|
| 84 |
+ // launched should not be inside their own cgroup namespaces |
|
| 85 |
+ containerCgroup, daemonCgroup := testRunWithCgroupNs(t, "host") |
|
| 86 |
+ assert.Assert(t, daemonCgroup == containerCgroup) |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 89 |
+func TestCgroupNamespacesRunHostMode(t *testing.T) {
|
|
| 90 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 91 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 92 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 93 |
+ |
|
| 94 |
+ // When the daemon defaults to private cgroup namespaces, containers launched |
|
| 95 |
+ // with a cgroup ns mode of "host" should not be inside their own cgroup namespaces |
|
| 96 |
+ containerCgroup, daemonCgroup := testRunWithCgroupNs(t, "private", container.WithCgroupnsMode("host"))
|
|
| 97 |
+ assert.Assert(t, daemonCgroup == containerCgroup) |
|
| 98 |
+} |
|
| 99 |
+ |
|
| 100 |
+func TestCgroupNamespacesRunPrivateMode(t *testing.T) {
|
|
| 101 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 102 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 103 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 104 |
+ |
|
| 105 |
+ // When the daemon defaults to private cgroup namespaces, containers launched |
|
| 106 |
+ // with a cgroup ns mode of "private" should be inside their own cgroup namespaces |
|
| 107 |
+ containerCgroup, daemonCgroup := testRunWithCgroupNs(t, "private", container.WithCgroupnsMode("private"))
|
|
| 108 |
+ assert.Assert(t, daemonCgroup != containerCgroup) |
|
| 109 |
+} |
|
| 110 |
+ |
|
| 111 |
+func TestCgroupNamespacesRunPrivilegedAndPrivate(t *testing.T) {
|
|
| 112 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 113 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 114 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 115 |
+ |
|
| 116 |
+ // Running with both privileged and cgroupns=private is not allowed |
|
| 117 |
+ errStr := "privileged mode is incompatible with private cgroup namespaces. You must run the container in the host cgroup namespace when running privileged mode" |
|
| 118 |
+ testCreateFailureWithCgroupNs(t, "private", errStr, container.WithPrivileged(true), container.WithCgroupnsMode("private"))
|
|
| 119 |
+} |
|
| 120 |
+ |
|
| 121 |
+func TestCgroupNamespacesRunInvalidMode(t *testing.T) {
|
|
| 122 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 123 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 124 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 125 |
+ |
|
| 126 |
+ // An invalid cgroup namespace mode should return an error on container creation |
|
| 127 |
+ errStr := "invalid cgroup namespace mode: invalid" |
|
| 128 |
+ testCreateFailureWithCgroupNs(t, "private", errStr, container.WithCgroupnsMode("invalid"))
|
|
| 129 |
+} |
|
| 130 |
+ |
|
| 131 |
+// Clients before 1.40 expect containers to be created in the host cgroup namespace, |
|
| 132 |
+// regardless of the default setting of the daemon |
|
| 133 |
+func TestCgroupNamespacesRunOlderClient(t *testing.T) {
|
|
| 134 |
+ skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 135 |
+ skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 136 |
+ skip.If(t, !requirement.CgroupNamespacesEnabled()) |
|
| 137 |
+ |
|
| 138 |
+ d := daemon.New(t, daemon.WithDefaultCgroupNamespaceMode("private"))
|
|
| 139 |
+ client := d.NewClientT(t, client.WithVersion("1.39"))
|
|
| 140 |
+ |
|
| 141 |
+ ctx := context.Background() |
|
| 142 |
+ d.StartWithBusybox(t) |
|
| 143 |
+ defer d.Stop(t) |
|
| 144 |
+ |
|
| 145 |
+ cID := container.Run(t, ctx, client) |
|
| 146 |
+ poll.WaitOn(t, container.IsInState(ctx, client, cID, "running"), poll.WithDelay(100*time.Millisecond)) |
|
| 147 |
+ |
|
| 148 |
+ daemonCgroup := d.CgroupNamespace(t) |
|
| 149 |
+ containerCgroup := containerCgroupNamespace(ctx, t, client, cID) |
|
| 150 |
+ assert.Assert(t, daemonCgroup == containerCgroup) |
|
| 151 |
+} |
| ... | ... |
@@ -2,10 +2,6 @@ package container // import "github.com/docker/docker/integration/container" |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"context" |
| 5 |
- "fmt" |
|
| 6 |
- "io/ioutil" |
|
| 7 |
- "os" |
|
| 8 |
- "path/filepath" |
|
| 9 | 5 |
"strconv" |
| 10 | 6 |
"strings" |
| 11 | 7 |
"testing" |
| ... | ... |
@@ -97,32 +93,3 @@ func TestNISDomainname(t *testing.T) {
|
| 97 | 97 |
assert.Equal(t, 0, res.ExitCode) |
| 98 | 98 |
assert.Check(t, is.Equal(domainname, strings.TrimSpace(res.Stdout()))) |
| 99 | 99 |
} |
| 100 |
- |
|
| 101 |
-func TestCgroupNamespaces(t *testing.T) {
|
|
| 102 |
- skip.If(t, testEnv.DaemonInfo.OSType != "linux") |
|
| 103 |
- skip.If(t, testEnv.IsRemoteDaemon()) |
|
| 104 |
- |
|
| 105 |
- if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
|
|
| 106 |
- t.Skip("cgroup namespaces are unsupported")
|
|
| 107 |
- } |
|
| 108 |
- |
|
| 109 |
- defer setupTest(t)() |
|
| 110 |
- client := testEnv.APIClient() |
|
| 111 |
- ctx := context.Background() |
|
| 112 |
- |
|
| 113 |
- cID := container.Run(t, ctx, client) |
|
| 114 |
- poll.WaitOn(t, container.IsInState(ctx, client, cID, "running"), poll.WithDelay(100*time.Millisecond)) |
|
| 115 |
- |
|
| 116 |
- path := filepath.Join(os.Getenv("DEST"), "docker.pid")
|
|
| 117 |
- b, err := ioutil.ReadFile(path) |
|
| 118 |
- assert.NilError(t, err) |
|
| 119 |
- link, err := os.Readlink(fmt.Sprintf("/proc/%s/ns/cgroup", string(b)))
|
|
| 120 |
- assert.NilError(t, err) |
|
| 121 |
- |
|
| 122 |
- // Check that the container's cgroup doesn't match the docker daemon's |
|
| 123 |
- res, err := container.Exec(ctx, client, cID, []string{"readlink", "/proc/1/ns/cgroup"})
|
|
| 124 |
- assert.NilError(t, err) |
|
| 125 |
- assert.Assert(t, is.Len(res.Stderr(), 0)) |
|
| 126 |
- assert.Equal(t, 0, res.ExitCode) |
|
| 127 |
- assert.Assert(t, link != strings.TrimSpace(res.Stdout())) |
|
| 128 |
-} |
| ... | ... |
@@ -20,9 +20,9 @@ type TestContainerConfig struct {
|
| 20 | 20 |
NetworkingConfig *network.NetworkingConfig |
| 21 | 21 |
} |
| 22 | 22 |
|
| 23 |
-// Create creates a container with the specified options |
|
| 23 |
+// create creates a container with the specified options |
|
| 24 | 24 |
// nolint: golint |
| 25 |
-func Create(t *testing.T, ctx context.Context, client client.APIClient, ops ...func(*TestContainerConfig)) string { // nolint: golint
|
|
| 25 |
+func create(t *testing.T, ctx context.Context, client client.APIClient, ops ...func(*TestContainerConfig)) (container.ContainerCreateCreatedBody, error) { // nolint: golint
|
|
| 26 | 26 |
t.Helper() |
| 27 | 27 |
config := &TestContainerConfig{
|
| 28 | 28 |
Config: &container.Config{
|
| ... | ... |
@@ -37,12 +37,23 @@ func Create(t *testing.T, ctx context.Context, client client.APIClient, ops ...f |
| 37 | 37 |
op(config) |
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 |
- c, err := client.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Name) |
|
| 40 |
+ return client.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Name) |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 43 |
+// Create creates a container with the specified options, asserting that there was no error |
|
| 44 |
+func Create(t *testing.T, ctx context.Context, client client.APIClient, ops ...func(*TestContainerConfig)) string { // nolint: golint
|
|
| 45 |
+ c, err := create(t, ctx, client, ops...) |
|
| 41 | 46 |
assert.NilError(t, err) |
| 42 | 47 |
|
| 43 | 48 |
return c.ID |
| 44 | 49 |
} |
| 45 | 50 |
|
| 51 |
+// CreateExpectingErr creates a container, expecting an error with the specified message |
|
| 52 |
+func CreateExpectingErr(t *testing.T, ctx context.Context, client client.APIClient, errMsg string, ops ...func(*TestContainerConfig)) { // nolint: golint
|
|
| 53 |
+ _, err := create(t, ctx, client, ops...) |
|
| 54 |
+ assert.ErrorContains(t, err, errMsg) |
|
| 55 |
+} |
|
| 56 |
+ |
|
| 46 | 57 |
// Run creates and start a container with the specified options |
| 47 | 58 |
// nolint: golint |
| 48 | 59 |
func Run(t *testing.T, ctx context.Context, client client.APIClient, ops ...func(*TestContainerConfig)) string { // nolint: golint
|
| ... | ... |
@@ -160,3 +160,23 @@ func WithUser(user string) func(c *TestContainerConfig) {
|
| 160 | 160 |
c.Config.User = user |
| 161 | 161 |
} |
| 162 | 162 |
} |
| 163 |
+ |
|
| 164 |
+// WithPrivileged sets privileged mode for the container |
|
| 165 |
+func WithPrivileged(privileged bool) func(*TestContainerConfig) {
|
|
| 166 |
+ return func(c *TestContainerConfig) {
|
|
| 167 |
+ if c.HostConfig == nil {
|
|
| 168 |
+ c.HostConfig = &containertypes.HostConfig{}
|
|
| 169 |
+ } |
|
| 170 |
+ c.HostConfig.Privileged = privileged |
|
| 171 |
+ } |
|
| 172 |
+} |
|
| 173 |
+ |
|
| 174 |
+// WithCgroupnsMode sets the cgroup namespace mode for the container |
|
| 175 |
+func WithCgroupnsMode(mode string) func(*TestContainerConfig) {
|
|
| 176 |
+ return func(c *TestContainerConfig) {
|
|
| 177 |
+ if c.HostConfig == nil {
|
|
| 178 |
+ c.HostConfig = &containertypes.HostConfig{}
|
|
| 179 |
+ } |
|
| 180 |
+ c.HostConfig.CgroupnsMode = containertypes.CgroupnsMode(mode) |
|
| 181 |
+ } |
|
| 182 |
+} |
| ... | ... |
@@ -1,12 +1,22 @@ |
| 1 | 1 |
package requirement // import "github.com/docker/docker/integration/internal/requirement" |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
+ "os" |
|
| 4 | 5 |
"strings" |
| 5 | 6 |
|
| 6 | 7 |
"github.com/docker/docker/pkg/parsers/kernel" |
| 7 | 8 |
"gotest.tools/icmd" |
| 8 | 9 |
) |
| 9 | 10 |
|
| 11 |
+// CgroupNamespacesEnabled checks if cgroup namespaces are enabled on this host |
|
| 12 |
+func CgroupNamespacesEnabled() bool {
|
|
| 13 |
+ if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
|
|
| 14 |
+ return false |
|
| 15 |
+ } |
|
| 16 |
+ |
|
| 17 |
+ return true |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 10 | 20 |
func overlayFSSupported() bool {
|
| 11 | 21 |
result := icmd.RunCommand("/bin/sh", "-c", "cat /proc/filesystems")
|
| 12 | 22 |
if result.Error != nil {
|
| ... | ... |
@@ -60,16 +60,17 @@ type Daemon struct {
|
| 60 | 60 |
UseDefaultHost bool |
| 61 | 61 |
UseDefaultTLSHost bool |
| 62 | 62 |
|
| 63 |
- id string |
|
| 64 |
- logFile *os.File |
|
| 65 |
- cmd *exec.Cmd |
|
| 66 |
- storageDriver string |
|
| 67 |
- userlandProxy bool |
|
| 68 |
- execRoot string |
|
| 69 |
- experimental bool |
|
| 70 |
- init bool |
|
| 71 |
- dockerdBinary string |
|
| 72 |
- log logT |
|
| 63 |
+ id string |
|
| 64 |
+ logFile *os.File |
|
| 65 |
+ cmd *exec.Cmd |
|
| 66 |
+ storageDriver string |
|
| 67 |
+ userlandProxy bool |
|
| 68 |
+ defaultCgroupNamespaceMode string |
|
| 69 |
+ execRoot string |
|
| 70 |
+ experimental bool |
|
| 71 |
+ init bool |
|
| 72 |
+ dockerdBinary string |
|
| 73 |
+ log logT |
|
| 73 | 74 |
|
| 74 | 75 |
// swarm related field |
| 75 | 76 |
swarmListenAddr string |
| ... | ... |
@@ -169,13 +170,18 @@ func (d *Daemon) ReadLogFile() ([]byte, error) {
|
| 169 | 169 |
} |
| 170 | 170 |
|
| 171 | 171 |
// NewClientT creates new client based on daemon's socket path |
| 172 |
-func (d *Daemon) NewClientT(t assert.TestingT) *client.Client {
|
|
| 172 |
+func (d *Daemon) NewClientT(t assert.TestingT, extraOpts ...client.Opt) *client.Client {
|
|
| 173 | 173 |
if ht, ok := t.(test.HelperT); ok {
|
| 174 | 174 |
ht.Helper() |
| 175 | 175 |
} |
| 176 |
- c, err := client.NewClientWithOpts( |
|
| 176 |
+ |
|
| 177 |
+ clientOpts := []client.Opt{
|
|
| 177 | 178 |
client.FromEnv, |
| 178 |
- client.WithHost(d.Sock())) |
|
| 179 |
+ client.WithHost(d.Sock()), |
|
| 180 |
+ } |
|
| 181 |
+ clientOpts = append(clientOpts, extraOpts...) |
|
| 182 |
+ |
|
| 183 |
+ c, err := client.NewClientWithOpts(clientOpts...) |
|
| 179 | 184 |
assert.NilError(t, err, "cannot create daemon client") |
| 180 | 185 |
return c |
| 181 | 186 |
} |
| ... | ... |
@@ -225,6 +231,9 @@ func (d *Daemon) StartWithLogFile(out *os.File, providedArgs ...string) error {
|
| 225 | 225 |
"--pidfile", fmt.Sprintf("%s/docker.pid", d.Folder),
|
| 226 | 226 |
fmt.Sprintf("--userland-proxy=%t", d.userlandProxy),
|
| 227 | 227 |
) |
| 228 |
+ if d.defaultCgroupNamespaceMode != "" {
|
|
| 229 |
+ args = append(args, []string{"--default-cgroupns-mode", d.defaultCgroupNamespaceMode}...)
|
|
| 230 |
+ } |
|
| 228 | 231 |
if d.experimental {
|
| 229 | 232 |
args = append(args, "--experimental") |
| 230 | 233 |
} |
| ... | ... |
@@ -3,11 +3,14 @@ |
| 3 | 3 |
package daemon // import "github.com/docker/docker/internal/test/daemon" |
| 4 | 4 |
|
| 5 | 5 |
import ( |
| 6 |
+ "fmt" |
|
| 6 | 7 |
"os" |
| 7 | 8 |
"path/filepath" |
| 9 |
+ "strings" |
|
| 8 | 10 |
|
| 9 | 11 |
"github.com/docker/docker/internal/test" |
| 10 | 12 |
"golang.org/x/sys/unix" |
| 13 |
+ "gotest.tools/assert" |
|
| 11 | 14 |
) |
| 12 | 15 |
|
| 13 | 16 |
func cleanupNetworkNamespace(t testingT, execRoot string) {
|
| ... | ... |
@@ -29,6 +32,14 @@ func cleanupNetworkNamespace(t testingT, execRoot string) {
|
| 29 | 29 |
}) |
| 30 | 30 |
} |
| 31 | 31 |
|
| 32 |
+// CgroupNamespace returns the cgroup namespace the daemon is running in |
|
| 33 |
+func (d *Daemon) CgroupNamespace(t assert.TestingT) string {
|
|
| 34 |
+ link, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/cgroup", d.Pid()))
|
|
| 35 |
+ assert.NilError(t, err) |
|
| 36 |
+ |
|
| 37 |
+ return strings.TrimSpace(link) |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 32 | 40 |
// SignalDaemonDump sends a signal to the daemon to write a dump file |
| 33 | 41 |
func SignalDaemonDump(pid int) {
|
| 34 | 42 |
unix.Kill(pid, unix.SIGQUIT) |
| ... | ... |
@@ -5,6 +5,7 @@ import ( |
| 5 | 5 |
"strconv" |
| 6 | 6 |
|
| 7 | 7 |
"golang.org/x/sys/windows" |
| 8 |
+ "gotest.tools/assert" |
|
| 8 | 9 |
) |
| 9 | 10 |
|
| 10 | 11 |
// SignalDaemonDump sends a signal to the daemon to write a dump file |
| ... | ... |
@@ -23,3 +24,9 @@ func signalDaemonReload(pid int) error {
|
| 23 | 23 |
|
| 24 | 24 |
func cleanupNetworkNamespace(t testingT, execRoot string) {
|
| 25 | 25 |
} |
| 26 |
+ |
|
| 27 |
+// CgroupNamespace returns the cgroup namespace the daemon is running in |
|
| 28 |
+func (d *Daemon) CgroupNamespace(t assert.TestingT) string {
|
|
| 29 |
+ assert.Assert(t, false) |
|
| 30 |
+ return "cgroup namespaces are not supported on Windows" |
|
| 31 |
+} |
| ... | ... |
@@ -2,6 +2,13 @@ package daemon |
| 2 | 2 |
|
| 3 | 3 |
import "github.com/docker/docker/internal/test/environment" |
| 4 | 4 |
|
| 5 |
+// WithDefaultCgroupNamespaceMode sets the default cgroup namespace mode for the daemon |
|
| 6 |
+func WithDefaultCgroupNamespaceMode(mode string) func(*Daemon) {
|
|
| 7 |
+ return func(d *Daemon) {
|
|
| 8 |
+ d.defaultCgroupNamespaceMode = mode |
|
| 9 |
+ } |
|
| 10 |
+} |
|
| 11 |
+ |
|
| 5 | 12 |
// WithExperimental sets the daemon in experimental mode |
| 6 | 13 |
func WithExperimental(d *Daemon) {
|
| 7 | 14 |
d.experimental = true |
| ... | ... |
@@ -14,6 +14,32 @@ import ( |
| 14 | 14 |
is "gotest.tools/assert/cmp" |
| 15 | 15 |
) |
| 16 | 16 |
|
| 17 |
+func TestCgroupnsModeTest(t *testing.T) {
|
|
| 18 |
+ cgroupNsModes := map[container.CgroupnsMode][]bool{
|
|
| 19 |
+ // private, host, empty, valid |
|
| 20 |
+ "": {false, false, true, true},
|
|
| 21 |
+ "something:weird": {false, false, false, false},
|
|
| 22 |
+ "host": {false, true, false, true},
|
|
| 23 |
+ "host:name": {false, false, false, false},
|
|
| 24 |
+ "private": {true, false, false, true},
|
|
| 25 |
+ "private:name": {false, false, false, false},
|
|
| 26 |
+ } |
|
| 27 |
+ for cgroupNsMode, state := range cgroupNsModes {
|
|
| 28 |
+ if cgroupNsMode.IsPrivate() != state[0] {
|
|
| 29 |
+ t.Fatalf("CgroupnsMode.IsPrivate for %v should have been %v but was %v", cgroupNsMode, state[0], cgroupNsMode.IsPrivate())
|
|
| 30 |
+ } |
|
| 31 |
+ if cgroupNsMode.IsHost() != state[1] {
|
|
| 32 |
+ t.Fatalf("CgroupnsMode.IsHost for %v should have been %v but was %v", cgroupNsMode, state[1], cgroupNsMode.IsHost())
|
|
| 33 |
+ } |
|
| 34 |
+ if cgroupNsMode.IsEmpty() != state[2] {
|
|
| 35 |
+ t.Fatalf("CgroupnsMode.Valid for %v should have been %v but was %v", cgroupNsMode, state[2], cgroupNsMode.Valid())
|
|
| 36 |
+ } |
|
| 37 |
+ if cgroupNsMode.Valid() != state[3] {
|
|
| 38 |
+ t.Fatalf("CgroupnsMode.Valid for %v should have been %v but was %v", cgroupNsMode, state[2], cgroupNsMode.Valid())
|
|
| 39 |
+ } |
|
| 40 |
+ } |
|
| 41 |
+} |
|
| 42 |
+ |
|
| 17 | 43 |
// TODO Windows: This will need addressing for a Windows daemon. |
| 18 | 44 |
func TestNetworkModeTest(t *testing.T) {
|
| 19 | 45 |
networkModes := map[container.NetworkMode][]bool{
|