From 96514e47389f1830c7cb3b5457359b6ac0949927 Mon Sep 17 00:00:00 2001 From: Antonio Aguilar Date: Thu, 30 May 2024 20:17:12 -0600 Subject: [PATCH] Make port arg optional in `docker compose port` This comand no longer needs a mandatory port. If no port is provided it will instead list all published ports. This behavior falls in line with the behavior provided by `docker port`. Closes #11859. Signed-off-by: Antonio Aguilar --- cmd/compose/port.go | 19 ++++++++++---- cmd/formatter/container.go | 4 +-- docs/reference/compose.md | 2 +- docs/reference/compose_port.md | 2 +- docs/reference/docker_compose_port.yaml | 5 ++-- pkg/api/api.go | 12 ++++++--- pkg/api/api_test.go | 33 ++++++++++++++++++++++++ pkg/compose/port.go | 34 ++++++++++++++++++++----- pkg/compose/ps.go | 4 +-- pkg/mocks/mock_docker_compose_api.go | 9 +++---- 10 files changed, 97 insertions(+), 27 deletions(-) diff --git a/cmd/compose/port.go b/cmd/compose/port.go index 59ea8ef1ce6..f99e4856ade 100644 --- a/cmd/compose/port.go +++ b/cmd/compose/port.go @@ -40,10 +40,15 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) ProjectOptions: p, } cmd := &cobra.Command{ - Use: "port [OPTIONS] SERVICE PRIVATE_PORT", - Short: "Print the public port for a port binding", - Args: cobra.MinimumNArgs(2), + Use: "port [OPTIONS] SERVICE [PRIVATE_PORT]", + Short: "List port mappings or print the public port of a specific mapping for the service", + Args: cobra.RangeArgs(1, 2), PreRunE: Adapt(func(ctx context.Context, args []string) error { + if len(args) == 1 { + opts.port = 0 + return nil + } + port, err := strconv.ParseUint(args[1], 10, 16) if err != nil { return err @@ -67,7 +72,8 @@ func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, op if err != nil { return err } - ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{ + + ports, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{ Protocol: opts.protocol, Index: opts.index, }) @@ -75,6 +81,9 @@ func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, op return err } - fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port) + for _, port := range ports { + fmt.Fprintf(dockerCli.Out(), "%s\n", port.String()) + } + return nil } diff --git a/cmd/formatter/container.go b/cmd/formatter/container.go index f1eb47caf56..8eacb0cf27c 100644 --- a/cmd/formatter/container.go +++ b/cmd/formatter/container.go @@ -216,8 +216,8 @@ func (c *ContainerContext) Ports() string { for _, publisher := range c.c.Publishers { ports = append(ports, types.Port{ IP: publisher.URL, - PrivatePort: uint16(publisher.TargetPort), - PublicPort: uint16(publisher.PublishedPort), + PrivatePort: publisher.TargetPort, + PublicPort: publisher.PublishedPort, Type: publisher.Protocol, }) } diff --git a/docs/reference/compose.md b/docs/reference/compose.md index 7770f785a0b..eec4e874602 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -20,7 +20,7 @@ Define and run multi-container applications with Docker | [`logs`](compose_logs.md) | View output from containers | | [`ls`](compose_ls.md) | List running compose projects | | [`pause`](compose_pause.md) | Pause services | -| [`port`](compose_port.md) | Print the public port for a port binding | +| [`port`](compose_port.md) | List port mappings or print the public port of a specific mapping for the service | | [`ps`](compose_ps.md) | List containers | | [`pull`](compose_pull.md) | Pull service images | | [`push`](compose_push.md) | Push service images | diff --git a/docs/reference/compose_port.md b/docs/reference/compose_port.md index 5e70b353292..8f946abbd2c 100644 --- a/docs/reference/compose_port.md +++ b/docs/reference/compose_port.md @@ -1,7 +1,7 @@ # docker compose port -Print the public port for a port binding +List port mappings or print the public port of a specific mapping for the service ### Options diff --git a/docs/reference/docker_compose_port.yaml b/docs/reference/docker_compose_port.yaml index 8a07f31ea50..e7eb87f7bfa 100644 --- a/docs/reference/docker_compose_port.yaml +++ b/docs/reference/docker_compose_port.yaml @@ -1,7 +1,8 @@ command: docker compose port -short: Print the public port for a port binding +short: | + List port mappings or print the public port of a specific mapping for the service long: Prints the public port for a port binding -usage: docker compose port [OPTIONS] SERVICE PRIVATE_PORT +usage: docker compose port [OPTIONS] SERVICE [PRIVATE_PORT] pname: docker compose plink: docker_compose.yaml options: diff --git a/pkg/api/api.go b/pkg/api/api.go index 31095ac86a6..ae3dfa33929 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -19,6 +19,8 @@ package api import ( "context" "fmt" + "net" + "strconv" "strings" "time" @@ -73,7 +75,7 @@ type Service interface { // Events executes the equivalent to a `compose events` Events(ctx context.Context, projectName string, options EventsOptions) error // Port executes the equivalent to a `compose port` - Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error) + Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (PortPublishers, error) // Publish executes the equivalent to a `compose publish` Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error // Images executes the equivalent of a `compose images` @@ -448,8 +450,8 @@ type CopyOptions struct { // PortPublisher hold status about published port type PortPublisher struct { URL string - TargetPort int - PublishedPort int + TargetPort uint16 + PublishedPort uint16 Protocol string } @@ -476,6 +478,10 @@ type ContainerSummary struct { LocalVolumes int } +func (p *PortPublisher) String() string { + return fmt.Sprintf("%d/%s -> %s", p.TargetPort, p.Protocol, net.JoinHostPort(p.URL, strconv.Itoa(int(p.PublishedPort)))) +} + // PortPublishers is a slice of PortPublisher type PortPublishers []PortPublisher diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index fc44abe7f1a..554727f0c94 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -36,3 +36,36 @@ func TestRunOptionsEnvironmentMap(t *testing.T) { assert.Equal(t, *env["ZOT"], "") assert.Check(t, env["QIX"] == nil) } + +func TestPortPublisherString(t *testing.T) { + tests := map[string]struct { + pub PortPublisher + expect string + }{ + "ipv6_udp": { + pub: PortPublisher{ + Protocol: "udp", + PublishedPort: 32769, + TargetPort: 5060, + URL: "::", + }, + expect: "5060/udp -> [::]:32769", + }, + "ipv4_tcp": { + pub: PortPublisher{ + Protocol: "tcp", + PublishedPort: 5060, + TargetPort: 5060, + URL: "0.0.0.0", + }, + expect: "5060/tcp -> 0.0.0.0:5060", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got := tt.pub.String() + assert.Equal(t, tt.expect, got) + }) + } +} diff --git a/pkg/compose/port.go b/pkg/compose/port.go index 570c6202ff8..8ea3cdfac1a 100644 --- a/pkg/compose/port.go +++ b/pkg/compose/port.go @@ -26,18 +26,40 @@ import ( moby "github.com/docker/docker/api/types" ) -func (s *composeService) Port(ctx context.Context, projectName string, service string, port uint16, options api.PortOptions) (string, int, error) { +func (s *composeService) Port(ctx context.Context, projectName string, service string, port uint16, options api.PortOptions) (api.PortPublishers, error) { projectName = strings.ToLower(projectName) container, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, service, options.Index) if err != nil { - return "", 0, err + return nil, err } - for _, p := range container.Ports { - if p.PrivatePort == port && p.Type == options.Protocol { - return p.IP, int(p.PublicPort), nil + + if port != 0 { + for _, p := range container.Ports { + if p.PrivatePort == port && p.Type == options.Protocol { + return api.PortPublishers{ + api.PortPublisher{ + URL: p.IP, + TargetPort: p.PrivatePort, + PublishedPort: p.PublicPort, + Protocol: p.Type, + }, + }, nil + } } + return nil, portNotFoundError(options.Protocol, port, container) } - return "", 0, portNotFoundError(options.Protocol, port, container) + + res := make(api.PortPublishers, len(container.Ports)) + for idx, p := range container.Ports { + res[idx] = api.PortPublisher{ + URL: p.IP, + TargetPort: p.PrivatePort, + PublishedPort: p.PublicPort, + Protocol: p.Type, + } + } + + return res, nil } func portNotFoundError(protocol string, port uint16, ctr moby.Container) error { diff --git a/pkg/compose/ps.go b/pkg/compose/ps.go index 93f5013ce5d..faf562ac76a 100644 --- a/pkg/compose/ps.go +++ b/pkg/compose/ps.go @@ -52,8 +52,8 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api for i, p := range container.Ports { publishers[i] = api.PortPublisher{ URL: p.IP, - TargetPort: int(p.PrivatePort), - PublishedPort: int(p.PublicPort), + TargetPort: p.PrivatePort, + PublishedPort: p.PublicPort, Protocol: p.Type, } } diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go index 1390a85fb7e..2cee4eb8e44 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -240,13 +240,12 @@ func (mr *MockServiceMockRecorder) Pause(ctx, projectName, options any) *gomock. } // Port mocks base method. -func (m *MockService) Port(ctx context.Context, projectName, service string, port uint16, options api.PortOptions) (string, int, error) { +func (m *MockService) Port(ctx context.Context, projectName, service string, port uint16, options api.PortOptions) (api.PortPublishers, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Port", ctx, projectName, service, port, options) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(int) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret0, _ := ret[0].(api.PortPublishers) + ret1, _ := ret[2].(error) + return ret0, ret1 } // Port indicates an expected call of Port.