From 77a07509a475ffe4db2358b6b906edfae855c631 Mon Sep 17 00:00:00 2001 From: Hayato Kiwata Date: Wed, 12 Nov 2025 10:55:00 +0000 Subject: [PATCH] fix: make PORTS in nerdctl ps or nerdctl compose ps easier to view Details are described in the following issue. - https://github.com/containerd/nerdctl/issues/4338 Signed-off-by: Hayato Kiwata --- pkg/formatter/formatter.go | 53 ++++++++++++++-- pkg/formatter/formatter_test.go | 105 ++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 4 deletions(-) diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index b72952ce68a..c5b8a8be305 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "fmt" + "sort" "strconv" "strings" "time" @@ -110,15 +111,59 @@ func Ellipsis(str string, maxDisplayWidth int) string { return str[:maxDisplayWidth-1] + "…" } +func formatRange(startHost, endHost, startContainer, endContainer int32) string { + if startHost == endHost && startContainer == endContainer { + return fmt.Sprintf("%d->%d", startHost, startContainer) + } + return fmt.Sprintf("%d-%d->%d-%d", startHost, endHost, startContainer, endContainer) +} + func FormatPorts(ports []cni.PortMapping) string { if len(ports) == 0 { return "" } - strs := make([]string, len(ports)) - for i, p := range ports { - strs[i] = fmt.Sprintf("%s:%d->%d/%s", p.HostIP, p.HostPort, p.ContainerPort, p.Protocol) + + type key struct { + HostIP string + Protocol string + } + grouped := make(map[key][]cni.PortMapping) + + for _, p := range ports { + k := key{HostIP: p.HostIP, Protocol: p.Protocol} + grouped[k] = append(grouped[k], p) } - return strings.Join(strs, ", ") + + var displayPorts []string + for k, pms := range grouped { + sort.Slice(pms, func(i, j int) bool { + return pms[i].HostPort < pms[j].HostPort + }) + + var i int + var ranges []string + for i = 0; i < len(pms); { + start, end := pms[i], pms[i] + for i+1 < len(pms) && + pms[i+1].HostPort == end.HostPort+1 && + pms[i+1].ContainerPort == end.ContainerPort+1 { + i++ + end = pms[i] + } + + ranges = append( + ranges, + formatRange(start.HostPort, end.HostPort, start.ContainerPort, end.ContainerPort), + ) + i++ + } + displayPorts = append( + displayPorts, + fmt.Sprintf("%s:%s/%s", k.HostIP, strings.Join(ranges, ", "), k.Protocol), + ) + } + + return strings.Join(displayPorts, ", ") } func TimeSinceInHuman(since time.Time) string { diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index 6e039039e11..9da5e6fcf11 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -21,6 +21,8 @@ import ( "time" "gotest.tools/v3/assert" + + "github.com/containerd/go-cni" ) func TestTimeSinceInHuman(t *testing.T) { @@ -87,3 +89,106 @@ func TestTimeSinceInHuman(t *testing.T) { }) } } + +func TestFormatPorts(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []cni.PortMapping + expected string + }{ + { + name: "a single tcp port on localhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + }, + expected: "127.0.0.1:3000->8080/tcp", + }, + { + name: "consecutive tcp ports on localhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + { + HostPort: 3001, + ContainerPort: 8081, + Protocol: "tcp", + HostIP: "127.0.0.1", + }, + }, + expected: "127.0.0.1:3000-3001->8080-8081/tcp", + }, + { + name: "a single tcp port on anyhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "0.0.0.0", + }, + }, + expected: "0.0.0.0:3000->8080/tcp", + }, + { + name: "a single udp port on anyhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "udp", + HostIP: "0.0.0.0", + }, + }, + expected: "0.0.0.0:3000->8080/udp", + }, + { + name: "mixed tcp and udp with consecutive ports on anyhost", + input: []cni.PortMapping{ + { + HostPort: 3000, + ContainerPort: 8080, + Protocol: "tcp", + HostIP: "0.0.0.0", + }, + { + HostPort: 3001, + ContainerPort: 8081, + Protocol: "tcp", + HostIP: "0.0.0.0", + }, + { + HostPort: 3002, + ContainerPort: 8082, + Protocol: "udp", + HostIP: "0.0.0.0", + }, + { + HostPort: 3003, + ContainerPort: 8083, + Protocol: "udp", + HostIP: "0.0.0.0", + }, + }, + expected: "0.0.0.0:3000-3001->8080-8081/tcp, 0.0.0.0:3002-3003->8082-8083/udp", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := FormatPorts(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +}