Skip to content

Commit

Permalink
ps: fix display of exposed ports
Browse files Browse the repository at this point in the history
This fixes two problems, first if a port is published and exposed it
should not be shown twice. It is enough to show the published one.

Second, if there is a huge range the ports were no grouped causing the
output to be unreadable basically. Now we group exposed ports like we do
with the normal published ports.

Fixes #23317

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
  • Loading branch information
Luap99 committed Oct 23, 2024
1 parent 73fb662 commit 0cdb9b3
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 11 deletions.
64 changes: 56 additions & 8 deletions cmd/podman/containers/ps.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package containers

import (
"cmp"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -491,6 +492,8 @@ func portsToString(ports []types.PortMapping, exposedPorts map[uint16][]string)
if len(ports) == 0 && len(exposedPorts) == 0 {
return ""
}
portMap := make(map[string]struct{})

sb := &strings.Builder{}
for _, port := range ports {
hostIP := port.HostIP
Expand All @@ -501,27 +504,72 @@ func portsToString(ports []types.PortMapping, exposedPorts map[uint16][]string)
fmt.Fprintf(sb, "%s:%d-%d->%d-%d/%s, ",
hostIP, port.HostPort, port.HostPort+port.Range-1,
port.ContainerPort, port.ContainerPort+port.Range-1, port.Protocol)
for i := range port.Range {
portMap[fmt.Sprintf("%d/%s", port.ContainerPort+i, port.Protocol)] = struct{}{}
}
} else {
fmt.Fprintf(sb, "%s:%d->%d/%s, ",
hostIP, port.HostPort,
port.ContainerPort, port.Protocol)
portMap[fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)] = struct{}{}
}
}

// iterating a map is not deterministic so let's convert slice first and sort by port to make it deterministic
sortedPorts := make([]uint16, 0, len(exposedPorts))
for port := range exposedPorts {
sortedPorts = append(sortedPorts, port)
// iterating a map is not deterministic so let's convert slice first and sort by protocol and port to make it deterministic
sortedPorts := make([]exposedPort, 0, len(exposedPorts))
for port, protocols := range exposedPorts {
for _, proto := range protocols {
sortedPorts = append(sortedPorts, exposedPort{num: port, protocol: proto})
}
}
slices.Sort(sortedPorts)
slices.SortFunc(sortedPorts, func(a, b exposedPort) int {
protoCmp := cmp.Compare(a.protocol, b.protocol)
if protoCmp != 0 {
return protoCmp
}
return cmp.Compare(a.num, b.num)
})

var prevPort *exposedPort
for _, port := range sortedPorts {
for _, protocol := range exposedPorts[port] {
// exposed ports do not have a host part and are just written as "NUM/PROTO"
fmt.Fprintf(sb, "%d/%s, ", port, protocol)
// only if it was not published already so we do not have duplicates
if _, ok := portMap[fmt.Sprintf("%d/%s", port.num, port.protocol)]; ok {
continue
}

if prevPort != nil {
// if the prevPort is one below us we know it is a range, do not print it and just increase the range by one
if prevPort.protocol == port.protocol && prevPort.num == port.num-prevPort.portRange-1 {
prevPort.portRange++
continue
}
// the new port is not a range with the previous one so print it
printExposedPort(prevPort, sb)
}
prevPort = &port
}
// do not forget to print the last port
if prevPort != nil {
printExposedPort(prevPort, sb)
}

display := sb.String()
// make sure to trim the last ", " of the string
return display[:len(display)-2]
}

type exposedPort struct {
num uint16
protocol string
// portRange is 0 indexed
portRange uint16
}

func printExposedPort(port *exposedPort, sb *strings.Builder) {
// exposed ports do not have a host part and are just written as "NUM[-RANGE]/PROTO"
if port.portRange > 0 {
fmt.Fprintf(sb, "%d-%d/%s, ", port.num, port.num+port.portRange, port.protocol)
} else {
fmt.Fprintf(sb, "%d/%s, ", port.num, port.protocol)
}
}
196 changes: 196 additions & 0 deletions cmd/podman/containers/ps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package containers

import (
"testing"

"github.com/containers/common/libnetwork/types"
"github.com/stretchr/testify/assert"
)

func Test_portsToString(t *testing.T) {
tests := []struct {
name string
ports []types.PortMapping
exposedPorts map[uint16][]string
want string
}{
{
name: "no ports",
want: "",
},
{
name: "no ports empty map/slice",
ports: []types.PortMapping{},
exposedPorts: map[uint16][]string{},
want: "",
},
{
name: "single published port",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
want: "0.0.0.0:8080->80/tcp",
},
{
name: "published port with host ip",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
HostIP: "127.0.0.1",
Protocol: "tcp",
Range: 1,
},
},
want: "127.0.0.1:8080->80/tcp",
},
{
name: "published port with same exposed port",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
exposedPorts: map[uint16][]string{
80: {"tcp"},
},
want: "0.0.0.0:8080->80/tcp",
},
{
name: "published port and exposed port with different protocol",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
exposedPorts: map[uint16][]string{
80: {"udp"},
},
want: "0.0.0.0:8080->80/tcp, 80/udp",
},
{
name: "published port range",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
},
want: "0.0.0.0:8080-8082->80-82/tcp",
},
{
name: "published port range and exposed port in that range",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
},
exposedPorts: map[uint16][]string{
81: {"tcp"},
},
want: "0.0.0.0:8080-8082->80-82/tcp",
},
{
name: "two published ports",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "udp",
Range: 1,
},
},
want: "0.0.0.0:8080-8082->80-82/tcp, 0.0.0.0:8080->80/udp",
},
{
name: "exposed port",
exposedPorts: map[uint16][]string{
80: {"tcp"},
},
want: "80/tcp",
},
{
name: "exposed port multiple protocols",
exposedPorts: map[uint16][]string{
80: {"tcp", "udp"},
},
want: "80/tcp, 80/udp",
},
{
name: "exposed port range",
exposedPorts: map[uint16][]string{
80: {"tcp"},
81: {"tcp"},
82: {"tcp"},
},
want: "80-82/tcp",
},
{
name: "exposed port range with different protocols",
exposedPorts: map[uint16][]string{
80: {"tcp", "udp"},
81: {"tcp", "sctp"},
82: {"tcp", "udp"},
},
want: "81/sctp, 80-82/tcp, 80/udp, 82/udp",
},
{
name: "multiple exposed port ranges",
exposedPorts: map[uint16][]string{
80: {"tcp"},
81: {"tcp"},
82: {"tcp"},
// 83 missing to split the range
84: {"tcp"},
85: {"tcp"},
86: {"tcp"},
},
want: "80-82/tcp, 84-86/tcp",
},
{
name: "published port range partially overlaps with exposed port range",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
},
exposedPorts: map[uint16][]string{
82: {"tcp"},
83: {"tcp"},
84: {"tcp"},
},
want: "0.0.0.0:8080-8082->80-82/tcp, 83-84/tcp",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := portsToString(tt.ports, tt.exposedPorts)
assert.Equal(t, tt.want, got)
})
}
}
36 changes: 33 additions & 3 deletions test/e2e/run_networking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,8 +377,18 @@ var _ = Describe("Podman run networking", func() {
})

