From a858677c1aaecac11f78da7af77c7dc7452f40d6 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Fri, 26 Dec 2025 19:14:59 +0900 Subject: [PATCH 1/2] test: refactor compose_kill_linux_test.go to use Tigron Signed-off-by: Hayato Kiwata --- .../compose/compose_kill_linux_test.go | 71 +++++++++++++++---- mod/tigron/expect/exit.go | 2 + pkg/testutil/nerdtest/utilities.go | 43 +++++++++++ 3 files changed, 102 insertions(+), 14 deletions(-) diff --git a/cmd/nerdctl/compose/compose_kill_linux_test.go b/cmd/nerdctl/compose/compose_kill_linux_test.go index 8c4687045b5..1d2813e7c1b 100644 --- a/cmd/nerdctl/compose/compose_kill_linux_test.go +++ b/cmd/nerdctl/compose/compose_kill_linux_test.go @@ -18,15 +18,23 @@ package compose import ( "fmt" + "path/filepath" + "regexp" "testing" - "time" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeKill(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerComposeYAML := fmt.Sprintf(` services: wordpress: @@ -54,17 +62,52 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + + wordpressContainerName := serviceparser.DefaultContainerName(projectName, "wordpress", "1") + dbContainerName := serviceparser.DefaultContainerName(projectName, "db", "1") + + data.Labels().Set("composeYAML", composePath) + data.Labels().Set("wordpressContainer", wordpressContainerName) + data.Labels().Set("dbContainer", dbContainerName) + + helpers.Ensure("compose", "-f", composePath, "up", "-d") + nerdtest.EnsureContainerStarted(helpers, wordpressContainerName) + nerdtest.EnsureContainerStarted(helpers, dbContainerName) + } + + testCase.SubTests = []*test.Case{ + { + Description: "kill db container and exit with 137", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYAML"), "kill", "db") + nerdtest.EnsureContainerExited(helpers, data.Labels().Get("dbContainer"), expect.ExitCodeSigkill) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "db", "-a") + }, + // Docker Compose v1: "Exit 137", v2: "exited (137)" + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Match(regexp.MustCompile(` 137|\(137\)`))), + }, + { + Description: "wordpress container is still running", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYAML"), "ps", "wordpress") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Match(regexp.MustCompile("Up|running"))), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("composeYAML") != "" { + helpers.Anyhow("compose", "-f", data.Labels().Get("composeYAML"), "down", "-v") + } + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "kill", "db").AssertOK() - time.Sleep(3 * time.Second) - // Docker Compose v1: "Exit 137", v2: "exited (137)" - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny(" 137", "(137)") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") + testCase.Run(t) } diff --git a/mod/tigron/expect/exit.go b/mod/tigron/expect/exit.go index 4ebdf0df594..897bc16d464 100644 --- a/mod/tigron/expect/exit.go +++ b/mod/tigron/expect/exit.go @@ -19,6 +19,8 @@ package expect const ( // ExitCodeSuccess will ensure that the command effectively ran returned with exit code zero. ExitCodeSuccess = 0 + // ExitCodeSigkill verifies a container exited due to SIGKILL. + ExitCodeSigkill = 137 // ExitCodeGenericFail will verify that the command ran and exited with a non-zero error code. // This does NOT include timeouts, cancellation, or signals. ExitCodeGenericFail = -10 diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index b3f1d15ac2e..a012aed201f 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -151,6 +151,49 @@ func EnsureContainerStarted(helpers test.Helpers, con string) { } } +func EnsureContainerExited(helpers test.Helpers, con string, exitCode int) { + helpers.T().Helper() + exited := false + for i := 0; i < maxRetry && !exited; i++ { + helpers.Command("container", "inspect", con). + Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Container + err := json.Unmarshal([]byte(stdout), &dc) + if err != nil || len(dc) == 0 || (len(dc) > 0 && dc[0].State == nil) { + return + } + assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n") + state := dc[0].State + if state.Running { + return + } + if state.Status != "exited" && state.Status != "dead" { + return + } + // Use a negative exitCode to ignore the exit code and only verify exited/dead state. + if exitCode >= 0 && state.ExitCode != exitCode { + return + } + exited = true + }, + }) + time.Sleep(sleep) + } + + if !exited { + ins := helpers.Capture("container", "inspect", con) + lgs := helpers.Capture("logs", con) + ps := helpers.Capture("ps", "-a") + helpers.T().Log(ins) + helpers.T().Log(lgs) + helpers.T().Log(ps) + helpers.T().Log(fmt.Sprintf("container %s still not exited after %d retries", con, maxRetry)) + helpers.T().FailNow() + } +} + func GenerateJWEKeyPair(data test.Data, helpers test.Helpers) (string, string) { helpers.T().Helper() From eb356cf1109baebe80f63419bb6328a342d42b47 Mon Sep 17 00:00:00 2001 From: Arjun Raja Yogidas Date: Wed, 7 Jan 2026 03:26:59 +0000 Subject: [PATCH 2/2] add MAC, IPv4, IPv6 addresses to nework inspect Signed-off-by: Arjun Raja Yogidas --- cmd/nerdctl/network/network_inspect_test.go | 57 +++++++++++++++++++ pkg/inspecttypes/dockercompat/dockercompat.go | 53 ++++++++++++++--- 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 3b7e5276420..1e4405f03bd 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -397,6 +397,63 @@ func TestNetworkInspect(t *testing.T) { } }, }, + { + Description: "Test container network details", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("test-network")) + + // See https://github.com/containerd/nerdctl/issues/4322 + if runtime.GOOS == "windows" { + time.Sleep(time.Second) + } + + // Create and start a container on this network + helpers.Ensure("run", "-d", "--name", data.Identifier("test-container"), + "--network", data.Identifier("test-network"), + testutil.CommonImage, "sleep", nerdtest.Infinity) + + // Get container ID for later use + containerID := strings.Trim(helpers.Capture("inspect", data.Identifier("test-container"), "--format", "{{.Id}}"), "\n") + data.Labels().Set("containerID", containerID) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("test-container")) + helpers.Anyhow("network", "remove", data.Identifier("test-network")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier("test-network")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, t tig.T) { + var dc []dockercompat.Network + err := json.Unmarshal([]byte(stdout), &dc) + assert.NilError(t, err, "Unable to unmarshal output") + assert.Equal(t, 1, len(dc), "Expected exactly one network") + + network := dc[0] + assert.Equal(t, network.Name, data.Identifier("test-network")) + assert.Equal(t, 1, len(network.Containers), "Expected exactly one container") + + // Get the container details + containerID := data.Labels().Get("containerID") + container := network.Containers[containerID] + + // Test container name + assert.Equal(t, container.Name, data.Identifier("test-container")) + + // Test IPv4 address field exists in response + assert.Assert(t, true, "IPv4Address field exists: %q", container.IPv4Address) + + // Test MAC address field exists in response + assert.Assert(t, true, "MacAddress field exists: %q", container.MacAddress) + + // Test IPv6 address field exists in response + assert.Assert(t, true, "IPv6Address field exists: %q", container.IPv6Address) + }, + } + }, + }, } testCase.Run(t) diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 5ebfb0c2980..31c3d79684d 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -929,9 +929,9 @@ type Network struct { type EndpointResource struct { Name string `json:"Name"` // EndpointID string `json:"EndpointID"` - // MacAddress string `json:"MacAddress"` - // IPv4Address string `json:"IPv4Address"` - // IPv6Address string `json:"IPv6Address"` + MacAddress string `json:"MacAddress"` + IPv4Address string `json:"IPv4Address"` + IPv6Address string `json:"IPv6Address"` } type structuredCNI struct { @@ -975,13 +975,50 @@ func NetworkFromNative(n *native.Network) (*Network, error) { res.Containers = make(map[string]EndpointResource) for _, container := range n.Containers { - res.Containers[container.ID] = EndpointResource{ + endpoint := EndpointResource{ Name: container.Labels[labels.Name], - // EndpointID: container.EndpointID, - // MacAddress: container.MacAddress, - // IPv4Address: container.IPv4Address, - // IPv6Address: container.IPv6Address, } + + // Extract network information from container's NetNS if available + if container.Process != nil && container.Process.NetNS != nil { + for _, x := range container.Process.NetNS.Interfaces { + if x.Interface.Flags&net.FlagLoopback != 0 { + continue + } + if x.Interface.Flags&net.FlagUp == 0 { + continue + } + + // Set MAC address + endpoint.MacAddress = x.HardwareAddr + + // Extract IPv4 and IPv6 addresses + for _, a := range x.Addrs { + ip, _, err := net.ParseCIDR(a) + if err != nil { + continue + } + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + continue + } + + // Check for IPv4 + if ip4 := ip.To4(); ip4 != nil { + endpoint.IPv4Address = ip4.String() + } else if ip6 := ip.To16(); ip6 != nil { + // It's IPv6 + endpoint.IPv6Address = ip6.String() + } + } + + // Use the primary interface if available, otherwise use the first valid interface + if x.Index == container.Process.NetNS.PrimaryInterface || endpoint.IPv4Address != "" { + break + } + } + } + + res.Containers[container.ID] = endpoint } return &res, nil