2018-02-05 16:05:59 -05:00
package container // import "github.com/docker/docker/integration/container"
2017-12-18 16:02:23 -05:00
import (
"context"
"fmt"
2018-01-18 16:55:27 -05:00
"path/filepath"
2017-12-18 16:02:23 -05:00
"testing"
2018-10-10 06:20:13 -04:00
"time"
2017-12-18 16:02:23 -05:00
"github.com/docker/docker/api/types"
2018-10-10 06:20:13 -04:00
containertypes "github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
2018-02-04 12:38:04 -05:00
"github.com/docker/docker/api/types/network"
2018-10-10 06:20:13 -04:00
"github.com/docker/docker/api/types/versions"
2018-02-04 12:38:04 -05:00
"github.com/docker/docker/client"
2018-10-10 06:20:13 -04:00
"github.com/docker/docker/integration/internal/container"
2018-02-04 12:38:04 -05:00
"github.com/docker/docker/pkg/system"
2020-03-13 19:38:24 -04:00
"github.com/moby/sys/mount"
2020-11-07 02:06:44 -05:00
"github.com/moby/sys/mountinfo"
2020-02-07 08:39:24 -05:00
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
"gotest.tools/v3/poll"
"gotest.tools/v3/skip"
2017-12-18 16:02:23 -05:00
)
2018-02-04 12:38:04 -05:00
func TestContainerNetworkMountsNoChown ( t * testing . T ) {
// chown only applies to Linux bind mounted volumes; must be same host to verify
2019-01-03 06:32:05 -05:00
skip . If ( t , testEnv . IsRemoteDaemon )
2018-02-04 12:38:04 -05:00
defer setupTest ( t ) ( )
ctx := context . Background ( )
tmpDir := fs . NewDir ( t , "network-file-mounts" , fs . WithMode ( 0755 ) , fs . WithFile ( "nwfile" , "network file bind mount" , fs . WithMode ( 0644 ) ) )
defer tmpDir . Remove ( )
tmpNWFileMount := tmpDir . Join ( "nwfile" )
2018-10-10 06:20:13 -04:00
config := containertypes . Config {
2018-02-04 12:38:04 -05:00
Image : "busybox" ,
}
2018-10-10 06:20:13 -04:00
hostConfig := containertypes . HostConfig {
Mounts : [ ] mounttypes . Mount {
2018-02-04 12:38:04 -05:00
{
Type : "bind" ,
Source : tmpNWFileMount ,
Target : "/etc/resolv.conf" ,
} ,
{
Type : "bind" ,
Source : tmpNWFileMount ,
Target : "/etc/hostname" ,
} ,
{
Type : "bind" ,
Source : tmpNWFileMount ,
Target : "/etc/hosts" ,
} ,
} ,
}
2019-01-03 16:49:00 -05:00
cli , err := client . NewClientWithOpts ( client . FromEnv )
2018-03-13 15:28:34 -04:00
assert . NilError ( t , err )
2018-02-04 12:38:04 -05:00
defer cli . Close ( )
2020-03-19 16:54:48 -04:00
ctrCreate , err := cli . ContainerCreate ( ctx , & config , & hostConfig , & network . NetworkingConfig { } , nil , "" )
2018-03-13 15:28:34 -04:00
assert . NilError ( t , err )
2018-02-04 12:38:04 -05:00
// container will exit immediately because of no tty, but we only need the start sequence to test the condition
err = cli . ContainerStart ( ctx , ctrCreate . ID , types . ContainerStartOptions { } )
2018-03-13 15:28:34 -04:00
assert . NilError ( t , err )
2018-02-04 12:38:04 -05:00
2018-02-06 19:19:38 -05:00
// Check that host-located bind mount network file did not change ownership when the container was started
// Note: If the user specifies a mountpath from the host, we should not be
// attempting to chown files outside the daemon's metadata directory
// (represented by `daemon.repository` at init time).
// This forces users who want to use user namespaces to handle the
// ownership needs of any external files mounted as network files
// (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the
// daemon. In all other volume/bind mount situations we have taken this
// same line--we don't chown host file content.
// See GitHub PR 34224 for details.
2018-02-04 12:38:04 -05:00
statT , err := system . Stat ( tmpNWFileMount )
2018-03-13 15:28:34 -04:00
assert . NilError ( t , err )
assert . Check ( t , is . Equal ( uint32 ( 0 ) , statT . UID ( ) ) , "bind mounted network file should not change ownership from root" )
2018-02-04 12:38:04 -05:00
}
2018-01-18 16:55:27 -05:00
func TestMountDaemonRoot ( t * testing . T ) {
2019-01-03 06:32:05 -05:00
skip . If ( t , testEnv . IsRemoteDaemon )
2018-01-18 16:55:27 -05:00
2019-01-02 08:16:25 -05:00
defer setupTest ( t ) ( )
client := testEnv . APIClient ( )
2018-01-18 16:55:27 -05:00
ctx := context . Background ( )
info , err := client . Info ( ctx )
if err != nil {
t . Fatal ( err )
}
for _ , test := range [ ] struct {
desc string
2018-10-10 06:20:13 -04:00
propagation mounttypes . Propagation
expected mounttypes . Propagation
2018-01-18 16:55:27 -05:00
} {
{
desc : "default" ,
propagation : "" ,
2018-10-10 06:20:13 -04:00
expected : mounttypes . PropagationRSlave ,
2018-01-18 16:55:27 -05:00
} ,
{
desc : "private" ,
2018-10-10 06:20:13 -04:00
propagation : mounttypes . PropagationPrivate ,
2018-01-18 16:55:27 -05:00
} ,
{
desc : "rprivate" ,
2018-10-10 06:20:13 -04:00
propagation : mounttypes . PropagationRPrivate ,
2018-01-18 16:55:27 -05:00
} ,
{
desc : "slave" ,
2018-10-10 06:20:13 -04:00
propagation : mounttypes . PropagationSlave ,
2018-01-18 16:55:27 -05:00
} ,
{
desc : "rslave" ,
2018-10-10 06:20:13 -04:00
propagation : mounttypes . PropagationRSlave ,
expected : mounttypes . PropagationRSlave ,
2018-01-18 16:55:27 -05:00
} ,
{
desc : "shared" ,
2018-10-10 06:20:13 -04:00
propagation : mounttypes . PropagationShared ,
2018-01-18 16:55:27 -05:00
} ,
{
desc : "rshared" ,
2018-10-10 06:20:13 -04:00
propagation : mounttypes . PropagationRShared ,
expected : mounttypes . PropagationRShared ,
2018-01-18 16:55:27 -05:00
} ,
} {
t . Run ( test . desc , func ( t * testing . T ) {
test := test
t . Parallel ( )
propagationSpec := fmt . Sprintf ( ":%s" , test . propagation )
if test . propagation == "" {
propagationSpec = ""
}
bindSpecRoot := info . DockerRootDir + ":" + "/foo" + propagationSpec
bindSpecSub := filepath . Join ( info . DockerRootDir , "containers" ) + ":/foo" + propagationSpec
2018-10-10 06:20:13 -04:00
for name , hc := range map [ string ] * containertypes . HostConfig {
2018-01-18 16:55:27 -05:00
"bind root" : { Binds : [ ] string { bindSpecRoot } } ,
"bind subpath" : { Binds : [ ] string { bindSpecSub } } ,
"mount root" : {
2018-10-10 06:20:13 -04:00
Mounts : [ ] mounttypes . Mount {
2018-01-18 16:55:27 -05:00
{
2018-10-10 06:20:13 -04:00
Type : mounttypes . TypeBind ,
2018-01-18 16:55:27 -05:00
Source : info . DockerRootDir ,
Target : "/foo" ,
2018-10-10 06:20:13 -04:00
BindOptions : & mounttypes . BindOptions { Propagation : test . propagation } ,
2018-01-18 16:55:27 -05:00
} ,
} ,
} ,
"mount subpath" : {
2018-10-10 06:20:13 -04:00
Mounts : [ ] mounttypes . Mount {
2018-01-18 16:55:27 -05:00
{
2018-10-10 06:20:13 -04:00
Type : mounttypes . TypeBind ,
2018-01-18 16:55:27 -05:00
Source : filepath . Join ( info . DockerRootDir , "containers" ) ,
Target : "/foo" ,
2018-10-10 06:20:13 -04:00
BindOptions : & mounttypes . BindOptions { Propagation : test . propagation } ,
2018-01-18 16:55:27 -05:00
} ,
} ,
} ,
} {
t . Run ( name , func ( t * testing . T ) {
hc := hc
t . Parallel ( )
2018-10-10 06:20:13 -04:00
c , err := client . ContainerCreate ( ctx , & containertypes . Config {
2018-01-18 16:55:27 -05:00
Image : "busybox" ,
Cmd : [ ] string { "true" } ,
2020-03-19 16:54:48 -04:00
} , hc , nil , nil , "" )
2018-01-18 16:55:27 -05:00
if err != nil {
if test . expected != "" {
t . Fatal ( err )
}
// expected an error, so this is ok and should not continue
return
}
if test . expected == "" {
t . Fatal ( "expected create to fail" )
}
defer func ( ) {
if err := client . ContainerRemove ( ctx , c . ID , types . ContainerRemoveOptions { Force : true } ) ; err != nil {
panic ( err )
}
} ( )
inspect , err := client . ContainerInspect ( ctx , c . ID )
if err != nil {
t . Fatal ( err )
}
if len ( inspect . Mounts ) != 1 {
t . Fatalf ( "unexpected number of mounts: %+v" , inspect . Mounts )
}
m := inspect . Mounts [ 0 ]
if m . Propagation != test . expected {
t . Fatalf ( "got unexpected propagation mode, expected %q, got: %v" , test . expected , m . Propagation )
}
} )
}
} )
}
}
2018-10-10 06:20:13 -04:00
func TestContainerBindMountNonRecursive ( t * testing . T ) {
2019-01-03 06:32:05 -05:00
skip . If ( t , testEnv . IsRemoteDaemon )
2018-10-10 06:20:13 -04:00
skip . If ( t , versions . LessThan ( testEnv . DaemonAPIVersion ( ) , "1.40" ) , "BindOptions.NonRecursive requires API v1.40" )
2020-02-18 04:43:56 -05:00
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)" )
2018-10-10 06:20:13 -04:00
defer setupTest ( t ) ( )
tmpDir1 := fs . NewDir ( t , "tmpdir1" , fs . WithMode ( 0755 ) ,
fs . WithDir ( "mnt" , fs . WithMode ( 0755 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt" )
tmpDir2 := fs . NewDir ( t , "tmpdir2" , fs . WithMode ( 0755 ) ,
fs . WithFile ( "file" , "should not be visible when NonRecursive" , fs . WithMode ( 0644 ) ) )
defer tmpDir2 . Remove ( )
err := mount . Mount ( tmpDir2 . Path ( ) , tmpDir1Mnt , "none" , "bind,ro" )
if err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1Mnt ) ; err != nil {
t . Fatal ( err )
}
} ( )
// implicit is recursive (NonRecursive: false)
implicit := mounttypes . Mount {
Type : "bind" ,
Source : tmpDir1 . Path ( ) ,
Target : "/foo" ,
ReadOnly : true ,
}
recursive := implicit
recursive . BindOptions = & mounttypes . BindOptions {
NonRecursive : false ,
}
recursiveVerifier := [ ] string { "test" , "-f" , "/foo/mnt/file" }
nonRecursive := implicit
nonRecursive . BindOptions = & mounttypes . BindOptions {
NonRecursive : true ,
}
nonRecursiveVerifier := [ ] string { "test" , "!" , "-f" , "/foo/mnt/file" }
ctx := context . Background ( )
2019-01-02 08:16:25 -05:00
client := testEnv . APIClient ( )
2018-10-10 06:20:13 -04:00
containers := [ ] string {
2019-06-06 07:15:31 -04:00
container . Run ( ctx , t , client , container . WithMount ( implicit ) , container . WithCmd ( recursiveVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithMount ( recursive ) , container . WithCmd ( recursiveVerifier ... ) ) ,
container . Run ( ctx , t , client , container . WithMount ( nonRecursive ) , container . WithCmd ( nonRecursiveVerifier ... ) ) ,
2018-10-10 06:20:13 -04:00
}
for _ , c := range containers {
poll . WaitOn ( t , container . IsSuccessful ( ctx , client , c ) , poll . WithDelay ( 100 * time . Millisecond ) )
}
}
2020-11-07 02:06:44 -05:00
func TestContainerVolumesMountedAsShared ( t * testing . T ) {
// Volume propagation is linux only. Also it creates directories for
// bind mounting, so needs to be same host.
skip . If ( t , testEnv . IsRemoteDaemon )
skip . If ( t , testEnv . IsUserNamespace )
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)" )
defer setupTest ( t ) ( )
// Prepare a source directory to bind mount
tmpDir1 := fs . NewDir ( t , "volume-source" , fs . WithMode ( 0755 ) ,
fs . WithDir ( "mnt1" , fs . WithMode ( 0755 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt1" )
// Convert this directory into a shared mount point so that we do
// not rely on propagation properties of parent mount.
if err := mount . Mount ( tmpDir1 . Path ( ) , tmpDir1 . Path ( ) , "none" , "bind,private" ) ; err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1 . Path ( ) ) ; err != nil {
t . Fatal ( err )
}
} ( )
if err := mount . Mount ( "none" , tmpDir1 . Path ( ) , "none" , "shared" ) ; err != nil {
t . Fatal ( err )
}
sharedMount := mounttypes . Mount {
Type : mounttypes . TypeBind ,
Source : tmpDir1 . Path ( ) ,
Target : "/volume-dest" ,
BindOptions : & mounttypes . BindOptions {
Propagation : mounttypes . PropagationShared ,
} ,
}
bindMountCmd := [ ] string { "mount" , "--bind" , "/volume-dest/mnt1" , "/volume-dest/mnt1" }
ctx := context . Background ( )
client := testEnv . APIClient ( )
containerID := container . Run ( ctx , t , client , container . WithPrivileged ( true ) , container . WithMount ( sharedMount ) , container . WithCmd ( bindMountCmd ... ) )
poll . WaitOn ( t , container . IsSuccessful ( ctx , client , containerID ) , poll . WithDelay ( 100 * time . Millisecond ) )
// Make sure a bind mount under a shared volume propagated to host.
if mounted , _ := mountinfo . Mounted ( tmpDir1Mnt ) ; ! mounted {
t . Fatalf ( "Bind mount under shared volume did not propagate to host" )
}
mount . Unmount ( tmpDir1Mnt )
}
func TestContainerVolumesMountedAsSlave ( t * testing . T ) {
// Volume propagation is linux only. Also it creates directories for
// bind mounting, so needs to be same host.
skip . If ( t , testEnv . IsRemoteDaemon )
skip . If ( t , testEnv . IsUserNamespace )
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)" )
// Prepare a source directory to bind mount
tmpDir1 := fs . NewDir ( t , "volume-source" , fs . WithMode ( 0755 ) ,
fs . WithDir ( "mnt1" , fs . WithMode ( 0755 ) ) )
defer tmpDir1 . Remove ( )
tmpDir1Mnt := filepath . Join ( tmpDir1 . Path ( ) , "mnt1" )
// Prepare a source directory with file in it. We will bind mount this
// directory and see if file shows up.
tmpDir2 := fs . NewDir ( t , "volume-source2" , fs . WithMode ( 0755 ) ,
fs . WithFile ( "slave-testfile" , "Test" , fs . WithMode ( 0644 ) ) )
defer tmpDir2 . Remove ( )
// Convert this directory into a shared mount point so that we do
// not rely on propagation properties of parent mount.
if err := mount . Mount ( tmpDir1 . Path ( ) , tmpDir1 . Path ( ) , "none" , "bind,private" ) ; err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1 . Path ( ) ) ; err != nil {
t . Fatal ( err )
}
} ( )
if err := mount . Mount ( "none" , tmpDir1 . Path ( ) , "none" , "shared" ) ; err != nil {
t . Fatal ( err )
}
slaveMount := mounttypes . Mount {
Type : mounttypes . TypeBind ,
Source : tmpDir1 . Path ( ) ,
Target : "/volume-dest" ,
BindOptions : & mounttypes . BindOptions {
Propagation : mounttypes . PropagationSlave ,
} ,
}
topCmd := [ ] string { "top" }
ctx := context . Background ( )
client := testEnv . APIClient ( )
containerID := container . Run ( ctx , t , client , container . WithTty ( true ) , container . WithMount ( slaveMount ) , container . WithCmd ( topCmd ... ) )
// Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside
// container then contents of tmpDir2/slave-testfile should become
// visible at "/volume-dest/mnt1/slave-testfile"
if err := mount . Mount ( tmpDir2 . Path ( ) , tmpDir1Mnt , "none" , "bind" ) ; err != nil {
t . Fatal ( err )
}
defer func ( ) {
if err := mount . Unmount ( tmpDir1Mnt ) ; err != nil {
t . Fatal ( err )
}
} ( )
mountCmd := [ ] string { "cat" , "/volume-dest/mnt1/slave-testfile" }
if result , err := container . Exec ( ctx , client , containerID , mountCmd ) ; err == nil {
if result . Stdout ( ) != "Test" {
t . Fatalf ( "Bind mount under slave volume did not propagate to container" )
}
} else {
t . Fatal ( err )
}
}