mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 22:16:18 +01:00
add ports column to containers table (#1481)
This commit is contained in:
@@ -16,6 +16,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -346,6 +348,33 @@ func updateContainerStatsValues(stats *container.Stats, cpuPct float64, usedMemo
|
||||
stats.PrevReadTime = readTime
|
||||
}
|
||||
|
||||
// convertContainerPortsToString formats the ports of a container into a sorted, deduplicated string.
|
||||
// ctr.Ports is nilled out after processing so the slice is not accidentally reused.
|
||||
func convertContainerPortsToString(ctr *container.ApiInfo) string {
|
||||
if len(ctr.Ports) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.Slice(ctr.Ports, func(i, j int) bool {
|
||||
return ctr.Ports[i].PublicPort < ctr.Ports[j].PublicPort
|
||||
})
|
||||
var builder strings.Builder
|
||||
seenPorts := make(map[uint16]struct{})
|
||||
for _, p := range ctr.Ports {
|
||||
_, ok := seenPorts[p.PublicPort]
|
||||
if p.PublicPort == 0 || ok {
|
||||
continue
|
||||
}
|
||||
seenPorts[p.PublicPort] = struct{}{}
|
||||
if builder.Len() > 0 {
|
||||
builder.WriteString(", ")
|
||||
}
|
||||
builder.WriteString(strconv.Itoa(int(p.PublicPort)))
|
||||
}
|
||||
// clear ports slice so it doesn't get reused and blend into next response
|
||||
ctr.Ports = nil
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func parseDockerStatus(status string) (string, container.DockerHealth) {
|
||||
trimmed := strings.TrimSpace(status)
|
||||
if trimmed == "" {
|
||||
@@ -414,6 +443,10 @@ func (dm *dockerManager) updateContainerStats(ctr *container.ApiInfo, cacheTimeM
|
||||
stats.Status = statusText
|
||||
stats.Health = health
|
||||
|
||||
if len(ctr.Ports) > 0 {
|
||||
stats.Ports = convertContainerPortsToString(ctr)
|
||||
}
|
||||
|
||||
// reset current stats
|
||||
stats.Cpu = 0
|
||||
stats.Mem = 0
|
||||
|
||||
@@ -1455,3 +1455,109 @@ func TestAnsiEscapePattern(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertContainerPortsToString(t *testing.T) {
|
||||
type port = struct {
|
||||
PrivatePort uint16
|
||||
PublicPort uint16
|
||||
Type string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
ports []port
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "empty ports",
|
||||
ports: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "single port public==private",
|
||||
ports: []port{
|
||||
{PublicPort: 80, PrivatePort: 80},
|
||||
},
|
||||
expected: "80",
|
||||
},
|
||||
{
|
||||
name: "single port public!=private",
|
||||
ports: []port{
|
||||
{PublicPort: 443, PrivatePort: 2019},
|
||||
},
|
||||
// expected: "443:2019",
|
||||
expected: "443",
|
||||
},
|
||||
{
|
||||
name: "zero PublicPort is skipped",
|
||||
ports: []port{
|
||||
{PublicPort: 0, PrivatePort: 8080},
|
||||
{PublicPort: 80, PrivatePort: 80},
|
||||
},
|
||||
expected: "80",
|
||||
},
|
||||
{
|
||||
name: "ports sorted ascending by PublicPort",
|
||||
ports: []port{
|
||||
{PublicPort: 443, PrivatePort: 443},
|
||||
{PublicPort: 80, PrivatePort: 80},
|
||||
{PublicPort: 8080, PrivatePort: 8080},
|
||||
},
|
||||
expected: "80, 443, 8080",
|
||||
},
|
||||
{
|
||||
name: "same PublicPort sorted by PrivatePort",
|
||||
ports: []port{
|
||||
{PublicPort: 443, PrivatePort: 9000},
|
||||
{PublicPort: 443, PrivatePort: 2019},
|
||||
},
|
||||
// expected: "443:2019,443:9000",
|
||||
expected: "443",
|
||||
},
|
||||
{
|
||||
name: "duplicates are deduplicated",
|
||||
ports: []port{
|
||||
{PublicPort: 80, PrivatePort: 80},
|
||||
{PublicPort: 80, PrivatePort: 80},
|
||||
{PublicPort: 443, PrivatePort: 2019},
|
||||
{PublicPort: 443, PrivatePort: 2019},
|
||||
},
|
||||
// expected: "80,443:2019",
|
||||
expected: "80, 443",
|
||||
},
|
||||
{
|
||||
name: "mixed zero and non-zero ports",
|
||||
ports: []port{
|
||||
{PublicPort: 0, PrivatePort: 5432},
|
||||
{PublicPort: 443, PrivatePort: 2019},
|
||||
{PublicPort: 80, PrivatePort: 80},
|
||||
{PublicPort: 0, PrivatePort: 9000},
|
||||
},
|
||||
// expected: "80,443:2019",
|
||||
expected: "80, 443",
|
||||
},
|
||||
{
|
||||
name: "ports slice is nilled after call",
|
||||
ports: []port{
|
||||
{PublicPort: 8080, PrivatePort: 8080},
|
||||
},
|
||||
expected: "8080",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctr := &container.ApiInfo{}
|
||||
for _, p := range tt.ports {
|
||||
ctr.Ports = append(ctr.Ports, struct {
|
||||
// PrivatePort uint16
|
||||
PublicPort uint16
|
||||
// Type string
|
||||
}{PublicPort: p.PublicPort})
|
||||
}
|
||||
result := convertContainerPortsToString(ctr)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
// Ports slice must be cleared to prevent bleed-over into the next response
|
||||
assert.Nil(t, ctr.Ports, "ctr.Ports should be nil after formatContainerPorts")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user