github.com/rumpl/bof@v23.0.0-rc.2+incompatible/integration/container/mounts_linux_test.go (about) 1 package container // import "github.com/docker/docker/integration/container" 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "testing" 8 "time" 9 10 "github.com/docker/docker/api/types" 11 containertypes "github.com/docker/docker/api/types/container" 12 mounttypes "github.com/docker/docker/api/types/mount" 13 "github.com/docker/docker/api/types/network" 14 "github.com/docker/docker/api/types/versions" 15 "github.com/docker/docker/client" 16 "github.com/docker/docker/integration/internal/container" 17 "github.com/docker/docker/pkg/system" 18 "github.com/moby/sys/mount" 19 "github.com/moby/sys/mountinfo" 20 "gotest.tools/v3/assert" 21 is "gotest.tools/v3/assert/cmp" 22 "gotest.tools/v3/fs" 23 "gotest.tools/v3/poll" 24 "gotest.tools/v3/skip" 25 ) 26 27 func TestContainerNetworkMountsNoChown(t *testing.T) { 28 // chown only applies to Linux bind mounted volumes; must be same host to verify 29 skip.If(t, testEnv.IsRemoteDaemon) 30 31 defer setupTest(t)() 32 33 ctx := context.Background() 34 35 tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0644))) 36 defer tmpDir.Remove() 37 38 tmpNWFileMount := tmpDir.Join("nwfile") 39 40 config := containertypes.Config{ 41 Image: "busybox", 42 } 43 hostConfig := containertypes.HostConfig{ 44 Mounts: []mounttypes.Mount{ 45 { 46 Type: "bind", 47 Source: tmpNWFileMount, 48 Target: "/etc/resolv.conf", 49 }, 50 { 51 Type: "bind", 52 Source: tmpNWFileMount, 53 Target: "/etc/hostname", 54 }, 55 { 56 Type: "bind", 57 Source: tmpNWFileMount, 58 Target: "/etc/hosts", 59 }, 60 }, 61 } 62 63 cli, err := client.NewClientWithOpts(client.FromEnv) 64 assert.NilError(t, err) 65 defer cli.Close() 66 67 ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "") 68 assert.NilError(t, err) 69 // container will exit immediately because of no tty, but we only need the start sequence to test the condition 70 err = cli.ContainerStart(ctx, ctrCreate.ID, types.ContainerStartOptions{}) 71 assert.NilError(t, err) 72 73 // Check that host-located bind mount network file did not change ownership when the container was started 74 // Note: If the user specifies a mountpath from the host, we should not be 75 // attempting to chown files outside the daemon's metadata directory 76 // (represented by `daemon.repository` at init time). 77 // This forces users who want to use user namespaces to handle the 78 // ownership needs of any external files mounted as network files 79 // (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the 80 // daemon. In all other volume/bind mount situations we have taken this 81 // same line--we don't chown host file content. 82 // See GitHub PR 34224 for details. 83 statT, err := system.Stat(tmpNWFileMount) 84 assert.NilError(t, err) 85 assert.Check(t, is.Equal(uint32(0), statT.UID()), "bind mounted network file should not change ownership from root") 86 } 87 88 func TestMountDaemonRoot(t *testing.T) { 89 skip.If(t, testEnv.IsRemoteDaemon) 90 91 defer setupTest(t)() 92 client := testEnv.APIClient() 93 ctx := context.Background() 94 info, err := client.Info(ctx) 95 if err != nil { 96 t.Fatal(err) 97 } 98 99 for _, test := range []struct { 100 desc string 101 propagation mounttypes.Propagation 102 expected mounttypes.Propagation 103 }{ 104 { 105 desc: "default", 106 propagation: "", 107 expected: mounttypes.PropagationRSlave, 108 }, 109 { 110 desc: "private", 111 propagation: mounttypes.PropagationPrivate, 112 }, 113 { 114 desc: "rprivate", 115 propagation: mounttypes.PropagationRPrivate, 116 }, 117 { 118 desc: "slave", 119 propagation: mounttypes.PropagationSlave, 120 }, 121 { 122 desc: "rslave", 123 propagation: mounttypes.PropagationRSlave, 124 expected: mounttypes.PropagationRSlave, 125 }, 126 { 127 desc: "shared", 128 propagation: mounttypes.PropagationShared, 129 }, 130 { 131 desc: "rshared", 132 propagation: mounttypes.PropagationRShared, 133 expected: mounttypes.PropagationRShared, 134 }, 135 } { 136 t.Run(test.desc, func(t *testing.T) { 137 test := test 138 t.Parallel() 139 140 propagationSpec := fmt.Sprintf(":%s", test.propagation) 141 if test.propagation == "" { 142 propagationSpec = "" 143 } 144 bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec 145 bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec 146 147 for name, hc := range map[string]*containertypes.HostConfig{ 148 "bind root": {Binds: []string{bindSpecRoot}}, 149 "bind subpath": {Binds: []string{bindSpecSub}}, 150 "mount root": { 151 Mounts: []mounttypes.Mount{ 152 { 153 Type: mounttypes.TypeBind, 154 Source: info.DockerRootDir, 155 Target: "/foo", 156 BindOptions: &mounttypes.BindOptions{Propagation: test.propagation}, 157 }, 158 }, 159 }, 160 "mount subpath": { 161 Mounts: []mounttypes.Mount{ 162 { 163 Type: mounttypes.TypeBind, 164 Source: filepath.Join(info.DockerRootDir, "containers"), 165 Target: "/foo", 166 BindOptions: &mounttypes.BindOptions{Propagation: test.propagation}, 167 }, 168 }, 169 }, 170 } { 171 t.Run(name, func(t *testing.T) { 172 hc := hc 173 t.Parallel() 174 175 c, err := client.ContainerCreate(ctx, &containertypes.Config{ 176 Image: "busybox", 177 Cmd: []string{"true"}, 178 }, hc, nil, nil, "") 179 180 if err != nil { 181 if test.expected != "" { 182 t.Fatal(err) 183 } 184 // expected an error, so this is ok and should not continue 185 return 186 } 187 if test.expected == "" { 188 t.Fatal("expected create to fail") 189 } 190 191 defer func() { 192 if err := client.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{Force: true}); err != nil { 193 panic(err) 194 } 195 }() 196 197 inspect, err := client.ContainerInspect(ctx, c.ID) 198 if err != nil { 199 t.Fatal(err) 200 } 201 if len(inspect.Mounts) != 1 { 202 t.Fatalf("unexpected number of mounts: %+v", inspect.Mounts) 203 } 204 205 m := inspect.Mounts[0] 206 if m.Propagation != test.expected { 207 t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation) 208 } 209 }) 210 } 211 }) 212 } 213 } 214 215 func TestContainerBindMountNonRecursive(t *testing.T) { 216 skip.If(t, testEnv.IsRemoteDaemon) 217 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "BindOptions.NonRecursive requires API v1.40") 218 skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)") 219 220 defer setupTest(t)() 221 222 tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0755), 223 fs.WithDir("mnt", fs.WithMode(0755))) 224 defer tmpDir1.Remove() 225 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt") 226 tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0755), 227 fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0644))) 228 defer tmpDir2.Remove() 229 230 err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro") 231 if err != nil { 232 t.Fatal(err) 233 } 234 defer func() { 235 if err := mount.Unmount(tmpDir1Mnt); err != nil { 236 t.Fatal(err) 237 } 238 }() 239 240 // implicit is recursive (NonRecursive: false) 241 implicit := mounttypes.Mount{ 242 Type: "bind", 243 Source: tmpDir1.Path(), 244 Target: "/foo", 245 ReadOnly: true, 246 } 247 recursive := implicit 248 recursive.BindOptions = &mounttypes.BindOptions{ 249 NonRecursive: false, 250 } 251 recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"} 252 nonRecursive := implicit 253 nonRecursive.BindOptions = &mounttypes.BindOptions{ 254 NonRecursive: true, 255 } 256 nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"} 257 258 ctx := context.Background() 259 client := testEnv.APIClient() 260 containers := []string{ 261 container.Run(ctx, t, client, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)), 262 container.Run(ctx, t, client, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)), 263 container.Run(ctx, t, client, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)), 264 } 265 266 for _, c := range containers { 267 poll.WaitOn(t, container.IsSuccessful(ctx, client, c), poll.WithDelay(100*time.Millisecond)) 268 } 269 } 270 271 func TestContainerVolumesMountedAsShared(t *testing.T) { 272 // Volume propagation is linux only. Also it creates directories for 273 // bind mounting, so needs to be same host. 274 skip.If(t, testEnv.IsRemoteDaemon) 275 skip.If(t, testEnv.IsUserNamespace) 276 skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)") 277 278 defer setupTest(t)() 279 280 // Prepare a source directory to bind mount 281 tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0755), 282 fs.WithDir("mnt1", fs.WithMode(0755))) 283 defer tmpDir1.Remove() 284 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1") 285 286 // Convert this directory into a shared mount point so that we do 287 // not rely on propagation properties of parent mount. 288 if err := mount.MakePrivate(tmpDir1.Path()); err != nil { 289 t.Fatal(err) 290 } 291 defer func() { 292 if err := mount.Unmount(tmpDir1.Path()); err != nil { 293 t.Fatal(err) 294 } 295 }() 296 if err := mount.MakeShared(tmpDir1.Path()); err != nil { 297 t.Fatal(err) 298 } 299 300 sharedMount := mounttypes.Mount{ 301 Type: mounttypes.TypeBind, 302 Source: tmpDir1.Path(), 303 Target: "/volume-dest", 304 BindOptions: &mounttypes.BindOptions{ 305 Propagation: mounttypes.PropagationShared, 306 }, 307 } 308 309 bindMountCmd := []string{"mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1"} 310 311 ctx := context.Background() 312 client := testEnv.APIClient() 313 containerID := container.Run(ctx, t, client, container.WithPrivileged(true), container.WithMount(sharedMount), container.WithCmd(bindMountCmd...)) 314 poll.WaitOn(t, container.IsSuccessful(ctx, client, containerID), poll.WithDelay(100*time.Millisecond)) 315 316 // Make sure a bind mount under a shared volume propagated to host. 317 if mounted, _ := mountinfo.Mounted(tmpDir1Mnt); !mounted { 318 t.Fatalf("Bind mount under shared volume did not propagate to host") 319 } 320 321 mount.Unmount(tmpDir1Mnt) 322 } 323 324 func TestContainerVolumesMountedAsSlave(t *testing.T) { 325 // Volume propagation is linux only. Also it creates directories for 326 // bind mounting, so needs to be same host. 327 skip.If(t, testEnv.IsRemoteDaemon) 328 skip.If(t, testEnv.IsUserNamespace) 329 skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)") 330 331 // Prepare a source directory to bind mount 332 tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0755), 333 fs.WithDir("mnt1", fs.WithMode(0755))) 334 defer tmpDir1.Remove() 335 tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1") 336 337 // Prepare a source directory with file in it. We will bind mount this 338 // directory and see if file shows up. 339 tmpDir2 := fs.NewDir(t, "volume-source2", fs.WithMode(0755), 340 fs.WithFile("slave-testfile", "Test", fs.WithMode(0644))) 341 defer tmpDir2.Remove() 342 343 // Convert this directory into a shared mount point so that we do 344 // not rely on propagation properties of parent mount. 345 if err := mount.MakePrivate(tmpDir1.Path()); err != nil { 346 t.Fatal(err) 347 } 348 defer func() { 349 if err := mount.Unmount(tmpDir1.Path()); err != nil { 350 t.Fatal(err) 351 } 352 }() 353 if err := mount.MakeShared(tmpDir1.Path()); err != nil { 354 t.Fatal(err) 355 } 356 357 slaveMount := mounttypes.Mount{ 358 Type: mounttypes.TypeBind, 359 Source: tmpDir1.Path(), 360 Target: "/volume-dest", 361 BindOptions: &mounttypes.BindOptions{ 362 Propagation: mounttypes.PropagationSlave, 363 }, 364 } 365 366 topCmd := []string{"top"} 367 368 ctx := context.Background() 369 client := testEnv.APIClient() 370 containerID := container.Run(ctx, t, client, container.WithTty(true), container.WithMount(slaveMount), container.WithCmd(topCmd...)) 371 372 // Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside 373 // container then contents of tmpDir2/slave-testfile should become 374 // visible at "/volume-dest/mnt1/slave-testfile" 375 if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil { 376 t.Fatal(err) 377 } 378 defer func() { 379 if err := mount.Unmount(tmpDir1Mnt); err != nil { 380 t.Fatal(err) 381 } 382 }() 383 384 mountCmd := []string{"cat", "/volume-dest/mnt1/slave-testfile"} 385 386 if result, err := container.Exec(ctx, client, containerID, mountCmd); err == nil { 387 if result.Stdout() != "Test" { 388 t.Fatalf("Bind mount under slave volume did not propagate to container") 389 } 390 } else { 391 t.Fatal(err) 392 } 393 }