Repository: moby/moby
File: integration/container/exec_test.go (Go)

Code before:

		err := apiClient.ContainerKill(ctx, cID, "SIGKILL")
		assert.NilError(t, err)

		err = apiClient.ContainerExecResize(ctx, execID, containertypes.ResizeOptions{
			Height: 40,
			Width:  40,
		})
		assert.Check(t, is.ErrorType(err, errdefs.IsConflict))
		assert.Check(t, is.ErrorContains(err, "is not running"))
	})
}

func TestExecUser(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME. Probably needs to wait for container to be in running state.")
	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	cID := container.Run(ctx, t, apiClient, container.WithTty(true), container.WithUser("1:1"))

	result, err := container.Exec(ctx, apiClient, cID, []string{"id"})
	assert.NilError(t, err)

	assert.Check(t, is.Contains(result.Stdout(), "uid=1(daemon) gid=1(daemon)"), "exec command not running as uid/gid 1")
}

// Test that additional groups set with `--group-add` are kept on exec when the container
// also has a user set.
// (regression test for https://github.com/moby/moby/issues/46712)
func TestExecWithGroupAdd(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME. Probably needs to wait for container to be in running state.")

	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	cID := container.Run(ctx, t, apiClient, container.WithTty(true), container.WithUser("root:root"), container.WithAdditionalGroups("staff", "wheel", "audio", "777"), container.WithCmd("sleep", "5"))

	result, err := container.Exec(ctx, apiClient, cID, []string{"id"})
	assert.NilError(t, err)

	const expected = "uid=0(root) gid=0(root) groups=0(root),10(wheel),29(audio),50(staff),777"
	assert.Check(t, is.Equal(strings.TrimSpace(result.Stdout()), expected), "exec command not keeping additional groups w/ user")
}


Review comment (bug):
I should point out that I had some fun with this part; if I tried to make these tests run without building an image (so running a container with `sh -c 'rm /etc/passwd && top'` as command), the test didn't fail as expected. I tried adding a `sync` in between to see if it was possibly that changes weren't written to disk, but that didn't work either. So ... something odd.

Trying to run the same scenario manually didn't reproduce that issue.

```bash
cid=$(docker run -dit --name foo busybox sh -c 'rm /etc/passwd && top') \
 && docker exec -it -u root:root $cid echo hello

# Error response from daemon: unable to find user root: no matching entries in passwd file
```

So either it's something in how the tests runs, or there's some actual race somewhere. I decided not to continue looking into that, because failing to detect the problem would still cause the actual exec to fail (the new check is just an _extra_ check), but perhaps something to try to find what's happening.


Code after:
		//          Error response from daemon: No such exec instance: cc728a332d3f594249fb7ee9adb3bb12a59a5d1776f8f6dedc56355364361711
		skip.If(t, testEnv.DaemonInfo.OSType == "windows" && !testEnv.RuntimeIsWindowsContainerd(), "FIXME. Windows + builtin returns a NotFound instead of a Conflict error")

		err := apiClient.ContainerKill(ctx, cID, "SIGKILL")
		assert.NilError(t, err)

		err = apiClient.ContainerExecResize(ctx, execID, containertypes.ResizeOptions{
			Height: 40,
			Width:  40,
		})
		assert.Check(t, is.ErrorType(err, errdefs.IsConflict))
		assert.Check(t, is.ErrorContains(err, "is not running"))
	})
}

func TestExecUser(t *testing.T) {
	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME. Probably needs to wait for container to be in running state.")
	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	ctrOpts := []func(*container.TestContainerConfig){
		container.WithTty(true),
		container.WithUser("1:1"),
	}
	withoutEtcGroups := container.WithImage(build.Do(ctx, t, apiClient, fakecontext.New(t, "", fakecontext.WithDockerfile("FROM busybox\nRUN rm /etc/group"))))
	withoutEtcPasswd := container.WithImage(build.Do(ctx, t, apiClient, fakecontext.New(t, "", fakecontext.WithDockerfile("FROM busybox\nRUN rm /etc/passwd"))))

	withUser := func(user string) func(options *containertypes.ExecOptions) {
		return func(options *containertypes.ExecOptions) { options.User = user }
	}

	tests := []struct {
		doc         string
		user        string
		ctrOpts     []func(*container.TestContainerConfig)
		expectedErr string
		expectedOut string
	}{
		{
			doc:         "default user",
			expectedOut: "uid=1(daemon) gid=1(daemon)",
		},
		{
			doc:         "uid",
			user:        "0",
			expectedOut: "uid=0(root) gid=0(root) groups=0(root)",
		},
		{
			doc:         "uid gid",
			user:        "0:0",
			expectedOut: "uid=0(root) gid=0(root) groups=0(root)",