diff --git a/cmd/compose/port.go b/cmd/compose/port.go index 862e3b5d68..89f76ccb3a 100644 --- a/cmd/compose/port.go +++ b/cmd/compose/port.go @@ -19,6 +19,8 @@ package compose import ( "context" "fmt" + "net" + "sort" "strconv" "strings" @@ -41,16 +43,20 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe 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 { - port, err := strconv.ParseUint(args[1], 10, 16) - if err != nil { - return err - } - opts.port = uint16(port) opts.protocol = strings.ToLower(opts.protocol) + if len(args) > 1 { + port, err := strconv.ParseUint(args[1], 10, 16) + if err != nil { + return err + } + opts.port = uint16(port) + } else { + opts.protocol = "" + } return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { @@ -73,7 +79,7 @@ func runPort(ctx context.Context, dockerCli command.Cli, backendOptions *Backend if err != nil { return err } - ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{ + publishers, err := backend.Ports(ctx, projectName, service, opts.port, api.PortOptions{ Protocol: opts.protocol, Index: opts.index, }) @@ -81,6 +87,14 @@ func runPort(ctx context.Context, dockerCli command.Cli, backendOptions *Backend return err } - _, _ = fmt.Fprintf(dockerCli.Out(), "%s:%d\n", ip, port) + if opts.port != 0 && len(publishers) > 0 { + p := publishers[0] + _, _ = fmt.Fprintf(dockerCli.Out(), "%s\n", net.JoinHostPort(p.URL, strconv.Itoa(p.PublishedPort))) + return nil + } + sort.Sort(publishers) + for _, p := range publishers { + _, _ = fmt.Fprintln(dockerCli.Out(), p.String()) + } return nil } diff --git a/docs/reference/compose.md b/docs/reference/compose.md index d80bb86ec6..bb68c0a515 100644 --- a/docs/reference/compose.md +++ b/docs/reference/compose.md @@ -28,7 +28,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 | | [`publish`](compose_publish.md) | Publish compose application | | [`pull`](compose_pull.md) | Pull service images | diff --git a/docs/reference/compose_port.md b/docs/reference/compose_port.md index bbbfbf1561..80aebc6271 100644 --- a/docs/reference/compose_port.md +++ b/docs/reference/compose_port.md @@ -1,7 +1,7 @@ # docker compose port -Prints the public port for a port binding +List port mappings or print the public port of a specific mapping for the service ### Options @@ -16,4 +16,4 @@ Prints the public port for a port binding ## Description -Prints the public port for a port binding +List port mappings or print the public port of a specific mapping for the service diff --git a/docs/reference/docker_compose_port.yaml b/docs/reference/docker_compose_port.yaml index 8a07f31ea5..ac089d9c5c 100644 --- a/docs/reference/docker_compose_port.yaml +++ b/docs/reference/docker_compose_port.yaml @@ -1,7 +1,9 @@ command: docker compose port -short: Print the public port for a port binding -long: Prints the public port for a port binding -usage: docker compose port [OPTIONS] SERVICE PRIVATE_PORT +short: | + List port mappings or print the public port of a specific mapping for the service +long: | + List port mappings or print the public port of a specific mapping for the service +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 deefc1e52e..5681657360 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -20,7 +20,9 @@ import ( "context" "fmt" "io" + "net" "slices" + "strconv" "strings" "time" @@ -125,8 +127,8 @@ type Compose interface { Top(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error) // 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) + // Ports executes the equivalent to a `compose port` + Ports(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` @@ -535,6 +537,10 @@ type PortPublisher struct { Protocol string } +func (p PortPublisher) String() string { + return fmt.Sprintf("%d/%s -> %s", p.TargetPort, p.Protocol, net.JoinHostPort(p.URL, strconv.Itoa(p.PublishedPort))) +} + // ContainerSummary hold high-level description of a container type ContainerSummary struct { ID string diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index fc44abe7f1..60c3323f5b 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -23,6 +23,22 @@ import ( "gotest.tools/v3/assert" ) +func TestPortPublisherString(t *testing.T) { + tests := []struct { + name string + pub PortPublisher + want string + }{ + {"ipv4", PortPublisher{URL: "0.0.0.0", TargetPort: 80, PublishedPort: 8080, Protocol: "tcp"}, "80/tcp -> 0.0.0.0:8080"}, + {"ipv6", PortPublisher{URL: "::", TargetPort: 5060, PublishedPort: 32769, Protocol: "udp"}, "5060/udp -> [::]:32769"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.pub.String(), tt.want) + }) + } +} + func TestRunOptionsEnvironmentMap(t *testing.T) { opts := RunOptions{ Environment: []string{ diff --git a/pkg/compose/port.go b/pkg/compose/port.go index 93fd816760..f5298f66be 100644 --- a/pkg/compose/port.go +++ b/pkg/compose/port.go @@ -26,18 +26,40 @@ import ( "github.com/docker/compose/v5/pkg/api" ) -func (s *composeService) Port(ctx context.Context, projectName string, service string, port uint16, options api.PortOptions) (string, int, error) { +func (s *composeService) Ports(ctx context.Context, projectName string, service string, port uint16, options api.PortOptions) (api.PortPublishers, error) { projectName = strings.ToLower(projectName) ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffInclude, false, service, options.Index) if err != nil { - return "", 0, err + return nil, err } + + if port != 0 { + for _, p := range ctr.Ports { + if p.PrivatePort == port && p.Type == options.Protocol { + return api.PortPublishers{{ + URL: p.IP.String(), + TargetPort: int(p.PrivatePort), + PublishedPort: int(p.PublicPort), + Protocol: p.Type, + }}, nil + } + } + return nil, portNotFoundError(options.Protocol, port, ctr) + } + + var publishers api.PortPublishers for _, p := range ctr.Ports { - if p.PrivatePort == port && p.Type == options.Protocol { - return p.IP.String(), int(p.PublicPort), nil + if options.Protocol != "" && p.Type != options.Protocol { + continue } + publishers = append(publishers, api.PortPublisher{ + URL: p.IP.String(), + TargetPort: int(p.PrivatePort), + PublishedPort: int(p.PublicPort), + Protocol: p.Type, + }) } - return "", 0, portNotFoundError(options.Protocol, port, ctr) + return publishers, nil } func portNotFoundError(protocol string, port uint16, ctr container.Summary) error { diff --git a/pkg/mocks/mock_docker_compose_api.go b/pkg/mocks/mock_docker_compose_api.go index db6ddb92ec..3066d6659b 100644 --- a/pkg/mocks/mock_docker_compose_api.go +++ b/pkg/mocks/mock_docker_compose_api.go @@ -270,20 +270,19 @@ func (mr *MockComposeMockRecorder) Pause(ctx, projectName, options any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pause", reflect.TypeOf((*MockCompose)(nil).Pause), ctx, projectName, options) } -// Port mocks base method. -func (m *MockCompose) Port(ctx context.Context, projectName, service string, port uint16, options api.PortOptions) (string, int, error) { +// Ports mocks base method. +func (m *MockCompose) Ports(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 + ret := m.ctrl.Call(m, "Ports", ctx, projectName, service, port, options) + ret0, _ := ret[0].(api.PortPublishers) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// Port indicates an expected call of Port. -func (mr *MockComposeMockRecorder) Port(ctx, projectName, service, port, options any) *gomock.Call { +// Ports indicates an expected call of Ports. +func (mr *MockComposeMockRecorder) Ports(ctx, projectName, service, port, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Port", reflect.TypeOf((*MockCompose)(nil).Port), ctx, projectName, service, port, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ports", reflect.TypeOf((*MockCompose)(nil).Ports), ctx, projectName, service, port, options) } // Ps mocks base method.