From e094328071ffe0fae834e9baef95282462796b7b Mon Sep 17 00:00:00 2001 From: Shubharanshu Mahapatra Date: Thu, 13 Feb 2025 16:18:49 +0530 Subject: [PATCH] feat: add support for userns Signed-off-by: Shubharanshu Mahapatra --- cmd/nerdctl/container/container_create.go | 7 + .../container/container_create_linux_test.go | 161 +++++++ cmd/nerdctl/container/container_run.go | 6 + .../container_run_user_linux_test.go | 89 ++++ go.mod | 31 +- go.sum | 85 ++-- pkg/api/types/container_types.go | 3 + pkg/cmd/container/create.go | 22 +- pkg/cmd/container/create_userns_opts_linux.go | 409 ++++++++++++++++++ .../create_userns_opts_linux_test.go | 154 +++++++ pkg/testutil/nerdtest/requirements.go | 22 + 11 files changed, 912 insertions(+), 77 deletions(-) create mode 100644 pkg/cmd/container/create_userns_opts_linux.go create mode 100644 pkg/cmd/container/create_userns_opts_linux_test.go diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index 97d1b2f8a4b..bdffdbfa648 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -423,6 +423,13 @@ func processContainerCreateOptions(cmd *cobra.Command) (types.ContainerCreateOpt } // #endregion + // #region for userns + opt.Userns, err = cmd.Flags().GetString("userns") + if err != nil { + return opt, err + } + // #endregion + return opt, nil } diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index a4b3bc64fa2..545a23f39e8 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -20,12 +20,16 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" + "strconv" "strings" + "syscall" "testing" "github.com/opencontainers/go-digest" "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" "github.com/containerd/containerd/v2/defaults" @@ -324,3 +328,160 @@ func TestCreateFromOCIArchive(t *testing.T) { base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK() base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive") } + +func TestUsernsMappingCreateCmd(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: test.Not(nerdtest.ContainerdV1), + SubTests: []*test.Case{ + { + Description: "Test container start with valid userns", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + data.Set("validUserns", "nerdctltestuser") + data.Set("expectedHostUID", "123456789") + // need to be compiled with containerd version >2.0.2 to support multi uidmap and gidmap. + if err := appendUsernsConfig(data.Get("validUserns"), data.Get("expectedHostUID")); err != nil { + t.Fatalf("Failed to append userns config: %v", err) + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(t, data.Get("validUserns"), data.Get("expectedHostUID")) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("create", "--tty", "--userns", data.Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + return helpers.Command("start", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == data.Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container start with invalid userns", + Setup: func(data test.Data, helpers test.Helpers) { + data.Set("invalidUserns", "invaliduser") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", "--tty", "--userns", data.Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + }, + } + testCase.Run(t) +} + +func runUsernsContainer(t *testing.T, name, userns, image, cmd string) *icmd.Result { + base := testutil.NewBase(t) + removeContainerArgs := []string{ + "rm", "-f", name, + } + base.Cmd(removeContainerArgs...).Run() + + args := []string{ + "run", "-d", "--userns", userns, "--name", name, image, "sh", "-c", cmd, + } + return base.Cmd(args...).Run() +} + +func getContainerHostUID(helpers test.Helpers, containerName string) (string, error) { + result := helpers.Capture("inspect", "--format", "{{.State.Pid}}", containerName) + pidStr := strings.TrimSpace(result) + pid, err := strconv.Atoi(pidStr) + if err != nil { + return "", fmt.Errorf("invalid PID: %v", err) + } + + stat, err := os.Stat(fmt.Sprintf("/proc/%d", pid)) + if err != nil { + return "", fmt.Errorf("failed to stat process: %v", err) + } + + uid := int(stat.Sys().(*syscall.Stat_t).Uid) + return strconv.Itoa(uid), nil +} + +func appendUsernsConfig(userns string, hostUid string) error { + if err := addUser(userns, hostUid); err != nil { + return fmt.Errorf("failed to add user %s: %w", userns, err) + } + + entry := fmt.Sprintf("%s:%s:65536\n", userns, hostUid) + + files := []string{"/etc/subuid", "/etc/subgid"} + for _, file := range files { + f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open %s: %w", file, err) + } + defer f.Close() + + if _, err := f.WriteString(entry); err != nil { + return fmt.Errorf("failed to write to %s: %w", file, err) + } + } + return nil +} + +func addUser(username string, hostId string) error { + cmd := exec.Command("groupadd", "-g", hostId, username) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("groupadd failed: %s, %w", string(output), err) + } + cmd = exec.Command("useradd", "-u", hostId, "-g", hostId, "-s", "/bin/false", username) + output, err = cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("useradd failed: %s, %w", string(output), err) + } + return nil +} + +func removeUsernsConfig(t *testing.T, userns string, hostUid string) { + + if err := delUser(userns); err != nil { + t.Logf("failed to del user %s", userns) + } + + entry := fmt.Sprintf("%s:%s:65536\n", userns, hostUid) + + files := []string{"/etc/subuid", "/etc/subgid"} + for _, file := range files { + content, err := os.ReadFile(file) + if err != nil { + t.Logf("Failed to read %s: %v", file, err) + continue + } + + newContent := strings.ReplaceAll(string(content), entry, "") + if err := os.WriteFile(file, []byte(newContent), 0644); err != nil { + t.Logf("Failed to write to %s: %v", file, err) + } + } +} + +func delUser(username string) error { + cmd := exec.Command("sudo", "userdel", username) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("userdel failed: %s, %w", string(output), err) + } + return nil +} diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 2053958fe00..7860c4d6767 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -279,6 +279,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") 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") + cmd.Flags().String("userns", "", "Support idmapping of containers. This options is only supported on linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively") cmd.RegisterFlagCompletionFunc("isolation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if runtime.GOOS == "windows" { return []string{"default", "host", "process", "hyperv"}, cobra.ShellCompDirectiveNoFileComp @@ -317,6 +318,11 @@ func processCreateCommandFlagsInRun(cmd *cobra.Command) (types.ContainerCreateOp return opt, err } + opt.Userns, err = cmd.Flags().GetString("userns") + if err != nil { + return opt, err + } + validAttachFlag := true for i, str := range opt.Attach { opt.Attach[i] = strings.ToUpper(str) diff --git a/cmd/nerdctl/container/container_run_user_linux_test.go b/cmd/nerdctl/container/container_run_user_linux_test.go index 73d1753a867..1506258500f 100644 --- a/cmd/nerdctl/container/container_run_user_linux_test.go +++ b/cmd/nerdctl/container/container_run_user_linux_test.go @@ -17,9 +17,13 @@ package container import ( + "fmt" "testing" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/test" + "gotest.tools/v3/assert" ) func TestRunUserGID(t *testing.T) { @@ -181,3 +185,88 @@ func TestRunAddGroup_CVE_2023_25173(t *testing.T) { base.Cmd(cmd...).AssertOutContains(testCase.expected + "\n") } } + +func TestUsernsMappingRunCmd(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: test.Require(test.Not(nerdtest.ContainerdV1), + test.Not(nerdtest.Docker)), + SubTests: []*test.Case{ + { + Description: "Test container start with valid userns", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + data.Set("validUserns", "nerdctltestuser") + data.Set("expectedHostUID", "123456789") + if err := appendUsernsConfig(data.Get("validUserns"), data.Get("expectedHostUID")); err != nil { + t.Fatalf("Failed to append userns config: %v", err) + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Get("validUserns"), data.Get("expectedHostUID")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns", data.Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == data.Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container network share with valid userns", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + data.Set("validUserns", "nerdctltestuser") + data.Set("expectedHostUID", "123456789") + data.Set("net-container", "net-container") + if err := appendUsernsConfig(data.Get("validUserns"), data.Get("expectedHostUID")); err != nil { + t.Fatalf("Failed to append userns config: %v", err) + } + + helpers.Ensure("run", "--tty", "-d", "--userns", data.Get("validUserns"), "--name", data.Get("net-container"), testutil.NginxAlpineImage) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Get("net-container")) + removeUsernsConfig(t, data.Get("validUserns"), data.Get("expectedHostUID")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns", data.Get("validUserns"), "--net", fmt.Sprintf("container:%s", data.Get("net-container")), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + } + }, + }, + { + Description: "Test container start with invalid userns", + Setup: func(data test.Data, helpers test.Helpers) { + data.Set("invalidUserns", "invaliduser") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns", data.Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + }, + } + testCase.Run(t) +} diff --git a/go.mod b/go.mod index 23662927753..600300503b2 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,13 @@ go 1.22.7 require ( github.com/Masterminds/semver/v3 v3.3.1 github.com/Microsoft/go-winio v0.6.2 - github.com/Microsoft/hcsshim v0.12.9 - github.com/compose-spec/compose-go/v2 v2.4.8 - github.com/containerd/accelerated-container-image v1.3.0 + github.com/Microsoft/hcsshim v0.13.0-rc.3 + github.com/compose-spec/compose-go/v2 v2.4.7 + github.com/containerd/accelerated-container-image v1.2.3 github.com/containerd/cgroups/v3 v3.0.5 github.com/containerd/console v1.0.4 github.com/containerd/containerd/api v1.8.0 - github.com/containerd/containerd/v2 v2.0.2 + github.com/containerd/containerd/v2 v2.0.1-0.20250211161307-525332b29211 github.com/containerd/continuity v0.4.5 github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 @@ -50,6 +50,7 @@ require ( github.com/moby/sys/mount v0.3.4 github.com/moby/sys/mountinfo v0.7.2 github.com/moby/sys/signal v0.7.1 + github.com/moby/sys/user v0.3.0 github.com/moby/sys/userns v0.1.0 github.com/moby/term v0.5.2 github.com/muesli/cancelreader v0.2.2 @@ -76,8 +77,6 @@ require ( ) require ( - github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect - github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect github.com/cilium/ebpf v0.16.0 // indirect @@ -108,7 +107,6 @@ require ( github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/symlink v0.3.0 // indirect - github.com/moby/sys/user v0.3.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect @@ -116,7 +114,6 @@ require ( github.com/multiformats/go-multibase v0.2.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect github.com/opencontainers/selinux v1.11.1 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect @@ -126,24 +123,20 @@ require ( github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect - github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tinylib/msgp v1.2.0 // indirect github.com/vbatts/tar-split v0.11.6 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.31.0 // indirect - go.opentelemetry.io/otel/metric v1.31.0 // indirect - go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/mod v0.22.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.2 // indirect + google.golang.org/protobuf v1.36.3 // indirect lukechampine.com/blake3 v1.3.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect - tags.cncf.io/container-device-interface v0.8.0 // indirect - tags.cncf.io/container-device-interface/specs-go v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 298c8f0bfac..ea1e1f6a170 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 h1:dIScnXFlF784X79oi7MzVT6GWqr/W1uUt0pB5CsDs9M= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -12,10 +10,8 @@ github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7r github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= -github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= -github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/Microsoft/hcsshim v0.13.0-rc.3 h1:c2Glm+kfftlSccp+rNIJ6mp1UppJYTq7q9SObIu3GZs= +github.com/Microsoft/hcsshim v0.13.0-rc.3/go.mod h1:rc/I5c+x7rZHik6V5qj31JTATiLKh2BV7CsZpbNlt88= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -23,18 +19,18 @@ github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go/v2 v2.4.8 h1:7Myl8wDRl/4mRz77S+eyDJymGGEHu0diQdGSSeyq90A= -github.com/compose-spec/compose-go/v2 v2.4.8/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= -github.com/containerd/accelerated-container-image v1.3.0 h1:sFbTgSuMboeKHa9f7MY11hWF1XxVWjFoiTsXYtOtvdU= -github.com/containerd/accelerated-container-image v1.3.0/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= +github.com/compose-spec/compose-go/v2 v2.4.7 h1:WNpz5bIbKG+G+w9pfu72B1ZXr+Og9jez8TMEo8ecXPk= +github.com/compose-spec/compose-go/v2 v2.4.7/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= +github.com/containerd/accelerated-container-image v1.2.3 h1:tAIoP7Z7b2xGhb7QCM5Fa+2xqWfPqRmyi5lodbsGGRA= +github.com/containerd/accelerated-container-image v1.2.3/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= -github.com/containerd/containerd/v2 v2.0.2 h1:GmH/tRBlTvrXOLwSpWE2vNAm8+MqI6nmxKpKBNKY8Wc= -github.com/containerd/containerd/v2 v2.0.2/go.mod h1:wIqEvQ/6cyPFUGJ5yMFanspPabMLor+bF865OHvNTTI= +github.com/containerd/containerd/v2 v2.0.1-0.20250211161307-525332b29211 h1:9zTx9bQi8rqmy2IValEueMxogwR3VxNSFqc/VOJZUdM= +github.com/containerd/containerd/v2 v2.0.1-0.20250211161307-525332b29211/go.mod h1:+35MiZ52NP+lrsh7eRmJ/JAs/upbEmbTqs5jTc4v+qM= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -154,20 +150,13 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= @@ -203,7 +192,6 @@ github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dz github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -226,7 +214,6 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= @@ -249,12 +236,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= -github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= -github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -271,8 +254,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBCQ3eKzKQgSW5DtlXFQJyJMAQk= github.com/rootless-containers/rootlesskit/v2 v2.3.2 h1:QZk7sKU3+B8UHretEeIg6NSTTpj0o4iHGNhNbJBnHOU= @@ -280,7 +263,6 @@ github.com/rootless-containers/rootlesskit/v2 v2.3.2/go.mod h1:RL7YzL02nA2d8HAzt github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= @@ -297,7 +279,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -305,11 +286,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY= github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro= -github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= @@ -331,18 +309,20 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -370,8 +350,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -407,8 +385,6 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -475,8 +451,8 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -493,10 +469,11 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -508,9 +485,3 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc= -tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= -tags.cncf.io/container-device-interface/specs-go v0.8.0 h1:QYGFzGxvYK/ZLMrjhvY0RjpUavIn4KcmRmVP/JjdBTA= -tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws= diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index d38a0e5da11..7196bf722bf 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -264,6 +264,9 @@ type ContainerCreateOptions struct { // ImagePullOpt specifies image pull options which holds the ImageVerifyOptions for verifying the image. ImagePullOpt ImagePullOptions + + // Userns name for user namespace mapping of container + Userns string } // ContainerStopOptions specifies options for `nerdctl (container) stop`. diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 3faf01c4dd4..b9249ebd498 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -185,6 +185,27 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa opts = append(opts, rootfsOpts...) cOpts = append(cOpts, rootfsCOpts...) + if options.Userns != "" { + if runtime.GOOS != "linux" || rootlessutil.IsRootless() { + return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), errors.New("userns is only supported on linux") + + } + userNameSpaceOpts, userNameSpaceCOpts, err := getUserNamespaceOpts(ctx, client, &options, *ensuredImage, id) + if err != nil { + return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err + } + opts = append(opts, userNameSpaceOpts...) + cOpts = append(cOpts, userNameSpaceCOpts...) + + userNsOpts, err := getContainerUserNamespaceNetOpts(ctx, client, netManager) + if err != nil { + return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err + } + opts = append(opts, userNsOpts...) + } else { + cOpts = append(cOpts, containerd.WithNewSnapshot(id, ensuredImage.Image)) + } + if options.Workdir != "" { opts = append(opts, oci.WithProcessCwd(options.Workdir)) } @@ -365,7 +386,6 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage, cOpts = append(cOpts, containerd.WithImage(ensured.Image), containerd.WithSnapshotter(ensured.Snapshotter), - containerd.WithNewSnapshot(id, ensured.Image), containerd.WithImageStopSignal(ensured.Image, "SIGTERM"), ) diff --git a/pkg/cmd/container/create_userns_opts_linux.go b/pkg/cmd/container/create_userns_opts_linux.go new file mode 100644 index 00000000000..0a60b5ea9ac --- /dev/null +++ b/pkg/cmd/container/create_userns_opts_linux.go @@ -0,0 +1,409 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/pkg/oci" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/containerutil" + "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" + "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/containerd/nerdctl/v2/pkg/netutil/nettype" + "github.com/moby/sys/user" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +// IDMap contains a single entry for user namespace range remapping. An array +// of IDMap entries represents the structure that will be provided to the Linux +// kernel for creating a user namespace. +type IDMap struct { + ContainerID int `json:"container_id"` + HostID int `json:"host_id"` + Size int `json:"size"` +} + +// IdentityMapping contains a mappings of UIDs and GIDs. +// The zero value represents an empty mapping. +type IdentityMapping struct { + UIDMaps []IDMap `json:"UIDMaps"` + GIDMaps []IDMap `json:"GIDMaps"` +} + +const ( + capabRemapIDs = "remap-ids" +) + +func getUserNamespaceOpts( + ctx context.Context, + client *containerd.Client, + options *types.ContainerCreateOptions, + ensuredImage imgutil.EnsuredImage, + id string, +) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) { + if isDefaultUserns(options) { + return nil, createDefaultSnapshotOpts(id, ensuredImage), nil + } + + supportsRemap, err := snapshotterSupportsRemapLabels(ctx, client, ensuredImage.Snapshotter) + if err != nil { + return nil, nil, err + } else if supportsRemap == false { + return nil, nil, errors.New("snapshotter does not support remap-ids capability") + } + + idMapping, err := loadAndValidateIDMapping(options.Userns) + if err != nil { + return nil, nil, err + } + + uidMaps, gidMaps := convertMappings(idMapping) + specOpts := getUserNamespaceSpecOpts(uidMaps, gidMaps) + snapshotOpts, err := createSnapshotOpts(id, ensuredImage, uidMaps, gidMaps) + if err != nil { + return nil, nil, err + } + + return specOpts, snapshotOpts, nil +} + +// getContainerUserNamespaceNetOpts retrieves the user namespace path for the specified container. +func getContainerUserNamespaceNetOpts( + ctx context.Context, + client *containerd.Client, + netManager containerutil.NetworkOptionsManager, +) ([]oci.SpecOpts, error) { + netOpts, err := netManager.InternalNetworkingOptionLabels(ctx) + netType, err := nettype.Detect(netOpts.NetworkSlice) + if err != nil { + return nil, err + } else if netType == nettype.Container { + containerName, err := getContainerNameFromNetworkSlice(netOpts) + if err != nil { + return nil, err + } + + container, err := findContainer(ctx, client, containerName) + if err != nil { + return nil, err + } + + if err := validateContainerStatus(ctx, container); err != nil { + return nil, err + } + + userNsPath, err := getUserNamespacePath(ctx, container) + if err != nil { + return nil, err + } + + var userNameSpaceSpecOpts []oci.SpecOpts + userNameSpaceSpecOpts = append(userNameSpaceSpecOpts, oci.WithLinuxNamespace(specs.LinuxNamespace{ + Type: specs.UserNamespace, + Path: userNsPath, + })) + return userNameSpaceSpecOpts, nil + } else if netType == nettype.Namespace { + netNsPath, err := getNamespacePathFromNetworkSlice(netOpts) + if err != nil { + return nil, err + } + userNsPath, err := getUserNamespacePathFromNetNsPath(netNsPath) + if err != nil { + return nil, err + } + var userNameSpaceSpecOpts []oci.SpecOpts + userNameSpaceSpecOpts = append(userNameSpaceSpecOpts, oci.WithLinuxNamespace(specs.LinuxNamespace{ + Type: specs.UserNamespace, + Path: userNsPath, + })) + return userNameSpaceSpecOpts, nil + + } + return []oci.SpecOpts{}, nil +} + +func getNamespacePathFromNetworkSlice(netOpts types.NetworkOptions) (string, error) { + if len(netOpts.NetworkSlice) > 1 { + return "", fmt.Errorf("only one network namespace is supported") + } + netItems := strings.Split(netOpts.NetworkSlice[0], ":") + if len(netItems) < 2 { + return "", fmt.Errorf("namespace networking argument format must be 'ns:', got: %q", netOpts.NetworkSlice[0]) + } + return netItems[1], nil +} + +func getUserNamespacePathFromNetNsPath(netNsPath string) (string, error) { + var path string + var maxSymlinkDepth = 255 + depth := 0 + for { + var err error + path, err = os.Readlink(netNsPath) + if err != nil { + break + } else if depth > maxSymlinkDepth { + return "", fmt.Errorf("EvalSymlinks: too many links") + } + + depth += 1 + _, err = os.Readlink(path) + if err != nil { + break + } else if depth > maxSymlinkDepth { + return "", fmt.Errorf("EvalSymlinks: too many links") + } + + netNsPath = path + depth += 1 + } + matched, err := regexp.MatchString(`^/proc/\d+/ns/net$`, netNsPath) + if err != nil { + return "", err + } else if !matched { + return "", fmt.Errorf("Path is not of the form /proc//ns/net, unable to resolve user namespace") + } + userNsPath := filepath.Join(filepath.Dir(netNsPath), "user") + + return userNsPath, nil +} + +func convertIDMapToLinuxIDMapping(idMaps []IDMap) []specs.LinuxIDMapping { + // Create a slice to hold the resulting LinuxIDMapping structs + linuxIDMappings := make([]specs.LinuxIDMapping, len(idMaps)) + + // Iterate through the IDMap slice and convert each one + for i, idMap := range idMaps { + linuxIDMappings[i] = specs.LinuxIDMapping{ + ContainerID: uint32(idMap.ContainerID), + HostID: uint32(idMap.HostID), + Size: uint32(idMap.Size), + } + } + + // Return the converted slice + return linuxIDMappings +} + +// findContainer searches for a container by name and returns it if found. +func findContainer( + ctx context.Context, + client *containerd.Client, + containerName string, +) (containerd.Container, error) { + var container containerd.Container + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(_ context.Context, found containerwalker.Found) error { + if found.MatchCount > 1 { + return fmt.Errorf("multiple containers found with prefix: %s", containerName) + } + container = found.Container + return nil + }, + } + + if n, err := walker.Walk(ctx, containerName); err != nil { + return container, err + } else if n == 0 { + return container, fmt.Errorf("container not found: %s", containerName) + } + + return container, nil +} + +// validateContainerStatus checks if the container is running. +func validateContainerStatus(ctx context.Context, container containerd.Container) error { + task, err := container.Task(ctx, nil) + if err != nil { + return err + } + + status, err := task.Status(ctx) + if err != nil { + return err + } + + if status.Status != containerd.Running { + return fmt.Errorf("container %s is not running", container.ID()) + } + + return nil +} + +// getUserNamespacePath returns the path to the container's user namespace. +func getUserNamespacePath(ctx context.Context, container containerd.Container) (string, error) { + task, err := container.Task(ctx, nil) + if err != nil { + return "", err + } + + return fmt.Sprintf("/proc/%d/ns/user", task.Pid()), nil +} + +// Determines if the default Userns should be used. +func isDefaultUserns(options *types.ContainerCreateOptions) bool { + return options.Userns == "" || options.Userns == "host" +} + +// Creates default snapshot options. +func createDefaultSnapshotOpts(id string, image imgutil.EnsuredImage) []containerd.NewContainerOpts { + return []containerd.NewContainerOpts{ + containerd.WithNewSnapshot(id, image.Image), + } +} + +// LoadIdentityMapping takes a requested username and +// using the data from /etc/sub{uid,gid} ranges, creates the +// proper uid and gid remapping ranges for that user/group pair +func LoadIdentityMapping(name string) (IdentityMapping, error) { + usr, err := user.LookupUser(name) + if err != nil { + return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) + } + + subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr) + if err != nil { + return IdentityMapping{}, err + } + subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr) + if err != nil { + return IdentityMapping{}, err + } + + return IdentityMapping{ + UIDMaps: subuidRanges, + GIDMaps: subgidRanges, + }, nil +} + +func lookupSubRangesFile(path string, usr user.User) ([]IDMap, error) { + uidstr := strconv.Itoa(usr.Uid) + rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool { + return sid.Name == usr.Name || sid.Name == uidstr + }) + if err != nil { + return nil, err + } + if len(rangeList) == 0 { + return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name) + } + + idMap := []IDMap{} + + containerID := 0 + for _, idrange := range rangeList { + idMap = append(idMap, IDMap{ + ContainerID: containerID, + HostID: int(idrange.SubID), + Size: int(idrange.Count), + }) + containerID = containerID + int(idrange.Count) + } + return idMap, nil +} + +// Loads and validates the ID mapping from the given Userns. +func loadAndValidateIDMapping(userns string) (IdentityMapping, error) { + idMapping, err := LoadIdentityMapping(userns) + if err != nil { + return IdentityMapping{}, err + } + if !validIDMapping(idMapping) { + return IdentityMapping{}, errors.New("no valid UID/GID mappings found") + } + return idMapping, nil +} + +// Validates that both UID and GID mappings are available. +func validIDMapping(mapping IdentityMapping) bool { + return len(mapping.UIDMaps) > 0 && len(mapping.GIDMaps) > 0 +} + +// Converts IDMapping into LinuxIDMapping structures. +func convertMappings(mapping IdentityMapping) ([]specs.LinuxIDMapping, []specs.LinuxIDMapping) { + return convertIDMapToLinuxIDMapping(mapping.UIDMaps), + convertIDMapToLinuxIDMapping(mapping.GIDMaps) +} + +// Builds OCI spec options for the user namespace. +func getUserNamespaceSpecOpts( + uidMaps, gidMaps []specs.LinuxIDMapping, +) []oci.SpecOpts { + return []oci.SpecOpts{oci.WithUserNamespace(uidMaps, gidMaps)} +} + +// Creates snapshot options based on ID mappings and snapshotter capabilities. +func createSnapshotOpts( + id string, + image imgutil.EnsuredImage, + uidMaps, gidMaps []specs.LinuxIDMapping, +) ([]containerd.NewContainerOpts, error) { + if !isValidMapping(uidMaps, gidMaps) { + return nil, errors.New("snapshotter uidmap gidmap config invalid") + } + return []containerd.NewContainerOpts{containerd.WithNewSnapshot(id, image.Image, containerd.WithUserNSRemapperLabels(uidMaps, gidMaps))}, nil +} + +func isValidMapping(uidMaps, gidMaps []specs.LinuxIDMapping) bool { + return len(uidMaps) > 0 && len(gidMaps) > 0 +} + +func getContainerNameFromNetworkSlice(netOpts types.NetworkOptions) (string, error) { + + netItems := strings.Split(netOpts.NetworkSlice[0], ":") + if len(netItems) < 2 { + return "", fmt.Errorf("container networking argument format must be 'container:', got: %q", netOpts.NetworkSlice[0]) + } + containerName := netItems[1] + + return containerName, nil +} + +func snapshotterSupportsRemapLabels( + ctx context.Context, + client *containerd.Client, + snapshotterName string, +) (bool, error) { + caps, err := client.GetSnapshotterCapabilities(ctx, snapshotterName) + if err != nil { + return false, err + } + return hasCapability(caps, capabRemapIDs), nil +} + +// Checks if the given capability exists in the list. +func hasCapability(caps []string, capability string) bool { + for _, cap := range caps { + if cap == capability { + return true + } + } + return false +} diff --git a/pkg/cmd/container/create_userns_opts_linux_test.go b/pkg/cmd/container/create_userns_opts_linux_test.go new file mode 100644 index 00000000000..1cb04e3608c --- /dev/null +++ b/pkg/cmd/container/create_userns_opts_linux_test.go @@ -0,0 +1,154 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "testing" + + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/imgutil" + "github.com/opencontainers/runtime-spec/specs-go" + "gotest.tools/v3/assert" +) + +// TestCreateSnapshotOpts tests the createSnapshotOpts function. +func TestCreateSnapshotOpts(t *testing.T) { + tests := []struct { + name string + id string + image imgutil.EnsuredImage + uidMaps []specs.LinuxIDMapping + gidMaps []specs.LinuxIDMapping + expectError bool + }{ + { + name: "Single remapping", + id: "container1", + image: imgutil.EnsuredImage{}, + uidMaps: []specs.LinuxIDMapping{ + {HostID: 1000, Size: 1}, + }, + gidMaps: []specs.LinuxIDMapping{ + {HostID: 1000, Size: 1}, + }, + expectError: false, + }, + { + name: "Multi remapping with support", + id: "container2", + image: imgutil.EnsuredImage{}, + uidMaps: []specs.LinuxIDMapping{ + {HostID: 1000, Size: 1}, + {HostID: 2000, Size: 1}, + }, + gidMaps: []specs.LinuxIDMapping{ + {HostID: 3000, Size: 1}, + }, + expectError: false, + }, + { + name: "Multi remapping without support", + id: "container3", + image: imgutil.EnsuredImage{}, + uidMaps: []specs.LinuxIDMapping{ + {HostID: 1000, Size: 1}, + {HostID: 2000, Size: 1}, + }, + gidMaps: []specs.LinuxIDMapping{ + {HostID: 3000, Size: 1}, + }, + expectError: true, + }, + { + name: "Empty UID/GID maps", + id: "container4", + image: imgutil.EnsuredImage{}, + uidMaps: []specs.LinuxIDMapping{}, + gidMaps: []specs.LinuxIDMapping{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts, err := createSnapshotOpts(tt.id, tt.image, tt.uidMaps, tt.gidMaps) + + if tt.expectError { + assert.Error(t, err, "Expected an error") + } else { + assert.NilError(t, err) + assert. (t, opts) + } + }) + } +} + +// TestGetContainerNameFromNetworkSlice tests the getContainerNameFromNetworkSlice function. +func TestGetContainerNameFromNetworkSlice(t *testing.T) { + tests := []struct { + name string + netOpts types.NetworkOptions + expected string + expectError bool + }{ + { + name: "Valid input with container name", + netOpts: types.NetworkOptions{ + NetworkSlice: []string{"container:mycontainer"}, + }, + expected: "mycontainer", + expectError: false, + }, + { + name: "Invalid input with no colon separator", + netOpts: types.NetworkOptions{ + NetworkSlice: []string{"container-mycontainer"}, + }, + expected: "", + expectError: true, + }, + { + name: "Empty NetworkSlice", + netOpts: types.NetworkOptions{ + NetworkSlice: []string{""}, + }, + expected: "", + expectError: true, + }, + { + name: "Missing container name", + netOpts: types.NetworkOptions{ + NetworkSlice: []string{"container:"}, + }, + expected: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + containerName, err := getContainerNameFromNetworkSlice(tt.netOpts) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, containerName) + } + }) + } +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index a5d89481203..a841827ccef 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -21,6 +21,8 @@ import ( "fmt" "os" "os/exec" + "regexp" + "strconv" "strings" "gotest.tools/v3/assert" @@ -336,3 +338,23 @@ var Private = &test.Requirement{ } }, } + +var ContainerdV1 = &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (ret bool, mess string) { + version := helpers.Capture("version", "-f", "{{range .Server.Components}}{{if eq .Name \"containerd\"}}{{.Version}}{{end}}{{end}}") + re := regexp.MustCompile(`v?(\d+)\.`) + matches := re.FindStringSubmatch(version) + if len(matches) > 1 { + v, err := strconv.Atoi(matches[1]) + if err != nil { + return true, fmt.Sprintf("Failed to parse containerd version: %v", err) + } + if v > 1 { + return false, fmt.Sprintf("Containerd version is %d, which is greater than 1", v) + } + } else { + return true, fmt.Sprintf("Failed to parse containerd version: %s", version) + } + return true, "containerd version is less than equal to 1" + }, +}