Skip to content

Commit 24f2bd7

Browse files
authored
Merge pull request #3941 from Shubhranshu153/feat-nerdctl-userns
feat: add support for userns
2 parents 05278e2 + 38d67f0 commit 24f2bd7

22 files changed

+1509
-12
lines changed

.github/workflows/job-test-in-container.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ jobs:
153153
# Besides, each job is running on a different instance, which means using host network here
154154
# is safe and has no side effects on others.
155155
[ "${{ inputs.target }}" == "rootful" ] \
156-
&& args=(test-integration ./hack/test-integration.sh) \
156+
&& args=(test-integration ./hack/test-integration.sh -test.allow-modify-users=true) \
157157
|| args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh)
158158
if [ "${{ inputs.ipv6 }}" == true ]; then
159159
docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${{ inputs.binary }}

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ linters:
114114
arguments: [7]
115115
- name: function-length
116116
# 155 occurrences (at default 0, 75). Really long functions should really be broken up in most cases.
117-
arguments: [0, 400]
117+
arguments: [0, 450]
118118
- name: cyclomatic
119119
# 204 occurrences (at default 10)
120120
arguments: [100]

cmd/nerdctl/builder/builder_build.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525

2626
"github.com/spf13/cobra"
2727

28+
"github.com/containerd/log"
29+
2830
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
2931
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
3032
"github.com/containerd/nerdctl/v2/pkg/api/types"
@@ -209,6 +211,13 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
209211
return types.BuilderBuildOptions{}, err
210212
}
211213

214+
usernsRemap, err := cmd.Flags().GetString("userns-remap")
215+
if err != nil {
216+
return types.BuilderBuildOptions{}, err
217+
} else if usernsRemap != "" {
218+
log.L.Warn("userns remap is not supported with nerdctl build. dropping the config.")
219+
}
220+
212221
return types.BuilderBuildOptions{
213222
GOptions: globalOptions,
214223
BuildKitHost: buildKitHost,

cmd/nerdctl/container/container_create.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,30 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) {
464464
}
465465
// #endregion
466466

467+
// #region for UserNS
468+
opt.UserNS, err = cmd.Flags().GetString("userns-remap")
469+
if err != nil {
470+
return opt, err
471+
}
472+
473+
userns, err := cmd.Flags().GetString("userns")
474+
if err != nil {
475+
return opt, err
476+
}
477+
478+
if userns == "host" {
479+
opt.UserNS = ""
480+
} else if userns != "" {
481+
return opt, fmt.Errorf("invalid user mode")
482+
}
483+
484+
if opt.Privileged && opt.UserNS != "" {
485+
//userns-remap is not supported with privileged flag.
486+
// Ref: https://docs.docker.com/engine/security/userns-remap/
487+
return opt, fmt.Errorf("privileged flag cannot be used with userns-remap")
488+
}
489+
// #endregion
490+
467491
return opt, nil
468492
}
469493

