Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions cmd/nerdctl/network/network_inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,64 @@ 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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests for the items added in this PR's changes. Should add assertions for IPv4Address, MacAddress, and IPv6Address.

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 IPv4Address has CIDR notation (not empty and contains '/')
assert.Assert(t, container.IPv4Address != "", "IPv4Address should not be empty")
assert.Assert(t, strings.Contains(container.IPv4Address, "/"), "IPv4Address should contain CIDR notation with /")

// Test MacAddress is present and has valid format
assert.Assert(t, container.MacAddress != "", "MacAddress should not be empty")

// Test IPv6Address is empty for IPv4-only network
assert.Equal(t, "", container.IPv6Address, "IPv6Address should be empty for IPv4-only network")
},
}
},
},
}

testCase.Run(t)
Expand Down
103 changes: 95 additions & 8 deletions pkg/inspecttypes/dockercompat/dockercompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -949,6 +949,88 @@ type MemorySetting struct {
DisableOOMKiller bool `json:"disableOOMKiller"`
}

// parseNetworkSubnets extracts and parses subnet configurations from IPAM config
func parseNetworkSubnets(ipamConfigs []IPAMConfig) []*net.IPNet {
var subnets []*net.IPNet
for _, config := range ipamConfigs {
if config.Subnet != "" {
_, subnet, err := net.ParseCIDR(config.Subnet)
if err != nil {
log.L.WithError(err).Warnf("failed to parse subnet %q", config.Subnet)
continue
}
subnets = append(subnets, subnet)
}
}
return subnets
}

// isUsableInterface checks if a network interface is usable (not loopback and up)
func isUsableInterface(iface *native.NetInterface) bool {
return iface.Interface.Flags&net.FlagLoopback == 0 &&
iface.Interface.Flags&net.FlagUp != 0
}

// setIPAddresses assigns IPv4 or IPv6 addresses from CIDR notation to the endpoint
func setIPAddresses(endpoint *EndpointResource, cidr string) {
ip, _, err := net.ParseCIDR(cidr)
if err != nil {
return
}
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return
}

if ip.To4() != nil {
endpoint.IPv4Address = cidr
} else if ip.To16() != nil {
endpoint.IPv6Address = cidr
}
}

// matchInterfaceToSubnets tries to match an interface to network subnets
func matchInterfaceToSubnets(endpoint *EndpointResource, iface *native.NetInterface, subnets []*net.IPNet) bool {
for _, addr := range iface.Addrs {
ip, _, err := net.ParseCIDR(addr)
if err != nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() {
continue
}

for _, subnet := range subnets {
if subnet.Contains(ip) {
endpoint.MacAddress = iface.HardwareAddr
setIPAddresses(endpoint, addr)
return true
}
}
}
return false
}

// populateEndpointFromNetNS finds and populates endpoint info from network namespace interfaces
func populateEndpointFromNetNS(endpoint *EndpointResource, interfaces []native.NetInterface, subnets []*net.IPNet) {
for _, iface := range interfaces {
if !isUsableInterface(&iface) {
continue
}

if len(subnets) > 0 {
if matchInterfaceToSubnets(endpoint, &iface, subnets) {
return // Found matching interface
}
// Continue to next interface if this one doesn't match any subnets
continue
}

// Fallback: use first usable interface (for networks without explicit subnets)
endpoint.MacAddress = iface.HardwareAddr
for _, addr := range iface.Addrs {
setIPAddresses(endpoint, addr)
}
return
}
}

func NetworkFromNative(n *native.Network) (*Network, error) {
var res Network

Expand All @@ -973,15 +1055,20 @@ func NetworkFromNative(n *native.Network) (*Network, error) {
res.Labels = *n.NerdctlLabels
}

// Parse network subnets for interface matching
networkSubnets := parseNetworkSubnets(res.IPAM.Config)

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,
}

if container.Process != nil && container.Process.NetNS != nil {
populateEndpointFromNetNS(&endpoint, container.Process.NetNS.Interfaces, networkSubnets)
}

res.Containers[container.ID] = endpoint
}

return &res, nil
Expand Down
Loading