It("podman run --expose port range", func() {
session := podmanTest.Podman([]string{"run", "-d", "--expose", "1000-9999", ALPINE})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())

session = podmanTest.Podman([]string{"ps", "-a", "--format", "{{.Ports}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// This must use Equal() to ensure we do not see anything extra
Expect(session.OutputToString()).To(Equal("1000-9999/tcp"))

name := "testctr"
session := podmanTest.Podman([]string{"run", "-d", "--expose", "222-223", "-P", "--name", name, ALPINE, "sleep", "100"})
session = podmanTest.Podman([]string{"run", "-d", "--expose", "222-223", "-P", "--name", name, ALPINE, "sleep", "100"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
inspectOut := podmanTest.InspectContainer(name)
Expand All @@ -396,12 +406,20 @@ var _ = Describe("Podman run networking", func() {
name := "testctr"
session := podmanTest.Podman([]string{"create", "-t", "--expose", "80", "-p", "80", "--name", name, ALPINE, "/bin/sh"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
inspectOut := podmanTest.InspectContainer(name)
Expect(inspectOut).To(HaveLen(1))
Expect(inspectOut[0].NetworkSettings.Ports).To(HaveLen(1))
Expect(inspectOut[0].NetworkSettings.Ports["80/tcp"]).To(HaveLen(1))
Expect(inspectOut[0].NetworkSettings.Ports["80/tcp"][0].HostPort).To(Not(Equal("80")))
hostPort := inspectOut[0].NetworkSettings.Ports["80/tcp"][0].HostPort
Expect(hostPort).To(Not(Equal("80")))
Expect(inspectOut[0].NetworkSettings.Ports["80/tcp"][0]).To(HaveField("HostIP", "0.0.0.0"))

session = podmanTest.Podman([]string{"ps", "-a", "--format", "{{.Ports}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// This must use Equal() to ensure we do not see the extra ", 80/tcp" from the exposed port
Expect(session.OutputToString()).To(Equal("0.0.0.0:" + hostPort + "->80/tcp"))
})

It("podman run --publish-all with EXPOSE port ranges in Dockerfile", func() {
Expand All @@ -417,16 +435,28 @@ EXPOSE 2004-2005/tcp`, ALPINE)
// Verify that the buildah is just passing through the EXPOSE keys
inspect := podmanTest.Podman([]string{"inspect", imageName})
inspect.WaitWithDefaultTimeout()
Expect(inspect).Should(ExitCleanly())
image := inspect.InspectImageJSON()
Expect(image).To(HaveLen(1))
Expect(image[0].Config.ExposedPorts).To(HaveLen(3))
Expect(image[0].Config.ExposedPorts).To(HaveKey("2002/tcp"))
Expect(image[0].Config.ExposedPorts).To(HaveKey("2001-2003/tcp"))
Expect(image[0].Config.ExposedPorts).To(HaveKey("2004-2005/tcp"))

session := podmanTest.Podman([]string{"create", imageName})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())

session = podmanTest.Podman([]string{"ps", "-a", "--format", "{{.Ports}}"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
// This must use Equal() to ensure we do not see anything extra
Expect(session.OutputToString()).To(Equal("2001-2005/tcp"))

containerName := "testcontainer"
session := podmanTest.Podman([]string{"create", "--publish-all", "--name", containerName, imageName, "true"})
session = podmanTest.Podman([]string{"create", "--publish-all", "--name", containerName, imageName})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitCleanly())
inspectOut := podmanTest.InspectContainer(containerName)
Expect(inspectOut).To(HaveLen(1))

Expand Down

0 comments on commit 0cdb9b3

Please sign in to comment.