Browse code

Make cgroup namespaces configurable

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>

Rob Gulewich authored on 2019/03/15 12:44:18
Showing 28 changed files
... ...
@@ -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{