Merge pull request #3892 from thaJeztah/port_sort

container port: sort ports before printing
This commit is contained in:
Sebastiaan van Stijn 2022-12-01 13:01:17 +01:00 committed by GitHub
commit d7e872ed64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 62 additions and 22 deletions

View File

@ -4,12 +4,15 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"sort"
"strconv"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/fvbommel/sortorder"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -43,6 +46,12 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
// runPort shows the port mapping for a given container. Optionally, it
// allows showing the mapping for a specific (container)port and proto.
//
// TODO(thaJeztah): currently this defaults to show the TCP port if no
// proto is specified. We should consider changing this to "any" protocol
// for the given private port.
func runPort(dockerCli command.Cli, opts *portOptions) error { func runPort(dockerCli command.Cli, opts *portOptions) error {
ctx := context.Background() ctx := context.Background()
@ -51,33 +60,35 @@ func runPort(dockerCli command.Cli, opts *portOptions) error {
return err return err
} }
var out []string
if opts.port != "" { if opts.port != "" {
port := opts.port port, proto, _ := strings.Cut(opts.port, "/")
proto := "tcp" if proto == "" {
parts := strings.SplitN(port, "/", 2) proto = "tcp"
if len(parts) == 2 && len(parts[1]) != 0 {
port = parts[0]
proto = parts[1]
} }
natPort := port + "/" + proto if _, err = strconv.ParseUint(port, 10, 16); err != nil {
newP, err := nat.NewPort(proto, port) return errors.Wrapf(err, "Error: invalid port (%s)", port)
if err != nil {
return err
} }
if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil { frontends, exists := c.NetworkSettings.Ports[nat.Port(port+"/"+proto)]
if !exists || frontends == nil {
return errors.Errorf("Error: No public port '%s' published for %s", opts.port, opts.container)
}
for _, frontend := range frontends {
out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort))
}
} else {
for from, frontends := range c.NetworkSettings.Ports {
for _, frontend := range frontends { for _, frontend := range frontends {
fmt.Fprintln(dockerCli.Out(), net.JoinHostPort(frontend.HostIP, frontend.HostPort)) out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort)))
} }
return nil
} }
return errors.Errorf("Error: No public port '%s' published for %s", natPort, opts.container)
} }
for from, frontends := range c.NetworkSettings.Ports { if len(out) > 0 {
for _, frontend := range frontends { sort.Slice(out, func(i, j int) bool {
fmt.Fprintf(dockerCli.Out(), "%s -> %s\n", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort)) return sortorder.NaturalLess(out[i], out[j])
} })
_, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(out, "\n"))
} }
return nil return nil

View File

@ -15,18 +15,31 @@ func TestNewPortCommandOutput(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
ips []string ips []string
port string
}{ }{
{ {
name: "container-port-ipv4", name: "container-port-ipv4",
ips: []string{"0.0.0.0"}, ips: []string{"0.0.0.0"},
port: "80",
}, },
{ {
name: "container-port-ipv6", name: "container-port-ipv6",
ips: []string{"::"}, ips: []string{"::"},
port: "80",
}, },
{ {
name: "container-port-ipv6-and-ipv4", name: "container-port-ipv6-and-ipv4",
ips: []string{"::", "0.0.0.0"}, ips: []string{"::", "0.0.0.0"},
port: "80",
},
{
name: "container-port-ipv6-and-ipv4-443-udp",
ips: []string{"::", "0.0.0.0"},
port: "443/udp",
},
{
name: "container-port-all-ports",
ips: []string{"::", "0.0.0.0"},
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -36,19 +49,27 @@ func TestNewPortCommandOutput(t *testing.T) {
inspectFunc: func(string) (types.ContainerJSON, error) { inspectFunc: func(string) (types.ContainerJSON, error) {
ci := types.ContainerJSON{NetworkSettings: &types.NetworkSettings{}} ci := types.ContainerJSON{NetworkSettings: &types.NetworkSettings{}}
ci.NetworkSettings.Ports = nat.PortMap{ ci.NetworkSettings.Ports = nat.PortMap{
"80/tcp": make([]nat.PortBinding, len(tc.ips)), "80/tcp": make([]nat.PortBinding, len(tc.ips)),
"443/tcp": make([]nat.PortBinding, len(tc.ips)),
"443/udp": make([]nat.PortBinding, len(tc.ips)),
} }
for i, ip := range tc.ips { for i, ip := range tc.ips {
ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{ ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{
HostIP: ip, HostPort: "3456", HostIP: ip, HostPort: "3456",
} }
ci.NetworkSettings.Ports["443/tcp"][i] = nat.PortBinding{
HostIP: ip, HostPort: "4567",
}
ci.NetworkSettings.Ports["443/udp"][i] = nat.PortBinding{
HostIP: ip, HostPort: "5678",
}
} }
return ci, nil return ci, nil
}, },
}, test.EnableContentTrust) }, test.EnableContentTrust)
cmd := NewPortCommand(cli) cmd := NewPortCommand(cli)
cmd.SetErr(io.Discard) cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"some_container", "80"}) cmd.SetArgs([]string{"some_container", tc.port})
err := cmd.Execute() err := cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
golden.Assert(t, cli.OutBuffer().String(), tc.name+".golden") golden.Assert(t, cli.OutBuffer().String(), tc.name+".golden")

View File

@ -0,0 +1,6 @@
80/tcp -> 0.0.0.0:3456
80/tcp -> [::]:3456
443/tcp -> 0.0.0.0:4567
443/tcp -> [::]:4567
443/udp -> 0.0.0.0:5678
443/udp -> [::]:5678

View File

@ -0,0 +1,2 @@
0.0.0.0:5678
[::]:5678

View File

@ -1,2 +1,2 @@
[::]:3456
0.0.0.0:3456 0.0.0.0:3456
[::]:3456