Skip to content

Commit 77a0750

Browse files
committed
fix: make PORTS in nerdctl ps or nerdctl compose ps easier to view
Details are described in the following issue. - #4338 Signed-off-by: Hayato Kiwata <[email protected]>
1 parent 2165e30 commit 77a0750

File tree

2 files changed

+154
-4
lines changed

2 files changed

+154
-4
lines changed

pkg/formatter/formatter.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"context"
2222
"encoding/json"
2323
"fmt"
24+
"sort"
2425
"strconv"
2526
"strings"
2627
"time"
@@ -110,15 +111,59 @@ func Ellipsis(str string, maxDisplayWidth int) string {
110111
return str[:maxDisplayWidth-1] + "…"
111112
}
112113

114+
func formatRange(startHost, endHost, startContainer, endContainer int32) string {
115+
if startHost == endHost && startContainer == endContainer {
116+
return fmt.Sprintf("%d->%d", startHost, startContainer)
117+
}
118+
return fmt.Sprintf("%d-%d->%d-%d", startHost, endHost, startContainer, endContainer)
119+
}
120+
113121
func FormatPorts(ports []cni.PortMapping) string {
114122
if len(ports) == 0 {
115123
return ""
116124
}
117-
strs := make([]string, len(ports))
118-
for i, p := range ports {
119-
strs[i] = fmt.Sprintf("%s:%d->%d/%s", p.HostIP, p.HostPort, p.ContainerPort, p.Protocol)
125+
126+
type key struct {
127+
HostIP string
128+
Protocol string
129+
}
130+
grouped := make(map[key][]cni.PortMapping)
131+
132+
for _, p := range ports {
133+
k := key{HostIP: p.HostIP, Protocol: p.Protocol}
134+
grouped[k] = append(grouped[k], p)
120135
}
121-
return strings.Join(strs, ", ")
136+
137+
var displayPorts []string
138+
for k, pms := range grouped {
139+
sort.Slice(pms, func(i, j int) bool {
140+
return pms[i].HostPort < pms[j].HostPort
141+
})
142+
143+
var i int
144+
var ranges []string
145+
for i = 0; i < len(pms); {
146+
start, end := pms[i], pms[i]
147+
for i+1 < len(pms) &&
148+
pms[i+1].HostPort == end.HostPort+1 &&
149+
pms[i+1].ContainerPort == end.ContainerPort+1 {
150+
i++
151+
end = pms[i]
152+
}
153+
154+
ranges = append(
155+
ranges,
156+
formatRange(start.HostPort, end.HostPort, start.ContainerPort, end.ContainerPort),
157+
)
158+
i++
159+
}
160+
displayPorts = append(
161+
displayPorts,
162+
fmt.Sprintf("%s:%s/%s", k.HostIP, strings.Join(ranges, ", "), k.Protocol),
163+
)
164+
}
165+
166+
return strings.Join(displayPorts, ", ")
122167
}
123168

124169
func TimeSinceInHuman(since time.Time) string {

pkg/formatter/formatter_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121
"time"
2222

2323
"gotest.tools/v3/assert"
24+
25+
"github.com/containerd/go-cni"
2426
)
2527

2628
func TestTimeSinceInHuman(t *testing.T) {
@@ -87,3 +89,106 @@ func TestTimeSinceInHuman(t *testing.T) {
8789
})
8890
}
8991
}
92+
93+
func TestFormatPorts(t *testing.T) {
94+
t.Parallel()
95+
96+
tests := []struct {
97+
name string
98+
input []cni.PortMapping
99+
expected string
100+
}{
101+
{
102+
name: "a single tcp port on localhost",
103+
input: []cni.PortMapping{
104+
{
105+
HostPort: 3000,
106+
ContainerPort: 8080,
107+
Protocol: "tcp",
108+
HostIP: "127.0.0.1",
109+
},
110+
},
111+
expected: "127.0.0.1:3000->8080/tcp",
112+
},
113+
{
114+
name: "consecutive tcp ports on localhost",
115+
input: []cni.PortMapping{
116+
{
117+
HostPort: 3000,
118+
ContainerPort: 8080,
119+
Protocol: "tcp",
120+
HostIP: "127.0.0.1",
121+
},
122+
{
123+
HostPort: 3001,
124+
ContainerPort: 8081,
125+
Protocol: "tcp",
126+
HostIP: "127.0.0.1",
127+
},
128+
},
129+
expected: "127.0.0.1:3000-3001->8080-8081/tcp",
130+
},
131+
{
132+
name: "a single tcp port on anyhost",
133+
input: []cni.PortMapping{
134+
{
135+
HostPort: 3000,
136+
ContainerPort: 8080,
137+
Protocol: "tcp",
138+
HostIP: "0.0.0.0",
139+
},
140+
},
141+
expected: "0.0.0.0:3000->8080/tcp",
142+
},
143+
{
144+
name: "a single udp port on anyhost",
145+
input: []cni.PortMapping{
146+
{
147+
HostPort: 3000,
148+
ContainerPort: 8080,
149+
Protocol: "udp",
150+
HostIP: "0.0.0.0",
151+
},
152+
},
153+
expected: "0.0.0.0:3000->8080/udp",
154+
},
155+
{
156+
name: "mixed tcp and udp with consecutive ports on anyhost",
157+
input: []cni.PortMapping{
158+
{
159+
HostPort: 3000,
160+
ContainerPort: 8080,
161+
Protocol: "tcp",
162+
HostIP: "0.0.0.0",
163+
},
164+
{
165+
HostPort: 3001,
166+
ContainerPort: 8081,
167+
Protocol: "tcp",
168+
HostIP: "0.0.0.0",
169+
},
170+
{
171+
HostPort: 3002,
172+
ContainerPort: 8082,
173+
Protocol: "udp",
174+
HostIP: "0.0.0.0",
175+
},
176+
{
177+
HostPort: 3003,
178+
ContainerPort: 8083,
179+
Protocol: "udp",
180+
HostIP: "0.0.0.0",
181+
},
182+
},
183+
expected: "0.0.0.0:3000-3001->8080-8081/tcp, 0.0.0.0:3002-3003->8082-8083/udp",
184+
},
185+
}
186+
187+
for _, tt := range tests {
188+
t.Run(tt.name, func(t *testing.T) {
189+
t.Parallel()
190+
result := FormatPorts(tt.input)
191+
assert.Equal(t, tt.expected, result)
192+
})
193+
}
194+
}

0 commit comments

Comments
 (0)