diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index fa849aa4cc..5741288ebb 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -20,6 +20,7 @@ import ( "github.com/docker/go-units" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "golang.org/x/exp/slices" ) var ( @@ -434,10 +435,7 @@ func (l psReporter) Networks() string { // Ports converts from Portmappings to the string form // required by ps func (l psReporter) Ports() string { - if len(l.ListContainer.Ports) < 1 { - return "" - } - return portsToString(l.ListContainer.Ports) + return portsToString(l.ListContainer.Ports, l.ListContainer.ExposedPorts) } // CreatedAt returns the container creation time in string format. podman @@ -489,8 +487,8 @@ func (l psReporter) UTS() string { // portsToString converts the ports used to a string of the from "port1, port2" // and also groups a continuous list of ports into a readable format. // The format is IP:HostPort(-Range)->ContainerPort(-Range)/Proto -func portsToString(ports []types.PortMapping) string { - if len(ports) == 0 { +func portsToString(ports []types.PortMapping, exposedPorts map[uint16][]string) string { + if len(ports) == 0 && len(exposedPorts) == 0 { return "" } sb := &strings.Builder{} @@ -512,6 +510,20 @@ func portsToString(ports []types.PortMapping) string { } } } + + // 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) + } + slices.Sort(sortedPorts) + 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) + } + } + display := sb.String() // make sure to trim the last ", " of the string return display[:len(display)-2] diff --git a/docs/source/markdown/options/expose.md b/docs/source/markdown/options/expose.md index 6615a350ad..745e25226c 100644 --- a/docs/source/markdown/options/expose.md +++ b/docs/source/markdown/options/expose.md @@ -2,7 +2,11 @@ ####> podman create, run ####> If file is edited, make sure the changes ####> are applicable to all of those. -#### **--expose**=*port* +#### **--expose**=*port[/protocol]* -Expose a port, or a range of ports (e.g. **--expose=3300-3310**) to set up port redirection -on the host system. +Expose a port or a range of ports (e.g. **--expose=3300-3310**). +The protocol can be `tcp`, `udp` or `sctp` and if not given `tcp` is assumed. +This option matches the EXPOSE instruction for image builds and has no effect on +the actual networking rules unless **-P/--publish-all** is used to forward to all +exposed ports from random host ports. To forward specific ports from the host +into the container use the **-p/--publish** option instead. diff --git a/docs/source/markdown/podman-ps.1.md b/docs/source/markdown/podman-ps.1.md index 7345e89bec..0c54d1c1d8 100644 --- a/docs/source/markdown/podman-ps.1.md +++ b/docs/source/markdown/podman-ps.1.md @@ -80,6 +80,7 @@ Valid placeholders for the Go template are listed below: | .ExitCode | Container exit code | | .Exited | "true" if container has exited | | .ExitedAt | Time (epoch seconds) that container exited | +| .ExposedPorts ... | Map of exposed ports on this container | | .ID | Container ID | | .Image | Image Name/ID | | .ImageID | Image ID | @@ -92,7 +93,7 @@ Valid placeholders for the Go template are listed below: | .Pid | Process ID on host system | | .Pod | Pod the container is associated with (SHA) | | .PodName | PodName of the container | -| .Ports | Exposed ports | +| .Ports | Forwarded and exposed ports | | .Restarts | Display the container restart count | | .RunningFor | Time elapsed since container was started | | .Size | Size of container | diff --git a/pkg/domain/entities/types/container_ps.go b/pkg/domain/entities/types/container_ps.go index 73f70dbe4c..139a87c036 100644 --- a/pkg/domain/entities/types/container_ps.go +++ b/pkg/domain/entities/types/container_ps.go @@ -25,6 +25,11 @@ type ListContainer struct { ExitedAt int64 // If container has exited, the return code from the command ExitCode int32 + // ExposedPorts contains the ports that are exposed but not forwarded, + // see Ports for forwarded ports. + // The key is the port number and the string slice contains the protocols, + // i.e. "tcp", "udp" and "sctp". + ExposedPorts map[uint16][]string // The unique identifier for the container ID string `json:"Id"` // Container image diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index a3bb9b9f90..243d238715 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -238,29 +238,30 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities } ps := entities.ListContainer{ - AutoRemove: ctr.AutoRemove(), - CIDFile: conConfig.Spec.Annotations[define.InspectAnnotationCIDFile], - Command: conConfig.Command, - Created: conConfig.CreatedTime, - ExitCode: exitCode, - Exited: exited, - ExitedAt: exitedTime.Unix(), - ID: conConfig.ID, - Image: conConfig.RootfsImageName, - ImageID: conConfig.RootfsImageID, - IsInfra: conConfig.IsInfra, - Labels: conConfig.Labels, - Mounts: ctr.UserVolumes(), - Names: []string{conConfig.Name}, - Networks: networks, - Pid: pid, - Pod: conConfig.Pod, - Ports: portMappings, - Restarts: restartCount, - Size: size, - StartedAt: startedTime.Unix(), - State: conState.String(), - Status: healthStatus, + AutoRemove: ctr.AutoRemove(), + CIDFile: conConfig.Spec.Annotations[define.InspectAnnotationCIDFile], + Command: conConfig.Command, + Created: conConfig.CreatedTime, + ExitCode: exitCode, + Exited: exited, + ExitedAt: exitedTime.Unix(), + ExposedPorts: conConfig.ExposedPorts, + ID: conConfig.ID, + Image: conConfig.RootfsImageName, + ImageID: conConfig.RootfsImageID, + IsInfra: conConfig.IsInfra, + Labels: conConfig.Labels, + Mounts: ctr.UserVolumes(), + Names: []string{conConfig.Name}, + Networks: networks, + Pid: pid, + Pod: conConfig.Pod, + Ports: portMappings, + Restarts: restartCount, + Size: size, + StartedAt: startedTime.Unix(), + State: conState.String(), + Status: healthStatus, } if opts.Pod && len(conConfig.Pod) > 0 { podName, err := rt.GetPodName(conConfig.Pod) diff --git a/test/e2e/container_inspect_test.go b/test/e2e/container_inspect_test.go index a174988edb..2a3861ff86 100644 --- a/test/e2e/container_inspect_test.go +++ b/test/e2e/container_inspect_test.go @@ -26,7 +26,7 @@ var _ = Describe("Podman container inspect", func() { It("podman inspect shows exposed ports", func() { name := "testcon" - session := podmanTest.Podman([]string{"run", "-d", "--stop-timeout", "0", "--expose", "8787/udp", "--name", name, ALPINE, "sleep", "inf"}) + session := podmanTest.Podman([]string{"run", "-d", "--stop-timeout", "0", "--expose", "8787/udp", "--name", name, ALPINE, "sleep", "100"}) session.WaitWithDefaultTimeout() Expect(session).Should(ExitCleanly()) data := podmanTest.InspectContainer(name) @@ -34,6 +34,11 @@ var _ = Describe("Podman container inspect", func() { Expect(data).To(HaveLen(1)) Expect(data[0].NetworkSettings.Ports). To(Equal(map[string][]define.InspectHostPort{"8787/udp": nil})) + + session = podmanTest.Podman([]string{"ps", "--format", "{{.Ports}}"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(Equal("8787/udp")) }) It("podman inspect shows exposed ports on image", func() { @@ -46,6 +51,11 @@ var _ = Describe("Podman container inspect", func() { Expect(data).To(HaveLen(1)) Expect(data[0].NetworkSettings.Ports). To(Equal(map[string][]define.InspectHostPort{"80/tcp": nil, "8989/tcp": nil})) + + session = podmanTest.Podman([]string{"ps", "--format", "{{.Ports}}"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + Expect(session.OutputToString()).To(Equal("80/tcp, 8989/tcp")) }) It("podman inspect shows volumes-from with mount options", func() {