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