PRIVATE_PORT optional for docker compose port

Signed-off-by: Max Proske <max@mproske.com>
This commit is contained in:
Max Proske 2026-02-07 21:57:11 -08:00
parent f9828dfab9
commit c350344f46
8 changed files with 93 additions and 34 deletions

View file

@ -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
}

View file

@ -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 |

View file

@ -1,7 +1,7 @@
# docker compose port
<!---MARKER_GEN_START-->
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

View file

@ -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:

View file

@ -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

View file

@ -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{

View file

@ -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
}
for _, p := range ctr.Ports {
if p.PrivatePort == port && p.Type == options.Protocol {
return p.IP.String(), int(p.PublicPort), nil
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)
}
return "", 0, portNotFoundError(options.Protocol, port, ctr)
var publishers api.PortPublishers
for _, p := range ctr.Ports {
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 publishers, nil
}
func portNotFoundError(protocol string, port uint16, ctr container.Summary) error {

View file

@ -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.