From 97a0efd7c307de6a2de67ca79ca9be29b4a1dde2 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 21 Sep 2021 15:21:28 +0200 Subject: [PATCH] group ports to render ranges as ... ranges Signed-off-by: Nicolas De Loof --- cmd/compose/ps.go | 81 +++++++++++++++++++++++++++++++++++++----- pkg/api/api.go | 31 +++++++++++++++- pkg/compose/ps.go | 7 +--- pkg/compose/ps_test.go | 2 +- 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go index 387c0a100..8e4f76a09 100644 --- a/cmd/compose/ps.go +++ b/cmd/compose/ps.go @@ -147,14 +147,7 @@ SERVICES: func writter(containers []api.ContainerSummary) func(w io.Writer) { return func(w io.Writer) { for _, container := range containers { - var ports []string - for _, p := range container.Publishers { - if p.URL == "" { - ports = append(ports, fmt.Sprintf("%d/%s", p.TargetPort, p.Protocol)) - } else { - ports = append(ports, fmt.Sprintf("%s->%d/%s", p.URL, p.TargetPort, p.Protocol)) - } - } + ports := DisplayablePorts(container) status := container.State if status == "running" && container.Health != "" { status = fmt.Sprintf("%s (%s)", container.State, container.Health) @@ -162,7 +155,7 @@ func writter(containers []api.ContainerSummary) func(w io.Writer) { status = fmt.Sprintf("%s (%d)", container.State, container.ExitCode) } command := formatter2.Ellipsis(container.Command, 20) - _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", container.Name, strconv.Quote(command), container.Service, status, strings.Join(ports, ", ")) + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", container.Name, strconv.Quote(command), container.Service, status, ports) } } } @@ -182,3 +175,73 @@ func filterByStatus(containers []api.ContainerSummary, status string) []api.Cont } return filtered } + +type portRange struct { + pStart int + pEnd int + tStart int + tEnd int + IP string + protocol string +} + +func (pr portRange) String() string { + var ( + pub string + tgt string + ) + + if pr.pEnd > pr.pStart { + pub = fmt.Sprintf("%s:%d-%d->", pr.IP, pr.pStart, pr.pEnd) + } else if pr.pStart > 0 { + pub = fmt.Sprintf("%s:%d->", pr.IP, pr.pStart) + } + if pr.tEnd > pr.tStart { + tgt = fmt.Sprintf("%d-%d", pr.tStart, pr.tEnd) + } else { + tgt = fmt.Sprintf("%d", pr.tStart) + } + return fmt.Sprintf("%s%s/%s", pub, tgt, pr.protocol) +} + +// DisplayablePorts is copy pasted from https://github.com/docker/cli/pull/581/files +func DisplayablePorts(c api.ContainerSummary) string { + if c.Publishers == nil { + return "" + } + + sort.Sort(c.Publishers) + + pr := portRange{} + ports := []string{} + for _, p := range c.Publishers { + prIsRange := pr.tEnd != pr.tStart + tOverlaps := p.TargetPort <= pr.tEnd + + // Start a new port-range if: + // - the protocol is different from the current port-range + // - published or target port are not consecutive to the current port-range + // - the current port-range is a _range_, and the target port overlaps with the current range's target-ports + if p.Protocol != pr.protocol || p.URL != pr.IP || p.PublishedPort-pr.pEnd > 1 || p.TargetPort-pr.tEnd > 1 || prIsRange && tOverlaps { + // start a new port-range, and print the previous port-range (if any) + if pr.pStart > 0 { + ports = append(ports, pr.String()) + } + pr = portRange{ + pStart: p.PublishedPort, + pEnd: p.PublishedPort, + tStart: p.TargetPort, + tEnd: p.TargetPort, + protocol: p.Protocol, + IP: p.URL, + } + continue + } + pr.pEnd = p.PublishedPort + pr.tEnd = p.TargetPort + } + if pr.tStart > 0 { + ports = append(ports, pr.String()) + } + return strings.Join(ports, ", ") +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 00f3a3116..1bd1e78f8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -297,7 +297,36 @@ type ContainerSummary struct { State string Health string ExitCode int - Publishers []PortPublisher + Publishers PortPublishers +} + +// PortPublishers is a slice of PortPublisher +type PortPublishers []PortPublisher + +// Len implements sort.Interface +func (p PortPublishers) Len() int { + return len(p) +} + +// Less implements sort.Interface +func (p PortPublishers) Less(i, j int) bool { + left := p[i] + right := p[j] + if left.URL != right.URL { + return left.URL < right.URL + } + if left.TargetPort != right.TargetPort { + return left.TargetPort < right.TargetPort + } + if left.PublishedPort != right.PublishedPort { + return left.PublishedPort < right.PublishedPort + } + return left.Protocol < right.Protocol +} + +// Swap implements sort.Interface +func (p PortPublishers) Swap(i, j int) { + p[i], p[j] = p[j], p[i] } // ContainerProcSummary holds container processes top data diff --git a/pkg/compose/ps.go b/pkg/compose/ps.go index 1c27f2967..e1a76cbb4 100644 --- a/pkg/compose/ps.go +++ b/pkg/compose/ps.go @@ -18,7 +18,6 @@ package compose import ( "context" - "fmt" "sort" "golang.org/x/sync/errgroup" @@ -46,12 +45,8 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api return container.Ports[i].PrivatePort < container.Ports[j].PrivatePort }) for _, p := range container.Ports { - var url string - if p.PublicPort != 0 { - url = fmt.Sprintf("%s:%d", p.IP, p.PublicPort) - } publishers = append(publishers, api.PortPublisher{ - URL: url, + URL: p.IP, TargetPort: int(p.PrivatePort), PublishedPort: int(p.PublicPort), Protocol: p.Type, diff --git a/pkg/compose/ps_test.go b/pkg/compose/ps_test.go index 0644cc0c4..5de346729 100644 --- a/pkg/compose/ps_test.go +++ b/pkg/compose/ps_test.go @@ -54,7 +54,7 @@ func TestPs(t *testing.T) { expected := []compose.ContainerSummary{ {ID: "123", Name: "123", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "healthy", Publishers: nil}, - {ID: "456", Name: "456", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost:80", TargetPort: 90, + {ID: "456", Name: "456", Project: strings.ToLower(testProject), Service: "service1", State: "running", Health: "", Publishers: []compose.PortPublisher{{URL: "localhost", TargetPort: 90, PublishedPort: 80}}}, {ID: "789", Name: "789", Project: strings.ToLower(testProject), Service: "service2", State: "exited", Health: "", ExitCode: 130, Publishers: nil}, }