From ae533fba32eb19d46fd9d497c1119f0be9c8e92a Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 11:05:57 +0100 Subject: [PATCH 01/59] refactor: migrate container_run_log_driver_syslog_test.go to nerdtest.Setup Part of the ongoing Tigron migration tracked in issue #4613. Converts the three syslog log-driver tests (TestSyslogNetwork, TestSyslogFacilities, TestSyslogFormat) from the legacy testutil.Base pattern to the nerdtest.Setup / test.Case framework. Key design decisions: - The cross-product of (network x facility x format) is expanded into independent SubTests via buildSyslogSubTests, mirroring the structure of the original table-driven loop. - Each sub-case owns its syslog server lifecycle (Start in Setup, receive in Cleanup) through closure-captured variables (addr, done, closer, containerName, tag, msg) so the channel-based validation can survive the Setup -> Command -> Cleanup split. - CA and cert are shared from the outer fixture to sub-cases via pointer-to-pointer (**testca.CA, **testca.Cert) populated in the outer testCase.Setup and read by each sub-case. - testca.New requires testing.TB; passed as *testing.T through the newSyslogTestCase(t) helper rather than helpers.T() which only implements the narrower tig.T interface. - Network skip logic honours rootless: the rootless path produces a more descriptive skip message per the existing pattern in the repo. Validator functions (rfc5424Validator, rfc3164Validator, emptyFormatValidator) are extracted as package-level helpers, identical in logic to the originals. Signed-off-by: Ogulcan Aydogan --- .../container_run_log_driver_syslog_test.go | 370 ++++++++++-------- 1 file changed, 212 insertions(+), 158 deletions(-) diff --git a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go index 2f263c61992..da64436951a 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -18,8 +18,8 @@ package container import ( "fmt" + "io" "os" - "runtime" "strconv" "strings" "testing" @@ -27,102 +27,176 @@ import ( syslog "github.com/yuchanns/srslog" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/testca" "github.com/containerd/nerdctl/v2/pkg/testutil/testsyslog" ) -func runSyslogTest(t *testing.T, networks []string, syslogFacilities map[string]syslog.Priority, fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error) { - if runtime.GOOS == "windows" { - t.Skip("syslog container logging is not officially supported on Windows") - } +// buildSyslogSubTests expands the (network x facility x format) cross product +// into independent Tigron sub-cases. Each sub-case starts its own syslog +// listener in Setup, runs a detached container in Command, and validates the +// received frame in Cleanup. +func buildSyslogSubTests( + networks []string, + syslogFacilities map[string]syslog.Priority, + fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error, + caRef **testca.CA, + certRef **testca.Cert, + hostnameRef *string, +) []*test.Case { + var cases []*test.Case - base := testutil.NewBase(t) - base.Cmd("pull", "--quiet", testutil.CommonImage).AssertOK() - hostname, err := os.Hostname() - if err != nil { - t.Fatalf("Error retrieving hostname") - } - ca := testca.New(base.T) - cert := ca.NewCert("127.0.0.1") - t.Cleanup(func() { - cert.Close() - ca.Close() - }) - rI := 0 for _, network := range networks { + network := network for rFK, rFV := range syslogFacilities { + rFK := rFK fPriV := rFV - // test both string and number facility for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} { + fPriK := fPriK for fmtK, fmtValidFunc := range fmtValidFuncs { + fmtK := fmtK + fmtValidFunc := fmtValidFunc + fmtKT := "empty" if fmtK != "" { fmtKT = fmtK } - subTestName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) - i := rI - rI++ - t.Run(subTestName, func(t *testing.T) { - tID := testutil.Identifier(t) - tag := tID + "_syslog_driver" - msg := "hello, " + tID + "_syslog_driver" - if !testsyslog.TestableNetwork(network) { - if rootlessutil.IsRootless() { - t.Skipf("skipping on %s/%s; '%s' for rootless containers are not supported", runtime.GOOS, runtime.GOARCH, network) + subName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) + + var ( + addr string + done chan string + closer io.Closer + containerName string + tag string + msg string + ) + + cases = append(cases, &test.Case{ + Description: subName, + Setup: func(data test.Data, helpers test.Helpers) { + if !testsyslog.TestableNetwork(network) { + if rootlessutil.IsRootless() { + helpers.T().Skip(fmt.Sprintf("%q for rootless containers is not supported", network)) + } + helpers.T().Skip(fmt.Sprintf("%q is not supported", network)) + } + tID := data.Identifier() + tag = tID + "_syslog_driver" + msg = "hello, " + tID + "_syslog_driver" + containerName = fmt.Sprintf("%s-%s", tID, fPriK) + done = make(chan string) + addr, closer = testsyslog.StartServer(network, "", done, *certRef) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + args := []string{ + "run", + "-d", + "--name", containerName, + "--restart=no", + "--log-driver=syslog", + "--log-opt=syslog-facility=" + fPriK, + "--log-opt=tag=" + tag, + "--log-opt=syslog-format=" + fmtK, + "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", network, addr), + } + if network == "tcp+tls" { + cert := *certRef + ca := *caRef + args = append(args, + "--log-opt=syslog-tls-cert="+cert.CertPath, + "--log-opt=syslog-tls-key="+cert.KeyPath, + "--log-opt=syslog-tls-ca-cert="+ca.CertPath, + ) } - t.Skipf("skipping on %s/%s; '%s' is not supported", runtime.GOOS, runtime.GOARCH, network) - } - testContainerName := fmt.Sprintf("%s-%d-%s", tID, i, fPriK) - done := make(chan string) - addr, closer := testsyslog.StartServer(network, "", done, cert) - args := []string{ - "run", - "-d", - "--name", - testContainerName, - "--restart=no", - "--log-driver=syslog", - "--log-opt=syslog-facility=" + fPriK, - "--log-opt=tag=" + tag, - "--log-opt=syslog-format=" + fmtK, - "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", network, addr), - } - if network == "tcp+tls" { - args = append(args, - "--log-opt=syslog-tls-cert="+cert.CertPath, - "--log-opt=syslog-tls-key="+cert.KeyPath, - "--log-opt=syslog-tls-ca-cert="+ca.CertPath, - ) - } - args = append(args, testutil.CommonImage, "echo", msg) - base.Cmd(args...).AssertOK() - t.Cleanup(func() { - base.Cmd("rm", "-f", testContainerName).AssertOK() - }) - defer closer.Close() - defer close(done) - select { - case rcvd := <-done: - if err := fmtValidFunc(rcvd, msg, tag, hostname, fPriV, network == "tcp+tls"); err != nil { - t.Error(err) + args = append(args, testutil.CommonImage, "echo", msg) + return helpers.Command(args...) + }, + Expected: test.Expects(0, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + if containerName != "" { + helpers.Anyhow("rm", "-f", containerName) } - case <-time.Tick(time.Second * 3): - t.Errorf("timeout with %s", subTestName) - } + if closer == nil || done == nil { + return + } + defer closer.Close() + defer close(done) + select { + case rcvd := <-done: + if err := fmtValidFunc(rcvd, msg, tag, *hostnameRef, fPriV, network == "tcp+tls"); err != nil { + helpers.T().Log(err) + helpers.T().Fail() + } + case <-time.After(time.Second * 3): + helpers.T().Log(fmt.Sprintf("timeout with %s", subName)) + helpers.T().Fail() + } + }, }) } } } } + + return cases +} + +// newSyslogTestCase wires the shared outer fixture: skip on Windows, pull the +// image, generate a CA/cert pair, and expose them to the sub-cases via the +// returned pointers. +func newSyslogTestCase(t *testing.T) (*test.Case, **testca.CA, **testca.Cert, *string) { + t.Helper() + + testCase := &test.Case{ + Require: require.Not(require.OS("windows")), + } + + var ( + ca *testca.CA + cert *testca.Cert + hostname string + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + hn, err := os.Hostname() + if err != nil { + helpers.T().Log(fmt.Sprintf("retrieving hostname: %v", err)) + helpers.T().FailNow() + } + hostname = hn + ca = testca.New(t) + cert = ca.NewCert("127.0.0.1") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if cert != nil { + cert.Close() + } + if ca != nil { + ca.Close() + } + } + + return testCase, &ca, &cert, &hostname } func TestSyslogNetwork(t *testing.T) { - var syslogFacilities = map[string]syslog.Priority{ + base := nerdtest.Setup() + tc, caRef, certRef, hostnameRef := newSyslogTestCase(t) + base.Require = tc.Require + base.Setup = tc.Setup + base.Cleanup = tc.Cleanup + + syslogFacilities := map[string]syslog.Priority{ "user": syslog.LOG_USER, } - networks := []string{ "udp", "tcp", @@ -131,28 +205,22 @@ func TestSyslogNetwork(t *testing.T) { "unixgram", } fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ - "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, + "rfc5424": rfc5424Validator, } - runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) + + base.SubTests = buildSyslogSubTests(networks, syslogFacilities, fmtValidFuncs, caRef, certRef, hostnameRef) + + base.Run(t) } func TestSyslogFacilities(t *testing.T) { - var syslogFacilities = map[string]syslog.Priority{ + base := nerdtest.Setup() + tc, caRef, certRef, hostnameRef := newSyslogTestCase(t) + base.Require = tc.Require + base.Setup = tc.Setup + base.Cleanup = tc.Cleanup + + syslogFacilities := map[string]syslog.Priority{ "kern": syslog.LOG_KERN, "user": syslog.LOG_USER, "mail": syslog.LOG_MAIL, @@ -174,86 +242,72 @@ func TestSyslogFacilities(t *testing.T) { "local6": syslog.LOG_LOCAL6, "local7": syslog.LOG_LOCAL7, } - networks := []string{"unix"} fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ - "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, + "rfc5424": rfc5424Validator, } - runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) + + base.SubTests = buildSyslogSubTests(networks, syslogFacilities, fmtValidFuncs, caRef, certRef, hostnameRef) + + base.Run(t) } func TestSyslogFormat(t *testing.T) { - var syslogFacilities = map[string]syslog.Priority{ + base := nerdtest.Setup() + tc, caRef, certRef, hostnameRef := newSyslogTestCase(t) + base.Require = tc.Require + base.Setup = tc.Setup + base.Cleanup = tc.Cleanup + + syslogFacilities := map[string]syslog.Priority{ "user": syslog.LOG_USER, } - networks := []string{"unix"} fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ - "": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isSTLS bool) error { - var mon, day, hrs string - var pid int - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s " + tag + "[%d]: " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &pid); n != 4 || err != nil { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - return nil - }, - "rfc3164": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, mon, day, hrs string - var pid int - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s %s " + tag + "[%d]: " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - return nil - }, - "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, - "rfc5424micro": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, + "": emptyFormatValidator, + "rfc3164": rfc3164Validator, + "rfc5424": rfc5424Validator, + "rfc5424micro": rfc5424Validator, + } + + base.SubTests = buildSyslogSubTests(networks, syslogFacilities, fmtValidFuncs, caRef, certRef, hostnameRef) + + base.Run(t) +} + +func rfc5424Validator(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, timestamp string + var length, version, pid int + if !isTLS { + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil + } + exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil +} + +func rfc3164Validator(rcvd, msg, tag, hostname string, pri syslog.Priority, _ bool) error { + var parsedHostname, mon, day, hrs string + var pid int + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s %s " + tag + "[%d]: " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil +} + +func emptyFormatValidator(rcvd, msg, tag, _ string, pri syslog.Priority, _ bool) error { + var mon, day, hrs string + var pid int + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s " + tag + "[%d]: " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &pid); n != 4 || err != nil { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } - runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) + return nil } From 4477c6b6abcd0caa6abcc8db4ba082c05d01c911 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 14:19:38 +0100 Subject: [PATCH 02/59] fix: start syslog server in Command to prevent premature timeout The runPacketSyslog goroutine in testsyslog has a 300ms window (3 x 100ms deadlines) before it gives up and sends an empty string on the done channel. When StartServer was called in Setup, the Tigron framework overhead between Setup and Command could exceed 300ms, causing the goroutine to time out before the container sent its first log entry -- resulting in rcvd="" and a validation failure. Moving StartServer to the Command callback ensures the goroutine starts immediately before nerdctl run -d, eliminating the gap. Signed-off-by: Ogulcan Aydogan --- .../container_run_log_driver_syslog_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go index da64436951a..7f6b01a4693 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -39,8 +39,9 @@ import ( // buildSyslogSubTests expands the (network x facility x format) cross product // into independent Tigron sub-cases. Each sub-case starts its own syslog -// listener in Setup, runs a detached container in Command, and validates the -// received frame in Cleanup. +// listener in Command (immediately before the container launch) to avoid the +// 300ms goroutine timeout in runPacketSyslog expiring before the container +// sends its first log entry. Validation happens in Cleanup. func buildSyslogSubTests( networks []string, syslogFacilities map[string]syslog.Priority, @@ -90,10 +91,14 @@ func buildSyslogSubTests( tag = tID + "_syslog_driver" msg = "hello, " + tID + "_syslog_driver" containerName = fmt.Sprintf("%s-%s", tID, fPriK) - done = make(chan string) - addr, closer = testsyslog.StartServer(network, "", done, *certRef) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Start the server here, immediately before launching the + // container, so the 300ms goroutine timeout in + // runPacketSyslog does not expire before the container + // produces its first log entry. + done = make(chan string) + addr, closer = testsyslog.StartServer(network, "", done, *certRef) args := []string{ "run", "-d", From 33cd71712295edef41177b99047350e699ba3ee4 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 23:41:07 +0100 Subject: [PATCH 03/59] fix: run syslog subtests sequentially to respect server deadline runPacketSyslog uses 4x100ms read deadlines (~400ms total). When subtests run in parallel on slow arm runners, container startup latency exceeds that window, causing the server to drain and send an empty string on the done channel before the log entry arrives. Setting NoParallel matches the original sequential t.Run behaviour. Signed-off-by: Ogulcan Aydogan --- .../container/container_run_log_driver_syslog_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go index 7f6b01a4693..7f412c367e6 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -80,6 +80,11 @@ func buildSyslogSubTests( cases = append(cases, &test.Case{ Description: subName, + // runPacketSyslog reads with 4x100ms deadlines (~400ms total). + // Parallel execution on slow arm runners pushes container startup + // past that window, causing the server to send "" on the channel. + // Sequential matches the original t.Run-based test behaviour. + NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { if !testsyslog.TestableNetwork(network) { if rootlessutil.IsRootless() { From 66c23586c76a59b1aea3386f7d0460719ce6b8d4 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 15 May 2026 09:32:35 +0100 Subject: [PATCH 04/59] fix: extend runPacketSyslog wait window to 2s before first packet UDP and unixgram transports rely on a deadline-based loop in runPacketSyslog. The original 4x100ms (400ms) window was too short on slow ARM runners where container startup + syslog driver initialization exceeds the budget, causing the server to close the socket and send an empty string on the done channel. Increase the pre-packet retry count from 3 to 20 (2s total). Once the first datagram arrives, reset to the original 3 retries (400ms) to drain any remaining bytes promptly. Signed-off-by: Ogulcan Aydogan --- pkg/testutil/testsyslog/testsyslog.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/testutil/testsyslog/testsyslog.go b/pkg/testutil/testsyslog/testsyslog.go index a5eeb0cbf37..401b3192b0f 100644 --- a/pkg/testutil/testsyslog/testsyslog.go +++ b/pkg/testutil/testsyslog/testsyslog.go @@ -109,6 +109,8 @@ func runPacketSyslog(c net.PacketConn, done chan<- string) { var buf [4096]byte var rcvd string ct := 0 + // 20 retries (2s) to wait for the first packet; drop to 3 (400ms drain) after. + maxRetries := 20 for { var n int var err error @@ -116,9 +118,13 @@ func runPacketSyslog(c net.PacketConn, done chan<- string) { _ = c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) n, _, err = c.ReadFrom(buf[:]) rcvd += string(buf[:n]) + if n > 0 { + maxRetries = 3 + ct = 0 + } if err != nil { if oe, ok := err.(*net.OpError); ok { - if ct < 3 && oe.Temporary() { + if ct < maxRetries && oe.Temporary() { ct++ continue } From 2ff6c0dc2b2f711b484c2c66742f9762b3c8d6d0 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 15 May 2026 14:30:40 +0100 Subject: [PATCH 05/59] refactor: use params struct and exit code constant in syslog sub-tests Replace nested loop variable shadowing (x := x) with a syslogCombination struct that pre-builds all (network x facility x format) combinations into a slice before creating test.Case entries. Each closure then captures the struct value from the range variable, making the data flow explicit. Also replace the hardcoded 0 literal in test.Expects with expect.ExitCodeSuccess for consistency with the rest of the Tigron test suite. Signed-off-by: Ogulcan Aydogan --- .../container_run_log_driver_syslog_test.go | 203 ++++++++++-------- 1 file changed, 109 insertions(+), 94 deletions(-) diff --git a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go index 7f412c367e6..2521ea6f709 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -27,6 +27,7 @@ import ( syslog "github.com/yuchanns/srslog" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -37,6 +38,15 @@ import ( "github.com/containerd/nerdctl/v2/pkg/testutil/testsyslog" ) +// syslogCombination holds one entry of the (network x facility x format) cross product. +type syslogCombination struct { + network string + fPriK string + fPriV syslog.Priority + fmtK string + fmtValidFunc func(string, string, string, string, syslog.Priority, bool) error +} + // buildSyslogSubTests expands the (network x facility x format) cross product // into independent Tigron sub-cases. Each sub-case starts its own syslog // listener in Command (immediately before the container launch) to avoid the @@ -50,110 +60,115 @@ func buildSyslogSubTests( certRef **testca.Cert, hostnameRef *string, ) []*test.Case { - var cases []*test.Case + var combinations []syslogCombination for _, network := range networks { - network := network for rFK, rFV := range syslogFacilities { - rFK := rFK - fPriV := rFV - for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} { - fPriK := fPriK + for _, fPriK := range []string{rFK, strconv.Itoa(int(rFV) >> 3)} { for fmtK, fmtValidFunc := range fmtValidFuncs { - fmtK := fmtK - fmtValidFunc := fmtValidFunc - - fmtKT := "empty" - if fmtK != "" { - fmtKT = fmtK - } - subName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) - - var ( - addr string - done chan string - closer io.Closer - containerName string - tag string - msg string - ) - - cases = append(cases, &test.Case{ - Description: subName, - // runPacketSyslog reads with 4x100ms deadlines (~400ms total). - // Parallel execution on slow arm runners pushes container startup - // past that window, causing the server to send "" on the channel. - // Sequential matches the original t.Run-based test behaviour. - NoParallel: true, - Setup: func(data test.Data, helpers test.Helpers) { - if !testsyslog.TestableNetwork(network) { - if rootlessutil.IsRootless() { - helpers.T().Skip(fmt.Sprintf("%q for rootless containers is not supported", network)) - } - helpers.T().Skip(fmt.Sprintf("%q is not supported", network)) - } - tID := data.Identifier() - tag = tID + "_syslog_driver" - msg = "hello, " + tID + "_syslog_driver" - containerName = fmt.Sprintf("%s-%s", tID, fPriK) - }, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - // Start the server here, immediately before launching the - // container, so the 300ms goroutine timeout in - // runPacketSyslog does not expire before the container - // produces its first log entry. - done = make(chan string) - addr, closer = testsyslog.StartServer(network, "", done, *certRef) - args := []string{ - "run", - "-d", - "--name", containerName, - "--restart=no", - "--log-driver=syslog", - "--log-opt=syslog-facility=" + fPriK, - "--log-opt=tag=" + tag, - "--log-opt=syslog-format=" + fmtK, - "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", network, addr), - } - if network == "tcp+tls" { - cert := *certRef - ca := *caRef - args = append(args, - "--log-opt=syslog-tls-cert="+cert.CertPath, - "--log-opt=syslog-tls-key="+cert.KeyPath, - "--log-opt=syslog-tls-ca-cert="+ca.CertPath, - ) - } - args = append(args, testutil.CommonImage, "echo", msg) - return helpers.Command(args...) - }, - Expected: test.Expects(0, nil, nil), - Cleanup: func(data test.Data, helpers test.Helpers) { - if containerName != "" { - helpers.Anyhow("rm", "-f", containerName) - } - if closer == nil || done == nil { - return - } - defer closer.Close() - defer close(done) - select { - case rcvd := <-done: - if err := fmtValidFunc(rcvd, msg, tag, *hostnameRef, fPriV, network == "tcp+tls"); err != nil { - helpers.T().Log(err) - helpers.T().Fail() - } - case <-time.After(time.Second * 3): - helpers.T().Log(fmt.Sprintf("timeout with %s", subName)) - helpers.T().Fail() - } - }, + combinations = append(combinations, syslogCombination{ + network: network, + fPriK: fPriK, + fPriV: rFV, + fmtK: fmtK, + fmtValidFunc: fmtValidFunc, }) } } } } + var cases []*test.Case + + for _, c := range combinations { + fmtKT := "empty" + if c.fmtK != "" { + fmtKT = c.fmtK + } + subName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(c.network, "+", "_"), c.fPriK, fmtKT) + + var ( + addr string + done chan string + closer io.Closer + containerName string + tag string + msg string + ) + + cases = append(cases, &test.Case{ + Description: subName, + // runPacketSyslog reads with 4x100ms deadlines (~400ms total). + // Parallel execution on slow arm runners pushes container startup + // past that window, causing the server to send "" on the channel. + // Sequential matches the original t.Run-based test behaviour. + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + if !testsyslog.TestableNetwork(c.network) { + if rootlessutil.IsRootless() { + helpers.T().Skip(fmt.Sprintf("%q for rootless containers is not supported", c.network)) + } + helpers.T().Skip(fmt.Sprintf("%q is not supported", c.network)) + } + tID := data.Identifier() + tag = tID + "_syslog_driver" + msg = "hello, " + tID + "_syslog_driver" + containerName = fmt.Sprintf("%s-%s", tID, c.fPriK) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Start the server here, immediately before launching the + // container, so the 300ms goroutine timeout in + // runPacketSyslog does not expire before the container + // produces its first log entry. + done = make(chan string) + addr, closer = testsyslog.StartServer(c.network, "", done, *certRef) + args := []string{ + "run", + "-d", + "--name", containerName, + "--restart=no", + "--log-driver=syslog", + "--log-opt=syslog-facility=" + c.fPriK, + "--log-opt=tag=" + tag, + "--log-opt=syslog-format=" + c.fmtK, + "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", c.network, addr), + } + if c.network == "tcp+tls" { + cert := *certRef + ca := *caRef + args = append(args, + "--log-opt=syslog-tls-cert="+cert.CertPath, + "--log-opt=syslog-tls-key="+cert.KeyPath, + "--log-opt=syslog-tls-ca-cert="+ca.CertPath, + ) + } + args = append(args, testutil.CommonImage, "echo", msg) + return helpers.Command(args...) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + if containerName != "" { + helpers.Anyhow("rm", "-f", containerName) + } + if closer == nil || done == nil { + return + } + defer closer.Close() + defer close(done) + select { + case rcvd := <-done: + if err := c.fmtValidFunc(rcvd, msg, tag, *hostnameRef, c.fPriV, c.network == "tcp+tls"); err != nil { + helpers.T().Log(err) + helpers.T().Fail() + } + case <-time.After(time.Second * 3): + helpers.T().Log(fmt.Sprintf("timeout with %s", subName)) + helpers.T().Fail() + } + }, + }) + } + return cases } From efc8f737be4c618add16e6df7cb8e77a5b2dafdd Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 15:56:29 +0100 Subject: [PATCH 06/59] refactor: migrate login_linux_test.go to nerdtest.Setup Replace testutil.NewBase-based test helpers with the Tigron test framework (nerdtest.Setup). Key changes: - Client.Run(base, host) replaced with Client.Cmd(helpers, host) returning test.TestableCommand; DOCKER_CONFIG set via Setenv. - testregistry.NewRegistry / testca.New replaced with nerdtest.RegistryWithBasicAuth, nerdtest.RegistryWithTokenAuth, and lower-level registry.NewCesantaAuthServer + registry.NewDockerRegistry for the token-auth/no-TLS case (HTTP registry, matching original behaviour). - TestLoginPersistence: two SubTests (basic, token) each owning their registry lifecycle via Setup/Cleanup. - TestLoginAgainstVariants: dynamically generated SubTests from the existing test-case table; inner regHost and assertion loops run sequentially inside Setup (equivalent coverage without nested t.Run). - testutil.DockerIncompatible replaced with require.Not(nerdtest.Docker). - TestAgainstNoAuth remains commented out, unchanged. Closes #4613 Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/login/login_linux_test.go | 691 ++++++++++++-------------- 1 file changed, 310 insertions(+), 381 deletions(-) diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 55544b33ad8..8ca77ecac00 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -23,21 +23,23 @@ package login import ( "fmt" "net" - "os" "strconv" "testing" - "gotest.tools/v3/icmd" - + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/utils" + "github.com/containerd/nerdctl/mod/tigron/utils/testca" "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" - "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" - "github.com/containerd/nerdctl/v2/pkg/testutil/testca" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) +// randomPort tells the registry helpers to acquire a free port automatically. +const randomPort = 0 + type Client struct { args []string configPath string @@ -68,106 +70,118 @@ func (ag *Client) WithConfigPath(value string) *Client { return ag } -func (ag *Client) GetConfigPath() string { - return ag.configPath -} - -func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { +func (ag *Client) Cmd(helpers test.Helpers, host string) test.TestableCommand { if ag.configPath == "" { - ag.configPath, _ = os.MkdirTemp(base.T.TempDir(), "docker-config") + ag.configPath = helpers.T().TempDir() } args := []string{"login"} if !nerdtest.IsDocker() { args = append(args, "--debug-full") } args = append(args, ag.args...) - icmdCmd := icmd.Command(base.Binary, append(base.Args, append(args, host)...)...) - icmdCmd.Env = append(base.Env, "HOME="+os.Getenv("HOME"), "DOCKER_CONFIG="+ag.configPath) - - return &testutil.Cmd{ - Cmd: icmdCmd, - Base: base, - } + args = append(args, host) + cmd := helpers.Command(args...) + cmd.Setenv("DOCKER_CONFIG", ag.configPath) + return cmd } func TestLoginPersistence(t *testing.T) { - base := testutil.NewBase(t) - t.Parallel() - - // Retrieve from the store - testCases := []struct { - auth string - }{ - { - "basic", - }, - { - "token", + nerdtest.Setup() + + var basicReg *registry.Server + var tokenReg *registry.Server + var tokenAS *registry.TokenAuthServer + + testCase := &test.Case{ + Require: require.All( + require.Linux, + nerdtest.Registry, + ), + SubTests: []*test.Case{ + { + Description: "basic", + Setup: func(data test.Data, helpers test.Helpers) { + username := utils.RandomStringBase64(30) + "∞" + password := utils.RandomStringBase64(30) + ":∞" + + basicReg = nerdtest.RegistryWithBasicAuth(data, helpers, username, password, randomPort, false) + basicReg.Setup(data, helpers) + + host := fmt.Sprintf("localhost:%d", basicReg.Port) + configPath := helpers.T().TempDir() + + (&Client{configPath: configPath}). + WithCredentials(username, password). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + WithCredentials("invalid", "invalid"). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if basicReg != nil { + basicReg.Cleanup(data, helpers) + } + }, + }, + { + Description: "token", + Setup: func(data test.Data, helpers test.Helpers) { + username := utils.RandomStringBase64(30) + "∞" + password := utils.RandomStringBase64(30) + ":∞" + + // Use HTTP registry (nil CA) so localhost is trusted without explicit hosts-dir, + // matching the original test behaviour. The auth server still uses a CA for JWT + // signing even without TLS on the auth server itself. + rca := testca.NewX509(data, helpers) + tokenAS = registry.NewCesantaAuthServer(data, helpers, rca, randomPort, username, password, false) + tokenAS.Setup(data, helpers) + tokenReg = registry.NewDockerRegistry(data, helpers, nil, randomPort, tokenAS.Auth) + tokenReg.Setup(data, helpers) + + host := fmt.Sprintf("localhost:%d", tokenReg.Port) + configPath := helpers.T().TempDir() + + (&Client{configPath: configPath}). + WithCredentials(username, password). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + WithCredentials("invalid", "invalid"). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if tokenReg != nil { + tokenReg.Cleanup(data, helpers) + } + if tokenAS != nil { + tokenAS.Cleanup(data, helpers) + } + }, + }, }, } - - for _, tc := range testCases { - tc := tc - t.Run(fmt.Sprintf("Server %s", tc.auth), func(t *testing.T) { - t.Parallel() - - username := utils.RandomStringBase64(30) + "∞" - password := utils.RandomStringBase64(30) + ":∞" - - // Add the requested authentication - var auth testregistry.Auth - var dependentCleanup func(error) - - auth = &testregistry.NoAuth{} - if tc.auth == "basic" { - auth = &testregistry.BasicAuth{ - Username: username, - Password: password, - } - } else if tc.auth == "token" { - authCa := testca.New(base.T) - as := testregistry.NewAuthServer(base, authCa, 0, username, password, false) - auth = &testregistry.TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, - } - dependentCleanup = as.Cleanup - } - - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup) - - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) - - // First, login successfully - c := (&Client{}). - WithCredentials(username, password) - - c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - - // Now, log in successfully without passing any explicit credentials - nc := (&Client{}). - WithConfigPath(c.GetConfigPath()) - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - - // Now fail while using invalid credentials - nc.WithCredentials("invalid", "invalid"). - Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertFail() - - // And login again without, reverting to the last saved good state - nc = (&Client{}). - WithConfigPath(c.GetConfigPath()) - - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - }) - } + testCase.Run(t) } /* @@ -202,10 +216,8 @@ func TestAgainstNoAuth(t *testing.T) { func TestLoginAgainstVariants(t *testing.T) { // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - t.Parallel() + nerdtest.Setup() testCases := []struct { port int @@ -213,238 +225,145 @@ func TestLoginAgainstVariants(t *testing.T) { auth string }{ // Basic auth, no TLS - { - 80, - false, - "basic", - }, - { - 443, - false, - "basic", - }, - { - 0, - false, - "basic", - }, + {80, false, "basic"}, + {443, false, "basic"}, + {0, false, "basic"}, // Token auth, no TLS - { - 80, - false, - "token", - }, - { - 443, - false, - "token", - }, - { - 0, - false, - "token", - }, + {80, false, "token"}, + {443, false, "token"}, + {0, false, "token"}, // Basic auth, with TLS /* // This is not working currently, unless we would force a server https:// in hosts // To be fixed with login rewrite - { - 80, - true, - "basic", - }, + {80, true, "basic"}, */ - { - 443, - true, - "basic", - }, - { - 0, - true, - "basic", - }, + {443, true, "basic"}, + {0, true, "basic"}, // Token auth, with TLS /* // This is not working currently, unless we would force a server https:// in hosts // To be fixed with login rewrite - { - 80, - true, - "token", - }, + {80, true, "token"}, */ - { - 443, - true, - "token", - }, - { - 0, - true, - "token", - }, + {443, true, "token"}, + {0, true, "token"}, } - // Iterate through all cases, that will present a variety of port (80, 443, random), TLS (yes or no), and authentication (basic, token) type combinations + var subtests []*test.Case for _, tc := range testCases { - port := tc.port - tls := tc.tls - auth := tc.auth - - t.Run(fmt.Sprintf("Login against `tls: %t port: %d auth: %s`", tls, port, auth), func(t *testing.T) { - // Tests with fixed ports should not be parallelized (although the port locking mechanism will prevent conflicts) - // as their children tests are parallelized, and this might deadlock given the way `Parallel` works - if port == 0 { - t.Parallel() - } - - // Generate credentials that are specific to each registry, so that we never cross hit another one - username := utils.RandomStringBase64(30) + "∞" - password := utils.RandomStringBase64(30) + ":∞" - - // Get a CA if we want TLS - var ca *testca.CA - if tls { - ca = testca.New(base.T) - } - - // Add the requested authenticator - var authenticator testregistry.Auth - var dependentCleanup func(error) - - authenticator = &testregistry.NoAuth{} - if auth == "basic" { - authenticator = &testregistry.BasicAuth{ - Username: username, - Password: password, + tc := tc + + var reg *registry.Server + var tokenAuthServer *registry.TokenAuthServer + + subtests = append(subtests, &test.Case{ + Description: fmt.Sprintf("tls:%t port:%d auth:%s", tc.tls, tc.port, tc.auth), + // Fixed-port cases must not run in parallel: children are parallelised, + // and mixing Parallel levels can deadlock in Go's test runner. + NoParallel: tc.port != 0, + Setup: func(data test.Data, helpers test.Helpers) { + username := utils.RandomStringBase64(30) + "∞" + password := utils.RandomStringBase64(30) + ":∞" + + switch { + case tc.auth == "basic": + reg = nerdtest.RegistryWithBasicAuth(data, helpers, username, password, tc.port, tc.tls) + reg.Setup(data, helpers) + case tc.auth == "token" && tc.tls: + reg, tokenAuthServer = nerdtest.RegistryWithTokenAuth(data, helpers, username, password, tc.port, tc.tls) + tokenAuthServer.Setup(data, helpers) + reg.Setup(data, helpers) + default: // token auth, no TLS: HTTP registry + HTTP auth server (CA used only for JWT) + rca := testca.NewX509(data, helpers) + tokenAuthServer = registry.NewCesantaAuthServer(data, helpers, rca, randomPort, username, password, false) + tokenAuthServer.Setup(data, helpers) + reg = registry.NewDockerRegistry(data, helpers, nil, tc.port, tokenAuthServer.Auth) + reg.Setup(data, helpers) } - } else if auth == "token" { - authCa := ca - // We could be on !tls, meaning no ca - but we still need a CA to sign jwt tokens - if authCa == nil { - authCa = testca.New(base.T) + + regHosts := []string{ + net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.Port)), + net.JoinHostPort("localhost", strconv.Itoa(reg.Port)), + net.JoinHostPort("127.0.0.1", strconv.Itoa(reg.Port)), + // TODO: ipv6 } - as := testregistry.NewAuthServer(base, authCa, 0, username, password, tls) - authenticator = &testregistry.TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, + if reg.Port == 443 { + regHosts = append(regHosts, + reg.IP.String(), + "localhost", + "127.0.0.1", + // TODO: ipv6 + ) } - dependentCleanup = as.Cleanup - } - - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, ca, port, authenticator, dependentCleanup) - - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) - - // Any registry is reachable through its ip+port, and localhost variants - regHosts := []string{ - net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.Port)), - net.JoinHostPort("localhost", strconv.Itoa(reg.Port)), - net.JoinHostPort("127.0.0.1", strconv.Itoa(reg.Port)), - // TODO: ipv6 - // net.JoinHostPort("::1", strconv.Itoa(reg.Port)), - } - - // Registries that use port 443 also allow access without specifying a port - if reg.Port == 443 { - regHosts = append(regHosts, reg.IP.String()) - regHosts = append(regHosts, "localhost") - regHosts = append(regHosts, "127.0.0.1") - // TODO: ipv6 - // regHosts = append(regHosts, "::1") - } - - // Iterate through these hosts access points, and create a test per-variant - for _, value := range regHosts { - regHost := value - t.Run(regHost, func(t *testing.T) { - t.Parallel() - - // 1. test with valid credentials but no access to the CA - t.Run("1. valid credentials (no CA) ", func(t *testing.T) { - t.Parallel() - c := (&Client{}). - WithCredentials(username, password) - - rl, _ := dockerconfigresolver.Parse(regHost) - // a. Insecure flag not being set - // TODO: remove specialization when we fix the localhost mess - if rl.IsLocalhost() && !tls { - c.Run(base, regHost). - AssertOK() - } else { - c.Run(base, regHost). - AssertFail() - } - - // b. Insecure flag set to false - // TODO: remove specialization when we fix the localhost mess - if !rl.IsLocalhost() { - (&Client{}). - WithCredentials(username, password). - WithInsecure(false). - Run(base, regHost). - AssertFail() - } + for _, regHost := range regHosts { + rl, _ := dockerconfigresolver.Parse(regHost) - // c. Insecure flag set to true - // TODO: remove specialization when we fix the localhost mess - if !rl.IsLocalhost() || !tls { - (&Client{}). - WithCredentials(username, password). - WithInsecure(true). - Run(base, regHost). - AssertOK() - } - }) + // 1. valid credentials (no CA) + // a. Insecure flag not being set + // TODO: remove specialization when we fix the localhost mess + if rl.IsLocalhost() && !tc.tls { + (&Client{}). + WithCredentials(username, password). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + } else { + (&Client{}). + WithCredentials(username, password). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + } - // 2. test with valid credentials AND access to the CA - t.Run("2. valid credentials (with access to server CA)", func(t *testing.T) { - t.Parallel() + // b. Insecure flag set to false + // TODO: remove specialization when we fix the localhost mess + if !rl.IsLocalhost() { + (&Client{}). + WithCredentials(username, password). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + } - rl, _ := dockerconfigresolver.Parse(regHost) + // c. Insecure flag set to true + // TODO: remove specialization when we fix the localhost mess + if !rl.IsLocalhost() || !tc.tls { + (&Client{}). + WithCredentials(username, password). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + } + // 2. valid credentials (with access to server CA) + { // a. Insecure flag not being set c := (&Client{}). WithCredentials(username, password). WithHostsDir(reg.HostsDir) - if tls || rl.IsLocalhost() { - c.Run(base, regHost). - AssertOK() + if tc.tls || rl.IsLocalhost() { + c.Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } else { - c.Run(base, regHost). - AssertFail() + c.Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) } // b. Insecure flag set to false - if tls { - c.WithInsecure(false). - Run(base, regHost). - AssertOK() + if tc.tls { + c.WithInsecure(false).Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } else { // TODO: remove specialization when we fix the localhost mess if !rl.IsLocalhost() { - c.WithInsecure(false). - Run(base, regHost). - AssertFail() + c.WithInsecure(false).Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) } } // c. Insecure flag set to true - c.WithInsecure(true). - Run(base, regHost). - AssertOK() - }) + c.WithInsecure(true).Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + } - t.Run("3. valid credentials, any url variant, should always succeed", func(t *testing.T) { - t.Parallel() + // 3. valid credentials, any url variant, should always succeed + { c := (&Client{}). WithCredentials(username, password). WithHostsDir(reg.HostsDir). @@ -453,98 +372,108 @@ func TestLoginAgainstVariants(t *testing.T) { WithInsecure(true) // TODO: remove specialization when we fix the localhost mess - rl, _ := dockerconfigresolver.Parse(regHost) - if !rl.IsLocalhost() || !tls { - c.Run(base, "http://"+regHost).AssertOK() - c.Run(base, "https://"+regHost).AssertOK() - c.Run(base, "http://"+regHost+"/whatever?foo=bar;foo:bar#foo=bar").AssertOK() - c.Run(base, "https://"+regHost+"/whatever?foo=bar&bar=foo;foo=foo+bar:bar#foo=bar").AssertOK() + if !rl.IsLocalhost() || !tc.tls { + c.Cmd(helpers, "http://"+regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + c.Cmd(helpers, "https://"+regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + c.Cmd(helpers, "http://"+regHost+"/whatever?foo=bar;foo:bar#foo=bar").Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + c.Cmd(helpers, "https://"+regHost+"/whatever?foo=bar&bar=foo;foo=foo+bar:bar#foo=bar").Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } - }) - - t.Run("4. wrong password should always fail", func(t *testing.T) { - t.Parallel() - - (&Client{}). - WithCredentials(username, "invalid"). - WithHostsDir(reg.HostsDir). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithHostsDir(reg.HostsDir). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithHostsDir(reg.HostsDir). - WithInsecure(true). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithInsecure(true). - Run(base, regHost). - AssertFail() - }) - - t.Run("5. wrong username should always fail", func(t *testing.T) { - t.Parallel() - - (&Client{}). - WithCredentials("invalid", password). - WithHostsDir(reg.HostsDir). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithHostsDir(reg.HostsDir). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithHostsDir(reg.HostsDir). - WithInsecure(true). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithInsecure(true). - Run(base, regHost). - AssertFail() - }) - }) - } + } + + // 4. wrong password should always fail + (&Client{}). + WithCredentials(username, "invalid"). + WithHostsDir(reg.HostsDir). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithHostsDir(reg.HostsDir). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithHostsDir(reg.HostsDir). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + // 5. wrong username should always fail + (&Client{}). + WithCredentials("invalid", password). + WithHostsDir(reg.HostsDir). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithHostsDir(reg.HostsDir). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithHostsDir(reg.HostsDir). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if reg != nil { + reg.Cleanup(data, helpers) + } + if tokenAuthServer != nil { + tokenAuthServer.Cleanup(data, helpers) + } + }, }) } + + testCase := &test.Case{ + Require: require.All( + require.Not(nerdtest.Docker), + nerdtest.Registry, + ), + SubTests: subtests, + } + testCase.Run(t) } From ec4fc3916826d031c085dc905aaeaef0f6b6f048 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Sat, 16 May 2026 23:10:55 +0100 Subject: [PATCH 07/59] refactor: use randomPort constant instead of raw 0 in registry init Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/login/login_linux_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 8ca77ecac00..90834490508 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -190,7 +190,7 @@ func TestAgainstNoAuth(t *testing.T) { t.Parallel() // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, 0, &testregistry.NoAuth{}, nil) + reg := testregistry.NewRegistry(base, nil, randomPort, &testregistry.NoAuth{}, nil) // Register registry cleanup t.Cleanup(func() { From fad90465828101f0ca4efe451d0e2a92836079dd Mon Sep 17 00:00:00 2001 From: ningmingxiao Date: Wed, 6 May 2026 17:14:35 +0800 Subject: [PATCH 08/59] fix: update status label should call after task is started Signed-off-by: ningmingxiao --- cmd/nerdctl/container/container_run.go | 13 ++++++++++++ .../container_run_restart_linux_test.go | 17 +++++++++++++++ pkg/cmd/container/create.go | 2 +- pkg/cmd/container/run_restart.go | 8 ++----- pkg/containerutil/containerutil.go | 21 ++++++++++--------- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 5daead7b8f4..38c85a097fa 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/console" containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" @@ -467,6 +468,18 @@ func runAction(cmd *cobra.Command, args []string) error { return err } + // Set status label running should call after task is started. + _, restartPolicyExist := lab[restart.PolicyLabel] + if restartPolicyExist { + if err := containerutil.UpdateStatusLabel(ctx, c, containerd.Running); err != nil { + return err + } + } + + if err := containerutil.UpdateExplicitlyStoppedLabel(ctx, c, false); err != nil { + return err + } + // Setup container healthchecks. if err := healthcheck.CreateTimer(ctx, c, (*config.Config)(&createOpt.GOptions), createOpt.NerdctlCmd, createOpt.NerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 795550696f6..c81729cdf0b 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -27,6 +27,8 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/poll" + "github.com/containerd/containerd/v2/core/runtime/restart" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" @@ -183,3 +185,18 @@ func TestAddRestartPolicy(t *testing.T) { inspect = base.InspectContainer(tID) assert.Equal(t, inspect.RestartCount, 1) } + +func TestRunRestartStatusLabel(t *testing.T) { + base := testutil.NewBase(t) + if !nerdtest.IsDocker() { + testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"always"}) + } + tID := testutil.Identifier(t) + defer base.Cmd("rm", "-f", tID).Run() + base.Cmd("create", "--restart=always", "--name", tID, testutil.CommonImage, "sleep", "infinity").AssertOK() + + inspect := base.InspectContainer(tID) + label := inspect.Config.Labels + statusLabel := label[restart.StatusLabel] + assert.Assert(t, statusLabel == "") +} diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index bf44e02c22c..20f1dcd6ef5 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -280,7 +280,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.logConfig.Driver = "json-file" } - restartOpts, err := generateRestartOpts(ctx, client, options.Restart, logConfig.LogURI, options.InRun) + restartOpts, err := generateRestartOpts(ctx, client, options.Restart, logConfig.LogURI) if err != nil { return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err } diff --git a/pkg/cmd/container/run_restart.go b/pkg/cmd/container/run_restart.go index 5932f2175d5..00416ebdbec 100644 --- a/pkg/cmd/container/run_restart.go +++ b/pkg/cmd/container/run_restart.go @@ -51,7 +51,7 @@ func checkRestartCapabilities(ctx context.Context, client *containerd.Client, re return true, nil } -func generateRestartOpts(ctx context.Context, client *containerd.Client, restartFlag, logURI string, inRun bool) ([]containerd.NewContainerOpts, error) { +func generateRestartOpts(ctx context.Context, client *containerd.Client, restartFlag, logURI string) ([]containerd.NewContainerOpts, error) { if restartFlag == "" || restartFlag == "no" { return nil, nil } @@ -63,11 +63,7 @@ func generateRestartOpts(ctx context.Context, client *containerd.Client, restart if err != nil { return nil, err } - desireStatus := containerd.Created - if inRun { - desireStatus = containerd.Running - } - opts := []containerd.NewContainerOpts{restart.WithPolicy(policy), restart.WithStatus(desireStatus)} + opts := []containerd.NewContainerOpts{restart.WithPolicy(policy)} if logURI != "" { opts = append(opts, restart.WithLogURIString(logURI)) } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 94a8e043069..fc17995dff9 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -257,16 +257,6 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i return nil } - _, restartPolicyExist := lab[restart.PolicyLabel] - if restartPolicyExist { - if err := UpdateStatusLabel(ctx, container, containerd.Running); err != nil { - return err - } - } - - if err := UpdateExplicitlyStoppedLabel(ctx, container, false); err != nil { - return err - } if oldTask, err := container.Task(ctx, nil); err == nil { if _, err := oldTask.Delete(ctx); err != nil { log.G(ctx).WithError(err).Debug("failed to delete old task") @@ -302,6 +292,17 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i return err } + // Set status label running should call after task is started. + _, restartPolicyExist := lab[restart.PolicyLabel] + if restartPolicyExist { + if err := UpdateStatusLabel(ctx, container, containerd.Running); err != nil { + return err + } + } + if err := UpdateExplicitlyStoppedLabel(ctx, container, false); err != nil { + return err + } + // If container has health checks configured, create and start systemd timer/service files. if err := healthcheck.CreateTimer(ctx, container, cfg, nerdctlCmd, nerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) From 12fd8ef34e4f79f22763fe4ef3e5823589c640fe Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Wed, 27 May 2026 19:38:33 +0900 Subject: [PATCH 09/59] Update RootlessKit (3.0.1) https://github.com/rootless-containers/rootlesskit/releases/tag/v3.0.1 Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 | 6 ------ Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 diff --git a/Dockerfile b/Dockerfile index 70250e01afc..669dc764bbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless -ARG ROOTLESSKIT_VERSION=v3.0.0@BINARY +ARG ROOTLESSKIT_VERSION=v3.0.1@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 deleted file mode 100644 index c3a4d72a3db..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 +++ /dev/null @@ -1,6 +0,0 @@ -9a6ca1f21c5a21be7738d5cd4cbd287b59fe0c76424750295e10405ca18f0ed5 rootlesskit-aarch64.tar.gz -925ff9f281f8658376ce0647e0e1703806fbc0c05d15a01408e080692583125b rootlesskit-armv7l.tar.gz -d4e8b82fdf104ab1e7bba3059d572b46323ff1da1adcd95cbf9f47a09ed3eb5d rootlesskit-ppc64le.tar.gz -5209498ab7c9446a0bcc8ad6b5e77796696da0dede815ef535017fb8412f99ba rootlesskit-riscv64.tar.gz -6ded9f92668c7838935a85fff51c664747f55343e279471d8871dfa793e7cbed rootlesskit-s390x.tar.gz -9e9e65f11b0a75ffe78f82284fa84528519b94c6c5032a33e6c80ec1924ef8d1 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 new file mode 100644 index 00000000000..97504505bb6 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 @@ -0,0 +1,6 @@ +fdd9d2aa12bb8914081dfe1cd129c5a6a06cf1f80c732713f905a92c07b9b45c rootlesskit-aarch64.tar.gz +9fafaf5bcbee74e86dc4c4b98c70e936fb224dbb0309e436f467d93218dda4d5 rootlesskit-armv7l.tar.gz +320bab519443a6c353f11e3e3c5e59875a00094acdcc040af239e0730f510fb2 rootlesskit-ppc64le.tar.gz +82e843a9b312f6b89fa5b0bd6b07ed2d32177f8f08950ba4c0eab376183a00de rootlesskit-riscv64.tar.gz +738982e4ad56e8c2e6c4a2958bbd7485b5ecb8fa27900cafc78d8b38da04eb77 rootlesskit-s390x.tar.gz +0850aa446151dfbdca15ed228ff0151751792cb5a99260b9a6738e1b490cc37b rootlesskit-x86_64.tar.gz From ed918b47bd3d1560a8cc3da6691d46a232324eec Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 17 May 2026 21:40:59 +0900 Subject: [PATCH 10/59] fix(healthcheck): release exec process resources after probe Each invocation of a health check runs the user-defined command inside the container via containerd's `task.Exec()` API. We can verify this in the `probeHealthCheck` section of the healthcheck package as follows: ```golang func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck, processSpec *specs.Process) (*HealthcheckResult, error) { ... process, err := task.Exec(ctx, execID, processSpec, cio.NewCreator( cio.WithStreams(nil, outputBuf, outputBuf), )) ... if err := process.Start(ctx); err != nil { log.G(ctx).Debugf("failed to start health check: %v", err) return nil, fmt.Errorf("start error: %w", err) } exitStatusC, err := process.Wait(ctx) if err != nil { return nil, fmt.Errorf("failed to wait for health check: %w", err) } ``` However, the current implementation does not release the resources allocated during the health check once the check is complete. As a result, for example, pipes that should normally be deleted are not removed and continue to accumulate over time. We can verify this behavior using the following command: ```bash $ sudo nerdctl run -d --name=health --health-cmd="curl -f http://localhost" --health-interval=1s --health-timeout=1m0s --health-retries=3 --health-start-period=2s nginx:alpine f2fdd8346a546bc6ef446980efdce1132a436fa63bd64a8ce6756c6197e7ec3f $ TASK_PID=$(sudo nerdctl inspect health --format '{{.State.Pid}}') $ SHIM_PID=$(ps -o ppid= -p $TASK_PID | tr -d ' ') $ sudo ls -la /proc/$SHIM_PID/fd | grep pipe | head -3 lr-x------ 1 root root 64 May 17 20:51 100 -> pipe:[1093131] lr-x------ 1 root root 64 May 17 20:52 101 -> pipe:[1092248] lr-x------ 1 root root 64 May 17 20:52 102 -> pipe:[1092228] $ sudo ls -la /proc/$SHIM_PID/fd | grep pipe | wc -l 16 $ sleep 10 $ sudo ls -la /proc/$SHIM_PID/fd | grep pipe | wc -l 26 ``` So, this change adds a deferred `process.Delete()` after `task.Exec()` so that the allocated resources are released when the exec process completes. Signed-off-by: Hayato Kiwata --- .../container_health_check_linux_test.go | 89 +++++++++++++++++++ pkg/healthcheck/executor.go | 5 ++ 2 files changed, 94 insertions(+) diff --git a/cmd/nerdctl/container/container_health_check_linux_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go index ebdf150fb26..15c012df5ee 100644 --- a/cmd/nerdctl/container/container_health_check_linux_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -20,6 +20,9 @@ import ( "encoding/json" "errors" "fmt" + "os" + "path/filepath" + "strconv" "strings" "testing" "time" @@ -1209,3 +1212,89 @@ func TestStartHealthcheckedContainerAfterExited(t *testing.T) { testCase.Run(t) } + +// TestHealthCheckDoesNotLeakShimPipeFDs ensures that running a health check does not leak anonymous pipe files in the containerd shim. +func TestHealthCheckDoesNotLeakShimPipeFDs(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker CLI does not provide a standalone healthcheck command. + require.Not(nerdtest.Docker), + ) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), + "--health-cmd", "true", "--health-interval", "1h", + testutil.CommonImage, "sleep", nerdtest.Infinity, + ) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + shimPID, err := parentPID(inspect.State.Pid) + assert.NilError(helpers.T(), err) + + oldPipes, err := countShimPipeFDs(shimPID) + assert.NilError(helpers.T(), err) + + data.Labels().Set("shimPID", strconv.Itoa(shimPID)) + data.Labels().Set("oldPipes", strconv.Itoa(oldPipes)) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "healthcheck", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + shimPID, _ := strconv.Atoi(data.Labels().Get("shimPID")) + oldPipes, _ := strconv.Atoi(data.Labels().Get("oldPipes")) + + newPipes, err := countShimPipeFDs(shimPID) + assert.NilError(t, err) + assert.Equal(t, oldPipes, newPipes, + "pipes leaked after health check: was %d, now %d", + oldPipes, newPipes) + }, + } + } + + testCase.Run(t) +} + +func parentPID(pid int) (int, error) { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) + if err != nil { + return 0, err + } + for _, line := range strings.Split(string(data), "\n") { + if v, ok := strings.CutPrefix(line, "PPid:"); ok { + return strconv.Atoi(strings.TrimSpace(v)) + } + } + return 0, fmt.Errorf("PPid not found in /proc/%d/status", pid) +} + +func countShimPipeFDs(shimPID int) (int, error) { + fdDir := fmt.Sprintf("/proc/%d/fd", shimPID) + entries, err := os.ReadDir(fdDir) + if err != nil { + return 0, err + } + count := 0 + for _, e := range entries { + target, err := os.Readlink(filepath.Join(fdDir, e.Name())) + if err != nil { + continue + } + if strings.HasPrefix(target, "pipe:") { + count++ + } + } + return count, nil +} diff --git a/pkg/healthcheck/executor.go b/pkg/healthcheck/executor.go index 2525ee7c54b..e86c65e7f4c 100644 --- a/pkg/healthcheck/executor.go +++ b/pkg/healthcheck/executor.go @@ -75,6 +75,11 @@ func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck log.G(ctx).Debugf("failed to exec health check: %v", err) return nil, fmt.Errorf("exec error: %w", err) } + defer func() { + if _, err := process.Delete(ctx); err != nil { + log.G(ctx).WithError(err).Debug("failed to delete exec process") + } + }() if err := process.Start(ctx); err != nil { log.G(ctx).Debugf("failed to start health check: %v", err) From f5bf3cc2f225a0f458eb4903693c76f36b1370c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:40:11 +0000 Subject: [PATCH 11/59] build(deps): bump docker/setup-qemu-action from 4.0.0 to 4.1.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/ce360397dd3f832beb865e1373c09c0e9f86d70a...06116385d9baf250c9f4dcb4858b16962ea869c3) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 0cbd280c1d9..75385f1ad74 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -37,7 +37,7 @@ jobs: # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 879319812c2..22a837a8688 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: "Install go" uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: From 819ba7a66afd9ee5f2d9c4907480c83ede4c5e1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:40:22 +0000 Subject: [PATCH 12/59] build(deps): bump github.com/rootless-containers/rootlesskit/v3 Bumps [github.com/rootless-containers/rootlesskit/v3](https://github.com/rootless-containers/rootlesskit) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/rootless-containers/rootlesskit/releases) - [Commits](https://github.com/rootless-containers/rootlesskit/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: github.com/rootless-containers/rootlesskit/v3 dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1d105cbc64f..60cf26d6615 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,7 @@ require ( github.com/opencontainers/selinux v1.15.0 github.com/pelletier/go-toml/v2 v2.3.1 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined github.com/spf13/pflag v1.0.10 //gomodjail:unconfined github.com/vishvananda/netlink v1.3.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 19d12545873..ae3be6309de 100644 --- a/go.sum +++ b/go.sum @@ -280,8 +280,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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/v3 v3.0.0 h1:esRHLVDYPWcqiPBTDR8gYeJB0kxVturOFYUP7kT2HgA= -github.com/rootless-containers/rootlesskit/v3 v3.0.0/go.mod h1:cAJ5ACtY9npaRpdeT6x1sJgt4gAbYB3/At7qX2LwpII= +github.com/rootless-containers/rootlesskit/v3 v3.0.1 h1:AbYUo1O3b8YFL9X8YuEaaWkVFTwdU551CKyuOt0nJok= +github.com/rootless-containers/rootlesskit/v3 v3.0.1/go.mod h1:7HrjR+SnuEMVvGexEinuuTmhvVW+kxj+6+pkQ/O4ja4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= From 3a2bfc0ced698465f5f19c2854bbc5e4fef61f79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:40:40 +0000 Subject: [PATCH 13/59] build(deps): bump github.com/containerd/typeurl/v2 from 2.2.3 to 2.3.0 Bumps [github.com/containerd/typeurl/v2](https://github.com/containerd/typeurl) from 2.2.3 to 2.3.0. - [Release notes](https://github.com/containerd/typeurl/releases) - [Commits](https://github.com/containerd/typeurl/compare/v2.2.3...v2.3.0) --- updated-dependencies: - dependency-name: github.com/containerd/typeurl/v2 dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 3 +-- go.sum | 21 ++------------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 1d105cbc64f..4f8e580280a 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/ipfs v0.18.2 //gomodjail:unconfined - github.com/containerd/typeurl/v2 v2.2.3 + github.com/containerd/typeurl/v2 v2.3.0 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.9.1 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined @@ -90,7 +90,6 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect diff --git a/go.sum b/go.sum index 19d12545873..26b05b2b0c7 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+X github.com/containerd/stargz-snapshotter/ipfs v0.18.2/go.mod h1:a7cHDMpUfj5f3aZ3/50jEyapRZGjyaOKSDgFH4eiEnE= github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= -github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= +github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= @@ -132,8 +132,6 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -176,8 +174,6 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= @@ -327,8 +323,6 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk= github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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= @@ -357,7 +351,6 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -372,8 +365,6 @@ golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -387,8 +378,6 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -403,8 +392,6 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -462,8 +449,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -471,9 +456,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= From 80cf9f88ef3e1596530219f2e5f0bcf067f6b792 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Sat, 30 May 2026 20:32:07 +0200 Subject: [PATCH 14/59] test: migrate stable compose run tests to nerdtest framework Migrates the non-flaky compose run tests to nerdtest.Setup(): - TestComposeRunWithEnv - TestComposeRunWithUser - TestComposeRunWithArgs - TestComposeRunWithEntrypoint - TestComposeRunWithLabel (uses nerdtest.InspectContainer) - TestComposeRunWithVolume (uses nerdtest.InspectContainer) Replaces the unbuffer(1) pty wrapper with cmd.WithPseudoTTY(), and uses expect.ExitCodeSuccess instead of literal 0 for exit codes. TestComposeRunWithServicePorts, TestComposeRunWithPublish, and TestComposePushAndPullWithCosignVerify are intentionally left on the legacy framework for now (flaky / complex). Signed-off-by: sathiraumesh --- cmd/nerdctl/compose/compose_run_linux_test.go | 326 ++++++++++++------ 1 file changed, 217 insertions(+), 109 deletions(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index c68d9fa258d..71efbb9d070 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -19,15 +19,16 @@ package compose import ( "fmt" "io" + "path/filepath" "strings" "testing" "time" "gotest.tools/v3/assert" - "github.com/containerd/log" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -68,7 +69,7 @@ services: cmd.WithPseudoTTY() return cmd }, - Expected: test.Expects(0, nil, expect.Contains(expectedOutput)), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)), Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", "-v", data.Identifier()) helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") @@ -233,10 +234,7 @@ services: } func TestComposeRunWithEnv(t *testing.T) { - base := testutil.NewBase(t) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. - containerName := testutil.Identifier(t) + const partialOutput = "bar" dockerComposeYAML := fmt.Sprintf(` services: @@ -248,26 +246,42 @@ services: - "echo $$FOO" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "bar" - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "-e", "FOO=bar", "--name", containerName, "alpine").AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "-e", + "FOO=bar", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) } func TestComposeRunWithUser(t *testing.T) { - base := testutil.NewBase(t) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. - containerName := testutil.Identifier(t) + const partialOutput = "5000" dockerComposeYAML := fmt.Sprintf(` services: @@ -278,25 +292,41 @@ services: - -u `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "5000" - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--user", "5000", "--name", containerName, "alpine").AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--user", + "5000", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) } func TestComposeRunWithLabel(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - dockerComposeYAML := fmt.Sprintf(` services: alpine: @@ -308,31 +338,54 @@ services: - "foo=bar" `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--label", "foo=rab", "--label", "x=y", "--name", containerName, "alpine").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } - container := base.InspectContainer(containerName) - if container.Config == nil { - log.L.Errorf("test failed, cannot fetch container config") - t.Fail() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--label", + "foo=rab", + "--label", + "x=y", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd } - assert.Equal(t, container.Config.Labels["foo"], "rab") - assert.Equal(t, container.Config.Labels["x"], "y") + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Assert(tt, container.Config != nil, "cannot fetch container config") + assert.Equal(tt, container.Config.Labels["foo"], "rab") + assert.Equal(tt, container.Config.Labels["x"], "y") + }, + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) } func TestComposeRunWithArgs(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + const partialOutput = "hello world" dockerComposeYAML := fmt.Sprintf(` services: @@ -342,26 +395,41 @@ services: - echo `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "hello world" - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--name", containerName, "alpine", partialOutput).AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--name", + data.Identifier(), + "alpine", + partialOutput, + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) } func TestComposeRunWithEntrypoint(t *testing.T) { - base := testutil.NewBase(t) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. - containerName := testutil.Identifier(t) + const partialOutput = "hello world" dockerComposeYAML := fmt.Sprintf(` services: @@ -371,25 +439,42 @@ services: - stty # should be changed `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "hello world" - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--entrypoint", "echo", "--name", containerName, "alpine", partialOutput).AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--entrypoint", + "echo", + "--name", + data.Identifier(), + "alpine", + partialOutput, + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) } func TestComposeRunWithVolume(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - dockerComposeYAML := fmt.Sprintf(` services: alpine: @@ -398,30 +483,53 @@ services: - stty # no meaning, just put any command `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + const destinationDir = "/data" - // The directory is automatically removed by Cleanup - tmpDir := t.TempDir() - destinationDir := "/data" - volumeFlagStr := fmt.Sprintf("%s:%s", tmpDir, destinationDir) + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - // unbuffer(1) emulates tty, which is required by `nerdctl run -t`. - // unbuffer(1) can be installed with `apt-get install expect`. - unbuffer := []string{"unbuffer"} - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--volume", volumeFlagStr, "--name", containerName, "alpine").AssertOK() - - container := base.InspectContainer(containerName) - errMsg := fmt.Sprintf("test failed, cannot find volume: %v", container.Mounts) - assert.Assert(t, container.Mounts != nil, errMsg) - assert.Assert(t, len(container.Mounts) == 1, errMsg) - assert.Assert(t, container.Mounts[0].Source == tmpDir, errMsg) - assert.Assert(t, container.Mounts[0].Destination == destinationDir, errMsg) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + volumeFlagStr := fmt.Sprintf("%s:%s", data.Temp().Path(), destinationDir) + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--volume", + volumeFlagStr, + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + errMsg := fmt.Sprintf("test failed, cannot find volume: %v", container.Mounts) + assert.Assert(tt, container.Mounts != nil, errMsg) + assert.Assert(tt, len(container.Mounts) == 1, errMsg) + assert.Assert(tt, container.Mounts[0].Source == data.Temp().Path(), errMsg) + assert.Assert(tt, container.Mounts[0].Destination == destinationDir, errMsg) + }, + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) } func TestComposePushAndPullWithCosignVerify(t *testing.T) { From 8631217fffb92fc82de17a70d6c05c51476d1589 Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Tue, 19 May 2026 12:02:17 +0900 Subject: [PATCH 15/59] test: refactor container_run_restart_linux_test.go to use Tigron Signed-off-by: Park jungtae --- .../container_run_restart_linux_test.go | 377 +++++++++++++----- pkg/testutil/nerdtest/requirements.go | 39 ++ 2 files changed, 316 insertions(+), 100 deletions(-) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index c81729cdf0b..5a3431b0d47 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -19,44 +19,118 @@ package container import ( "fmt" "io" + "os" "os/exec" + "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" - "gotest.tools/v3/poll" "github.com/containerd/containerd/v2/core/runtime/restart" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/portlock" ) -func TestRunRestart(t *testing.T) { +func daemonSystemctlTarget() string { + if nerdtest.IsDocker() { + return "docker.service" + } + return "containerd.service" +} + +func daemonSystemctlArgs() []string { + if os.Geteuid() != 0 { + return []string{"--user"} + } + return nil +} + +func killDaemon(t tig.T) { + t.Helper() + target := daemonSystemctlTarget() + t.Log(fmt.Sprintf("killing %q", target)) + cmd := exec.Command("systemctl", append(daemonSystemctlArgs(), "kill", target)...) + if out, err := cmd.CombinedOutput(); err != nil { + t.Log(fmt.Sprintf("cannot kill %q: %q: %v", target, string(out), err)) + t.FailNow() + } + // the daemon should restart automatically +} + +func ensureDaemonActive(t tig.T) { + t.Helper() + target := daemonSystemctlTarget() + t.Log(fmt.Sprintf("checking activity of %q", target)) const ( - hostPort = 8080 + maxRetry = 30 + sleep = 3 * time.Second ) - testContainerName := testutil.Identifier(t) + for i := 0; i < maxRetry; i++ { + cmd := exec.Command("systemctl", append(daemonSystemctlArgs(), "is-active", target)...) + out, err := cmd.CombinedOutput() + t.Log(fmt.Sprintf("(retry=%d) %s", i, string(out))) + if err == nil { + // The daemon is now running, but the daemon may still refuse connections to containerd.sock + t.Log(fmt.Sprintf("daemon %q is now running, checking whether the daemon can handle requests", target)) + infoOut, infoErr := exec.Command(testutil.GetTarget(), "info").CombinedOutput() + if infoErr == nil { + t.Log(fmt.Sprintf("daemon %q can now handle requests", target)) + return + } + t.Log(fmt.Sprintf("(retry=%d) info failed: %s: %v", i, string(infoOut), infoErr)) + } + time.Sleep(sleep) + } + t.Log(fmt.Sprintf("daemon %q not running?", target)) + t.FailNow() +} + +func dumpDaemonLogs(t tig.T, minutes int) { + t.Helper() + target := daemonSystemctlTarget() + cmd := exec.Command("journalctl", + append(daemonSystemctlArgs(), "-u", target, "--no-pager", "-S", fmt.Sprintf("%d min ago", minutes))...) + t.Log(fmt.Sprintf("===== %v =====", cmd.Args)) + out, err := cmd.CombinedOutput() + if err != nil { + t.Log(fmt.Sprintf("failed to dump daemon logs: %v", err)) + return + } + t.Log(string(out)) + t.Log("==========") +} + +// assertRestartCount asserts that `container inspect` reports the expected RestartCount. +func assertRestartCount(expected int) test.Comparator { + return expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Equal(t, dc[0].RestartCount, expected) + }) +} + +func TestRunRestart(t *testing.T) { if testing.Short() { t.Skipf("test is long") } - base := testutil.NewBase(t) - if !base.DaemonIsKillable { + if !testutil.GetDaemonIsKillable() { t.Skip("daemon is not killable (hint: set \"-test.allow-kill-daemon\")") } t.Log("NOTE: this test may take a while") - defer base.Cmd("rm", "-f", testContainerName).Run() + testCase := nerdtest.Setup() + testCase.NoParallel = true - base.Cmd("run", "-d", - "--restart=always", - "--name", testContainerName, - "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), - testutil.NginxAlpineImage).AssertOK() - - check := func(httpGetRetry int) error { + httpCheck := func(data test.Data, httpGetRetry int) error { + hostPort, _ := strconv.Atoi(data.Labels().Get("hostPort")) resp, err := nettestutil.HTTPGet(fmt.Sprintf("http://127.0.0.1:%d", hostPort), httpGetRetry, false) if err != nil { return err @@ -71,132 +145,235 @@ func TestRunRestart(t *testing.T) { } return nil } - assert.NilError(t, check(5)) - base.KillDaemon() - base.EnsureDaemonActive() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + port, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("failed to acquire port: %v", err)) + helpers.T().FailNow() + } + data.Labels().Set("hostPort", strconv.Itoa(port)) - const ( - maxRetry = 30 - sleep = 3 * time.Second - ) - for i := 0; i < maxRetry; i++ { - t.Logf("(retry %d) ps -a: %q", i, base.Cmd("ps", "-a").Run().Combined()) - err := check(1) + helpers.Ensure("run", "-d", + "--restart=always", + "--name", data.Identifier(), + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + testutil.NginxAlpineImage) + + assert.NilError(helpers.T(), httpCheck(data, 5)) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + port, err := strconv.Atoi(data.Labels().Get("hostPort")) if err == nil { - t.Logf("test is passing, after %d retries", i) - return + portlock.Release(port) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + killDaemon(helpers.T()) + ensureDaemonActive(helpers.T()) + return helpers.Command("ps", "-a") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + t.Log(fmt.Sprintf("initial ps -a: %q", stdout)) + + const ( + maxRetry = 30 + sleep = 3 * time.Second + ) + for i := 0; i < maxRetry; i++ { + err := httpCheck(data, 1) + if err == nil { + t.Log(fmt.Sprintf("test is passing, after %d retries", i)) + return + } + time.Sleep(sleep) + t.Log(fmt.Sprintf("(retry %d) ps -a: %q", i, helpers.Capture("ps", "-a"))) + } + dumpDaemonLogs(t, 10) + t.Log("the container does not seem to be restarted") + t.FailNow() + }, } - time.Sleep(sleep) } - base.DumpDaemonLogs(10) - t.Fatalf("the container does not seem to be restarted") + + testCase.Run(t) } func TestRunRestartWithOnFailure(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=on-failure:2", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--restart=on-failure:2", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "exited" { - return poll.Success() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + nerdtest.EnsureContainerExited(helpers, data.Identifier(), -1) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(2), } - return poll.Continue("container is not yet exited") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect := base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 2) + + testCase.Run(t) } func TestRunRestartWithUnlessStopped(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"unless-stopped"}) + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"unless-stopped"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=unless-stopped", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--restart=unless-stopped", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "exited" { - return poll.Success() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + if inspect.State != nil && inspect.State.Status == "exited" { + break + } + if inspect.RestartCount == 2 { + helpers.Ensure("stop", data.Identifier()) + } + time.Sleep(100 * time.Millisecond) } - if inspect.RestartCount == 2 { - base.Cmd("stop", tID).AssertOK() + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(2), } - return poll.Continue("container is not yet exited") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect := base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 2) + + testCase.Run(t) } func TestUpdateRestartPolicy(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) - } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--restart=on-failure:1", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() - base.Cmd("update", "--restart=on-failure:2", tID).AssertOK() - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "exited" { - return poll.Success() + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=on-failure:1", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") + helpers.Ensure("update", "--restart=on-failure:2", data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + nerdtest.EnsureContainerExited(helpers, data.Identifier(), -1) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(2), } - return poll.Continue("container is not yet exited") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect := base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 2) + + testCase.Run(t) } // The test is to add a restart policy to a container which has not restart policy before, // and check it can work correctly. func TestAddRestartPolicy(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) - } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK() - base.Cmd("update", "--restart=on-failure", tID).AssertOK() - inspect := base.InspectContainer(tID) - orgialPid := inspect.State.Pid - exec.Command("kill", "-9", fmt.Sprintf("%v", orgialPid)).Run() - - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "running" && inspect.State.Pid != orgialPid { - return poll.Success() + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.NginxAlpineImage) + helpers.Ensure("update", "--restart=on-failure", data.Identifier()) + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + data.Labels().Set("originalPid", strconv.Itoa(inspect.State.Pid)) + exec.Command("kill", "-9", strconv.Itoa(inspect.State.Pid)).Run() + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + originalPid, _ := strconv.Atoi(data.Labels().Get("originalPid")) + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + if inspect.State != nil && inspect.State.Status == "running" && inspect.State.Pid != originalPid { + break + } + time.Sleep(100 * time.Millisecond) + } + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(1), } - return poll.Continue("container is not yet running") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect = base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 1) + + testCase.Run(t) } func TestRunRestartStatusLabel(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"always"}) + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"always"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--restart=always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Assert(t, dc[0].Config.Labels[restart.StatusLabel] == "") + }), + } } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("create", "--restart=always", "--name", tID, testutil.CommonImage, "sleep", "infinity").AssertOK() - inspect := base.InspectContainer(tID) - label := inspect.Config.Labels - statusLabel := label[restart.StatusLabel] - assert.Assert(t, statusLabel == "") + testCase.Run(t) } diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 7fc0ff005de..2be8b6f9b4c 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -37,6 +37,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerdutil" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" @@ -196,6 +197,44 @@ var RootlessWithoutDetachNetNS = require.All(Rootless, require.Not(RootlessWithD // Rootful marks a test as suitable only for rootful env var Rootful = require.Not(Rootless) +// ContainerdPlugin requires that the given containerd plugin (and capabilities) is available. +var ContainerdPlugin = func(requiredType, requiredID string, requiredCaps []string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + if IsDocker() { + return false, "ContainerdPlugin is not applicable for Docker" + } + stdout := helpers.Capture("info", "--mode", "native", "--format", "{{ json . }}") + var info native.Info + if err := json.Unmarshal([]byte(stdout), &info); err != nil { + return false, fmt.Sprintf("failed to parse info: %v", err) + } + if info.Daemon == nil || info.Daemon.Plugins == nil { + return false, fmt.Sprintf("test requires containerd plugin %q.%q", requiredType, requiredID) + } + for _, p := range info.Daemon.Plugins.Plugins { + if p.Type != requiredType || p.ID != requiredID { + continue + } + capMap := make(map[string]struct{}, len(p.Capabilities)) + for _, c := range p.Capabilities { + capMap[c] = struct{}{} + } + for _, c := range requiredCaps { + if _, ok := capMap[c]; !ok { + return false, fmt.Sprintf("test requires containerd plugin %q.%q with capability %q", requiredType, requiredID, c) + } + } + return true, "" + } + if len(requiredCaps) == 0 { + return false, fmt.Sprintf("test requires containerd plugin %q.%q", requiredType, requiredID) + } + return false, fmt.Sprintf("test requires containerd plugin %q.%q with capabilities %v", requiredType, requiredID, requiredCaps) + }, + } +} + // Info requires that `nerdctl info` satisfies the condition function passed as argument. func Info(f func(dockercompat.Info) error) *test.Requirement { return &test.Requirement{ From 267a9ce305aae8454e7bf1e0119cf50a70aa0e09 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:46:49 +0000 Subject: [PATCH 16/59] build(deps): bump actions/checkout from 6.0.2 to 6.0.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 6.0.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-in-vagrant.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-lint.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 75385f1ad74..3e29cc3c49c 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 7969ef16c7c..bb04ce034f0 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -35,7 +35,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index dbe6c08cf83..fc61d0725a8 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -39,7 +39,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 5ebcfb1a24c..7ebab308780 100644 --- a/.github/workflows/job-lint-other.yml +++ b/.github/workflows/job-lint-other.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index eeea1a363f7..6bc1fac639d 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -30,7 +30,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 100 path: src/github.com/containerd/nerdctl diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml index 4c269d8d01c..6e8f823ae48 100644 --- a/.github/workflows/job-test-dependencies.yml +++ b/.github/workflows/job-test-dependencies.yml @@ -31,7 +31,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index b4626d83e67..72eb0aeaa72 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -67,7 +67,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 6de3830ae00..e768186e1a9 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -83,7 +83,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index c339f738611..d8c0911dfd1 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -31,7 +31,7 @@ jobs: GUEST: ${{ inputs.guest }} steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml index cfb9c1feef0..1d0bca0d5e4 100644 --- a/.github/workflows/job-test-in-vagrant.yml +++ b/.github/workflows/job-test-in-vagrant.yml @@ -20,7 +20,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 6bea385bdc5..6d81764564b 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -46,7 +46,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 22a837a8688..37d39d42620 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index ab750f61212..b76fdfa41f6 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -49,7 +49,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index decb53fe2c0..98932baf37b 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index d9aec7baaf3..f72aa8771a8 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -35,7 +35,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 100 persist-credentials: false From b19c23282110cc649c7a9603b9bf37191716e577 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:46:59 +0000 Subject: [PATCH 17/59] build(deps): bump github.com/opencontainers/selinux Bumps [github.com/opencontainers/selinux](https://github.com/opencontainers/selinux) from 1.15.0 to 1.15.1. - [Release notes](https://github.com/opencontainers/selinux/releases) - [Commits](https://github.com/opencontainers/selinux/compare/v1.15.0...v1.15.1) --- updated-dependencies: - dependency-name: github.com/opencontainers/selinux dependency-version: 1.15.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2e8e62fe107..24a524d64e3 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 - github.com/opencontainers/selinux v1.15.0 + github.com/opencontainers/selinux v1.15.1 github.com/pelletier/go-toml/v2 v2.3.1 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4f8be8673fd..952cec9a50d 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,8 @@ github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5 github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= -github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M= -github.com/opencontainers/selinux v1.15.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA= +github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= From e7f7aac2bcde2d0be43d5f33d0060d9880239ab9 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Wed, 3 Jun 2026 09:33:36 -0400 Subject: [PATCH 18/59] Update Windows installation section in README.md Signed-off-by: Ofek Lev --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3b00cf2ec30..90b92ff807e 100644 --- a/README.md +++ b/README.md @@ -159,15 +159,23 @@ $ limactl start $ lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine ``` -### FreeBSD +### Windows -See [`./docs/freebsd.md`](docs/freebsd.md). +Install with [Scoop](https://scoop.sh): -### Windows +``` +scoop install nerdctl +``` + +Regarding compatibility, note that: - Linux containers: Known to work on WSL2 - Windows containers: experimental support for Windows (see below for features that are currently known to work) +### FreeBSD + +See [`./docs/freebsd.md`](docs/freebsd.md). + ### Docker To run containerd and nerdctl inside Docker: From 737508fa644077d761c272480be9df34c67112b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 03:23:55 +0000 Subject: [PATCH 19/59] build(deps): bump the docker group with 2 updates Bumps the docker group with 2 updates: [github.com/docker/cli](https://github.com/docker/cli) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.5.2+incompatible to 29.5.3+incompatible - [Commits](https://github.com/docker/cli/compare/v29.5.2...v29.5.3) Updates `github.com/moby/moby/v2` from 2.0.0-beta.15 to 2.0.0-beta.16 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.15...v2.0.0-beta.16) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.5.3+incompatible dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 2e8e62fe107..af2f82edac5 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.5.3+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.7.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined @@ -44,7 +44,7 @@ require ( github.com/klauspost/compress v1.18.6 github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined github.com/moby/moby/client v0.4.1 - github.com/moby/moby/v2 v2.0.0-beta.15 + github.com/moby/moby/v2 v2.0.0-beta.16 github.com/moby/sys/mount v0.3.4 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 4f8be8673fd..6c0f82ad878 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ= -github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= +github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -208,8 +208,8 @@ github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= -github.com/moby/moby/v2 v2.0.0-beta.15 h1:v9Uk1WYdov0Pdxu68Kp7bhjieH/sis41ARNaNG+6rmk= -github.com/moby/moby/v2 v2.0.0-beta.15/go.mod h1:jXQSSDlWvnbjDsq3HxKOTATchnpeb2G+N8dvR0Fpgj4= +github.com/moby/moby/v2 v2.0.0-beta.16 h1:Q/PcJ+Oq8QKBauKpWwHJPJIH7qiIDceDviZSO+Qz4VA= +github.com/moby/moby/v2 v2.0.0-beta.16/go.mod h1:HuTyDPU29YJ5EMCvQ2tcomx72rt9FK5bmjFYTZMiMTM= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= From 7194fec73541a49873ebbc884391c17729363961 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Wed, 27 May 2026 13:24:26 +0400 Subject: [PATCH 20/59] fix: make formatter ellipsis unicode safe Signed-off-by: immanuwell --- pkg/formatter/formatter.go | 7 ++--- pkg/formatter/formatter_test.go | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index ff25312f9a2..c0201450c30 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -97,18 +97,19 @@ func Ellipsis(str string, maxDisplayWidth int) string { return "" } - lenStr := len(str) + runes := []rune(str) + lenStr := len(runes) if maxDisplayWidth == 1 { if lenStr <= maxDisplayWidth { return str } - return string(str[0]) + return string(runes[0]) } if lenStr <= maxDisplayWidth { return str } - return str[:maxDisplayWidth-1] + "…" + return string(runes[:maxDisplayWidth-1]) + "…" } func formatRange(startHost, endHost, startContainer, endContainer int32) string { diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index 9da5e6fcf11..98ab70d7128 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -19,6 +19,7 @@ package formatter import ( "testing" "time" + "unicode/utf8" "gotest.tools/v3/assert" @@ -192,3 +193,49 @@ func TestFormatPorts(t *testing.T) { }) } } + +func TestEllipsis(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + maxDisplayWidth int + expected string + }{ + { + name: "ascii under limit", + input: "hello", + maxDisplayWidth: 5, + expected: "hello", + }, + { + name: "ascii truncated", + input: "hello", + maxDisplayWidth: 4, + expected: "hel…", + }, + { + name: "unicode truncated", + input: "éclair", + maxDisplayWidth: 4, + expected: "écl…", + }, + { + name: "unicode truncated to single rune", + input: "éclair", + maxDisplayWidth: 1, + expected: "é", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := Ellipsis(tt.input, tt.maxDisplayWidth) + assert.Assert(t, utf8.ValidString(result), "expected valid UTF-8, got %q", result) + assert.Equal(t, tt.expected, result) + }) + } +} From 0ec7ea7a0ad807425bd69a1112feac0c06c758b2 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sun, 7 Jun 2026 19:14:49 +0400 Subject: [PATCH 21/59] test(formatter): remove redundant utf8 validity assertion Signed-off-by: immanuwell --- pkg/formatter/formatter_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index 98ab70d7128..dda59e80fe0 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -19,7 +19,6 @@ package formatter import ( "testing" "time" - "unicode/utf8" "gotest.tools/v3/assert" @@ -234,7 +233,6 @@ func TestEllipsis(t *testing.T) { t.Parallel() result := Ellipsis(tt.input, tt.maxDisplayWidth) - assert.Assert(t, utf8.ValidString(result), "expected valid UTF-8, got %q", result) assert.Equal(t, tt.expected, result) }) } From 4d5bb2c639e4bd46d365b580b8d95fc0160061c1 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 7 Jun 2026 23:25:59 +0900 Subject: [PATCH 22/59] fix: reject `/` as the `-v` destination in nerdctl run for Docker compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the current implementation, when specifying `/` as the destination of the `-v` option in the nerdctl run command, the following error occurs but the container is created. ```bash > sudo nerdctl run -d --name nginx -v ./:/ nginx FATA[0000] failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: open /proc/thread-self/attr/exec: no such file or directory > sudo nerdctl ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0f880e0e68f8 docker.io/library/nginx:latest "/docker-entrypoint.…" 5 seconds ago Created nginx ``` However, in the same situation, Docker fails to create the container. ```bash $ docker run -d --name stop -v ./:/ nginx docker: Error response from daemon: invalid volume specification: '/Users/haytok/workspace:/': invalid mount config for type "bind": invalid specification: destination can't be '/' $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ``` Therefore, this commit fixes the behavior so that the container creation fails when `/` is specified as the destination of the `-v` option for compatibility with Docker. Signed-off-by: Hayato Kiwata --- .../container_run_mount_linux_test.go | 28 +++++++++++++++ .../container_run_mount_windows_test.go | 34 +++++++++++++++++++ pkg/mountutil/mountutil_linux_test.go | 4 +++ pkg/mountutil/mountutil_unix.go | 20 +++++++++++ pkg/mountutil/mountutil_windows.go | 11 ++++++ pkg/mountutil/mountutil_windows_test.go | 4 +++ 6 files changed, 101 insertions(+) diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index f66f62e46b2..083ddb47adf 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/mountutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -762,3 +763,30 @@ func TestBindMountWhenHostFolderDoesNotExist(t *testing.T) { _, err = os.Stat(hp) assert.ErrorIs(t, err, os.ErrNotExist) } + +func TestRunVolumeWithRootDestination(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "-v", data.Temp().Dir()+":/", testutil.AlpineImage) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{mountutil.ErrVolumeTargetIsRoot}, + Output: func(stdout string, t tig.T) { + psOutput := helpers.Capture("ps", "-a", "--format", "{{.Names}}") + assert.Assert(t, !strings.Contains(psOutput, data.Identifier()), + "no container should be created when the volume destination is '/'") + }, + } + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run_mount_windows_test.go b/cmd/nerdctl/container/container_run_mount_windows_test.go index b0e6afde18a..d75ef3f221e 100644 --- a/cmd/nerdctl/container/container_run_mount_windows_test.go +++ b/cmd/nerdctl/container/container_run_mount_windows_test.go @@ -17,14 +17,21 @@ package container import ( + "errors" "fmt" "os" + "strings" "testing" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunMountVolume(t *testing.T) { @@ -213,3 +220,30 @@ func TestRunMountVolumeSpec(t *testing.T) { // If -v is an empty string, it will be ignored base.Cmd("run", "--rm", "-v", "", testutil.CommonImage).AssertOK() } + +func TestRunVolumeWithDriveRootDestination(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "-v", data.Temp().Dir()+`:C:\.`, testutil.CommonImage) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("destination path (c:\\\\) cannot be 'c:' or 'c:\\\\'")}, + Output: func(stdout string, t tig.T) { + psOutput := helpers.Capture("ps", "-a", "--format", "{{.Names}}") + assert.Assert(t, !strings.Contains(psOutput, data.Identifier()), + "no container should be created when the volume destination is the drive root") + }, + } + } + + testCase.Run(t) +} diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 80e21542cea..74484cbbbfe 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -259,6 +259,10 @@ func TestProcessFlagV(t *testing.T) { rawSpec: `/mnt/foo:./foo`, err: "expected an absolute path, got \"./foo\"", }, + { + rawSpec: `./:/`, + err: "invalid specification: destination can't be '/'", + }, } for _, tt := range tests { diff --git a/pkg/mountutil/mountutil_unix.go b/pkg/mountutil/mountutil_unix.go index 5bf7e4d2420..32ed5548ec7 100644 --- a/pkg/mountutil/mountutil_unix.go +++ b/pkg/mountutil/mountutil_unix.go @@ -16,9 +16,17 @@ limitations under the License. */ +/* + Portions from https://github.com/moby/moby/blob/docker-v29.5.2/daemon/volume/mounts/linux_parser.go + Copyright (C) Docker/Moby authors. + Licensed under the Apache License, Version 2.0 + NOTICE: https://github.com/moby/moby/blob/docker-v29.5.2/NOTICE +*/ + package mountutil import ( + "errors" "fmt" "path/filepath" "strings" @@ -26,6 +34,8 @@ import ( "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" ) +var ErrVolumeTargetIsRoot = errors.New("invalid specification: destination can't be '/'") + func splitVolumeSpec(s string) ([]string, error) { s = strings.TrimLeft(s, ":") split := strings.Split(s, ":") @@ -48,7 +58,17 @@ func cleanMount(p string) string { return filepath.Clean(p) } +func validateNotRoot(p string) error { + if cleanMount(p) == "/" { + return ErrVolumeTargetIsRoot + } + return nil +} + func isValidPath(s string) (bool, error) { + if err := validateNotRoot(s); err != nil { + return false, err + } if filepath.IsAbs(s) { return true, nil } diff --git a/pkg/mountutil/mountutil_windows.go b/pkg/mountutil/mountutil_windows.go index 98fe6471a63..d036d420580 100644 --- a/pkg/mountutil/mountutil_windows.go +++ b/pkg/mountutil/mountutil_windows.go @@ -161,7 +161,18 @@ func cleanMount(p string) string { return filepath.Clean(p) } +func validateNotRoot(p string) error { + p = strings.ToLower(cleanMount(p)) + if p == "c:" || p == `c:\` { + return fmt.Errorf(`destination path (%v) cannot be 'c:' or 'c:\'`, p) + } + return nil +} + func isValidPath(s string) (bool, error) { + if err := validateNotRoot(s); err != nil { + return false, err + } if isNamedPipe(s) || filepath.IsAbs(s) { return true, nil } diff --git a/pkg/mountutil/mountutil_windows_test.go b/pkg/mountutil/mountutil_windows_test.go index 05428b113c5..aeb55d2bb2c 100644 --- a/pkg/mountutil/mountutil_windows_test.go +++ b/pkg/mountutil/mountutil_windows_test.go @@ -262,6 +262,10 @@ func TestProcessFlagV(t *testing.T) { rawSpec: `C:\TestVolume\Path:TestVolume`, err: "expected an absolute path or a named pipe, got \"TestVolume\"", }, + { + rawSpec: `C:\TestVolume\Path:c:\.`, + err: "destination path (c:\\) cannot be 'c:' or 'c:\\'", + }, } for _, tt := range tests { From 23a8437799f18c7d7a1cdf4474ede7c8cb0ba2bd Mon Sep 17 00:00:00 2001 From: Skywalkr-dev Date: Fri, 29 May 2026 10:23:47 +0000 Subject: [PATCH 23/59] fix(container): chunk mounts metadata to prevent max label size crash When a container has a large number of volume mounts, storing the marshaled JSON metadata inside a containerd label (`nerdctl/mounts`) exceeds the 4096-byte protocol buffer limit, causing container creation to fail. The current patch addresses this by storing all mounts individually as separate key value pairs to avoid any buffer limit and is returned as a JSON string while fetching mount metadata Unit tests have been added accordingly Signed-off-by: Naveen --- .../container/container_inspect_linux_test.go | 2 +- pkg/cmd/container/create.go | 6 +- pkg/cmd/container/run_mount.go | 6 +- pkg/cmd/volume/rm.go | 5 +- pkg/containerutil/containerutil.go | 11 ++- pkg/containerutil/containerutil_test.go | 39 +++++++++++ pkg/inspecttypes/dockercompat/dockercompat.go | 2 +- .../dockercompat/dockercompat_test.go | 4 +- pkg/labels/labels.go | 3 + pkg/labels/mount.go | 70 +++++++++++++++++++ 10 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 pkg/labels/mount.go diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 8f7d956a61c..99c6fcebc17 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -266,7 +266,7 @@ func TestContainerInspectContainsInternalLabel(t *testing.T) { lbs := inspect.Config.Labels // TODO: add more internal labels testcases - labelMount := lbs[labels.Mounts] + labelMount := labels.GetMount(lbs) expectedLabelMount := "[{\"Type\":\"bind\",\"Source\":\"/tmp\",\"Destination\":\"/app\",\"Mode\":\"rprivate,rbind\",\"RW\":true,\"Propagation\":\"rprivate\"}]" assert.Equal(tt, expectedLabelMount, labelMount) }) diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 20f1dcd6ef5..264c4ecede2 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -825,11 +825,13 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO if len(internalLabels.mountPoints) > 0 { mounts := dockercompatMounts(internalLabels.mountPoints) - mountPointsJSON, err := json.Marshal(mounts) + jsonMountBytes, err := json.Marshal(mounts) if err != nil { + return nil, fmt.Errorf("failed to marshal mounts: %w", err) + } + if err := labels.SetMount(m, jsonMountBytes); err != nil { return nil, err } - m[labels.Mounts] = string(mountPointsJSON) } if internalLabels.macAddress != "" { diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index bd0c4a08645..5850cad92f0 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -335,8 +335,10 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm return nil, nil, nil, err } } - if m, found := ls[labels.Mounts]; found { - err = json.Unmarshal([]byte(m), &vfMountPoints) + + nerdctlMounts := labels.GetMount(ls) + if nerdctlMounts != "" { + err = json.Unmarshal([]byte(nerdctlMounts), &vfMountPoints) if err != nil { return nil, nil, nil, err } diff --git a/pkg/cmd/volume/rm.go b/pkg/cmd/volume/rm.go index 4b01564c009..0a41b3f23a5 100644 --- a/pkg/cmd/volume/rm.go +++ b/pkg/cmd/volume/rm.go @@ -92,8 +92,9 @@ func usedVolumes(ctx context.Context, containers []containerd.Container) (map[st } return nil, err } - mountsJSON, ok := l[labels.Mounts] - if !ok { + + mountsJSON := labels.GetMount(l) + if mountsJSON == "" { continue } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index fc17995dff9..fa27533f080 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -584,8 +584,15 @@ func GetContainerVolumes(containerLabels map[string]string) []*ContainerVolume { var vols []*ContainerVolume volLabels := []string{labels.AnonymousVolumes, labels.Mounts} for _, volLabel := range volLabels { - names, ok := containerLabels[volLabel] - if !ok { + var names string + + if volLabel == labels.Mounts { + names = labels.GetMount(containerLabels) + } else { + names = containerLabels[volLabel] + } + + if names == "" { continue } var ( diff --git a/pkg/containerutil/containerutil_test.go b/pkg/containerutil/containerutil_test.go index 88d6c42be94..e45b3bb9773 100644 --- a/pkg/containerutil/containerutil_test.go +++ b/pkg/containerutil/containerutil_test.go @@ -19,6 +19,8 @@ package containerutil import ( "reflect" "testing" + + "github.com/containerd/nerdctl/v2/pkg/labels" ) func TestParseExtraHosts(t *testing.T) { @@ -81,3 +83,40 @@ func TestParseExtraHosts(t *testing.T) { }) } } + +func TestGetContainerVolumes_Indexed(t *testing.T) { + m0 := `{"Type":"volume","Name":"vol-0","Source":"/var/lib/vol-0","Destination":"/mnt/vol-0"}` + m1 := `{"Type":"volume","Name":"vol-1","Source":"/var/lib/vol-1","Destination":"/mnt/vol-1"}` + m2 := `{"Type":"volume","Name":"vol-2","Source":"/var/lib/vol-2","Destination":"/mnt/vol-2"}` + + rawJSON := "[" + m0 + "," + m1 + "," + m2 + "]" + + indexedLabels := map[string]string{ + "nerdctl/mounts.0": m0, + "nerdctl/mounts.1": m1, + "nerdctl/mounts.2": m2, + } + + legacyLabels := map[string]string{ + labels.Mounts: rawJSON, + } + + indexedResult := GetContainerVolumes(indexedLabels) + legacyResult := GetContainerVolumes(legacyLabels) + + if len(indexedResult) == 0 { + t.Fatal("Expected to extract volumes from indexed labels, but got 0 results") + } + + if len(indexedResult) != len(legacyResult) { + t.Errorf("Mismatched output! Indexed found %d volumes, Legacy found %d volumes.", + len(indexedResult), len(legacyResult)) + } + + if indexedResult[0].Name != "vol-0" { + t.Errorf("Expected first volume to be named 'vol-0', got '%s'", indexedResult[0].Name) + } + if len(indexedResult) > 2 && indexedResult[2].Name != "vol-2" { + t.Errorf("Expected third volume to be named 'vol-2', got '%s'", indexedResult[2].Name) + } +} diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 7bb1f4ea9b7..7626c3f5b92 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -400,7 +400,7 @@ func ContainerFromNative(n *native.Container) (*Container, error) { } c.HostConfig.Tmpfs = make(map[string]string) - if nerdctlMounts := n.Labels[labels.Mounts]; nerdctlMounts != "" { + if nerdctlMounts := labels.GetMount(n.Labels); nerdctlMounts != "" { mounts, err := parseMounts(nerdctlMounts) if err != nil { return nil, err diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 945228d7323..4c9986ca3b5 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -71,7 +71,7 @@ func TestContainerFromNative(t *testing.T) { n: &native.Container{ Container: containers.Container{ Labels: map[string]string{ - "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]", + "nerdctl/mounts.0": "{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}", "nerdctl/state-dir": tempStateDir, "nerdctl/hostname": "host1", "nerdctl/user": "test-user", @@ -164,7 +164,7 @@ func TestContainerFromNative(t *testing.T) { }, Config: &Config{ Labels: map[string]string{ - "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]", + "nerdctl/mounts.0": `{"Type":"bind","Source":"/mnt/foo","Destination":"/mnt/foo","Mode":"rshared,rw","RW":true,"Propagation":"rshared"}`, "nerdctl/state-dir": tempStateDir, "nerdctl/hostname": "host1", "nerdctl/user": "test-user", diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 46ca0e9c4a4..eaec0720efb 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -86,6 +86,9 @@ const ( // Mounts is the mount points for the container. Mounts = Prefix + "mounts" + // MountsKeyFormat is used to dynamically store individual mounts + MountsKeyFormat = Prefix + "mounts.%d" + // StopTimeout is seconds to wait for stop a container. StopTimeout = Prefix + "stop-timeout" diff --git a/pkg/labels/mount.go b/pkg/labels/mount.go new file mode 100644 index 00000000000..6a95d6069eb --- /dev/null +++ b/pkg/labels/mount.go @@ -0,0 +1,70 @@ +/* + 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 labels + +import ( + "encoding/json" + "fmt" + "strings" +) + +// SetMount parses a JSON array of mounts and stores each individual mount object as an indexed label. +func SetMount(m map[string]string, jsonMountBytes []byte) error { + if len(jsonMountBytes) == 0 || string(jsonMountBytes) == "null" || string(jsonMountBytes) == "[]" { + return nil + } + + var rawMounts []json.RawMessage + if err := json.Unmarshal(jsonMountBytes, &rawMounts); err != nil { + // Fallback: If it's somehow not a slice, write the whole thing to the legacy key + m[Mounts] = string(jsonMountBytes) + return nil + } + + for i, rawMount := range rawMounts { + key := fmt.Sprintf(MountsKeyFormat, i) + m[key] = string(rawMount) + } + + return nil +} + +// GetMount extracts and reassembles indexed mount metadata back into a single JSON array string +func GetMount(containerLabels map[string]string) string { + // Try legacy label first for backward compatibility + if legacyMount, ok := containerLabels[Mounts]; ok && legacyMount != "" { + return legacyMount + } + + var rawMounts []string + + for i := 0; i < len(containerLabels); i++ { + key := fmt.Sprintf(MountsKeyFormat, i) + chunk, found := containerLabels[key] + + if !found || chunk == "" { + break + } + rawMounts = append(rawMounts, chunk) + } + + if len(rawMounts) > 0 { + return "[" + strings.Join(rawMounts, ",") + "]" + } + + return "" +} From 0929f623311b6096d980e0bf513c3edcdfd043dd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 8 Jun 2026 12:45:26 +0900 Subject: [PATCH 24/59] CI: replace Vagrant with Lima for FreeBSD tests Signed-off-by: Akihiro Suda --- .dockerignore | 3 - .../workflows/job-test-in-lima-freebsd.yml | 65 +++++++++++++++++ .github/workflows/job-test-in-vagrant.yml | 61 ---------------- .github/workflows/workflow-flaky.yml | 2 +- .gitignore | 4 -- Vagrantfile.freebsd | 70 ------------------- hack/provisioning/gpg/hashicorp | 64 ----------------- 7 files changed, 66 insertions(+), 203 deletions(-) create mode 100644 .github/workflows/job-test-in-lima-freebsd.yml delete mode 100644 .github/workflows/job-test-in-vagrant.yml delete mode 100644 Vagrantfile.freebsd delete mode 100644 hack/provisioning/gpg/hashicorp diff --git a/.dockerignore b/.dockerignore index e892fca64ae..4a674952409 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,3 @@ _output # golangci-lint /build - -# vagrant -/.vagrant diff --git a/.github/workflows/job-test-in-lima-freebsd.yml b/.github/workflows/job-test-in-lima-freebsd.yml new file mode 100644 index 00000000000..ef22c852a99 --- /dev/null +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -0,0 +1,65 @@ +name: job-test-in-lima-freebsd + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + +jobs: + test: + name: "FreeBSD" + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + steps: + - name: "Init: checkout" + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 1 + persist-credentials: false + + - name: "Init: lima" + uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 + id: lima-actions-setup + + - name: "Init: Cache" + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/lima + key: lima-${{ steps.lima-actions-setup.outputs.version }}-freebsd + + - name: "Init: Install xorriso (required by Lima for running FreeBSD)" + run: | + set -eux + sudo apt-get update + sudo apt-get install -y xorriso + + - name: "Init: start the guest VM" + run: | + set -eux + limactl start --plain --name=default template://freebsd + lima freebsd-version -kru + lima sudo pkg install -y bash go containerd runj + + - name: "Init: copy source into the guest VM" + run: | + set -eux + limactl copy -r . default:/tmp/nerdctl + + - name: "Init: build nerdctl" + run: lima --workdir /tmp/nerdctl sudo go build -o /usr/local/bin/nerdctl ./cmd/nerdctl + + - name: "Run: test-unit" + run: lima --workdir /tmp/nerdctl go test -v ./pkg/... + + - name: "Run: test-integration" + timeout-minutes: 3 + run: | + set -eux + lima sudo containerd >containerd.log 2>&1 & + sleep 3 + lima sudo /usr/local/bin/nerdctl run --rm --net=none dougrabson/freebsd-minimal:13 echo 'Nerdctl is up and running.' diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml deleted file mode 100644 index 1d0bca0d5e4..00000000000 --- a/.github/workflows/job-test-in-vagrant.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Right now, this is testing solely FreeBSD, but could be used to test other targets. -# Alternatively, this might get replaced entirely by Lima eventually. -name: job-test-in-vagrant - -on: - workflow_call: - inputs: - timeout: - required: true - type: number - runner: - required: true - type: string - -jobs: - test: - # Will appear as freebsd / 14 in GitHub UI - name: "14" - timeout-minutes: ${{ inputs.timeout }} - runs-on: "${{ inputs.runner }}" - steps: - - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 1 - persist-credentials: false - - - name: "Init: setup cache" - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: /root/.vagrant.d - key: vagrant - - - name: "Init: set up vagrant" - run: | - # from https://github.com/containerd/containerd/blob/v2.0.2/.github/workflows/ci.yml#L583-L596 - # which is based on https://github.com/opencontainers/runc/blob/v1.1.8/.cirrus.yml#L41-L49 - # FIXME: https://github.com/containerd/nerdctl/issues/4163 - cat ./hack/provisioning/gpg/hashicorp | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg - echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list - sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources - sudo apt-get update -qq - sudo apt-get install -qq libvirt-daemon libvirt-daemon-system vagrant ovmf - # https://github.com/vagrant-libvirt/vagrant-libvirt/issues/1725#issuecomment-1454058646 - sudo cp /usr/share/OVMF/OVMF_VARS_4M.fd /var/lib/libvirt/qemu/nvram/ - sudo systemctl enable --now libvirtd - sudo apt-get build-dep -qq ruby-libvirt - sudo apt-get install -qq --no-install-recommends libxslt-dev libxml2-dev libvirt-dev ruby-bundler ruby-dev zlib1g-dev - # Disable strict dependency enforcement to bypass gem version conflicts during the installation of the vagrant-libvirt plugin. - sudo env VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin install vagrant-libvirt - - - name: "Init: boot VM" - run: | - ln -sf Vagrantfile.freebsd Vagrantfile - sudo vagrant up --no-tty - - - name: "Run: test-unit" - run: sudo vagrant up --provision-with=test-unit - - - name: "Run: test-integration" - run: sudo vagrant up --provision-with=test-integration diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index b76fdfa41f6..70a8b67aefb 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -36,7 +36,7 @@ jobs: test-integration-freebsd: name: "FreeBSD" - uses: ./.github/workflows/job-test-in-vagrant.yml + uses: ./.github/workflows/job-test-in-lima-freebsd.yml with: timeout: 15 runner: ubuntu-24.04 diff --git a/.gitignore b/.gitignore index 1078655195f..4a674952409 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,3 @@ _output # golangci-lint /build - -# vagrant -/.vagrant -Vagrantfile diff --git a/Vagrantfile.freebsd b/Vagrantfile.freebsd deleted file mode 100644 index a1928268038..00000000000 --- a/Vagrantfile.freebsd +++ /dev/null @@ -1,70 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# 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. - -# Vagrantfile for FreeBSD -Vagrant.configure("2") do |config| - config.vm.box = "generic/freebsd14" - - memory = 2048 - cpus = 1 - config.vm.provider :virtualbox do |v, o| - v.memory = memory - v.cpus = cpus - end - config.vm.provider :libvirt do |v| - v.memory = memory - v.cpus = cpus - end - - config.vm.synced_folder ".", "/vagrant", type: "rsync" - - config.vm.provision "install", type: "shell", run: "once" do |sh| - sh.inline = <<~SHELL - #!/usr/bin/env bash - set -eux -o pipefail - freebsd-version -kru - # switching to "release_2" ensures compatibility with the current Vagrant box - # https://github.com/moby/buildkit/pull/5893 - sed -i '' 's/latest/release_2/' /usr/local/etc/pkg/repos/FreeBSD.conf - # `pkg install go` still installs Go 1.20 (March 2024) - pkg install -y go122 containerd runj - ln -s go122 /usr/local/bin/go - cd /vagrant - go install ./cmd/nerdctl - SHELL - end - - config.vm.provision "test-unit", type: "shell", run: "never" do |sh| - sh.inline = <<~SHELL - #!/usr/bin/env bash - set -eux -o pipefail - cd /vagrant - go test -v ./pkg/... - SHELL - end - - config.vm.provision "test-integration", type: "shell", run: "never" do |sh| - sh.inline = <<~SHELL - #!/usr/bin/env bash - set -eux -o pipefail - daemon -o containerd.out containerd - sleep 3 - CONTAINERD_ADDRESS=/run/containerd/containerd.sock /root/go/bin/nerdctl run --rm --quiet --net=none dougrabson/freebsd-minimal:13 echo "Nerdctl is up and running." - SHELL - end - -end diff --git a/hack/provisioning/gpg/hashicorp b/hack/provisioning/gpg/hashicorp deleted file mode 100644 index 495865561d5..00000000000 --- a/hack/provisioning/gpg/hashicorp +++ /dev/null @@ -1,64 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBGO9u+MBEADmE9i8rpt8xhRqxbzlBG06z3qe+e1DI+SyjscyVVRcGDrEfo+J -W5UWw0+afey7HFkaKqKqOHVVGSjmh6HO3MskxcpRm/pxRzfni/OcBBuJU2DcGXnG -nuRZ+ltqBncOuONi6Wf00McTWviLKHRrP6oWwWww7sYF/RbZp5xGmMJ2vnsNhtp3 -8LIMOmY2xv9LeKMh++WcxQDpIeRohmSJyknbjJ0MNlhnezTIPajrs1laLh/IVKVz -7/Z73UWX+rWI/5g+6yBSEtj368N7iyq+hUvQ/bL00eyg1Gs8nE1xiCmRHdNjMBLX -lHi0V9fYgg3KVGo6Hi/Is2gUtmip4ZPnThVmB5fD5LzS7Y5joYVjHpwUtMD0V3s1 -HiHAUbTH+OY2JqxZDO9iW8Gl0rCLkfaFDBS2EVLPjo/kq9Sn7vfp2WHffWs1fzeB -HI6iUl2AjCCotK61nyMR33rNuNcbPbp+17NkDEy80YPDRbABdgb+hQe0o8htEB2t -CDA3Ev9t2g9IC3VD/jgncCRnPtKP3vhEhlhMo3fUCnJI7XETgbuGntLRHhmGJpTj -ydudopoMWZAU/H9KxJvwlVXiNoBYFvdoxhV7/N+OBQDLMevB8XtPXNQ8ZOEHl22G -hbL8I1c2SqjEPCa27OIccXwNY+s0A41BseBr44dmu9GoQVhI7TsetpR+qwARAQAB -tFFIYXNoaUNvcnAgU2VjdXJpdHkgKEhhc2hpQ29ycCBQYWNrYWdlIFNpZ25pbmcp -IDxzZWN1cml0eStwYWNrYWdpbmdAaGFzaGljb3JwLmNvbT6JAlQEEwEIAD4CGwMF -CwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQR5iuxlTlwVQoyOQu6qFvy8piHnAQUC -Y728PQUJCWYB2gAKCRCqFvy8piHnAd16EADeBtTgkdVEvct40TH/9HKkR/Lc/ohM -rer6FFHdKmceJ6Ma8/Qm4nCO5C7c4+EPjsUXdhK5w8DSdC5VbKLJDY1EnDlmU5B1 -wSFkGoYKoB8lUn30E77E33MTu2kfrSuF605vetq269CyBwIJV7oNN6311dW8iQ6z -IytTtlJbVr4YZ7Vst40/uR4myumk9bVBGEd6JhFAPmr/um+BZFhRf9/8xtOryOyB -GF2d+bc9IoAugpxwv0IowHEqkI4RpK2U9hvxG80sTOcmerOuFbmNyPwnEgtJ6CM1 -bc8WAmObJiQcRSLbcgF+a7+2wqrUbCqRE7QoS2wjd1HpUVPmSdJN925c2uaua2A4 -QCbTEg8kV2HiP0HGXypVNhZJt5ouo0YgR6BSbMlsMHniDQaSIP1LgmEz5xD4UAxO -Y/GRR3LWojGzVzBb0T98jpDgPtOu/NpKx3jhSpE2U9h/VRDiL/Pf7gvEIxPUTKuV -5D8VqAiXovlk4wSH13Q05d9dIAjuinSlxb4DVr8IL0lmx9DyHehticmJVooHDyJl -HoA2q2tFnlBBAFbN92662q8Pqi9HbljVRTD1vUjof6ohaoM+5K1C043dmcwZZMTc -7gV1rbCuxh69rILpjwM1stqgI1ONUIkurKVGZHM6N2AatNKqtBRdGEroQo1aL4+4 -u+DKFrMxOqa5b7kCDQRjvbwTARAA0ut7iKLj9sOcp5kRG/5V+T0Ak2k2GSus7w8e -kFh468SVCNUgLJpLzc5hBiXACQX6PEnyhLZa8RAG+ehBfPt03GbxW6cK9nx7HRFQ -GA79H5B4AP3XdEdT1gIL2eaHdQot0mpF2b07GNfADgj99MhpxMCtTdVbBqHY8YEQ -Uq7+E9UCNNs45w5ddq07EDk+o6C3xdJ42fvS2x44uNH6Z6sdApPXLrybeun74C1Z -Oo4Ypre4+xkcw2q2WIhy0Qzeuw+9tn4CYjrhw/+fvvPGUAhtYlFGF6bSebmyua8Q -MTKhwqHqwJxpjftM3ARdgFkhlH1H+PcmpnVutgTNKGcy+9b/lu/Rjq/47JZ+5VkK -ZtYT/zO1oW5zRklHvB6R/OcSlXGdC0mfReIBcNvuNlLhNcBA9frNdOk3hpJgYDzg -f8Ykkc+4z8SZ9gA3g0JmDHY1X3SnSadSPyMas3zH5W+16rq9E+MZztR0RWwmpDtg -Ff1XGMmvc+FVEB8dRLKFWSt/E1eIhsK2CRnaR8uotKW/A/gosao0E3mnIygcyLB4 -fnOM3mnTF3CcRumxJvnTEmSDcoKSOpv0xbFgQkRAnVSn/gHkcbVw/ZnvZbXvvseh -7dstp2ljCs0queKU+Zo22TCzZqXX/AINs/j9Ll67NyIJev445l3+0TWB0kego5Fi -UVuSWkMAEQEAAYkEcgQYAQgAJhYhBHmK7GVOXBVCjI5C7qoW/LymIecBBQJjvbwT -AhsCBQkJZgGAAkAJEKoW/LymIecBwXQgBBkBCAAdFiEE6wr14plJaVlvmYc+cG5m -g2nAhekFAmO9vBMACgkQcG5mg2nAhenPURAAimI0EBZbqpyHpwpbeYq3Pygg1bdo -IlBQUVoutaN1lR7kqGXwYH+BP6G40x79LwVy/fWV8gO7cDX6D1yeKLNbhnJHPBus -FJDmzDPbjTlyWlDqJoWMiPqfAOc1A1cHodsUJDUlA01j1rPTho0S9iALX5R50Wa9 -sIenpfe7RVunDwW5gw6y8me7ncl5trD0LM2HURw6nYnLrxePiTAF1MF90jrAhJDV -+krYqd6IFq5RHKveRtCuTvpL7DlgVCtntmbXLbVC/Fbv6w1xY3A7rXko/03nswAi -AXHKMP14UutVEcLYDBXbDrvgpb2p2ZUJnujs6cNyx9cOPeuxnke8+ACWvpnWxwjL -M5u8OckiqzRRobNxQZ1vLxzdovYTwTlUAG7QjIXVvOk9VNp/ERhh0eviZK+1/ezk -Z8nnPjx+elThQ+r16EM7hD0RDXtOR1VZ0R3OL64AlZYDZz1jEA3lrGhvbjSIfBQk -T6mxKUsCy3YbElcOyuohmPRgT1iVDIZ/1iPL0Q0HGm4+EsWCdH6fAPB7TlHD8z2D -7JCFLihFDWs5lrZyuWMO9nryZiVjJrOLPcStgJYVd/MhRHR4hC6g09bgo25RMJ6f -gyzL4vlEB7aSUih7yjgL9s5DKXP2J71dAhIlF8nnM403R2xEeHyivnyeR/9Ifn7M -PJvUMUuoG+ZANSMkrw//XA31o//TVk9WsLD1Edxt5XZCoR+fS+Vz8ScLwP1d/vQE -OW/EWzeMRG15C0td1lfHvwPKvf2MN+WLenp9TGZ7A1kEHIpjKvY51AIkX2kW5QLu -Y3LBb+HGiZ6j7AaU4uYR3kS1+L79v4kyvhhBOgx/8V+b3+2pQIsVOp79ySGvVwpL -FJ2QUgO15hnlQJrFLRYa0PISKrSWf35KXAy04mjqCYqIGkLsz2qQCY2lGcD5k05z -bBC4TvxwVxv0ftl2C5Bd0ydl/2YM7GfLrmZmTijK067t4OO+2SROT2oYPDsMtZ6S -E8vUXvoGpQ8tf5Nkrn2t0zDG3UDtgZY5UVYnZI+xT7WHsCz//8fY3QMvPXAuc33T -vVdiSfP0aBnZXj6oGs/4Vl1Dmm62XLr13+SMoepMWg2Vt7C8jqKOmhFmSOWyOmRH -UZJR7nKvTpFnL8atSyFDa4o1bk2U3alOscWS8u8xJ/iMcoONEBhItft6olpMVdzP -CTrnCAqMjTSPlQU/9EGtp21KQBed2KdAsJBYuPgwaQeyNIvQEOXmINavl58VD72Y -2T4TFEY8dUiExAYpSodbwBL2fr8DJxOX68WH6e3fF7HwX8LRBjZq0XUwh0KxgHN+ -b9gGXBvgWnJr4NSQGGPiSQVNNHt2ZcBAClYhm+9eC5/VwB+Etg4+1wDmggztiqE= -=FdUF ------END PGP PUBLIC KEY BLOCK----- \ No newline at end of file From b8cc2ac763a7cee0c9901487caa6b307032c7f1f Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 15:07:10 +0100 Subject: [PATCH 25/59] refactor: migrate container_create_linux_test.go to nerdtest.Setup Migrate TestCreateWithLabel, TestCreateWithMACAddress, TestCreateWithTty, and TestCreateFromOCIArchive from testutil.NewBase to the Tigron-based nerdtest.Setup pattern. Also fix TestUsernsMappingCreateCmd which called nerdtest.Setup() without using its return value; now uses the returned *test.Case directly. Helper function removeUsernsConfig updated to accept tig.T instead of *testing.T to align with the rest of the Tigron-based test infrastructure. Part of #4613. Signed-off-by: Ogulcan Aydogan --- .../container/container_create_linux_test.go | 592 +++++++++++------- 1 file changed, 365 insertions(+), 227 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 1551cdf3555..6e8290902b4 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -36,148 +36,277 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) func TestCreateWithLabel(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - - base.Cmd("create", "--name", tID, "--label", "foo=bar", testutil.NginxAlpineImage, "echo", "foo").AssertOK() - defer base.Cmd("rm", "-f", tID).Run() - inspect := base.InspectContainer(tID) - assert.Equal(base.T, "bar", inspect.Config.Labels["foo"]) - // the label `maintainer`` is defined by image - assert.Equal(base.T, "NGINX Docker Maintainers ", inspect.Config.Labels["maintainer"]) + testCase := nerdtest.Setup() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", "--name", data.Identifier(), "--label", "foo=bar", testutil.NginxAlpineImage, "echo", "foo") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + fooLabel := strings.TrimSpace(helpers.Capture("inspect", "--format", `{{index .Config.Labels "foo"}}`, data.Identifier())) + assert.Equal(t, "bar", fooLabel) + maintainerLabel := strings.TrimSpace(helpers.Capture("inspect", "--format", `{{index .Config.Labels "maintainer"}}`, data.Identifier())) + assert.Equal(t, "NGINX Docker Maintainers ", maintainerLabel) + }, + } + } + testCase.Run(t) } func TestCreateWithMACAddress(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - networkBridge := "testNetworkBridge" + tID - networkMACvlan := "testNetworkMACvlan" + tID - networkIPvlan := "testNetworkIPvlan" + tID - - tearDown := func() { - base.Cmd("network", "rm", networkBridge).Run() - base.Cmd("network", "rm", networkMACvlan).Run() - base.Cmd("network", "rm", networkIPvlan).Run() + testCase := nerdtest.Setup() + + const ( + networkBridgeKey = "networkBridge" + networkMACvlanKey = "networkMACvlan" + networkIPvlanKey = "networkIPvlan" + defaultMacKey = "defaultMac" + macAddressKey = "macAddress" + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set(networkBridgeKey, "testNetworkBridge"+data.Identifier()) + data.Labels().Set(networkMACvlanKey, "testNetworkMACvlan"+data.Identifier()) + data.Labels().Set(networkIPvlanKey, "testNetworkIPvlan"+data.Identifier()) + helpers.Ensure("network", "create", data.Labels().Get(networkBridgeKey), "--driver", "bridge") + helpers.Ensure("network", "create", data.Labels().Get(networkMACvlanKey), "--driver", "macvlan") + helpers.Ensure("network", "create", data.Labels().Get(networkIPvlanKey), "--driver", "ipvlan") + defaultMac := strings.TrimSpace(helpers.Capture("run", "--rm", "--network", "host", + testutil.CommonImage, "sh", "-c", "ip addr show eth0 | grep ether | awk '{printf $2}'")) + data.Labels().Set(defaultMacKey, defaultMac) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Labels().Get(networkBridgeKey)) + helpers.Anyhow("network", "rm", data.Labels().Get(networkMACvlanKey)) + helpers.Anyhow("network", "rm", data.Labels().Get(networkIPvlanKey)) } - tearDown() - t.Cleanup(tearDown) - - base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK() - base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK() - base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK() - - defaultMac := base.Cmd("run", "--rm", "-i", "--network", "host", testutil.CommonImage). - CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))). - Run().Stdout() - - passedMac := "we expect the generated mac on the output" - tests := []struct { - Network string - WantErr bool - Expect string - }{ - {"host", false, defaultMac}, // anything but the actual address being passed - {"none", false, ""}, - {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container" - {"bridge", false, passedMac}, - {networkBridge, false, passedMac}, - {networkMACvlan, false, passedMac}, - {networkIPvlan, true, "not support"}, + setupMAC := func(data test.Data, helpers test.Helpers) { + macAddress, err := nettestutil.GenerateMACAddress() + assert.NilError(helpers.T(), err, "failed to generate MAC address") + data.Labels().Set(macAddressKey, macAddress) } - for i, test := range tests { - containerName := fmt.Sprintf("%s_%d", tID, i) - testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect) - expect := test.Expect - network := test.Network - wantErr := test.WantErr - t.Run(testName, func(tt *testing.T) { - tt.Parallel() - - macAddress, err := nettestutil.GenerateMACAddress() - if err != nil { - tt.Errorf("failed to generate MAC address: %s", err) - } - if expect == passedMac { - expect = macAddress - } - tearDown := func() { - base.Cmd("rm", "-f", containerName).Run() - } - tearDown() - tt.Cleanup(tearDown) - // This is currently blocked by https://github.com/containerd/nerdctl/pull/3104 - // res := base.Cmd("create", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage).Run() - res := base.Cmd("create", "--network", network, "--name", containerName, - "--mac-address", macAddress, testutil.CommonImage, - "sh", "-c", "--", "ip addr show").Run() - - if !wantErr { - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) - // This is currently blocked by: https://github.com/containerd/nerdctl/pull/3104 - // res = base.Cmd("start", "-i", containerName). - // CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() - res = base.Cmd("start", "-a", containerName).Run() - // FIXME: flaky - this has failed on the CI once, with the output NOT containing anything - // https://github.com/containerd/nerdctl/actions/runs/11392051487/job/31697214002?pr=3535#step:7:271 - assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout())) - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded") - } else { - if nerdtest.IsDocker() && - (network == networkIPvlan || network == "container:whatever"+tID) { - // unlike nerdctl - // when using network ipvlan or container in Docker - // it delays fail on executing start command - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) - res = base.Cmd("start", "-i", "-a", containerName). - CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() - } - // See https://github.com/containerd/nerdctl/issues/3101 - if nerdtest.IsDocker() && - (network == networkBridge) { - expect = "" + makeCreateCommand := func(network string) test.Executor { + return func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", + "--network", network, + "--name", data.Identifier(), + "--mac-address", data.Labels().Get(macAddressKey), + testutil.CommonImage, "sh", "-c", "--", "ip addr show") + } + } + makeDynamicCreateCommand := func(networkKey string) test.Executor { + return func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", + "--network", data.Labels().Get(networkKey), + "--name", data.Identifier(), + "--mac-address", data.Labels().Get(macAddressKey), + testutil.CommonImage, "sh", "-c", "--", "ip addr show") + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "host network - container inherits host MAC", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("host"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(defaultMacKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(defaultMacKey), startOut)) + }, + } + }, + }, + { + Description: "none network - MAC address flag is accepted", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("none"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + helpers.Ensure("start", "-a", data.Identifier()) + }, + } + }, + }, + { + Description: "container network - nonexistent container fails", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("container:nonexistent-container-for-test"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if nerdtest.IsDocker() { + // Docker delays the failure to start time + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + helpers.Command("start", "-i", "-a", data.Identifier()). + Run(&test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("container")}, + }) + }, + } } - if expect != "" { - assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined())) - } else { - assert.Assert(t, res.Combined() == "", fmt.Sprintf("expected output to be empty: %q", res.Combined())) + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("container")}, + } + }, + }, + { + Description: "bridge network - MAC address is applied", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("bridge"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, + } + }, + }, + { + Description: "custom bridge network - MAC address is applied", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeDynamicCreateCommand(networkBridgeKey), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, + } + }, + }, + { + Description: "macvlan network - MAC address is applied", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeDynamicCreateCommand(networkMACvlanKey), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, + } + }, + }, + { + Description: "ipvlan network - MAC address setting not supported", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeDynamicCreateCommand(networkIPvlanKey), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if nerdtest.IsDocker() { + // Docker delays the failure to start time + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + helpers.Command("start", "-i", "-a", data.Identifier()). + Run(&test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("not support")}, + }) + }, + } + } + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("not support")}, } - assert.Assert(t, res.ExitCode != 0, "Command should have failed", res) - } - }) + }, + }, } + testCase.Run(t) } func TestCreateWithTty(t *testing.T) { - base := testutil.NewBase(t) - imageName := testutil.CommonImage - withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) - withTtyContainerName := "with-terminal-" + testutil.Identifier(t) - - // without -t, fail - base.Cmd("create", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() - base.Cmd("start", withoutTtyContainerName).AssertOK() - defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() - base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") - withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) - assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) - - // with -t, success - base.Cmd("create", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() - base.Cmd("start", withTtyContainerName).AssertOK() - defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() - base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") - withTtyContainer := base.InspectContainer(withTtyContainerName) - assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + Description: "create without tty - stty exits with error", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--name", data.Identifier(), testutil.CommonImage, "stty") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", "-a", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + Errors: []error{errors.New("stty: standard input: Not a tty")}, + } + }, + }, + { + Description: "create with tty - stty succeeds", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "-t", "--name", data.Identifier(), testutil.CommonImage, "stty") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", "-a", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, "speed 38400 baud; line = 0;"), + fmt.Sprintf("expected stdout to contain speed info: %q", stdout)) + }, + } + }, + }, + } + testCase.Run(t) } // TestIssue2993 tests https://github.com/containerd/nerdctl/issues/2993 @@ -301,110 +430,121 @@ func TestIssue2993(t *testing.T) { } func TestCreateFromOCIArchive(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - // Docker does not support creating containers from OCI archive. - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - containerName := testutil.Identifier(t) - - teardown := func() { - base.Cmd("rm", "-f", containerName).Run() - base.Cmd("rmi", "-f", imageName).Run() - } - defer teardown() - teardown() + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.Build, + require.Not(nerdtest.Docker), + ) const sentinel = "test-nerdctl-create-from-oci-archive" - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "%s"]`, testutil.CommonImage, sentinel) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - tag := fmt.Sprintf("%s:latest", imageName) - tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) - - base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() - 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") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) + err := os.WriteFile(filepath.Join(data.Temp().Path(), "Dockerfile"), []byte(dockerfile), 0644) + assert.NilError(helpers.T(), err) + + imageName := data.Identifier("image") + ":latest" + tarPath := data.Temp().Path("image.tar") + data.Labels().Set("imageName", imageName) + data.Labels().Set("tarPath", tarPath) + + helpers.Ensure("build", "--tag", imageName, + fmt.Sprintf("--output=type=oci,dest=%s", tarPath), + data.Temp().Path()) + helpers.Ensure("create", "--rm", "--name", data.Identifier(), + fmt.Sprintf("oci-archive://%s", tarPath)) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", data.Labels().Get("imageName")) + helpers.Anyhow("builder", "prune", "--all", "--force") + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", "--attach", data.Identifier()) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, sentinel), + fmt.Sprintf("expected stdout to contain %q: %q", sentinel, stdout)) + }, + } + } + testCase.Run(t) } func TestUsernsMappingCreateCmd(t *testing.T) { - nerdtest.Setup() - - testCase := &test.Case{ - Require: require.All( - nerdtest.AllowModifyUserns, - nerdtest.RemapIDs, - require.Not(nerdtest.Docker)), - NoParallel: true, - Setup: func(data test.Data, helpers test.Helpers) { - data.Labels().Set("validUserns", "nerdctltestuser") - data.Labels().Set("expectedHostUID", "123456789") - data.Labels().Set("invalidUserns", "invaliduser") + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.AllowModifyUserns, + nerdtest.RemapIDs, + require.Not(nerdtest.Docker)) + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("validUserns", "nerdctltestuser") + data.Labels().Set("expectedHostUID", "123456789") + data.Labels().Set("invalidUserns", "invaliduser") + } + testCase.SubTests = []*test.Case{ + { + Description: "Test container create with valid Userns", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(helpers.T(), err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(helpers.T(), data.Labels().Get("validUserns"), helpers) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("create", "--tty", "--userns-remap", data.Labels().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, t tig.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + assert.NilError(t, err, "Failed to get container host UID") + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) + }, + } + }, }, - SubTests: []*test.Case{ - { - Description: "Test container create with valid Userns", - NoParallel: true, // Changes system config so running in non parallel mode - Setup: func(data test.Data, helpers test.Helpers) { - err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) - assert.NilError(t, err, "Failed to append Userns config") - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) - helpers.Anyhow("rm", "-f", data.Identifier()) - }, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - helpers.Ensure("create", "--tty", "--userns-remap", data.Labels().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, t tig.T) { - actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) - assert.NilError(t, err, "Failed to get container host UID") - assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID")) - }, - } - }, - }, - { - Description: "Test container create failure with valid Userns and privileged flag", - NoParallel: true, // Changes system config so running in non parallel mode - Setup: func(data test.Data, helpers test.Helpers) { - err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) - assert.NilError(t, err, "Failed to append Userns config") - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) - }, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("create", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 1, - } - }, - }, - { - Description: "Test container create with invalid Userns", - NoParallel: true, // Changes system config so running in non parallel mode - 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-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 1, - } - }, + { + Description: "Test container create failure with valid Userns and privileged flag", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(helpers.T(), err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(helpers.T(), data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "Test container create with invalid Userns", + NoParallel: true, // Changes system config so running in non parallel mode + 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-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } }, }, } @@ -473,34 +613,32 @@ func addUser(username string, hostID string, helpers test.Helpers) { ExitCode: 0}) } -func removeUsernsConfig(t *testing.T, userns string, helpers test.Helpers) { +func removeUsernsConfig(t tig.T, userns string, helpers test.Helpers) { delUser(userns, helpers) delGroup(userns, helpers) - tempDir := helpers.T().TempDir() + tempDir := t.TempDir() files := []string{"subuid", "subgid"} for _, file := range files { fileBak := filepath.Join(tempDir, file) s, err := os.Open(fileBak) if err != nil { - t.Logf("failed to open %s, Error: %s", fileBak, err) + t.Log(fmt.Sprintf("failed to open %s, Error: %s", fileBak, err)) continue } defer s.Close() d, err := os.Open(filepath.Join("/etc/%s", file)) if err != nil { - t.Logf("failed to open %s, Error: %s", file, err) + t.Log(fmt.Sprintf("failed to open %s, Error: %s", file, err)) continue - } defer d.Close() _, err = io.Copy(d, s) if err != nil { - t.Logf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err) + t.Log(fmt.Sprintf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err)) continue } - } } From 673393687bdd998c5481dc48dca19ec7ad8b2f91 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 14 May 2026 23:42:59 +0100 Subject: [PATCH 26/59] fix: use logs instead of start -a for tty container in TestCreateWithTty With -t, the container's output goes through a pseudoTTY. Attaching via "start -a" does not reliably forward PTY output through Tigron's subprocess pipe, causing the stty check to fail on certain containerd versions (e.g. v1.7.30). Match the original test's approach: start the container without -a, then read its output via "nerdctl logs" which goes through the log driver and is always available after the container exits. Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/container/container_create_linux_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 6e8290902b4..06a712053fc 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -288,20 +288,20 @@ func TestCreateWithTty(t *testing.T) { Description: "create with tty - stty succeeds", Setup: func(data test.Data, helpers test.Helpers) { helpers.Ensure("create", "-t", "--name", data.Identifier(), testutil.CommonImage, "stty") + // start without -a: tty output is not forwarded over a pipe, so + // capturing it via "start -a" is unreliable. Use "logs" instead, + // which reads from the containerd log driver regardless of tty. + helpers.Ensure("start", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("container", "rm", "-f", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("start", "-a", data.Identifier()) + return helpers.Command("logs", data.Identifier()) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, t tig.T) { - assert.Assert(t, strings.Contains(stdout, "speed 38400 baud; line = 0;"), - fmt.Sprintf("expected stdout to contain speed info: %q", stdout)) - }, + Output: expect.Contains("speed 38400 baud; line = 0;"), } }, }, From 3806454768f2bef393b5166f49ab6450a5d43799 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Fri, 15 May 2026 11:45:06 +0100 Subject: [PATCH 27/59] refactor: use tigron expect constants for exit codes Replace hardcoded exit code literals (0, 1) in Tigron test.Expected structs with the named constants from the expect package: ExitCodeSuccess, ExitCodeGenericFail. Existing ExitCodeNoCheck usages were already correct. Signed-off-by: Ogulcan Aydogan --- .../container/container_create_linux_test.go | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 06a712053fc..72431064fcb 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -51,7 +51,7 @@ func TestCreateWithLabel(t *testing.T) { } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { fooLabel := strings.TrimSpace(helpers.Capture("inspect", "--format", `{{index .Config.Labels "foo"}}`, data.Identifier())) assert.Equal(t, "bar", fooLabel) @@ -126,7 +126,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeCreateCommand("host"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { startOut := helpers.Capture("start", "-a", data.Identifier()) assert.Assert(t, strings.Contains(startOut, data.Labels().Get(defaultMacKey)), @@ -144,7 +144,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeCreateCommand("none"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { helpers.Ensure("start", "-a", data.Identifier()) }, @@ -162,18 +162,18 @@ func TestCreateWithMACAddress(t *testing.T) { if nerdtest.IsDocker() { // Docker delays the failure to start time return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { helpers.Command("start", "-i", "-a", data.Identifier()). Run(&test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("container")}, }) }, } } return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("container")}, } }, @@ -187,7 +187,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeCreateCommand("bridge"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { startOut := helpers.Capture("start", "-a", data.Identifier()) assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), @@ -205,7 +205,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeDynamicCreateCommand(networkBridgeKey), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { startOut := helpers.Capture("start", "-a", data.Identifier()) assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), @@ -223,7 +223,7 @@ func TestCreateWithMACAddress(t *testing.T) { Command: makeDynamicCreateCommand(networkMACvlanKey), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { startOut := helpers.Capture("start", "-a", data.Identifier()) assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), @@ -243,18 +243,18 @@ func TestCreateWithMACAddress(t *testing.T) { if nerdtest.IsDocker() { // Docker delays the failure to start time return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { helpers.Command("start", "-i", "-a", data.Identifier()). Run(&test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("not support")}, }) }, } } return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("not support")}, } }, @@ -279,7 +279,7 @@ func TestCreateWithTty(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("stty: standard input: Not a tty")}, } }, @@ -363,7 +363,7 @@ func TestIssue2993(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("is already used by ID")}, Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) @@ -410,7 +410,7 @@ func TestIssue2993(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Errors: []error{}, Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) @@ -464,7 +464,7 @@ func TestCreateFromOCIArchive(t *testing.T) { } testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { assert.Assert(t, strings.Contains(stdout, sentinel), fmt.Sprintf("expected stdout to contain %q: %q", sentinel, stdout)) @@ -504,7 +504,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) assert.NilError(t, err, "Failed to get container host UID") @@ -528,7 +528,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, } }, }, @@ -543,7 +543,7 @@ func TestUsernsMappingCreateCmd(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, } }, }, @@ -608,9 +608,9 @@ func appendUsernsConfig(userns string, hostUID string, helpers test.Helpers) err func addUser(username string, hostID string, helpers test.Helpers) { helpers.Custom("groupadd", "-g", hostID, username).Run(&test.Expected{ - ExitCode: 0}) + ExitCode: expect.ExitCodeSuccess}) helpers.Custom("useradd", "-u", hostID, "-g", hostID, "-s", "/bin/false", username).Run(&test.Expected{ - ExitCode: 0}) + ExitCode: expect.ExitCodeSuccess}) } func removeUsernsConfig(t tig.T, userns string, helpers test.Helpers) { From a271d8b6ee640ba40e3ad907adac757d133536b5 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Sat, 16 May 2026 23:09:29 +0100 Subject: [PATCH 28/59] refactor: use explicit ExitCode constant in test.Expected struct Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/container/container_create_linux_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 72431064fcb..e0921d1937e 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -301,7 +301,8 @@ func TestCreateWithTty(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains("speed 38400 baud; line = 0;"), + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("speed 38400 baud; line = 0;"), } }, }, From 819768e87b6ab219dcb6144101ef85007d17be2d Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Mon, 8 Jun 2026 17:21:36 +0100 Subject: [PATCH 29/59] test: address review feedback on container_create migration Capture the start -a output and assert the passed MAC is absent for the none-network case, instead of only checking the exit code. Use data.Temp().Save for the Dockerfile instead of os.WriteFile, and drop the manual builder prune --all --force which the Build requirement deliberately omits to keep build tests parallelizable. Signed-off-by: Ogulcan Aydogan --- cmd/nerdctl/container/container_create_linux_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index e0921d1937e..81aa8be40c4 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -146,7 +146,9 @@ func TestCreateWithMACAddress(t *testing.T) { return &test.Expected{ ExitCode: expect.ExitCodeSuccess, Output: func(stdout string, t tig.T) { - helpers.Ensure("start", "-a", data.Identifier()) + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, !strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to not contain MAC %q: %q", data.Labels().Get(macAddressKey), startOut)) }, } }, @@ -441,8 +443,7 @@ func TestCreateFromOCIArchive(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) - err := os.WriteFile(filepath.Join(data.Temp().Path(), "Dockerfile"), []byte(dockerfile), 0644) - assert.NilError(helpers.T(), err) + data.Temp().Save(dockerfile, "Dockerfile") imageName := data.Identifier("image") + ":latest" tarPath := data.Temp().Path("image.tar") @@ -458,7 +459,6 @@ func TestCreateFromOCIArchive(t *testing.T) { testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) helpers.Anyhow("rmi", "-f", data.Labels().Get("imageName")) - helpers.Anyhow("builder", "prune", "--all", "--force") } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("start", "--attach", data.Identifier()) From b2432a46783bfd6244115a961852a90b12308325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:32:40 +0000 Subject: [PATCH 30/59] build(deps): bump the golang-x group with 5 updates Bumps the golang-x group with 5 updates: | Package | From | To | | --- | --- | --- | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.52.0` | `0.53.0` | | [golang.org/x/sync](https://github.com/golang/sync) | `0.20.0` | `0.21.0` | | [golang.org/x/sys](https://github.com/golang/sys) | `0.45.0` | `0.46.0` | | [golang.org/x/term](https://github.com/golang/term) | `0.43.0` | `0.44.0` | | [golang.org/x/text](https://github.com/golang/text) | `0.37.0` | `0.38.0` | Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0 - [Commits](https://github.com/golang/crypto/compare/v0.52.0...v0.53.0) Updates `golang.org/x/sync` from 0.20.0 to 0.21.0 - [Commits](https://github.com/golang/sync/compare/v0.20.0...v0.21.0) Updates `golang.org/x/sys` from 0.45.0 to 0.46.0 - [Commits](https://github.com/golang/sys/compare/v0.45.0...v0.46.0) Updates `golang.org/x/term` from 0.43.0 to 0.44.0 - [Commits](https://github.com/golang/term/compare/v0.43.0...v0.44.0) Updates `golang.org/x/text` from 0.37.0 to 0.38.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sync dependency-version: 0.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/sys dependency-version: 0.46.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/term dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x - dependency-name: golang.org/x/text dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 9fe72491c4b..cfcf4697c39 100644 --- a/go.mod +++ b/go.mod @@ -64,12 +64,12 @@ require ( github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.52.0 + golang.org/x/crypto v0.53.0 golang.org/x/net v0.55.0 - golang.org/x/sync v0.20.0 //gomodjail:unconfined - golang.org/x/sys v0.45.0 //gomodjail:unconfined - golang.org/x/term v0.43.0 //gomodjail:unconfined - golang.org/x/text v0.37.0 + golang.org/x/sync v0.21.0 //gomodjail:unconfined + golang.org/x/sys v0.46.0 //gomodjail:unconfined + golang.org/x/term v0.44.0 //gomodjail:unconfined + golang.org/x/text v0.38.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) diff --git a/go.sum b/go.sum index e7b238f02d3..f9071828469 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= @@ -398,8 +398,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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= @@ -419,8 +419,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -430,8 +430,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -441,8 +441,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= From cb8cf0042057b02e1bead6cda4314fafb26a4a80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:32:42 +0000 Subject: [PATCH 31/59] build(deps): bump golang.org/x/net in the golang-x group Bumps the golang-x group with 1 update: [golang.org/x/net](https://github.com/golang/net). Updates `golang.org/x/net` from 0.55.0 to 0.56.0 - [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: golang-x ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index cfcf4697c39..8af11a60589 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.53.0 - golang.org/x/net v0.55.0 + golang.org/x/net v0.56.0 golang.org/x/sync v0.21.0 //gomodjail:unconfined golang.org/x/sys v0.46.0 //gomodjail:unconfined golang.org/x/term v0.44.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index f9071828469..1a0e9ab0970 100644 --- a/go.sum +++ b/go.sum @@ -386,8 +386,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 36844386ad4c8b8128143c719aeb1434b5baa990 Mon Sep 17 00:00:00 2001 From: fourierrr Date: Thu, 11 Jun 2026 21:25:38 +0800 Subject: [PATCH 32/59] image: add overlaybd vsize option Signed-off-by: fourierrr --- cmd/nerdctl/image/image_convert.go | 6 ++ cmd/nerdctl/image/image_convert_test.go | 86 +++++++++++++++++++++++++ docs/command-reference.md | 4 ++ docs/overlaybd.md | 2 + pkg/api/types/image_types.go | 2 + pkg/cmd/image/convert.go | 1 + 6 files changed, 101 insertions(+) create mode 100644 cmd/nerdctl/image/image_convert_test.go diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index c253a2274e4..105dd0ea161 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -88,6 +88,7 @@ func convertCommand() *cobra.Command { cmd.Flags().Bool("overlaybd", false, "Convert tar.gz layers to overlaybd layers") cmd.Flags().String("overlaybd-fs-type", "ext4", "Filesystem type for overlaybd") cmd.Flags().String("overlaybd-dbstr", "", "Database config string for overlaybd") + cmd.Flags().Int("overlaybd-vsize", 64, "Virtual block device size in GB for overlaybd") // #endregion // #region soci flags @@ -226,6 +227,10 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { if err != nil { return types.ImageConvertOptions{}, err } + overlaybdVsize, err := cmd.Flags().GetInt("overlaybd-vsize") + if err != nil { + return types.ImageConvertOptions{}, err + } // #endregion // #region soci flags @@ -307,6 +312,7 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { Overlaybd: overlaybd, OverlayFsType: overlaybdFsType, OverlaydbDBStr: overlaybdDbstr, + OverlaybdVsize: overlaybdVsize, }, SociConvertOptions: types.SociConvertOptions{ Soci: soci, diff --git a/cmd/nerdctl/image/image_convert_test.go b/cmd/nerdctl/image/image_convert_test.go new file mode 100644 index 00000000000..c9ab36ff2e5 --- /dev/null +++ b/cmd/nerdctl/image/image_convert_test.go @@ -0,0 +1,86 @@ +/* + 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 image + +import ( + "testing" + + "github.com/spf13/cobra" + "gotest.tools/v3/assert" +) + +func TestConvertOptionsOverlaybdVsize(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + want int + }{ + { + name: "default", + want: 64, + }, + { + name: "explicit value", + flags: map[string]string{ + "overlaybd-vsize": "128", + }, + want: 128, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := convertCommand() + addRootFlagsForConvertOptionsTest(t, cmd) + assert.NilError(t, cmd.Flags().Set("overlaybd", "true")) + for name, value := range tc.flags { + assert.NilError(t, cmd.Flags().Set(name, value)) + } + + got, err := convertOptions(cmd) + assert.NilError(t, err) + assert.Equal(t, got.OverlaybdVsize, tc.want) + }) + } +} + +func addRootFlagsForConvertOptionsTest(t *testing.T, cmd *cobra.Command) { + t.Helper() + + flags := cmd.Flags() + flags.Bool("debug", false, "") + flags.Bool("debug-full", false, "") + flags.String("address", "", "") + flags.String("namespace", "default", "") + flags.String("snapshotter", "", "") + flags.String("cni-path", "", "") + flags.String("cni-netconfpath", "", "") + flags.String("data-root", t.TempDir(), "") + flags.String("cgroup-manager", "", "") + flags.Bool("insecure-registry", false, "") + flags.StringSlice("hosts-dir", nil, "") + flags.Bool("experimental", false, "") + flags.String("host-gateway-ip", "", "") + flags.String("bridge-ip", "", "") + flags.Bool("kube-hide-dupe", false, "") + flags.StringSlice("cdi-spec-dirs", nil, "") + flags.StringSlice("global-dns", nil, "") + flags.StringSlice("global-dns-opts", nil, "") + flags.StringSlice("global-dns-search", nil, "") + flags.Bool("selinux-enabled", false, "") +} diff --git a/docs/command-reference.md b/docs/command-reference.md index b32633b2139..2195e6ee7bb 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1012,6 +1012,10 @@ Flags: - `--zstdchunked-record-in=` : read `ctr-remote optimize --record-out=` record file. :warning: This flag is experimental and subject to change. - `--zstdchunked-compression-level=`: zstd:chunked compression level (default: 3) - `--zstdchunked-chunk-size=`: zstd:chunked chunk size +- `--overlaybd` : convert tar.gz layers to overlaybd layers. Should be used in conjunction with '--oci' +- `--overlaybd-fs-type=` : filesystem type for overlaybd (default: `ext4`) +- `--overlaybd-dbstr=` : database config string for overlaybd +- `--overlaybd-vsize=` : virtual block device size in GB for overlaybd (default: 64) - `--uncompress` : convert tar.gz layers to uncompressed tar layers - `--oci` : convert Docker media types to OCI media types - `--platform=` : convert content for a specific platform diff --git a/docs/overlaybd.md b/docs/overlaybd.md index e6e6284c42b..c6ca36d4bf3 100644 --- a/docs/overlaybd.md +++ b/docs/overlaybd.md @@ -38,3 +38,5 @@ For more details about how to build overlaybd image, please refer to [accelerate Nerdctl supports to convert an OCI image or docker format v2 image to OverlayBD image by using the `nerdctl image convert` command. Before the conversion, you should have the `overlaybd-snapshotter` binary installed, which build from [accelerated-container-image](https://github.com/containerd/accelerated-container-image). You can run the command like `nerdctl image convert --overlaybd --oci ` to convert the `` to a OverlayBD image whose tag is ``. + +By default, `nerdctl image convert --overlaybd` uses a 64 GB virtual block device size. You can customize it with `--overlaybd-vsize`. diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 29ba08607d9..2053d7dca06 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -139,6 +139,8 @@ type OverlaybdOptions struct { OverlayFsType string // OverlaydbDBStr database config string for overlaybd OverlaydbDBStr string + // OverlaybdVsize virtual block device size in GB for overlaybd + OverlaybdVsize int // #endregion } diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index 7bb0b926ebb..005309fce92 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -394,6 +394,7 @@ func getOBDConvertOpts(options types.ImageConvertOptions) ([]overlaybdconvert.Op obdOpts := []overlaybdconvert.Option{ overlaybdconvert.WithFsType(options.OverlayFsType), overlaybdconvert.WithDbstr(options.OverlaydbDBStr), + overlaybdconvert.WithVsize(options.OverlaybdVsize), } return obdOpts, nil } From 83b54f0c8681143e0361839590beef6504fd4852 Mon Sep 17 00:00:00 2001 From: Ogulcan Aydogan Date: Thu, 11 Jun 2026 20:21:08 +0100 Subject: [PATCH 33/59] refactor: migrate multi_platform_linux_test.go to nerdtest.Setup Replace the legacy testutil.NewBase pattern with the nerdtest.Setup / Tigron test.Case framework throughout multi_platform_linux_test.go. - Migrate all five test cases to test.Case{Setup, Cleanup, Command, Expected} - Replace SubTests with sequential assertMultiPlatformRun calls to fix concurrent platform invocations under the docker driver - Extract randomPort constant and requireMultiPlatformExec requirement - Use Tigron expect constants for exit codes - Add comments explaining why require.Not(nerdtest.Docker) is needed (non-buildx docker build lacks multi-platform; docker push lacks --platform) Signed-off-by: Ogulcan Aydogan --- .../container/multi_platform_linux_test.go | 315 ++++++++++++------ 1 file changed, 219 insertions(+), 96 deletions(-) diff --git a/cmd/nerdctl/container/multi_platform_linux_test.go b/cmd/nerdctl/container/multi_platform_linux_test.go index 01ae208528c..0f7d326c398 100644 --- a/cmd/nerdctl/container/multi_platform_linux_test.go +++ b/cmd/nerdctl/container/multi_platform_linux_test.go @@ -22,17 +22,39 @@ import ( "strings" "testing" - "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func testMultiPlatformRun(base *testutil.Base, alpineImage string) { - t := base.T - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") +// randomPort asks the registry helpers to acquire a free port automatically. +const randomPort = 0 + +// requireMultiPlatformExec skips the test when the host cannot execute +// linux/amd64, linux/arm64 and linux/arm/v7 images (e.g. no binfmt_misc). +var requireMultiPlatformExec = &test.Requirement{ + Check: func(_ test.Data, _ test.Helpers) (bool, string) { + ok, err := platformutil.CanExecProbably("linux/amd64", "linux/arm64", "linux/arm/v7") + if !ok { + msg := "requires multi-platform exec support (linux/amd64, linux/arm64, linux/arm/v7)" + if err != nil { + msg += ": " + err.Error() + } + return false, msg + } + return true, "" + }, +} + +// assertMultiPlatformRun runs uname -m inside image on each platform and +// asserts the expected machine type string. +func assertMultiPlatformRun(helpers test.Helpers, image string) { testCases := map[string]string{ "amd64": "x86_64", "arm64": "aarch64", @@ -41,92 +63,174 @@ func testMultiPlatformRun(base *testutil.Base, alpineImage string) { "linux/arm/v7": "armv7l", } for plat, expectedUnameM := range testCases { - t.Logf("Testing %q (%q)", plat, expectedUnameM) - cmd := base.Cmd("run", "--rm", "--platform="+plat, alpineImage, "uname", "-m") - cmd.AssertOutExactly(expectedUnameM + "\n") + helpers.T().Log(fmt.Sprintf("Testing platform %q (%q)", plat, expectedUnameM)) + helpers.Command("run", "--rm", "--platform="+plat, image, "uname", "-m"). + Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(expectedUnameM + "\n"), + }) } } func TestMultiPlatformRun(t *testing.T) { - base := testutil.NewBase(t) - testMultiPlatformRun(base, testutil.AlpineImage) + testCase := nerdtest.Setup() + + testCase.Require = requireMultiPlatformExec + + testCase.Setup = func(_ test.Data, helpers test.Helpers) { + assertMultiPlatformRun(helpers, testutil.AlpineImage) + } + + testCase.Run(t) } func TestMultiPlatformBuildPush(t *testing.T) { - testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform. - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s -RUN echo dummy - `, testutil.AlpineImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK() - testMultiPlatformRun(base, imageName) - base.Cmd("push", "--platform=amd64,arm64,linux/arm/v7", imageName).AssertOK() + testCase := nerdtest.Setup() + + testCase.Require = require.All( + // non-buildx `docker build` lacks multi-platform support; `docker push` lacks --platform + require.Not(nerdtest.Docker), + nerdtest.Build, + requireMultiPlatformExec, + nerdtest.Registry, + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, randomPort, false) + reg.Setup(data, helpers) + imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, data.Identifier()) + data.Labels().Set("image", imageName) + + dockerfile := fmt.Sprintf("FROM %s\nRUN echo dummy\n", testutil.AlpineImage) + buildCtx := data.Temp().Dir() + data.Temp().Save(dockerfile, "Dockerfile") + + helpers.Ensure("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if img := data.Labels().Get("image"); img != "" { + helpers.Anyhow("rmi", img) + } + helpers.Anyhow("builder", "prune", "--all", "--force") + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + imageName := data.Labels().Get("image") + assertMultiPlatformRun(helpers, imageName) + return helpers.Command("push", "--platform=amd64,arm64,linux/arm/v7", imageName) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } -// TestMultiPlatformBuildPushNoRun tests if the push succeeds in a situation where nerdctl builds -// a Dockerfile without RUN, COPY, etc commands. In such situation, BuildKit doesn't download the base image -// so nerdctl needs to ensure these blobs to be locally available. func TestMultiPlatformBuildPushNoRun(t *testing.T) { - testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform. - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s -CMD echo dummy - `, testutil.AlpineImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK() - testMultiPlatformRun(base, imageName) - base.Cmd("push", "--platform=amd64,arm64,linux/arm/v7", imageName).AssertOK() + testCase := nerdtest.Setup() + + testCase.Require = require.All( + // non-buildx `docker build` lacks multi-platform support; `docker push` lacks --platform + require.Not(nerdtest.Docker), + nerdtest.Build, + requireMultiPlatformExec, + nerdtest.Registry, + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, randomPort, false) + reg.Setup(data, helpers) + imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, data.Identifier()) + data.Labels().Set("image", imageName) + + dockerfile := fmt.Sprintf("FROM %s\nCMD echo dummy\n", testutil.AlpineImage) + buildCtx := data.Temp().Dir() + data.Temp().Save(dockerfile, "Dockerfile") + + helpers.Ensure("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if img := data.Labels().Get("image"); img != "" { + helpers.Anyhow("rmi", img) + } + helpers.Anyhow("builder", "prune", "--all", "--force") + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + imageName := data.Labels().Get("image") + assertMultiPlatformRun(helpers, imageName) + return helpers.Command("push", "--platform=amd64,arm64,linux/arm/v7", imageName) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } func TestMultiPlatformPullPushAllPlatforms(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) - defer base.Cmd("rmi", pushImageName).Run() - - base.Cmd("pull", "--quiet", "--all-platforms", testutil.AlpineImage).AssertOK() - base.Cmd("tag", testutil.AlpineImage, pushImageName).AssertOK() - base.Cmd("push", "--all-platforms", pushImageName).AssertOK() - testMultiPlatformRun(base, pushImageName) + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Docker), + requireMultiPlatformExec, + nerdtest.Registry, + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, randomPort, false) + reg.Setup(data, helpers) + pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, data.Identifier()) + data.Labels().Set("image", pushImageName) + helpers.Ensure("pull", "--quiet", "--all-platforms", testutil.AlpineImage) + helpers.Ensure("tag", testutil.AlpineImage, pushImageName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if img := data.Labels().Get("image"); img != "" { + helpers.Anyhow("rmi", img) + } + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + pushImageName := data.Labels().Get("image") + helpers.Ensure("push", "--all-platforms", pushImageName) + assertMultiPlatformRun(helpers, pushImageName) + return helpers.Command("inspect", "--type=image", pushImageName) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } func TestMultiPlatformComposeUpBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Docker), + nerdtest.Build, + requireMultiPlatformExec, + ) - const dockerComposeYAML = ` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf("FROM %s\nRUN uname -m > /usr/share/nginx/html/index.html\n", testutil.NginxAlpineImage) + composeYAML := ` services: svc0: build: . @@ -144,30 +248,49 @@ services: ports: - 8082:80 ` - dockerfile := fmt.Sprintf(`FROM %s -RUN uname -m > /usr/share/nginx/html/index.html -`, testutil.NginxAlpineImage) + buildCtx := data.Temp().Dir() + composePath := data.Temp().Save(composeYAML, "compose.yaml") + _ = buildCtx + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("composePath", composePath) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - comp.WriteFile("Dockerfile", dockerfile) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + helpers.Ensure("compose", "-f", composePath, "up", "-d", "--build") + } - testCases := map[string]string{ - "http://127.0.0.1:8080": "x86_64", - "http://127.0.0.1:8081": "aarch64", - "http://127.0.0.1:8082": "armv7l", + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if cp := data.Labels().Get("composePath"); cp != "" { + helpers.Anyhow("compose", "-f", cp, "down", "-v") + } + helpers.Anyhow("builder", "prune", "--all", "--force") } - for testURL, expectedIndexHTML := range testCases { - resp, err := nettestutil.HTTPGet(testURL, 5, false) - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - t.Logf("respBody=%q", respBody) - assert.Assert(t, strings.Contains(string(respBody), expectedIndexHTML)) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + urlExpected := map[string]string{ + "http://127.0.0.1:8080": "x86_64", + "http://127.0.0.1:8081": "aarch64", + "http://127.0.0.1:8082": "armv7l", + } + for url, expected := range urlExpected { + resp, err := nettestutil.HTTPGet(url, 5, false) + if err != nil { + helpers.T().Log(fmt.Sprintf("GET %s: %v", url, err)) + helpers.T().FailNow() + } + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + helpers.T().Log(fmt.Sprintf("reading body from %s: %v", url, err)) + helpers.T().FailNow() + } + if !strings.Contains(string(body), expected) { + helpers.T().Log(fmt.Sprintf("expected %q in body from %s, got %q", expected, url, string(body))) + helpers.T().Fail() + } + } + return helpers.Command("compose", "-f", data.Labels().Get("composePath"), "ps") } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } From 5a04185fd9c9f5de6e2b6c7141ce43ff0515f9d5 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Fri, 12 Jun 2026 17:57:42 +0400 Subject: [PATCH 34/59] fix(network): allow inspecting pseudo networks Signed-off-by: immanuwell --- cmd/nerdctl/network/network_inspect_test.go | 2 - pkg/netutil/cni_plugin.go | 8 ++++ pkg/netutil/netutil.go | 41 +++++++++++++++++++-- pkg/netutil/netutil_test.go | 30 +++++++++++++++ 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 980ade0ad57..84e5e6f9dcb 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -62,7 +62,6 @@ func TestNetworkInspectBasic(t *testing.T) { }, { Description: "none", - Require: nerdtest.NerdctlNeedsFixing("no issue opened"), Command: test.Command("network", "inspect", "none"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network @@ -74,7 +73,6 @@ func TestNetworkInspectBasic(t *testing.T) { }, { Description: "host", - Require: nerdtest.NerdctlNeedsFixing("no issue opened"), Command: test.Command("network", "inspect", "host"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network diff --git a/pkg/netutil/cni_plugin.go b/pkg/netutil/cni_plugin.go index f4d65359f2e..b44e76042e2 100644 --- a/pkg/netutil/cni_plugin.go +++ b/pkg/netutil/cni_plugin.go @@ -20,6 +20,14 @@ type CNIPlugin interface { GetPluginType() string } +type pseudoNetworkPlugin struct { + PluginType string `json:"type"` +} + +func (p *pseudoNetworkPlugin) GetPluginType() string { + return p.PluginType +} + type IPAMRange struct { Subnet string `json:"subnet"` RangeStart string `json:"rangeStart,omitempty"` diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index c3312bb5191..5f213d8692f 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -60,8 +60,17 @@ func (e *CNIEnv) ListNetworksMatch(reqs []string, allowPseudoNetwork bool) (list list = make(map[string][]*NetworkConfig) for _, req := range reqs { - if !allowPseudoNetwork && (req == "host" || req == "none") { - errs = append(errs, fmt.Errorf("pseudo network not allowed: %s", req)) + if req == "host" || req == "none" { + if !allowPseudoNetwork { + errs = append(errs, fmt.Errorf("pseudo network not allowed: %s", req)) + continue + } + cfg, err := newPseudoNetworkConfig(req) + if err != nil { + errs = append(errs, err) + continue + } + list[req] = []*NetworkConfig{cfg} continue } @@ -88,6 +97,30 @@ func (e *CNIEnv) ListNetworksMatch(reqs []string, allowPseudoNetwork bool) (list return list, errs } +func newPseudoNetworkConfig(name string) (*NetworkConfig, error) { + confJSON, err := json.Marshal(&cniNetworkConfig{ + CNIVersion: "1.0.0", + Name: name, + // Pseudo networks are not backed by real CNI config files. We still need a + // parseable config object so network inspect can render them consistently. + Plugins: []CNIPlugin{ + &pseudoNetworkPlugin{PluginType: "nerdctl-pseudo"}, + }, + }) + if err != nil { + return nil, err + } + + confList, err := libcni.ConfListFromBytes(confJSON) + if err != nil { + return nil, err + } + + return &NetworkConfig{ + NetworkConfigList: confList, + }, nil +} + func UsedNetworks(ctx context.Context, client *containerd.Client) (map[string][]string, error) { nsService := client.NamespaceService() nsList, err := nsService.List(ctx) @@ -288,8 +321,8 @@ type NetworkConfig struct { type cniNetworkConfig struct { CNIVersion string `json:"cniVersion"` Name string `json:"name"` - ID string `json:"nerdctlID"` - Labels map[string]string `json:"nerdctlLabels"` + ID string `json:"nerdctlID,omitempty"` + Labels map[string]string `json:"nerdctlLabels,omitempty"` Plugins []CNIPlugin `json:"plugins"` } diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index 383054ebd3e..f818c59a1b2 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -379,3 +379,33 @@ func TestFSExistsPropagatesStatError(t *testing.T) { assert.Assert(t, !exists) assert.Assert(t, err != nil) } + +func TestListNetworksMatchIncludesPseudoNetworks(t *testing.T) { + cniConfTestDir := t.TempDir() + cniEnv := CNIEnv{ + Path: t.TempDir(), + NetconfPath: cniConfTestDir, + } + + values := map[string]string{ + "network_name": "regular-network", + "subnet": "10.7.1.0/24", + "gateway": "10.7.1.1", + } + tpl, err := template.New("test").Parse(preExistingNetworkConfigTemplate) + assert.NilError(t, err) + buf := &bytes.Buffer{} + assert.NilError(t, tpl.ExecuteTemplate(buf, "test", values)) + + testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", testutil.Identifier(t))) + assert.NilError(t, filesystem.WriteFile(testConfFile, buf.Bytes(), 0600)) + + matches, errs := cniEnv.ListNetworksMatch([]string{"host", "none", "regular-network"}, true) + assert.Assert(t, len(errs) == 0) + assert.Equal(t, len(matches["host"]), 1) + assert.Equal(t, matches["host"][0].Name, "host") + assert.Equal(t, len(matches["none"]), 1) + assert.Equal(t, matches["none"][0].Name, "none") + assert.Equal(t, len(matches["regular-network"]), 1) + assert.Equal(t, matches["regular-network"][0].Name, "regular-network") +} From 92cfa9136f0273acc9b0b28c1b8e832216317042 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:07:56 +0900 Subject: [PATCH 35/59] update runc (1.4.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 669dc764bbb..9db6cd32e46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- ARG CONTAINERD_VERSION=v2.3.1@64b425cf570b3b8dd1d4cc46da7c1fce65c6651a -ARG RUNC_VERSION=v1.4.2@c241c0bb5e60a8e8c1b2e53d4eca8d0068d8d57e +ARG RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build From b2cb37ffac3e1a5c73841537fd67fac86196b6cc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:05 +0900 Subject: [PATCH 36/59] update fuse-overlayfs (1.17) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 | 6 ------ Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 | 6 ++++++ 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 create mode 100644 Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 diff --git a/Dockerfile b/Dockerfile index 9db6cd32e46..4927a99fd61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ ARG ROOTLESSKIT_VERSION=v3.0.1@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS -ARG FUSE_OVERLAYFS_VERSION=v1.16@BINARY +ARG FUSE_OVERLAYFS_VERSION=v1.17@BINARY ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.7@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 deleted file mode 100644 index edf43283f18..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 +++ /dev/null @@ -1,6 +0,0 @@ -6c9ee54166fe7d33ebbfb085812585441f22ebe2a24a868d0a878d3127bcb89e fuse-overlayfs-aarch64 -fc2a73ace8eb6a0553204532de615d782cb98d86deeb0fa7b5d14347d0b95823 fuse-overlayfs-armv7l -3c07b76b432a5b4e5e0ccd986919b478d096701178617175b0c71bcce7c6f6a0 fuse-overlayfs-ppc64le -404fd7a762255d554e70849612fb6979639e1eb23a740487dbe3bac2bccc37c1 fuse-overlayfs-riscv64 -9e96cfe091b4342b8de3e239a96d5fecfb8692fbb4a201c256790c270526fd1b fuse-overlayfs-s390x -30c6b9e192600d6854e13397974c709d7cabd980b7d1a4d67defd8eb69677e91 fuse-overlayfs-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 new file mode 100644 index 00000000000..16001445004 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 @@ -0,0 +1,6 @@ +34c9995c929dd52f45cca985858d7e58d9a9626104bc2610db218aaa11115c23 fuse-overlayfs-aarch64 +1611b906052e22bcdbcb4ede5d208823c175a41368c589fb6fb3cb58d4b32b2b fuse-overlayfs-armv7l +9481de8b724e53a2a7f582f4b8bac59240231cda9fcc6131b05e76cafb95b06a fuse-overlayfs-ppc64le +5821eed68e1aed7ca2c510c2f714f95bfcefb87c49d3f703cd18754e5cb23a3c fuse-overlayfs-riscv64 +a5b991b3edb080ce4160370e3f5b1dd424ffdc4d6e0d8bb0c4a42adfa2fb5f8c fuse-overlayfs-s390x +1684ef18c337702a0378a4e9942802770c83b11aed6a93c445d43e641a1f3c90 fuse-overlayfs-x86_64 From fc283bc5b092c4e9fff40d541535a16d307b939b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:15 +0900 Subject: [PATCH 37/59] update nydus (2.4.3) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4927a99fd61..5e79dd12b2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,7 +47,7 @@ ARG GO_VERSION=1.26 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 -ARG NYDUS_VERSION=v2.4.1 +ARG NYDUS_VERSION=v2.4.3 ARG SOCI_SNAPSHOTTER_VERSION=0.13.0 ARG KUBO_VERSION=v0.41.0 From fd2a303034261dc52712a20a82a00d509fa76aa1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:26 +0900 Subject: [PATCH 38/59] update soci-snapshotter (0.14.1) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5e79dd12b2a..3b35150cec8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.4.3 -ARG SOCI_SNAPSHOTTER_VERSION=0.13.0 +ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 ARG KUBO_VERSION=v0.41.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 9ec133365c2c46d1b4408ff17a19816fcacfb25e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Sun, 14 Jun 2026 03:08:47 +0900 Subject: [PATCH 39/59] update kubo (0.42.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3b35150cec8..6666a4e1cdc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,7 +49,7 @@ ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 ARG NYDUS_VERSION=v2.4.3 ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 -ARG KUBO_VERSION=v0.41.0 +ARG KUBO_VERSION=v0.42.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx From 0925492a3389f29170ee54eb0caf031687e95092 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Sun, 14 Jun 2026 22:07:10 +0900 Subject: [PATCH 40/59] fix: show Pid as `0` for stopped containers in `nerdctl container inspect` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Docker, running `docker container inspect` on a stopped container shows the Pid as `0`. ```bash > docker ps -a --filter=name=nginx CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c8dcc84e427f nginx "/docker-entrypoint.…" 14 seconds ago Exited (0) 8 seconds ago nginx > docker container inspect nginx | grep 'Pid"' "Pid": 0, ``` However, running `nerdctl container inspect` on a stopped container still shows a stale Pid from the already-exited process. ```bash > sudo nerdctl ps -a --filter=name=nginx CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4ce4cb9b1ed1 docker.io/library/nginx:latest "/docker-entrypoint.…" 16 seconds ago Exited (0) 4 seconds ago nginx > sn container inspect nginx | grep 'Pid"' "Pid": 1531853, ``` The docker daemon subscribes to containerd's events and when it detects a task exit event, it deletes the task and updates the state of the container it manages, setting Pid to `0`. Therefore, in Docker the Pid becomes `0` after the container stops. - https://github.com/moby/moby/blob/docker-v29.5.3/daemon/monitor.go#L34 On the other hand, nerdctl has no long-running daemon like the docker daemon, so it does not subscribe to containerd's events. As a result, the task is not deleted even after the container stops, and `nerdctl container inspect` still shows the stale Pid of the already-exited process returned by `task.Pid()`. Therefore, this commit changes the behavior so that `nerdctl container inspect` on a stopped container shows the Pid as `0`, for compatibility with Docker. Signed-off-by: Hayato Kiwata --- cmd/nerdctl/container/container_inspect_linux_test.go | 2 ++ pkg/containerinspector/containerinspector.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 99c6fcebc17..6be9a2fd510 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -336,6 +336,7 @@ func TestContainerInspectState(t *testing.T) { } assert.Assert(tt, strings.Contains(inspect.State.Error, "executable file not found in $PATH"), fmt.Sprintf("expected: %s, actual: %s", "executable file not found in $PATH", inspect.State.Error)) assert.Equal(tt, expectedErrStatus, inspect.State.Status) + assert.Equal(tt, 0, inspect.State.Pid) }), }, { @@ -361,6 +362,7 @@ func TestContainerInspectState(t *testing.T) { inspect := dc[0] assert.Assert(tt, strings.Contains(inspect.State.Error, ""), fmt.Sprintf("expected: %s, actual: %s", "", inspect.State.Error)) assert.Equal(tt, "exited", inspect.State.Status) + assert.Equal(tt, 0, inspect.State.Pid) }), }, } diff --git a/pkg/containerinspector/containerinspector.go b/pkg/containerinspector/containerinspector.go index a3a77e1e60d..22ea8b2b6ba 100644 --- a/pkg/containerinspector/containerinspector.go +++ b/pkg/containerinspector/containerinspector.go @@ -57,6 +57,9 @@ func Inspect(ctx context.Context, container containerd.Container) (*native.Conta log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect Status") return n, nil } + if st.Status == containerd.Stopped { + n.Process.Pid = 0 + } n.Process.Status = st netNS, err := InspectNetNS(ctx, n.Process.Pid) if err != nil { From d15940d148f0b178150572651a3679c025c0945b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:29:43 +0000 Subject: [PATCH 41/59] build(deps): bump github.com/moby/sys/mount Bumps the moby-sys group with 1 update in the / directory: [github.com/moby/sys/mount](https://github.com/moby/sys). Updates `github.com/moby/sys/mount` from 0.3.4 to 0.3.5 - [Release notes](https://github.com/moby/sys/releases) - [Commits](https://github.com/moby/sys/compare/mount/v0.3.4...mount/v0.3.5) --- updated-dependencies: - dependency-name: github.com/moby/sys/mount dependency-version: 0.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: moby-sys ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8af11a60589..b9ea2201cba 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined github.com/moby/moby/client v0.4.1 github.com/moby/moby/v2 v2.0.0-beta.16 - github.com/moby/sys/mount v0.3.4 + github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined github.com/moby/sys/userns v0.1.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 1a0e9ab0970..afe2f7a567f 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ github.com/moby/moby/v2 v2.0.0-beta.16 h1:Q/PcJ+Oq8QKBauKpWwHJPJIH7qiIDceDviZSO+ github.com/moby/moby/v2 v2.0.0-beta.16/go.mod h1:HuTyDPU29YJ5EMCvQ2tcomx72rt9FK5bmjFYTZMiMTM= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= -github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= -github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= +github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= From 8bf3c1b57e74afba962ec7efd566b334bd16e105 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Mon, 15 Jun 2026 23:26:33 +0900 Subject: [PATCH 42/59] fix: suppress warning message on a stopped container in nerdctl container inspect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently, when running `nerdctl container inspect` on a stopped container, the following warning message is shown: ```bash > sudo nerdctl ps -a --filter=name=nginx CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4ce4cb9b1ed1 docker.io/library/nginx:latest "/docker-entrypoint.…" 14 hours ago Exited (0) 8 seconds ago nginx > sudo nerdctl container inspect nginx WARN[0000] failed to inspect NetNS error="failed to Statfs \"/proc/1548927/ns/net\": no such file or directory" id=4ce4cb9b1ed12f65b02e0f4af8a753d0e9ad1d42d5b77dac206c396021d90ab0 ... ``` This warning occurs because `/proc//ns/net` is referenced using a stale PID, but the process for a stopped container has already been released. `/proc//ns/net` should not be referenced when the container is stopped. Therefore, this commit suppresses the warning message above when running `nerdctl container inspect` on a stopped container. Signed-off-by: Hayato Kiwata --- pkg/containerinspector/containerinspector.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/containerinspector/containerinspector.go b/pkg/containerinspector/containerinspector.go index a3a77e1e60d..c8bddd15e84 100644 --- a/pkg/containerinspector/containerinspector.go +++ b/pkg/containerinspector/containerinspector.go @@ -58,11 +58,13 @@ func Inspect(ctx context.Context, container containerd.Container) (*native.Conta return n, nil } n.Process.Status = st - netNS, err := InspectNetNS(ctx, n.Process.Pid) - if err != nil { - log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect NetNS") - return n, nil + if st.Status == containerd.Running || st.Status == containerd.Paused { + netNS, err := InspectNetNS(ctx, n.Process.Pid) + if err != nil { + log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect NetNS") + return n, nil + } + n.Process.NetNS = netNS } - n.Process.NetNS = netNS return n, nil } From a73a720b92a8ba30d4406d6ad05b29ab9434305a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:32:33 +0000 Subject: [PATCH 43/59] build(deps): bump github.com/pelletier/go-toml/v2 from 2.3.1 to 2.4.0 Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.3.1 to 2.4.0. - [Release notes](https://github.com/pelletier/go-toml/releases) - [Commits](https://github.com/pelletier/go-toml/compare/v2.3.1...v2.4.0) --- updated-dependencies: - dependency-name: github.com/pelletier/go-toml/v2 dependency-version: 2.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b9ea2201cba..fc5039168d7 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 github.com/opencontainers/selinux v1.15.1 - github.com/pelletier/go-toml/v2 v2.3.1 + github.com/pelletier/go-toml/v2 v2.4.0 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined diff --git a/go.sum b/go.sum index afe2f7a567f..d73dcf35a0a 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1: github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA= github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.4.0 h1:Mwu0mAkUKbittDs3/ADDWXqMmq3EOK2VHiuCkV00Row= +github.com/pelletier/go-toml/v2 v2.4.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= From 5397366f67b9d925759d2cd2317046ba7cd06e71 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Thu, 18 Jun 2026 23:12:19 +0400 Subject: [PATCH 44/59] fix: remove duplicated defaults from help output Signed-off-by: immanuwell --- cmd/nerdctl/container/container_run.go | 6 +-- .../container/container_run_help_test.go | 44 +++++++++++++++++++ cmd/nerdctl/image/image_history.go | 2 +- cmd/nerdctl/image/image_history_help_test.go | 40 +++++++++++++++++ 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 cmd/nerdctl/container/container_run_help_test.go create mode 100644 cmd/nerdctl/image/image_history_help_test.go diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 38c85a097fa..1a7384a7101 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -89,7 +89,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().Bool("help", false, "show help") cmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY") - cmd.Flags().Bool("sig-proxy", true, "Proxy received signals to the process (default true)") + cmd.Flags().Bool("sig-proxy", true, "Proxy received signals to the process") cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always|on-failure:n|unless-stopped")`) cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -159,7 +159,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringP("memory", "m", "", "Memory limit") cmd.Flags().String("memory-reservation", "", "Memory soft limit") cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100) (default -1)") + cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)") cmd.Flags().Bool("oom-kill-disable", false, "Disable OOM Killer") cmd.Flags().Int("oom-score-adj", 0, "Tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)") @@ -219,7 +219,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux capabilities") cmd.RegisterFlagCompletionFunc("cap-drop", capShellComplete) cmd.Flags().Bool("privileged", false, "Give extended privileges to this container") - cmd.Flags().String("systemd", "false", "Allow running systemd in this container (default: false)") + cmd.Flags().String("systemd", "false", "Allow running systemd in this container") // #endregion // #region runtime flags diff --git a/cmd/nerdctl/container/container_run_help_test.go b/cmd/nerdctl/container/container_run_help_test.go new file mode 100644 index 00000000000..91021bdcc2f --- /dev/null +++ b/cmd/nerdctl/container/container_run_help_test.go @@ -0,0 +1,44 @@ +/* + 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 ( + "bytes" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestRunHelpDoesNotDuplicateDefaults(t *testing.T) { + cmd := RunCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stdout) + cmd.SetArgs([]string{"--help"}) + + err := cmd.Execute() + assert.NilError(t, err) + + help := stdout.String() + assert.Assert(t, strings.Contains(help, "Proxy received signals to the process (default true)")) + assert.Assert(t, !strings.Contains(help, "Proxy received signals to the process (default true) (default true)")) + assert.Assert(t, strings.Contains(help, "Tune container memory swappiness (0 to 100) (default -1)")) + assert.Assert(t, !strings.Contains(help, "Tune container memory swappiness (0 to 100) (default -1) (default -1)")) + assert.Assert(t, strings.Contains(help, "Allow running systemd in this container (default \"false\")")) + assert.Assert(t, !strings.Contains(help, "Allow running systemd in this container (default: false) (default \"false\")")) +} diff --git a/cmd/nerdctl/image/image_history.go b/cmd/nerdctl/image/image_history.go index 79384701f9e..a6f20416f58 100644 --- a/cmd/nerdctl/image/image_history.go +++ b/cmd/nerdctl/image/image_history.go @@ -63,7 +63,7 @@ func addHistoryFlags(cmd *cobra.Command) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().BoolP("quiet", "q", false, "Only show numeric IDs") - cmd.Flags().BoolP("human", "H", true, "Print sizes and dates in human readable format (default true)") + cmd.Flags().BoolP("human", "H", true, "Print sizes and dates in human readable format") cmd.Flags().Bool("no-trunc", false, "Don't truncate output") } diff --git a/cmd/nerdctl/image/image_history_help_test.go b/cmd/nerdctl/image/image_history_help_test.go new file mode 100644 index 00000000000..f1b1bec4f2a --- /dev/null +++ b/cmd/nerdctl/image/image_history_help_test.go @@ -0,0 +1,40 @@ +/* + 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 image + +import ( + "bytes" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestHistoryHelpDoesNotDuplicateDefaults(t *testing.T) { + cmd := HistoryCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stdout) + cmd.SetArgs([]string{"--help"}) + + err := cmd.Execute() + assert.NilError(t, err) + + help := stdout.String() + assert.Assert(t, strings.Contains(help, "Print sizes and dates in human readable format (default true)")) + assert.Assert(t, !strings.Contains(help, "Print sizes and dates in human readable format (default true) (default true)")) +} From c8b1e8eec9b862e4f2a20dc1088c9ea12a3fdbdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:32:28 +0000 Subject: [PATCH 45/59] build(deps): bump actions/checkout from 6.0.3 to 7.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ghcr-image-build-and-publish.yml | 2 +- .github/workflows/job-build.yml | 2 +- .github/workflows/job-lint-go.yml | 2 +- .github/workflows/job-lint-other.yml | 2 +- .github/workflows/job-lint-project.yml | 2 +- .github/workflows/job-test-dependencies.yml | 2 +- .github/workflows/job-test-in-container.yml | 2 +- .github/workflows/job-test-in-host.yml | 2 +- .github/workflows/job-test-in-lima-freebsd.yml | 2 +- .github/workflows/job-test-in-lima.yml | 2 +- .github/workflows/job-test-unit.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/workflow-flaky.yml | 2 +- .github/workflows/workflow-lint.yml | 2 +- .github/workflows/workflow-tigron.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 3e29cc3c49c..8e554fd72d8 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index bb04ce034f0..0040f9cd89d 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -35,7 +35,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index fc61d0725a8..4ba1d420c3b 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -39,7 +39,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 7ebab308780..b754a2482b7 100644 --- a/.github/workflows/job-lint-other.yml +++ b/.github/workflows/job-lint-other.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index 6bc1fac639d..fb4d849db3e 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -30,7 +30,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 100 path: src/github.com/containerd/nerdctl diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml index 6e8f823ae48..c100b9446b5 100644 --- a/.github/workflows/job-test-dependencies.yml +++ b/.github/workflows/job-test-dependencies.yml @@ -31,7 +31,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index 72eb0aeaa72..76789ef09c7 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -67,7 +67,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index e768186e1a9..09430928c1a 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -83,7 +83,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima-freebsd.yml b/.github/workflows/job-test-in-lima-freebsd.yml index ef22c852a99..a7d53d64c8d 100644 --- a/.github/workflows/job-test-in-lima-freebsd.yml +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -17,7 +17,7 @@ jobs: runs-on: "${{ inputs.runner }}" steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index d8c0911dfd1..3eec402053b 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -31,7 +31,7 @@ jobs: GUEST: ${{ inputs.guest }} steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml index 6d81764564b..7e444244616 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -46,7 +46,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37d39d42620..1913f7f3ed3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index 70a8b67aefb..5d507c9d281 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -49,7 +49,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 98932baf37b..ebc5bd6ed91 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Init: checkout" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index f72aa8771a8..9de351395f9 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -35,7 +35,7 @@ jobs: canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 100 persist-credentials: false From 195e5a859514fe99b5d3bf24c86a8191fb4d8328 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:32:35 +0000 Subject: [PATCH 46/59] build(deps): bump github.com/cyphar/filepath-securejoin Bumps [github.com/cyphar/filepath-securejoin](https://github.com/cyphar/filepath-securejoin) from 0.6.1 to 0.7.0. - [Release notes](https://github.com/cyphar/filepath-securejoin/releases) - [Changelog](https://github.com/cyphar/filepath-securejoin/blob/main/CHANGELOG.md) - [Commits](https://github.com/cyphar/filepath-securejoin/compare/v0.6.1...v0.7.0) --- updated-dependencies: - dependency-name: github.com/cyphar/filepath-securejoin dependency-version: 0.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b9ea2201cba..31a1d6876ec 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/containernetworking/plugins v1.9.1 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.7.0 - github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined + github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 github.com/docker/cli v29.5.3+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.7.0 @@ -147,7 +147,7 @@ require ( ) require ( - cyphar.com/go-pathrs v0.2.1 // indirect + cyphar.com/go-pathrs v0.2.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/moby/moby/api v1.54.2 // indirect github.com/moby/sys/capability v0.4.0 // indirect diff --git a/go.sum b/go.sum index afe2f7a567f..84a43d93560 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= -cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik= +cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= 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/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -81,8 +81,8 @@ github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= +github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= From 0879c88272172c998feb8c6f27e4cf89fde2fd64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 22:33:05 +0000 Subject: [PATCH 47/59] build(deps): bump the docker group across 1 directory with 3 updates Bumps the docker group with 3 updates in the / directory: [github.com/docker/cli](https://github.com/docker/cli), [github.com/moby/moby/client](https://github.com/moby/moby) and [github.com/moby/moby/v2](https://github.com/moby/moby). Updates `github.com/docker/cli` from 29.5.3+incompatible to 29.6.0+incompatible - [Commits](https://github.com/docker/cli/compare/v29.5.3...v29.6.0) Updates `github.com/moby/moby/client` from 0.4.1 to 0.5.0 - [Release notes](https://github.com/moby/moby/releases) - [Changelog](https://github.com/moby/moby/blob/v0.5.0/CHANGELOG.md) - [Commits](https://github.com/moby/moby/compare/v0.4.1...v0.5.0) Updates `github.com/moby/moby/v2` from 2.0.0-beta.16 to 2.0.0-beta.18 - [Release notes](https://github.com/moby/moby/releases) - [Commits](https://github.com/moby/moby/compare/v2.0.0-beta.16...v2.0.0-beta.18) --- updated-dependencies: - dependency-name: github.com/docker/cli dependency-version: 29.6.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/client dependency-version: 0.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker - dependency-name: github.com/moby/moby/v2 dependency-version: 2.0.0-beta.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: docker ... Signed-off-by: dependabot[bot] --- go.mod | 24 +++++++++++------------ go.sum | 60 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index b9ea2201cba..8a467b523cc 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/coreos/go-systemd/v22 v22.7.0 github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.3+incompatible //gomodjail:unconfined + github.com/docker/cli v29.6.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.7.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined @@ -43,8 +43,8 @@ require ( github.com/ipfs/go-cid v0.6.1 github.com/klauspost/compress v1.18.6 github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined - github.com/moby/moby/client v0.4.1 - github.com/moby/moby/v2 v2.0.0-beta.16 + github.com/moby/moby/client v0.5.0 + github.com/moby/moby/v2 v2.0.0-beta.18 github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined @@ -103,7 +103,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect github.com/moby/sys/symlink v0.3.0 // indirect github.com/mr-tron/base58 v1.3.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect @@ -128,16 +128,16 @@ require ( github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/mod v0.36.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + golang.org/x/mod v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect //gomodjail:unconfined - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/grpc v1.81.1 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -149,7 +149,7 @@ require ( require ( cyphar.com/go-pathrs v0.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/api v1.55.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect sigs.k8s.io/knftables v0.0.18 // indirect diff --git a/go.sum b/go.sum index afe2f7a567f..ac47cb01419 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= -github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc= +github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -204,20 +204,20 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N 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= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= -github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= -github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= -github.com/moby/moby/v2 v2.0.0-beta.16 h1:Q/PcJ+Oq8QKBauKpWwHJPJIH7qiIDceDviZSO+Qz4VA= -github.com/moby/moby/v2 v2.0.0-beta.16/go.mod h1:HuTyDPU29YJ5EMCvQ2tcomx72rt9FK5bmjFYTZMiMTM= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/moby/v2 v2.0.0-beta.18 h1:eOu0ZKNhBbtLmjVVHfEhpM6PEVG0SZZoRhdSLMZo2TY= +github.com/moby/moby/v2 v2.0.0-beta.18/go.mod h1:Br23XQzTa+rD+5bhxB1E6+0CkXTN+jMWKZC8m8rnih8= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= @@ -270,8 +270,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= @@ -328,18 +328,18 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -370,8 +370,8 @@ 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.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= 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= @@ -464,15 +464,15 @@ 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-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= 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= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 9f714bd95b71ab7b5ec27488346e543a70954432 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 10:34:06 +0900 Subject: [PATCH 48/59] CI: disable canary Failing since the release of go1.27rc1 ``` level=warning msg="[runner] Can't run linter goanalysis_metalinter: inspect: failed to load package context: could not load export data: internal error in importing \"context\" (cannot decode \"context\", export data version 4 is greater than maximum supported version 2); please report an issue" level=error msg="Running error: can't run linter goanalysis_metalinter\ninspect: failed to load package context: could not load export data: internal error in importing \"context\" (cannot decode \"context\", export data version 4 is greater than maximum supported version 2); please report an issue" make[1]: *** [Makefile:142: lint-go] Error 3 make[1]: Leaving directory '/home/runner/work/nerdctl/nerdctl' make: *** [Makefile:148: lint-go-all] Error 2 ``` Workaround for issue 4979 Signed-off-by: Akihiro Suda --- .github/workflows/workflow-lint.yml | 12 +++++++----- .github/workflows/workflow-test.yml | 17 ++++++++++------- .github/workflows/workflow-tigron.yml | 5 +++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 98932baf37b..72643ded61f 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -33,9 +33,10 @@ jobs: - runner: ubuntu-24.04 goos: windows # Additionally lint for canary - - runner: ubuntu-24.04 - goos: linux - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: ubuntu-24.04 + # goos: linux + # canary: true with: timeout: 10 go-version: "1.26" @@ -72,8 +73,9 @@ jobs: include: - go-version: "1.26" # Additionally build for canary - - go-version: "1.26" - canary: true + # FIXME: failing since the release of go1.27rc1 + # - go-version: "1.26" + # canary: true with: timeout: 10 go-version: ${{ matrix.go-version }} diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 8afed875f1c..0a791265daa 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -26,8 +26,9 @@ jobs: - runner: "ubuntu-24.04" - runner: "macos-15" - runner: "windows-2025" - - runner: "ubuntu-24.04" - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: "ubuntu-24.04" + # canary: true with: runner: ${{ matrix.runner }} canary: ${{ matrix.canary && true || false }} @@ -104,9 +105,10 @@ jobs: ipv6: true skip-flaky: true # all canary - - runner: ubuntu-24.04 - target: rootful - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: ubuntu-24.04 + # target: rootful + # canary: true with: timeout: 80 @@ -128,8 +130,9 @@ jobs: include: # Test on windows w/o canary - runner: windows-2022 - - runner: windows-2025 - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: windows-2025 + # canary: true # Test docker on linux - runner: ubuntu-24.04 binary: docker diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index f72aa8771a8..3a7ed5cad85 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -31,8 +31,9 @@ jobs: - runner: windows-2022 - runner: ubuntu-24.04 goos: freebsd - - runner: ubuntu-24.04 - canary: go-canary + # FIXME: failing since the release of go1.27rc1 + # - runner: ubuntu-24.04 + # canary: go-canary steps: - name: "Checkout project" uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 From c7088353ccbc294ed786c0614d79107dbfeba4dd Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 12:26:56 +0900 Subject: [PATCH 49/59] CI: docker: skip TestRunSeccompCapSysPtrace and TestUpdateRestartPolicy These tests have been failing on Docker since ubuntu-24.04 image 20260615.205.1. Workaround for issue 4978 Signed-off-by: Akihiro Suda --- cmd/nerdctl/container/container_run_restart_linux_test.go | 7 ++++++- cmd/nerdctl/container/container_run_security_linux_test.go | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 5a3431b0d47..17761188e39 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -30,6 +30,7 @@ import ( "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" @@ -276,7 +277,11 @@ func TestRunRestartWithUnlessStopped(t *testing.T) { func TestUpdateRestartPolicy(t *testing.T) { testCase := nerdtest.Setup() - if !nerdtest.IsDocker() { + if nerdtest.IsDocker() { + // FIXME: failing on Docker since ubuntu-24.04 image 20260615.205.1 + // https://github.com/containerd/nerdctl/issues/4978 + testCase.Require = require.Not(nerdtest.Docker) + } else { testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) } diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index dd99f2d4699..3f0a6e0c891 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -345,6 +345,10 @@ func TestRunSelinuxWithVolumeLabel(t *testing.T) { func TestRunSeccompCapSysPtrace(t *testing.T) { testCase := nerdtest.Setup() + // FIXME: failing on Docker since ubuntu-24.04 image 20260615.205.1 + // https://github.com/containerd/nerdctl/issues/4978 + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--cap-add", "sys_ptrace", testutil.AlpineImage, "sh", "-euxc", "apk add -q strace && strace true") } From 9e39e33441383bdc016143a0b0f910bb64170c2f Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 10:29:30 +0900 Subject: [PATCH 50/59] update BuildKit (0.31.0) Signed-off-by: Akihiro Suda --- Dockerfile | 2 +- Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 | 2 -- Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 | 2 ++ 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 create mode 100644 Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 diff --git a/Dockerfile b/Dockerfile index 6666a4e1cdc..384b8cba0ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ ARG RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.30.0@BINARY +ARG BUILDKIT_VERSION=v0.31.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 deleted file mode 100644 index 0beafcc769b..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 +++ /dev/null @@ -1,2 +0,0 @@ -2da148c50540409988c837e62b72176e4a9ad7855548a2dd6ea1cd0c0d2d360d buildkit-v0.30.0.linux-amd64.tar.gz -d0c9601e49f441dcc5c6493c884275a5470dcc9c188c96573c4dd503f2fde97d buildkit-v0.30.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 new file mode 100644 index 00000000000..2ce0cd2cb67 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 @@ -0,0 +1,2 @@ +a13e961a4e4e1afbece1ddfd6818ff726fe4577a99b2763e677456016827f4b7 buildkit-v0.31.0.linux-amd64.tar.gz +74efb4326bac95ecc562a139d34833f4ae7a8df884780910c7ef2cb50e92db66 buildkit-v0.31.0.linux-arm64.tar.gz From db4ef3e6a8d64c1766a9e8a421f7c0fd71d43be2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 11:08:10 +0900 Subject: [PATCH 51/59] tests: mark TestIPFSAddrWithKubo flaky Workaround for issue 4838 Signed-off-by: Akihiro Suda --- cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go index de1dc16d239..e770d9f8422 100644 --- a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go @@ -43,6 +43,7 @@ func TestIPFSAddrWithKubo(t *testing.T) { require.Not(nerdtest.Docker), nerdtest.Registry, nerdtest.Private, + nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4838"), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { From 6594ceba4c75f0575ecfa5332a8c0a394dc9b3f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 04:40:30 +0000 Subject: [PATCH 52/59] build(deps): bump github.com/containerd/containerd/v2 Bumps [github.com/containerd/containerd/v2](https://github.com/containerd/containerd) from 2.3.1 to 2.3.2. - [Release notes](https://github.com/containerd/containerd/releases) - [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md) - [Commits](https://github.com/containerd/containerd/compare/v2.3.1...v2.3.2) --- updated-dependencies: - dependency-name: github.com/containerd/containerd/v2 dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 34eee991f14..246436fa89e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.11.1 - github.com/containerd/containerd/v2 v2.3.1 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.3.2 //gomodjail:unconfined github.com/containerd/continuity v0.5.0 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined diff --git a/go.sum b/go.sum index 6ff2145f0b8..de4e8c13c0b 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/q github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.1 h1:4dVXBdlvotRBlaP2TmNbY/EGc06KJrMDDUqQdxX/HOk= -github.com/containerd/containerd/v2 v2.3.1/go.mod h1:xVoxGPWZBwwph8DF2IbDhriLKdHfjdpO0b3wFP9wQ1I= +github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM= +github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= From 366a10b3deb5624cf590b2ee8b1aed970ab90617 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 19 Jun 2026 10:26:58 +0900 Subject: [PATCH 53/59] update containerd (2.3.2) Signed-off-by: Akihiro Suda --- .github/workflows/workflow-test.yml | 14 +++++++------- Dockerfile | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 0a791265daa..2afcd149afb 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -52,7 +52,7 @@ jobs: - runner: ubuntu-24.04-arm # Additionally build for old containerd on amd - runner: ubuntu-24.04 - containerd-version: v1.7.30 + containerd-version: v1.7.33 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -81,7 +81,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.7.30 + containerd-version: v1.7.33 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-24.04 @@ -98,7 +98,7 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.7.30 + containerd-version: v1.7.33 # ipv6 - runner: ubuntu-24.04 target: rootful @@ -159,13 +159,13 @@ jobs: # Windows CI still requires containerd v2.2. # [v2.3.0 regression] The virtual machine or container JSON document is invalid. (Hyper-V container) # https://github.com/containerd/containerd/issues/13254 - windows-containerd-version: 2.2.3 - windows-containerd-sha: 81314dd5e3baad958acae0e4d1ff21eb27b7c8f8809232ab06c9f397cd221e02 - linux-containerd-version: 2.3.1 + windows-containerd-version: 2.2.5 + windows-containerd-sha: 8724c3a873b4984f5ee092c8f15c1a98ebbb0f968106cf8f5849ea100f3a0236 + linux-containerd-version: 2.3.2 # FIXME: containerd SHAs are not verified for authenticity (only affects tests) # https://github.com/containerd/nerdctl/issues/4666 # Note: these are for amd64 - linux-containerd-sha: 628448bd973610c656c1cbea8e88b32fafd85b23cc1aa4a3372eb7198478c054 + linux-containerd-sha: 75625e6f6595bb95f3fb9c8123a60534af4a8d9b52d7617065967bcefe71a17a linux-containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.1 linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/Dockerfile b/Dockerfile index 384b8cba0ee..244c8590452 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.1@64b425cf570b3b8dd1d4cc46da7c1fce65c6651a +ARG CONTAINERD_VERSION=v2.3.2@fff62f14765df376e5fc36f5a8f8e795b5670f61 ARG RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY From 2254f8e537c2a4ab6f4f832a2c1359f11204d40b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 16 Jun 2026 20:29:06 +0900 Subject: [PATCH 54/59] Use client.WithImageConfigLabels for image config labels Signed-off-by: Akihiro Suda --- pkg/cmd/container/create.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 264c4ecede2..eb1de8df27d 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -206,6 +206,12 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa if err != nil { return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err } + // Image config labels must be applied before any other label-setting opt: + // containerd.WithImageConfigLabels resets the container labels, so running it + // later would clear labels set by other opts (e.g. the restart policy). + if ensuredImage != nil { + cOpts = append(cOpts, containerd.WithImageConfigLabels(ensuredImage.Image)) + } opts = append(opts, rootfsOpts...) cOpts = append(cOpts, rootfsCOpts...) if options.UserNS != "" { @@ -357,7 +363,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.healthcheck = healthcheckConfig } - lCOpts, err := withContainerLabels(options.Label, options.LabelFile, ensuredImage) + lCOpts, err := withContainerLabels(options.Label, options.LabelFile) if err != nil { return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err } @@ -650,15 +656,9 @@ func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) { }, nil } -func withContainerLabels(label, labelFile []string, ensuredImage *imgutil.EnsuredImage) ([]containerd.NewContainerOpts, error) { +func withContainerLabels(label, labelFile []string) ([]containerd.NewContainerOpts, error) { var opts []containerd.NewContainerOpts - // add labels defined by image - if ensuredImage != nil { - imageLabelOpts := containerd.WithAdditionalContainerLabels(ensuredImage.ImageConfig.Labels) - opts = append(opts, imageLabelOpts) - } - labelMap, err := readKVStringsMapfFromLabel(label, labelFile) if err != nil { return nil, err From 97e5145980326488928f9add62986232cc74b20e Mon Sep 17 00:00:00 2001 From: Park jungtae Date: Sun, 14 Jun 2026 13:19:44 +0900 Subject: [PATCH 55/59] test: refactor container_run_test.go to use Tigron Signed-off-by: Park jungtae --- cmd/nerdctl/container/container_run_test.go | 850 +++++++++++--------- 1 file changed, 454 insertions(+), 396 deletions(-) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index ea424a2c889..8ad9c6d8c21 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -17,8 +17,6 @@ package container import ( - "bufio" - "bytes" "errors" "fmt" "os" @@ -32,15 +30,12 @@ import ( "gotest.tools/v3/assert" "gotest.tools/v3/icmd" - "gotest.tools/v3/poll" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -309,175 +304,237 @@ func TestRunStdin(t *testing.T) { } func TestRunWithJsonFileLogDriver(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("json-file log driver is not yet implemented on Windows") + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--log-driver", "json-file", "--log-opt", "max-size=5K", "--log-opt", "max-file=2", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000") } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK() - - time.Sleep(3 * time.Second) - inspectedContainer := base.InspectContainer(containerName) - logJSONPath := filepath.Dir(inspectedContainer.LogPath) - // matches = current log file + old log files to retain - matches, err := filepath.Glob(filepath.Join(logJSONPath, inspectedContainer.ID+"*")) - assert.NilError(t, err) - if len(matches) != 2 { - t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - for _, file := range matches { - fInfo, err := os.Stat(file) - assert.NilError(t, err) - // The log file size is compared to 5200 bytes (instead 5k) to keep docker compatibility. - // Docker log rotation lacks precision because the size check is done at the log entry level - // and not at the byte level (io.Writer), so docker log files can exceed 5k - if fInfo.Size() > 5200 { - t.Fatal("file size exceeded 5k") + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + time.Sleep(3 * time.Second) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + logJSONPath := filepath.Dir(inspect.LogPath) + // matches = current log file + old log files to retain + matches, err := filepath.Glob(filepath.Join(logJSONPath, inspect.ID+"*")) + assert.NilError(t, err) + assert.Equal(t, len(matches), 2, "the number of log files is not equal to 2 files, got: %v", matches) + for _, file := range matches { + fInfo, err := os.Stat(file) + assert.NilError(t, err) + // The log file size is compared to 5200 bytes (instead 5k) to keep docker compatibility. + // Docker log rotation lacks precision because the size check is done at the log entry level + // and not at the byte level (io.Writer), so docker log files can exceed 5k + assert.Assert(t, fInfo.Size() <= 5200, "file size exceeded 5k: %s", file) + } + }, } } + + testCase.Run(t) } func TestRunWithJsonFileLogDriverAndLogPathOpt(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("json-file log driver is not yet implemented on Windows") + testCase := nerdtest.Setup() + testCase.Require = require.All(require.Not(require.Windows), require.Not(nerdtest.Docker)) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + customLogJSONPath := filepath.Join(data.Temp().Path(), data.Identifier(), data.Identifier()+"-json.log") + data.Labels().Set("logPath", customLogJSONPath) + helpers.Ensure("run", "-d", "--log-driver", "json-file", + "--log-opt", fmt.Sprintf("log-path=%s", customLogJSONPath), + "--log-opt", "max-size=5K", "--log-opt", "max-file=2", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000") } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", containerName).AssertOK() - customLogJSONPath := filepath.Join(t.TempDir(), containerName, containerName+"-json.log") - base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", fmt.Sprintf("log-path=%s", customLogJSONPath), "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK() - - time.Sleep(3 * time.Second) - rawBytes, err := os.ReadFile(customLogJSONPath) - assert.NilError(t, err) - if len(rawBytes) == 0 { - t.Fatalf("logs are not written correctly to log-path: %s", customLogJSONPath) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - // matches = current log file + old log files to retain - matches, err := filepath.Glob(filepath.Join(filepath.Dir(customLogJSONPath), containerName+"*")) - assert.NilError(t, err) - if len(matches) != 2 { - t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + time.Sleep(3 * time.Second) + return helpers.Command("inspect", data.Identifier()) } - for _, file := range matches { - fInfo, err := os.Stat(file) - assert.NilError(t, err) - if fInfo.Size() > 5200 { - t.Fatal("file size exceeded 5k") + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + customLogJSONPath := data.Labels().Get("logPath") + rawBytes, err := os.ReadFile(customLogJSONPath) + assert.NilError(t, err) + assert.Assert(t, len(rawBytes) > 0, "logs are not written correctly to log-path: %s", customLogJSONPath) + // matches = current log file + old log files to retain + matches, err := filepath.Glob(filepath.Join(filepath.Dir(customLogJSONPath), data.Identifier()+"*")) + assert.NilError(t, err) + assert.Equal(t, len(matches), 2, "the number of log files is not equal to 2 files, got: %v", matches) + for _, file := range matches { + fInfo, err := os.Stat(file) + assert.NilError(t, err) + assert.Assert(t, fInfo.Size() <= 5200, "file size exceeded 5k: %s", file) + } + }, } } + + testCase.Run(t) } -func TestRunWithJournaldLogDriver(t *testing.T) { - testutil.RequireExecutable(t, "journalctl") +func waitForJournaldLogs(since, filter string, expected ...string) { journalctl, _ := exec.LookPath("journalctl") - res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) - if res.ExitCode != 0 { - t.Skipf("current user is not allowed to access journal logs: %s", res.Combined()) + deadline := time.Now().Add(20 * time.Second) + for time.Now().Before(deadline) { + res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", since, filter)) + found := true + for _, s := range expected { + if !strings.Contains(res.Stdout(), s) { + found = false + break + } + } + if found { + break + } + time.Sleep(100 * time.Millisecond) } +} - if runtime.GOOS == "windows" { - t.Skip("journald log driver is not yet implemented on Windows") +func journaldRequire() *test.Requirement { + return require.All( + require.Not(require.Windows), + require.Binary("journalctl"), + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + journalctl, _ := exec.LookPath("journalctl") + res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) + if res.ExitCode != expect.ExitCodeSuccess { + return false, fmt.Sprintf("current user is not allowed to access journal logs: %s", res.Combined()) + } + return true, "journald is accessible" + }, + }, + ) +} + +func journaldExpected() func(data test.Data, helpers test.Helpers) *test.Expected { + return func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("foo"), + expect.Contains("bar"), + ), + } } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) +} - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", "journald", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() +func TestRunWithJournaldLogDriver(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = journaldRequire() - time.Sleep(3 * time.Second) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + startTime := time.Now().Format("2006-01-02 15:04:05") + helpers.Ensure("run", "-d", "--log-driver", "journald", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + data.Labels().Set("startTime", startTime) + data.Labels().Set("shortID", inspect.ID[:12]) + data.Labels().Set("containerName", data.Identifier()) + } - inspectedContainer := base.InspectContainer(containerName) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - type testCase struct { - name string - filter string + type journaldTC struct { + description string + filter func(data test.Data) string } - testCases := []testCase{ + + tcs := []journaldTC{ { - name: "filter journald logs using SYSLOG_IDENTIFIER field", - filter: fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12]), + description: "filter journald logs using SYSLOG_IDENTIFIER field", + filter: func(data test.Data) string { return fmt.Sprintf("SYSLOG_IDENTIFIER=%s", data.Labels().Get("shortID")) }, }, { - name: "filter journald logs using CONTAINER_NAME field", - filter: fmt.Sprintf("CONTAINER_NAME=%s", containerName), + description: "filter journald logs using CONTAINER_NAME field", + filter: func(data test.Data) string { + return fmt.Sprintf("CONTAINER_NAME=%s", data.Labels().Get("containerName")) + }, }, { - name: "filter journald logs using IMAGE_NAME field", - filter: fmt.Sprintf("IMAGE_NAME=%s", testutil.CommonImage), + description: "filter journald logs using IMAGE_NAME field", + filter: func(data test.Data) string { return fmt.Sprintf("IMAGE_NAME=%s", testutil.CommonImage) }, }, } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - found := 0 - check := func(log poll.LogT) poll.Result { - res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", tc.filter)) - assert.Equal(t, 0, res.ExitCode, res) - if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") { - found = 1 - return poll.Success() - } - return poll.Continue("reading from journald is not yet finished") - } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second)) - assert.Equal(t, 1, found) + + for _, tc := range tcs { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: tc.description, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + journalctl, _ := exec.LookPath("journalctl") + filter := tc.filter(data) + since := data.Labels().Get("startTime") + waitForJournaldLogs(since, filter, "foo", "bar") + return helpers.Custom(journalctl, "--no-pager", "--since", since, filter) + }, + Expected: journaldExpected(), }) } + + testCase.Run(t) } func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) { - testutil.RequireExecutable(t, "journalctl") - journalctl, _ := exec.LookPath("journalctl") - res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) - if res.ExitCode != 0 { - t.Skipf("current user is not allowed to access journal logs: %s", res.Combined()) + testCase := nerdtest.Setup() + testCase.Require = journaldRequire() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + startTime := time.Now().Format("2006-01-02 15:04:05") + helpers.Ensure("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + data.Labels().Set("startTime", startTime) + data.Labels().Set("fullID", inspect.ID) + waitForJournaldLogs(startTime, fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspect.ID), "foo", "bar") } - if runtime.GOOS == "windows" { - t.Skip("journald log driver is not yet implemented on Windows") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() - - time.Sleep(3 * time.Second) - inspectedContainer := base.InspectContainer(containerName) - found := 0 - check := func(log poll.LogT) poll.Result { - res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID))) - assert.Equal(t, 0, res.ExitCode, res) - if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") { - found = 1 - return poll.Success() - } - return poll.Continue("reading from journald is not yet finished") + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + journalctl, _ := exec.LookPath("journalctl") + return helpers.Custom(journalctl, "--no-pager", "--since", data.Labels().Get("startTime"), + fmt.Sprintf("SYSLOG_IDENTIFIER=%s", data.Labels().Get("fullID"))) } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second)) - assert.Equal(t, 1, found) + + testCase.Expected = journaldExpected() + + testCase.Run(t) } func TestRunWithLogBinary(t *testing.T) { - testutil.RequiresBuild(t) - if runtime.GOOS == "windows" { - t.Skip("buildkit is not enabled on windows, this feature may work on windows.") - } - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) + "-image" - containerName := testutil.Identifier(t) + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.Build, + require.Not(require.Windows), + require.Not(nerdtest.Docker), + ) var dockerfile = ` FROM ` + testutil.GolangImage + ` as builder @@ -539,312 +596,323 @@ FROM scratch COPY --from=builder /go/src/logger/logger / ` - buildCtx := helpers.CreateBuildContext(t, dockerfile) - tmpDir := t.TempDir() - base.Cmd("build", buildCtx, "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", tmpDir)).AssertOK() - defer base.Cmd("image", "rm", "-f", imageName).AssertOK() - - base.Cmd("container", "rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", fmt.Sprintf("binary://%s/logger", tmpDir), "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - - inspectedContainer := base.InspectContainer(containerName) - bytes, err := os.ReadFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s.log", inspectedContainer.ID, "stdout"))) - assert.NilError(t, err) - log := string(bytes) - assert.Check(t, strings.Contains(log, "foo")) - assert.Check(t, strings.Contains(log, "bar")) -} + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path(), + "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", data.Temp().Path())) + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + } -// history: There was a bug that the --add-host items disappear when the another container created. -// This test ensures that it doesn't happen. -// (https://github.com/containerd/nerdctl/issues/2560) -func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("ocihook is not yet supported on Windows") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + helpers.Anyhow("builder", "prune", "--all", "--force") } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - hostMapping := "test-add-host:10.0.0.1" - base.Cmd("run", "-d", "--add-host", hostMapping, "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("container", "rm", "-f", containerName).Run() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", + "--log-driver", fmt.Sprintf("binary://%s/logger", data.Temp().Path()), + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + } - checkEtcHosts := func(stdout string) error { - matcher, err := regexp.Compile(`^10.0.0.1\s+test-add-host$`) - if err != nil { - return err - } - var found bool - sc := bufio.NewScanner(bytes.NewBufferString(stdout)) - for sc.Scan() { - if matcher.Match(sc.Bytes()) { - found = true - } - } - if !found { - return fmt.Errorf("host not found") + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerID := strings.TrimSpace(stdout) + logBytes, err := os.ReadFile(filepath.Join(os.TempDir(), + fmt.Sprintf("%s_stdout.log", containerID))) + assert.NilError(t, err) + log := string(logBytes) + assert.Assert(t, strings.Contains(log, "foo")) + assert.Assert(t, strings.Contains(log, "bar")) + }, } - return nil } - base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts) - - // run another container - base.Cmd("run", "--rm", testutil.CommonImage).AssertOK() - base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts) + testCase.Run(t) } -// https://github.com/containerd/nerdctl/issues/2726 -func TestRunRmTime(t *testing.T) { - base := testutil.NewBase(t) - base.Cmd("pull", "--quiet", testutil.CommonImage) - t0 := time.Now() - base.Cmd("run", "--rm", testutil.CommonImage, "true").AssertOK() - t1 := time.Now() - took := t1.Sub(t0) - var deadline = 3 * time.Second - // FIXME: Investigate? it appears that since the move to containerd 2 on Windows, this is taking longer. - if runtime.GOOS == "windows" { - deadline = 10 * time.Second - } - if took > deadline { - t.Fatalf("expected to have completed in %v, took %v", deadline, took) - } -} +// history: There was a bug that the --add-host items disappear when the another container created. +// This test ensures that it doesn't happen. +// (https://github.com/containerd/nerdctl/issues/2560) +func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) -func runAttachStdin(t *testing.T, testStr string, args []string) string { - if runtime.GOOS == "windows" { - t.Skip("run attach test is not yet implemented on Windows") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--add-host", "test-add-host:10.0.0.1", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("exec", data.Identifier(), "grep", "10.0.0.1.*test-add-host", "/etc/hosts") } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + } - opts := []func(*testutil.Cmd){ - testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")), + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // run another container to verify --add-host entry is not disturbed + helpers.Ensure("run", "--rm", testutil.CommonImage) + return helpers.Command("exec", data.Identifier(), "cat", "/etc/hosts") } - fullArgs := []string{"run", "--rm", "-i"} - fullArgs = append(fullArgs, args...) - fullArgs = append(fullArgs, - "--name", - containerName, - testutil.CommonImage, + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, + expect.Match(regexp.MustCompile(`(?m)^10\.0\.0\.1\s+test-add-host$`)), ) - defer base.Cmd("rm", "-f", containerName).AssertOK() - result := base.Cmd(fullArgs...).CmdOption(opts...).Run() - - return result.Combined() + testCase.Run(t) } -func runAttach(t *testing.T, testStr string, args []string) string { - if runtime.GOOS == "windows" { - t.Skip("run attach test is not yet implemented on Windows") +// https://github.com/containerd/nerdctl/issues/2726 +func TestRunRmTime(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - fullArgs := []string{"run"} - fullArgs = append(fullArgs, args...) - fullArgs = append(fullArgs, - "--name", - containerName, - testutil.CommonImage, - "sh", - "-euxc", - "echo "+testStr, - ) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + data.Labels().Set("start", time.Now().Format(time.RFC3339Nano)) + return helpers.Command("run", "--rm", testutil.CommonImage, "true") + } - defer base.Cmd("rm", "-f", containerName).AssertOK() - result := base.Cmd(fullArgs...).Run() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + start, _ := time.Parse(time.RFC3339Nano, data.Labels().Get("start")) + took := time.Since(start) + deadline := 3 * time.Second + // FIXME: Investigate? it appears that since the move to containerd 2 on Windows, this is taking longer. + if runtime.GOOS == "windows" { + deadline = 10 * time.Second + } + assert.Assert(t, took <= deadline, "expected to have completed in %v, took %v", deadline, took) + }, + } + } - return result.Combined() + testCase.Run(t) } func TestRunAttachFlag(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) - type testCase struct { - name string + type attachTC struct { + description string args []string - testFunc func(t *testing.T, testStr string, args []string) string + useStdin bool + isError bool testStr string expectedOut string dockerOut string } - testCases := []testCase{ + + tcs := []attachTC{ { - name: "AttachFlagStdin", + description: "AttachFlagStdin", args: []string{"-a", "STDIN", "-a", "STDOUT"}, - testFunc: runAttachStdin, + useStdin: true, testStr: "test-run-stdio", expectedOut: "test-run-stdio", dockerOut: "test-run-stdio", }, { - name: "AttachFlagStdOut", + description: "AttachFlagStdOut", args: []string{"-a", "STDOUT"}, - testFunc: runAttach, testStr: "foo", expectedOut: "foo", dockerOut: "foo", }, { - name: "AttachFlagMixedValue", + description: "AttachFlagMixedValue", args: []string{"-a", "STDIN", "-a", "invalid-value"}, - testFunc: runAttach, + isError: true, testStr: "foo", expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR", dockerOut: "valid streams are STDIN, STDOUT and STDERR", }, { - name: "AttachFlagInvalidValue", + description: "AttachFlagInvalidValue", args: []string{"-a", "invalid-stream"}, - testFunc: runAttach, + isError: true, testStr: "foo", expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR", dockerOut: "valid streams are STDIN, STDOUT and STDERR", }, { - name: "AttachFlagCaseInsensitive", + description: "AttachFlagCaseInsensitive", args: []string{"-a", "stdin", "-a", "stdout"}, - testFunc: runAttachStdin, + useStdin: true, testStr: "test-run-stdio", expectedOut: "test-run-stdio", dockerOut: "test-run-stdio", }, } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - actualOut := tc.testFunc(t, tc.testStr, tc.args) - errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut) - if nerdtest.IsDocker() { - assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg) - } else { - assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg) - } + for _, tc := range tcs { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: tc.description, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + var args []string + if tc.useStdin { + args = append([]string{"run", "--rm", "-i"}, tc.args...) + } else { + args = append([]string{"run"}, tc.args...) + } + args = append(args, "--name", data.Identifier(), testutil.CommonImage) + if !tc.useStdin { + args = append(args, "sh", "-euxc", "echo "+tc.testStr) + } + cmd := helpers.Command(args...) + if tc.useStdin { + cmd.Feed(strings.NewReader("echo " + tc.testStr + "\nexit\n")) + } + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + out := tc.expectedOut + if nerdtest.IsDocker() { + out = tc.dockerOut + } + if tc.isError { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New(out)}, + } + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains(out), + } + }, }) } + + testCase.Run(t) } func TestRunQuiet(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - teardown := func() { - base.Cmd("rmi", "-f", testutil.CommonImage).Run() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", testutil.CommonImage) } - defer teardown() - teardown() - sentinel := "test run quiet" - result := base.Cmd("run", "--rm", "--quiet", testutil.CommonImage, fmt.Sprintf(`echo "%s"`, sentinel)).Run() - assert.Assert(t, strings.Contains(result.Combined(), sentinel)) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + } - wasQuiet := func(output, sentinel string) bool { - return !strings.Contains(output, sentinel) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--quiet", testutil.CommonImage, "echo", "test run quiet") } - // Docker and nerdctl image pulls are not 1:1. - if nerdtest.IsDocker() { - sentinel = "Pull complete" - } else { - sentinel = "resolved" + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + // Docker and nerdctl image pulls are not 1:1. + pullSentinel := "resolved" + if nerdtest.IsDocker() { + pullSentinel = "Pull complete" + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("test run quiet"), + expect.DoesNotContain(pullSentinel), + ), + } } - assert.Assert(t, wasQuiet(result.Combined(), sentinel), "Found %s in container run output", sentinel) + testCase.Run(t) } func TestRunFromOCIArchive(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) + testCase := nerdtest.Setup() + testCase.Require = require.All(nerdtest.Build, require.Not(nerdtest.Docker)) - // Docker does not support running container images from OCI archive. - testutil.DockerIncompatible(t) + const sentinel = "test-nerdctl-run-from-oci-archive" - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + tag := fmt.Sprintf("%s:latest", data.Identifier()) + helpers.Anyhow("rmi", "-f", tag) + + dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) + data.Temp().Save(dockerfile, "Dockerfile") + tarPath := data.Temp().Path(data.Identifier() + ".tar") + helpers.Ensure("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), data.Temp().Path()) + data.Labels().Set("tag", tag) + data.Labels().Set("tarPath", tarPath) + } - teardown := func() { - base.Cmd("rmi", "-f", imageName).Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Labels().Get("tag")) + helpers.Anyhow("builder", "prune", "--all", "--force") } - defer teardown() - teardown() - const sentinel = "test-nerdctl-run-from-oci-archive" - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "%s"]`, testutil.CommonImage, sentinel) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", fmt.Sprintf("oci-archive://%s", data.Labels().Get("tarPath"))) + } - buildCtx := helpers.CreateBuildContext(t, dockerfile) - tag := fmt.Sprintf("%s:latest", imageName) - tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(sentinel)) - base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() - base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(tag, sentinel) + testCase.Run(t) } func TestRunDomainname(t *testing.T) { - t.Parallel() - - if runtime.GOOS == "windows" { - t.Skip("run --hostname not implemented on Windows yet") - } + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) - testCases := []struct { - name string + type domainnameTC struct { + description string hostname string domainname string - Cmd string - CmdFlag string + cmd string + cmdFlag string expectedOut string - }{ + } + + tcs := []domainnameTC{ { - name: "Check domain name", + description: "Check domain name", hostname: "foobar", domainname: "example.com", - Cmd: "hostname", - CmdFlag: "-d", + cmd: "hostname", + cmdFlag: "-d", expectedOut: "example.com", }, { - name: "check fqdn", + description: "check fqdn", hostname: "foobar", domainname: "example.com", - Cmd: "hostname", - CmdFlag: "-f", + cmd: "hostname", + cmdFlag: "-f", expectedOut: "foobar.example.com", }, } - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - - base.Cmd("run", - "--rm", - "--hostname", tc.hostname, - "--domainname", tc.domainname, - testutil.CommonImage, - tc.Cmd, - tc.CmdFlag, - ).AssertOutContains(tc.expectedOut) + for _, tc := range tcs { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: tc.description, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--hostname", tc.hostname, + "--domainname", tc.domainname, + testutil.CommonImage, + tc.cmd, tc.cmdFlag, + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(tc.expectedOut)), }) } + + testCase.Run(t) } func TestRunHealthcheckFlags(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("healthcheck tests are skipped in rootless environment") - } testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Rootless) testCases := []struct { name string @@ -940,8 +1008,6 @@ func TestRunHealthcheckFlags(t *testing.T) { } for _, tc := range testCases { - tc := tc - testCase.SubTests = append(testCase.SubTests, &test.Case{ Description: tc.name, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { @@ -993,72 +1059,66 @@ func TestRunHealthcheckFlags(t *testing.T) { } func TestRunHealthcheckFromImage(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("healthcheck tests are skipped in rootless environment") - } - nerdtest.Setup() - dockerfile := fmt.Sprintf(`FROM %s HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8080 || exit 1 `, testutil.CommonImage) - testCase := &test.Case{ - Require: nerdtest.Build, - Setup: func(data test.Data, helpers test.Helpers) { - data.Temp().Save(dockerfile, "Dockerfile") - data.Labels().Set("image", data.Identifier()) - helpers.Ensure("build", "-t", data.Labels().Get("image"), data.Temp().Path()) + testCase := nerdtest.Setup() + testCase.Require = require.All(nerdtest.Build, require.Not(nerdtest.Rootless)) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("image", data.Identifier()) + helpers.Ensure("build", "-t", data.Labels().Get("image"), data.Temp().Path()) + } + testCase.SubTests = []*test.Case{ + { + Description: "merge_with_image", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--health-retries=5", + "--health-interval=45s", + data.Labels().Get("image")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + hc := inspect.Config.Healthcheck + assert.Assert(t, hc != nil, "expected healthcheck config to be present") + assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "wget -q --spider http://localhost:8080 || exit 1"}) + assert.Equal(t, 5, hc.Retries) // From CLI flags + assert.Equal(t, 45*time.Second, hc.Interval) // From CLI flags + assert.Equal(t, 10*time.Second, hc.Timeout) // From Dockerfile + }), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, }, - SubTests: []*test.Case{ - { - Description: "merge_with_image", - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "-d", "--name", data.Identifier(), - "--health-retries=5", - "--health-interval=45s", - data.Labels().Get("image")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout string, t tig.T) { - inspect := nerdtest.InspectContainer(helpers, data.Identifier()) - hc := inspect.Config.Healthcheck - assert.Assert(t, hc != nil, "expected healthcheck config to be present") - assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "wget -q --spider http://localhost:8080 || exit 1"}) - assert.Equal(t, 5, hc.Retries) // From CLI flags - assert.Equal(t, 45*time.Second, hc.Interval) // From CLI flags - assert.Equal(t, 10*time.Second, hc.Timeout) // From Dockerfile - }), - } - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, + { + Description: "Disable image health checks via runtime flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "run", "-d", "--name", data.Identifier(), + "--no-healthcheck", + data.Labels().Get("image"), + ) }, - { - Description: "Disable image health checks via runtime flag", - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command( - "run", "-d", "--name", data.Identifier(), - "--no-healthcheck", - data.Labels().Get("image"), - ) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout string, t tig.T) { - inspect := nerdtest.InspectContainer(helpers, data.Identifier()) - hc := inspect.Config.Healthcheck - assert.Assert(t, hc != nil, "expected healthcheck config to be present") - assert.DeepEqual(t, hc.Test, []string{"NONE"}) - }), - } - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + hc := inspect.Config.Healthcheck + assert.Assert(t, hc != nil, "expected healthcheck config to be present") + assert.DeepEqual(t, hc.Test, []string{"NONE"}) + }), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) }, }, } @@ -1080,20 +1140,18 @@ func countFIFOFiles(root string) (int, error) { return count, err } func TestCleanupFIFOs(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("/run/containerd/fifo/ doesn't exist on rootless") - } - if runtime.GOOS == "windows" { - t.Skip("test is not compatible with windows") - } - testutil.DockerIncompatible(t) testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(require.Windows), + require.Not(nerdtest.Docker), + require.Not(nerdtest.Rootless), // /run/containerd/fifo/ doesn't exist on rootless + ) testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, }) oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) @@ -1101,7 +1159,7 @@ func TestCleanupFIFOs(t *testing.T) { cmd = helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, }) newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) From 188f20f0c4de1bb6b61d52ca579f265c62c8d03b Mon Sep 17 00:00:00 2001 From: Immanuel Tikhonov Date: Fri, 19 Jun 2026 13:03:06 +0400 Subject: [PATCH 56/59] fix: honor --workdir in compose run Signed-off-by: Immanuel Tikhonov --- cmd/nerdctl/compose/compose_run_linux_test.go | 45 +++++++++++++++++++ pkg/composer/run.go | 2 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index 71efbb9d070..217c21708d0 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -326,6 +326,51 @@ services: testCase.Run(t) } +func TestComposeRunWithWorkdir(t *testing.T) { + const expectedOutput = "/tmp" + + dockerComposeYAML := fmt.Sprintf(` +services: + alpine: + image: %s + entrypoint: + - pwd +`, testutil.CommonImage) + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--workdir", + "/tmp", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.Run(t) +} + func TestComposeRunWithLabel(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` services: diff --git a/pkg/composer/run.go b/pkg/composer/run.go index 84807cd6dc6..b6885c85e4d 100644 --- a/pkg/composer/run.go +++ b/pkg/composer/run.go @@ -155,7 +155,7 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error { } } if ro.WorkDir != "" { - c.project.WorkingDir = ro.WorkDir + targetSvc.WorkingDir = ro.WorkDir } // `compose run` command does not create any of the ports specified in the service configuration. From 814617f5aea6a9d5be9fdbf8d8b899bb7786d433 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Sat, 20 Jun 2026 11:10:02 +0400 Subject: [PATCH 57/59] fix: clarify healthcheck help defaults Signed-off-by: immanuwell --- cmd/nerdctl/container/container_run.go | 6 +++--- cmd/nerdctl/container/container_run_help_test.go | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 1a7384a7101..b52994dd063 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -250,9 +250,9 @@ func setCreateFlags(cmd *cobra.Command) { // Health check flags cmd.Flags().String("health-cmd", "", "Command to run to check health") - cmd.Flags().Duration("health-interval", 0, "Time between running the check (default: 30s)") - cmd.Flags().Duration("health-timeout", 0, "Maximum time to allow one check to run (default: 30s)") - cmd.Flags().Int("health-retries", 0, "Consecutive failures needed to report unhealthy (default: 3)") + cmd.Flags().Duration("health-interval", 0, "Time between running the check; 0 uses the image value or 30s when unset there too") + cmd.Flags().Duration("health-timeout", 0, "Maximum time to allow one check to run; 0 uses the image value or 30s when unset there too") + cmd.Flags().Int("health-retries", 0, "Consecutive failures needed to report unhealthy; 0 uses the image value or 3 when unset there too") cmd.Flags().Duration("health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown") cmd.Flags().Bool("no-healthcheck", false, "Disable any container-specified HEALTHCHECK") diff --git a/cmd/nerdctl/container/container_run_help_test.go b/cmd/nerdctl/container/container_run_help_test.go index 91021bdcc2f..9f8197200b5 100644 --- a/cmd/nerdctl/container/container_run_help_test.go +++ b/cmd/nerdctl/container/container_run_help_test.go @@ -41,4 +41,7 @@ func TestRunHelpDoesNotDuplicateDefaults(t *testing.T) { assert.Assert(t, !strings.Contains(help, "Tune container memory swappiness (0 to 100) (default -1) (default -1)")) assert.Assert(t, strings.Contains(help, "Allow running systemd in this container (default \"false\")")) assert.Assert(t, !strings.Contains(help, "Allow running systemd in this container (default: false) (default \"false\")")) + assert.Assert(t, strings.Contains(help, "Time between running the check; 0 uses the image value or 30s when unset there too")) + assert.Assert(t, strings.Contains(help, "Maximum time to allow one check to run; 0 uses the image value or 30s when unset there too")) + assert.Assert(t, strings.Contains(help, "Consecutive failures needed to report unhealthy; 0 uses the image value or 3 when unset there too")) } From 1e8a519d9d4b381945e45b4abdf15ffb2596f75b Mon Sep 17 00:00:00 2001 From: Aaron Mark <64331623+amarkdotdev@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:07:37 +0000 Subject: [PATCH 58/59] feat: show RootlessKit version in nerdctl version output When running in rootless mode, nerdctl version now includes the RootlessKit version as a server component, matching the format used by docker version. The version is retrieved via the RootlessKit API socket (rootlessutil.NewRootlessKitClient + Info(ctx)) rather than shelling out to rootlesskit --version, which is more robust and avoids PATH issues. The version is only shown when rootless mode is active (i.e. when rootlessutil.IsRootless() is true). If the API socket is unavailable or the Info call fails, a warning is logged and the component is shown without a version string. Fixes https://github.com/containerd/nerdctl/issues/4936 Signed-off-by: Aaron Mark <64331623+amarkdotdev@users.noreply.github.com> --- pkg/infoutil/infoutil.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index 2dee6f88a85..16d0b1f1379 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -35,6 +35,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/version" ) @@ -142,6 +143,9 @@ func ServerVersion(ctx context.Context, client *containerd.Client) (*dockercompa runcVersion(), }, } + if rootlessutil.IsRootless() { + v.Components = append(v.Components, rootlessKitVersion(ctx)) + } return v, nil } @@ -233,6 +237,35 @@ func parseRuncVersion(runcVersionStdout []byte) (*dockercompat.ComponentVersion, } // getMobySysInfo returns the moby system info for the given cgroup manager + +func rootlessKitVersion(ctx context.Context) dockercompat.ComponentVersion { + rc, err := rootlessutil.NewRootlessKitClient() + if err != nil { + log.L.WithError(err).Warnf("unable to connect to RootlessKit API socket") + return dockercompat.ComponentVersion{Name: "rootlesskit"} + } + info, err := rc.Info(ctx) + if err != nil { + log.L.WithError(err).Warnf("unable to retrieve RootlessKit version via API") + return dockercompat.ComponentVersion{Name: "rootlesskit"} + } + details := map[string]string{ + "ApiVersion": info.APIVersion, + "StateDir": info.StateDir, + } + if info.NetworkDriver != nil { + details["NetworkDriver"] = info.NetworkDriver.Driver + } + if info.PortDriver != nil { + details["PortDriver"] = info.PortDriver.Driver + } + return dockercompat.ComponentVersion{ + Name: "rootlesskit", + Version: info.Version, + Details: details, + } +} + func getMobySysInfo(cgroupManager string) *sysinfo.SysInfo { var info dockercompat.Info info.CgroupVersion = CgroupsVersion() From 40b425aa72c7855b9dc77027bcf1ce7e2bfb7fa2 Mon Sep 17 00:00:00 2001 From: Arjun Yogidas Date: Wed, 24 Jun 2026 17:24:20 +0000 Subject: [PATCH 59/59] fix premature exits in image save Signed-off-by: Arjun Yogidas --- pkg/cmd/image/save.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index a35a83a97f5..6aeff7b565c 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -101,22 +101,43 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio storeOpts = append(storeOpts, transferimage.WithExtraReference(imageRef)) } - w := nopWriteCloser{options.Stdout} + w := &syncWriteCloser{ + Writer: options.Stdout, + done: make(chan struct{}), + } pf, done := transferutil.ProgressHandler(ctx, os.Stderr) defer done() - return client.Transfer(ctx, + err = client.Transfer(ctx, transferimage.NewStore("", storeOpts...), tarchive.NewImageExportStream(w, "", exportOpts...), transfer.WithProgress(pf), ) + if err != nil { + return err + } + + // Wait for the stream copy goroutine to finish writing before returning. + // client.Transfer returns when the server-side export completes, but the + // client-side goroutine in containerd's ImageExportStream.MarshalAny may + // still be copying streamed data to the writer. Without this wait, the + // caller may close the output file while the goroutine is still writing, + // resulting in "file already closed" errors and a truncated tar archive. + <-w.done + + return nil } -type nopWriteCloser struct { +// syncWriteCloser wraps an io.Writer and signals when Close is called, +// allowing the caller to wait until the transfer stream goroutine has +// finished writing all data. +type syncWriteCloser struct { io.Writer + done chan struct{} } -func (nopWriteCloser) Close() error { +func (s *syncWriteCloser) Close() error { + close(s.done) return nil }