github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/integration/container/ipcmode_linux_test.go (about)

     1  package container // import "github.com/demonoid81/moby/integration/container"
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"io/ioutil"
     7  	"os"
     8  	"regexp"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/demonoid81/moby/api/types"
    13  	containertypes "github.com/demonoid81/moby/api/types/container"
    14  	"github.com/demonoid81/moby/api/types/versions"
    15  	"github.com/demonoid81/moby/client"
    16  	"github.com/demonoid81/moby/integration/internal/container"
    17  	"github.com/demonoid81/moby/testutil/daemon"
    18  	"github.com/demonoid81/moby/testutil/request"
    19  	"gotest.tools/v3/assert"
    20  	is "gotest.tools/v3/assert/cmp"
    21  	"gotest.tools/v3/fs"
    22  	"gotest.tools/v3/skip"
    23  )
    24  
    25  // testIpcCheckDevExists checks whether a given mount (identified by its
    26  // major:minor pair from /proc/self/mountinfo) exists on the host system.
    27  //
    28  // The format of /proc/self/mountinfo is like:
    29  //
    30  // 29 23 0:24 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw
    31  //       ^^^^\
    32  //            - this is the minor:major we look for
    33  func testIpcCheckDevExists(mm string) (bool, error) {
    34  	f, err := os.Open("/proc/self/mountinfo")
    35  	if err != nil {
    36  		return false, err
    37  	}
    38  	defer f.Close()
    39  
    40  	s := bufio.NewScanner(f)
    41  	for s.Scan() {
    42  		fields := strings.Fields(s.Text())
    43  		if len(fields) < 7 {
    44  			continue
    45  		}
    46  		if fields[2] == mm {
    47  			return true, nil
    48  		}
    49  	}
    50  
    51  	return false, s.Err()
    52  }
    53  
    54  // testIpcNonePrivateShareable is a helper function to test "none",
    55  // "private" and "shareable" modes.
    56  func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool, mustBeShared bool) {
    57  	defer setupTest(t)()
    58  
    59  	cfg := containertypes.Config{
    60  		Image: "busybox",
    61  		Cmd:   []string{"top"},
    62  	}
    63  	hostCfg := containertypes.HostConfig{
    64  		IpcMode: containertypes.IpcMode(mode),
    65  	}
    66  	client := testEnv.APIClient()
    67  	ctx := context.Background()
    68  
    69  	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
    70  	assert.NilError(t, err)
    71  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
    72  
    73  	err = client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
    74  	assert.NilError(t, err)
    75  
    76  	// get major:minor pair for /dev/shm from container's /proc/self/mountinfo
    77  	cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo"
    78  	result, err := container.Exec(ctx, client, resp.ID, []string{"sh", "-c", cmd})
    79  	assert.NilError(t, err)
    80  	mm := result.Combined()
    81  	if !mustBeMounted {
    82  		assert.Check(t, is.Equal(mm, ""))
    83  		// no more checks to perform
    84  		return
    85  	}
    86  	assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm)))
    87  
    88  	shared, err := testIpcCheckDevExists(mm)
    89  	assert.NilError(t, err)
    90  	t.Logf("[testIpcPrivateShareable] ipcmode: %v, ipcdev: %v, shared: %v, mustBeShared: %v\n", mode, mm, shared, mustBeShared)
    91  	assert.Check(t, is.Equal(shared, mustBeShared))
    92  }
    93  
    94  // TestIpcModeNone checks the container "none" IPC mode
    95  // (--ipc none) works as expected. It makes sure there is no
    96  // /dev/shm mount inside the container.
    97  func TestIpcModeNone(t *testing.T) {
    98  	skip.If(t, testEnv.IsRemoteDaemon)
    99  
   100  	testIpcNonePrivateShareable(t, "none", false, false)
   101  }
   102  
   103  // TestAPIIpcModePrivate checks the container private IPC mode
   104  // (--ipc private) works as expected. It gets the minor:major pair
   105  // of /dev/shm mount from the container, and makes sure there is no
   106  // such pair on the host.
   107  func TestIpcModePrivate(t *testing.T) {
   108  	skip.If(t, testEnv.IsRemoteDaemon)
   109  
   110  	testIpcNonePrivateShareable(t, "private", true, false)
   111  }
   112  
   113  // TestAPIIpcModeShareable checks the container shareable IPC mode
   114  // (--ipc shareable) works as expected. It gets the minor:major pair
   115  // of /dev/shm mount from the container, and makes sure such pair
   116  // also exists on the host.
   117  func TestIpcModeShareable(t *testing.T) {
   118  	skip.If(t, testEnv.IsRemoteDaemon)
   119  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   120  
   121  	testIpcNonePrivateShareable(t, "shareable", true, true)
   122  }
   123  
   124  // testIpcContainer is a helper function to test --ipc container:NNN mode in various scenarios
   125  func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
   126  	t.Helper()
   127  
   128  	defer setupTest(t)()
   129  
   130  	cfg := containertypes.Config{
   131  		Image: "busybox",
   132  		Cmd:   []string{"top"},
   133  	}
   134  	hostCfg := containertypes.HostConfig{
   135  		IpcMode: containertypes.IpcMode(donorMode),
   136  	}
   137  	ctx := context.Background()
   138  	client := testEnv.APIClient()
   139  
   140  	// create and start the "donor" container
   141  	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
   142  	assert.NilError(t, err)
   143  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   144  	name1 := resp.ID
   145  
   146  	err = client.ContainerStart(ctx, name1, types.ContainerStartOptions{})
   147  	assert.NilError(t, err)
   148  
   149  	// create and start the second container
   150  	hostCfg.IpcMode = containertypes.IpcMode("container:" + name1)
   151  	resp, err = client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
   152  	assert.NilError(t, err)
   153  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   154  	name2 := resp.ID
   155  
   156  	err = client.ContainerStart(ctx, name2, types.ContainerStartOptions{})
   157  	if !mustWork {
   158  		// start should fail with a specific error
   159  		assert.Check(t, is.ErrorContains(err, "non-shareable IPC"))
   160  		// no more checks to perform here
   161  		return
   162  	}
   163  
   164  	// start should succeed
   165  	assert.NilError(t, err)
   166  
   167  	// check that IPC is shared
   168  	// 1. create a file in the first container
   169  	_, err = container.Exec(ctx, client, name1, []string{"sh", "-c", "printf covfefe > /dev/shm/bar"})
   170  	assert.NilError(t, err)
   171  	// 2. check it's the same file in the second one
   172  	result, err := container.Exec(ctx, client, name2, []string{"cat", "/dev/shm/bar"})
   173  	assert.NilError(t, err)
   174  	out := result.Combined()
   175  	assert.Check(t, is.Equal(true, regexp.MustCompile("^covfefe$").MatchString(out)))
   176  }
   177  
   178  // TestAPIIpcModeShareableAndPrivate checks that
   179  // 1) a container created with --ipc container:ID can use IPC of another shareable container.
   180  // 2) a container created with --ipc container:ID can NOT use IPC of another private container.
   181  func TestAPIIpcModeShareableAndContainer(t *testing.T) {
   182  	skip.If(t, testEnv.IsRemoteDaemon)
   183  
   184  	testIpcContainer(t, "shareable", true)
   185  
   186  	testIpcContainer(t, "private", false)
   187  }
   188  
   189  /* TestAPIIpcModeHost checks that a container created with --ipc host
   190   * can use IPC of the host system.
   191   */
   192  func TestAPIIpcModeHost(t *testing.T) {
   193  	skip.If(t, testEnv.IsRemoteDaemon)
   194  	skip.If(t, testEnv.IsUserNamespace)
   195  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   196  
   197  	cfg := containertypes.Config{
   198  		Image: "busybox",
   199  		Cmd:   []string{"top"},
   200  	}
   201  	hostCfg := containertypes.HostConfig{
   202  		IpcMode: containertypes.IpcMode("host"),
   203  	}
   204  	ctx := context.Background()
   205  
   206  	client := testEnv.APIClient()
   207  	resp, err := client.ContainerCreate(ctx, &cfg, &hostCfg, nil, "")
   208  	assert.NilError(t, err)
   209  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   210  	name := resp.ID
   211  
   212  	err = client.ContainerStart(ctx, name, types.ContainerStartOptions{})
   213  	assert.NilError(t, err)
   214  
   215  	// check that IPC is shared
   216  	// 1. create a file inside container
   217  	_, err = container.Exec(ctx, client, name, []string{"sh", "-c", "printf covfefe > /dev/shm/." + name})
   218  	assert.NilError(t, err)
   219  	// 2. check it's the same on the host
   220  	bytes, err := ioutil.ReadFile("/dev/shm/." + name)
   221  	assert.NilError(t, err)
   222  	assert.Check(t, is.Equal("covfefe", string(bytes)))
   223  	// 3. clean up
   224  	_, err = container.Exec(ctx, client, name, []string{"rm", "-f", "/dev/shm/." + name})
   225  	assert.NilError(t, err)
   226  }
   227  
   228  // testDaemonIpcPrivateShareable is a helper function to test "private" and "shareable" daemon default ipc modes.
   229  func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...string) {
   230  	defer setupTest(t)()
   231  
   232  	d := daemon.New(t)
   233  	d.StartWithBusybox(t, arg...)
   234  	defer d.Stop(t)
   235  
   236  	c := d.NewClientT(t)
   237  
   238  	cfg := containertypes.Config{
   239  		Image: "busybox",
   240  		Cmd:   []string{"top"},
   241  	}
   242  	ctx := context.Background()
   243  
   244  	resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, "")
   245  	assert.NilError(t, err)
   246  	assert.Check(t, is.Equal(len(resp.Warnings), 0))
   247  
   248  	err = c.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{})
   249  	assert.NilError(t, err)
   250  
   251  	// get major:minor pair for /dev/shm from container's /proc/self/mountinfo
   252  	cmd := "awk '($5 == \"/dev/shm\") {printf $3}' /proc/self/mountinfo"
   253  	result, err := container.Exec(ctx, c, resp.ID, []string{"sh", "-c", cmd})
   254  	assert.NilError(t, err)
   255  	mm := result.Combined()
   256  	assert.Check(t, is.Equal(true, regexp.MustCompile("^[0-9]+:[0-9]+$").MatchString(mm)))
   257  
   258  	shared, err := testIpcCheckDevExists(mm)
   259  	assert.NilError(t, err)
   260  	t.Logf("[testDaemonIpcPrivateShareable] ipcdev: %v, shared: %v, mustBeShared: %v\n", mm, shared, mustBeShared)
   261  	assert.Check(t, is.Equal(shared, mustBeShared))
   262  }
   263  
   264  // TestDaemonIpcModeShareable checks that --default-ipc-mode shareable works as intended.
   265  func TestDaemonIpcModeShareable(t *testing.T) {
   266  	skip.If(t, testEnv.IsRemoteDaemon)
   267  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   268  
   269  	testDaemonIpcPrivateShareable(t, true, "--default-ipc-mode", "shareable")
   270  }
   271  
   272  // TestDaemonIpcModePrivate checks that --default-ipc-mode private works as intended.
   273  func TestDaemonIpcModePrivate(t *testing.T) {
   274  	skip.If(t, testEnv.IsRemoteDaemon)
   275  
   276  	testDaemonIpcPrivateShareable(t, false, "--default-ipc-mode", "private")
   277  }
   278  
   279  // used to check if an IpcMode given in config works as intended
   280  func testDaemonIpcFromConfig(t *testing.T, mode string, mustExist bool) {
   281  	skip.If(t, testEnv.IsRootless, "cannot test /dev/shm in rootless")
   282  	config := `{"default-ipc-mode": "` + mode + `"}`
   283  	file := fs.NewFile(t, "test-daemon-ipc-config", fs.WithContent(config))
   284  	defer file.Remove()
   285  
   286  	testDaemonIpcPrivateShareable(t, mustExist, "--config-file", file.Path())
   287  }
   288  
   289  // TestDaemonIpcModePrivateFromConfig checks that "default-ipc-mode: private" config works as intended.
   290  func TestDaemonIpcModePrivateFromConfig(t *testing.T) {
   291  	skip.If(t, testEnv.IsRemoteDaemon)
   292  
   293  	testDaemonIpcFromConfig(t, "private", false)
   294  }
   295  
   296  // TestDaemonIpcModeShareableFromConfig checks that "default-ipc-mode: shareable" config works as intended.
   297  func TestDaemonIpcModeShareableFromConfig(t *testing.T) {
   298  	skip.If(t, testEnv.IsRemoteDaemon)
   299  
   300  	testDaemonIpcFromConfig(t, "shareable", true)
   301  }
   302  
   303  // TestIpcModeOlderClient checks that older client gets shareable IPC mode
   304  // by default, even when the daemon default is private.
   305  func TestIpcModeOlderClient(t *testing.T) {
   306  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "requires a daemon with DefaultIpcMode: private")
   307  	c := testEnv.APIClient()
   308  	skip.If(t, versions.LessThan(c.ClientVersion(), "1.40"), "requires client API >= 1.40")
   309  
   310  	t.Parallel()
   311  
   312  	ctx := context.Background()
   313  
   314  	// pre-check: default ipc mode in daemon is private
   315  	cID := container.Create(ctx, t, c, container.WithAutoRemove)
   316  
   317  	inspect, err := c.ContainerInspect(ctx, cID)
   318  	assert.NilError(t, err)
   319  	assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "private"))
   320  
   321  	// main check: using older client creates "shareable" container
   322  	c = request.NewAPIClient(t, client.WithVersion("1.39"))
   323  	cID = container.Create(ctx, t, c, container.WithAutoRemove)
   324  
   325  	inspect, err = c.ContainerInspect(ctx, cID)
   326  	assert.NilError(t, err)
   327  	assert.Check(t, is.Equal(string(inspect.HostConfig.IpcMode), "shareable"))
   328  }