cmd/nerdctl/container/container_create_linux_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ package container
1919
import (
2020
"errors"
2121
"fmt"
22+
"io"
2223
"os"
2324
"path/filepath"
25+
"strconv"
2426
"strings"
27+
"syscall"
2528
"testing"
2629

2730
"github.com/opencontainers/go-digest"
2831
"gotest.tools/v3/assert"
2932

3033
"github.com/containerd/containerd/v2/defaults"
34+
"github.com/containerd/nerdctl/mod/tigron/expect"
3135
"github.com/containerd/nerdctl/mod/tigron/require"
3236
"github.com/containerd/nerdctl/mod/tigron/test"
3337

@@ -325,3 +329,184 @@ func TestCreateFromOCIArchive(t *testing.T) {
325329
base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK()
326330
base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive")
327331
}
332+
333+
func TestUsernsMappingCreateCmd(t *testing.T) {
334+
nerdtest.Setup()
335+
336+
testCase := &test.Case{
337+
Require: require.All(
338+
nerdtest.AllowModifyUserns,
339+
nerdtest.RemapIDs,
340+
require.Not(nerdtest.Docker)),
341+
NoParallel: true,
342+
Setup: func(data test.Data, helpers test.Helpers) {
343+
data.Labels().Set("validUserns", "nerdctltestuser")
344+
data.Labels().Set("expectedHostUID", "123456789")
345+
data.Labels().Set("invalidUserns", "invaliduser")
346+
},
347+
SubTests: []*test.Case{
348+
{
349+
Description: "Test container create with valid Userns",
350+
NoParallel: true, // Changes system config so running in non parallel mode
351+
Setup: func(data test.Data, helpers test.Helpers) {
352+
err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers)
353+
assert.NilError(t, err, "Failed to append Userns config")
354+
},
355+
Cleanup: func(data test.Data, helpers test.Helpers) {
356+
removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers)
357+
helpers.Anyhow("rm", "-f", data.Identifier())
358+
},
359+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
360+
helpers.Ensure("create", "--tty", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage)
361+
return helpers.Command("start", data.Identifier())
362+
},
363+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
364+
return &test.Expected{
365+
ExitCode: 0,
366+
Output: func(stdout string, info string, t *testing.T) {
367+
actualHostUID, err := getContainerHostUID(helpers, data.Identifier())
368+
assert.NilError(t, err, "Failed to get container host UID")
369+
assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info)
370+
},
371+
}
372+
},
373+
},
374+
{
375+
Description: "Test container create failure with valid Userns and privileged flag",
376+
NoParallel: true, // Changes system config so running in non parallel mode
377+
Setup: func(data test.Data, helpers test.Helpers) {
378+
err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers)
379+
assert.NilError(t, err, "Failed to append Userns config")
380+
},
381+
Cleanup: func(data test.Data, helpers test.Helpers) {
382+
removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers)
383+
},
384+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
385+
return helpers.Command("create", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage)
386+
},
387+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
388+
return &test.Expected{
389+
ExitCode: 1,
390+
}
391+
},
392+
},
393+
{
394+
Description: "Test container create with invalid Userns",
395+
NoParallel: true, // Changes system config so running in non parallel mode
396+
Cleanup: func(data test.Data, helpers test.Helpers) {
397+
helpers.Anyhow("rm", "-f", data.Identifier())
398+
},
399+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
400+
return helpers.Command("create", "--tty", "--userns-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage)
401+
},
402+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
403+
return &test.Expected{
404+
ExitCode: 1,
405+
}
406+
},
407+
},
408+
},
409+
}
410+
testCase.Run(t)
411+
}
412+
413+
func getContainerHostUID(helpers test.Helpers, containerName string) (string, error) {
414+
result := helpers.Capture("inspect", "--format", "{{.State.Pid}}", containerName)
415+
pidStr := strings.TrimSpace(result)
416+
pid, err := strconv.Atoi(pidStr)
417+
if err != nil {
418+
return "", fmt.Errorf("invalid PID: %v", err)
419+
}
420+
421+
stat, err := os.Stat(fmt.Sprintf("/proc/%d", pid))
422+
if err != nil {
423+
return "", fmt.Errorf("failed to stat process: %v", err)
424+
}
425+
426+
uid := int(stat.Sys().(*syscall.Stat_t).Uid)
427+
return strconv.Itoa(uid), nil
428+
}
429+
430+
func appendUsernsConfig(userns string, hostUID string, helpers test.Helpers) error {
431+
addUser(userns, hostUID, helpers)
432+
entry := fmt.Sprintf("%s:%s:65536\n", userns, hostUID)
433+
tempDir := helpers.T().TempDir()
434+
files := []string{"subuid", "subgid"}
435+
for _, file := range files {
436+
437+
fileBak := filepath.Join(tempDir, file)
438+
defer os.Remove(fileBak)
439+
d, err := os.Create(fileBak)
440+
if err != nil {
441+
return fmt.Errorf("failed to create %s: %w", fileBak, err)
442+
}
443+
444+
s, err := os.Open(filepath.Join("/etc", file))
445+
if err != nil {
446+
return fmt.Errorf("failed to open %s: %w", file, err)
447+
}
448+
defer s.Close()
449+
450+
_, err = io.Copy(d, s)
451+
if err != nil {
452+
return fmt.Errorf("failed to copy %s to %s: %w", file, fileBak, err)
453+
}
454+
455+
f, err := os.OpenFile(fmt.Sprintf("/etc/%s", file), os.O_APPEND|os.O_WRONLY, 0644)
456+
if err != nil {
457+
return fmt.Errorf("failed to open %s: %w", file, err)
458+
}
459+
defer f.Close()
460+
461+
if _, err := f.WriteString(entry); err != nil {
462+
return fmt.Errorf("failed to write to %s: %w", file, err)
463+
}
464+
}
465+
return nil
466+
}
467+
468+
func addUser(username string, hostID string, helpers test.Helpers) {
469+
helpers.Custom("groupadd", "-g", hostID, username).Run(&test.Expected{
470+
ExitCode: 0})
471+
helpers.Custom("useradd", "-u", hostID, "-g", hostID, "-s", "/bin/false", username).Run(&test.Expected{
472+
ExitCode: 0})
473+
}
474+
475+
func removeUsernsConfig(t *testing.T, userns string, helpers test.Helpers) {
476+
delUser(userns, helpers)
477+
delGroup(userns, helpers)
478+
tempDir := helpers.T().TempDir()
479+
files := []string{"subuid", "subgid"}
480+
for _, file := range files {
481+
fileBak := filepath.Join(tempDir, file)
482+
s, err := os.Open(fileBak)
483+
if err != nil {
484+
t.Logf("failed to open %s, Error: %s", fileBak, err)
485+
continue
486+
}
487+
defer s.Close()
488+
489+
d, err := os.Open(filepath.Join("/etc/%s", file))
490+
if err != nil {
491+
t.Logf("failed to open %s, Error: %s", file, err)
492+
continue
493+
494+
}
495+
defer d.Close()
496+
497+
_, err = io.Copy(d, s)
498+
if err != nil {
499+
t.Logf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err)
500+
continue
501+
}
502+
503+
}
504+
}
505+
506+
func delUser(username string, helpers test.Helpers) {
507+
helpers.Custom("userdel", username).Run(&test.Expected{ExitCode: expect.ExitCodeNoCheck})
508+
}
509+
510+
func delGroup(groupname string, helpers test.Helpers) {
511+
helpers.Custom("groupdel", groupname).Run(&test.Expected{ExitCode: expect.ExitCodeNoCheck})
512+
}

cmd/nerdctl/container/container_run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,14 +288,14 @@ func setCreateFlags(cmd *cobra.Command) {
288288
// #endregion
289289

290290
cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)")
291-
292291
cmd.Flags().String("isolation", "default", "Specify isolation technology for container. On Linux the only valid value is default. Windows options are host, process and hyperv with process isolation as the default")
293292
cmd.RegisterFlagCompletionFunc("isolation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
294293
if runtime.GOOS == "windows" {
295294
return []string{"default", "host", "process", "hyperv"}, cobra.ShellCompDirectiveNoFileComp
296295
}
297296
return []string{"default"}, cobra.ShellCompDirectiveNoFileComp
298297
})
298+
cmd.Flags().String("userns", "", "Specify host to disable userns-remap")
299299

300300
}
301301

0 commit comments

Comments
 (0)