Skip to content

Commit

Permalink
fix: Add HTTPStrategy WithForcedIPv4LocalHost To Fix Docker Port Map (#…
Browse files Browse the repository at this point in the history
…1775)

* Add HTTPStrategy WithForcedIPv4LocalHost To Fix Docker Port Map Bug
Associated docker port mapping bugs:
moby/moby#42442
moby/moby#42375
If ipv6 is enabled in docker then these bugs affect this library. This is even if we build the docker network with ipv6 disabled since the ipv6 ports are still forwarded. This creates the potential for localhost for a container to be mapped to two different ports between ipv4 and ipv6. This is fine if you only have one container but once you have multiple containers spun up these ports can overlap where one containers ipv4 port is the same as another containers ipv6 port, at which point if you use localhost you are not guaranteed the ipv4 address and thus can end up calling into the wrong container.

* chore: rename variable

* chore: add print for the assertion

---------

Co-authored-by: Manuel de la Peña <mdelapenya@gmail.com>
  • Loading branch information
tateexon and mdelapenya committed Jan 30, 2024
1 parent c2cc09f commit 83ae8bf
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 17 deletions.
35 changes: 24 additions & 11 deletions wait/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"

"github.com/docker/go-connections/nat"
Expand All @@ -27,17 +28,18 @@ type HTTPStrategy struct {
timeout *time.Duration

// additional properties
Port nat.Port
Path string
StatusCodeMatcher func(status int) bool
ResponseMatcher func(body io.Reader) bool
UseTLS bool
AllowInsecure bool
TLSConfig *tls.Config // TLS config for HTTPS
Method string // http method
Body io.Reader // http request body
PollInterval time.Duration
UserInfo *url.Userinfo
Port nat.Port
Path string
StatusCodeMatcher func(status int) bool
ResponseMatcher func(body io.Reader) bool
UseTLS bool
AllowInsecure bool
TLSConfig *tls.Config // TLS config for HTTPS
Method string // http method
Body io.Reader // http request body
PollInterval time.Duration
UserInfo *url.Userinfo
ForceIPv4LocalHost bool
}

// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
Expand Down Expand Up @@ -119,6 +121,13 @@ func (ws *HTTPStrategy) WithPollInterval(pollInterval time.Duration) *HTTPStrate
return ws
}

// WithForcedIPv4LocalHost forces usage of localhost to be ipv4 127.0.0.1
// to avoid ipv6 docker bugs https://github.com/moby/moby/issues/42442 https://github.com/moby/moby/issues/42375
func (ws *HTTPStrategy) WithForcedIPv4LocalHost() *HTTPStrategy {
ws.ForceIPv4LocalHost = true
return ws
}

// ForHTTP is a convenience method similar to Wait.java
// https://github.com/testcontainers/testcontainers-java/blob/1d85a3834bd937f80aad3a4cec249c027f31aeb4/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java
func ForHTTP(path string) *HTTPStrategy {
Expand All @@ -143,6 +152,10 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
if err != nil {
return err
}
// to avoid ipv6 docker bugs https://github.com/moby/moby/issues/42442 https://github.com/moby/moby/issues/42375
if ws.ForceIPv4LocalHost {
ipAddress = strings.Replace(ipAddress, "localhost", "127.0.0.1", 1)
}

var mappedPort nat.Port
if ws.Port == "" {
Expand Down
45 changes: 39 additions & 6 deletions wait/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func ExampleHTTPStrategy() {
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(10 * time.Second),
}

gogs, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
Expand All @@ -42,12 +42,12 @@ func ExampleHTTPStrategy() {
// }

defer func() {
if err := gogs.Terminate(ctx); err != nil {
if err := c.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err)
}
}()

state, err := gogs.State(ctx)
state, err := c.State(ctx)
if err != nil {
panic(err)
}
Expand All @@ -67,7 +67,7 @@ func ExampleHTTPStrategy_WithPort() {
WaitingFor: wait.ForHTTP("/").WithPort("80/tcp"),
}

gogs, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
Expand All @@ -77,12 +77,45 @@ func ExampleHTTPStrategy_WithPort() {
// }

defer func() {
if err := gogs.Terminate(ctx); err != nil {
if err := c.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err)
}
}()

state, err := gogs.State(ctx)
state, err := c.State(ctx)
if err != nil {
panic(err)
}

fmt.Println(state.Running)

// Output:
// true
}

func ExampleHTTPStrategy_WithForcedIPv4LocalHost() {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "nginx:latest",
ExposedPorts: []string{"8080/tcp", "80/tcp"},
WaitingFor: wait.ForHTTP("/").WithForcedIPv4LocalHost(),
}

c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
panic(err)
}

defer func() {
if err := c.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err)
}
}()

state, err := c.State(ctx)
if err != nil {
panic(err)
}
Expand Down

0 comments on commit 83ae8bf

Please sign in to comment.