diff --git a/container.go b/container.go index 30d0ced2b0..b772ba948f 100644 --- a/container.go +++ b/container.go @@ -36,7 +36,7 @@ type DeprecatedContainer interface { // Container allows getting info about and controlling a single container instance type Container interface { GetContainerID() string // get the container id from the provider - Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the first exposed port + Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the lowest exposed port PortEndpoint(context.Context, nat.Port, string) (string, error) // get proto://ip:port string for the given exposed port Host(context.Context) (string, error) // get host where the container port is exposed Inspect(context.Context) (*types.ContainerJSON, error) // get container info diff --git a/docker.go b/docker.go index 4a641b8708..7688a77d7d 100644 --- a/docker.go +++ b/docker.go @@ -113,7 +113,7 @@ func (c *DockerContainer) IsRunning() bool { return c.isRunning } -// Endpoint gets proto://host:port string for the first exposed port +// Endpoint gets proto://host:port string for the lowest numbered exposed port // Will returns just host:port if proto is "" func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, error) { inspect, err := c.Inspect(ctx) @@ -121,16 +121,15 @@ func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, e return "", err } - ports := inspect.NetworkSettings.Ports - - // get first port - var firstPort nat.Port - for p := range ports { - firstPort = p - break + // Get lowest numbered bound port. + var lowestPort nat.Port + for port := range inspect.NetworkSettings.Ports { + if lowestPort == "" || port.Int() < lowestPort.Int() { + lowestPort = port + } } - return c.PortEndpoint(ctx, firstPort, proto) + return c.PortEndpoint(ctx, lowestPort, proto) } // PortEndpoint gets proto://host:port string for the given exposed port diff --git a/docs/features/wait/host_port.md b/docs/features/wait/host_port.md index 5d52f99cc1..edfad5a188 100644 --- a/docs/features/wait/host_port.md +++ b/docs/features/wait/host_port.md @@ -3,7 +3,7 @@ The host-port wait strategy will check if the container is listening to a specific port and allows to set the following conditions: - a port exposed by the container. The port and protocol to be used, which is represented by a string containing the port number and protocol in the format "80/tcp". -- alternatively, wait for the first exposed port in the container. +- alternatively, wait for the lowest exposed port in the container. - the startup timeout to be used, default is 60 seconds. - the poll interval to be used, default is 100 milliseconds. @@ -19,9 +19,9 @@ req := ContainerRequest{ } ``` -## First exposed port in the container +## Lowest exposed port in the container -The wait strategy will use the first exposed port from the container configuration. +The wait strategy will use the lowest exposed port from the container configuration. ```golang req := ContainerRequest{ @@ -30,7 +30,7 @@ req := ContainerRequest{ } ``` -Said that, it could be the case that the container request included ports to be exposed. Therefore using `wait.ForExposedPort` will wait for the first exposed port in the request, because the container configuration retrieved from Docker will already include them. +Said that, it could be the case that the container request included ports to be exposed. Therefore using `wait.ForExposedPort` will wait for the lowest exposed port in the request, because the container configuration retrieved from Docker will already include them. ```golang req := ContainerRequest{ @@ -38,4 +38,4 @@ req := ContainerRequest{ ExposedPorts: []string{"80/tcp", "9080/tcp"}, WaitingFor: wait.ForExposedPort(), } -``` \ No newline at end of file +``` diff --git a/docs/features/wait/http.md b/docs/features/wait/http.md index ab9d294fb5..b4b9b57b95 100644 --- a/docs/features/wait/http.md +++ b/docs/features/wait/http.md @@ -2,7 +2,7 @@ The HTTP wait strategy will check the result of an HTTP(S) request against the container and allows to set the following conditions: -- the port to be used. If no port is passed, it will use the first exposed port in the image. +- the port to be used. If no port is passed, it will use the lowest exposed port in the image. - the path to be used. - the HTTP method to be used. - the HTTP request body to be sent. diff --git a/wait/host_port.go b/wait/host_port.go index fc95492a0d..9d47a0b39d 100644 --- a/wait/host_port.go +++ b/wait/host_port.go @@ -89,18 +89,14 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT internalPort := hp.Port if internalPort == "" { - var ports nat.PortMap inspect, err := target.Inspect(ctx) if err != nil { return err } - ports = inspect.NetworkSettings.Ports - - if len(ports) > 0 { - for p := range ports { - internalPort = p - break + for port := range inspect.NetworkSettings.Ports { + if internalPort == "" || port.Int() < internalPort.Int() { + internalPort = port } } } diff --git a/wait/http.go b/wait/http.go index c93cec59c8..11452ecbf0 100644 --- a/wait/http.go +++ b/wait/http.go @@ -76,6 +76,8 @@ func (ws *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy return ws } +// WithPort set the port to wait for. +// Default is the lowest numbered port. func (ws *HTTPStrategy) WithPort(port nat.Port) *HTTPStrategy { ws.Port = port return ws @@ -173,38 +175,43 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge var mappedPort nat.Port if ws.Port == "" { - var err error - var ports nat.PortMap - // we wait one polling interval before we grab the ports otherwise they might not be bound yet on startup - for err != nil || ports == nil { - select { - case <-ctx.Done(): - return fmt.Errorf("%w: %w", ctx.Err(), err) - case <-time.After(ws.PollInterval): - if err := checkTarget(ctx, target); err != nil { - return err - } + // We wait one polling interval before we grab the ports + // otherwise they might not be bound yet on startup. + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(ws.PollInterval): + // Port should now be bound so just continue. + } - inspect, err := target.Inspect(ctx) - if err != nil { - return err - } + if err := checkTarget(ctx, target); err != nil { + return err + } - ports = inspect.NetworkSettings.Ports - } + inspect, err := target.Inspect(ctx) + if err != nil { + return err } - for k, bindings := range ports { - if len(bindings) == 0 || k.Proto() != "tcp" { + // Find the lowest numbered exposed tcp port. + var lowestPort nat.Port + var hostPort string + for port, bindings := range inspect.NetworkSettings.Ports { + if len(bindings) == 0 || port.Proto() != "tcp" { continue } - mappedPort, _ = nat.NewPort(k.Proto(), bindings[0].HostPort) - break + + if lowestPort == "" || port.Int() < lowestPort.Int() { + lowestPort = port + hostPort = bindings[0].HostPort + } } - if mappedPort == "" { + if lowestPort == "" { return errors.New("No exposed tcp ports or mapped ports - cannot wait for status") } + + mappedPort, _ = nat.NewPort(lowestPort.Proto(), hostPort) } else { mappedPort, err = target.MappedPort(ctx, ws.Port)