Skip to content

Commit

Permalink
feat(wait): skip internal host port check (testcontainers#2691)
Browse files Browse the repository at this point in the history
* feat(wait): skip internal host port check

Add the ability to skip the internal host port check when performing as
HostPortStrategy check. This is useful when a container does bind to the
internal port until additional conditions are met.

Also improve wait for port function documentation while there.

* docs: document skip internal check

---------

Co-authored-by: Manuel de la Peña <[email protected]>
  • Loading branch information
stevenh and mdelapenya authored Aug 12, 2024
1 parent 8dbd4cd commit 4e9cb66
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 9 deletions.
21 changes: 21 additions & 0 deletions docs/features/wait/host_port.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The host-port wait strategy will check if the container is listening to a specif
- 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.
- skip the internal check.

Variations on the HostPort wait strategy are supported, including:

Expand Down Expand Up @@ -39,3 +40,23 @@ req := ContainerRequest{
WaitingFor: wait.ForExposedPort(),
}
```

## Skipping the internal check

_Testcontainers for Go_ checks if the container is listening to the port internally before returning the control to the caller. For that it uses a shell command to check the port status:

<!--codeinclude-->
[Internal check](../../../wait/host_port.go) inside_block:buildInternalCheckCommand
<!--/codeinclude-->

But there are cases where this internal check is not needed, for example when a shell is not available in the container or
when the container doesn't bind the port internally until additional conditions are met.
In this case, the `wait.ForExposedPort.SkipInternalCheck` can be used to skip the internal check.

```golang
req := ContainerRequest{
Image: "docker.io/nginx:alpine",
ExposedPorts: []string{"80/tcp", "9080/tcp"},
WaitingFor: wait.ForExposedPort().SkipInternalCheck(),
}
```
37 changes: 28 additions & 9 deletions wait/host_port.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ type HostPortStrategy struct {
// all WaitStrategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
PollInterval time.Duration

// skipInternalCheck is a flag to skip the internal check, which is useful when
// a shell is not available in the container or when the container doesn't bind
// the port internally until additional conditions are met.
skipInternalCheck bool
}

// NewHostPortStrategy constructs a default host port strategy
// NewHostPortStrategy constructs a default host port strategy that waits for the given
// port to be exposed. The default startup timeout is 60 seconds.
func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
return &HostPortStrategy{
Port: port,
Expand All @@ -42,18 +48,28 @@ func NewHostPortStrategy(port nat.Port) *HostPortStrategy {
// since go has neither covariance nor generics, the return type must be the type of the concrete implementation
// this is true for all properties, even the "shared" ones like startupTimeout

// ForListeningPort is a helper similar to those in Wait.java
// https://github.com/testcontainers/testcontainers-java/blob/1d85a3834bd937f80aad3a4cec249c027f31aeb4/core/src/main/java/org/testcontainers/containers/wait/strategy/Wait.java
// ForListeningPort returns a host port strategy that waits for the given port
// to be exposed and bound internally the container.
// Alias for `NewHostPortStrategy(port)`.
func ForListeningPort(port nat.Port) *HostPortStrategy {
return NewHostPortStrategy(port)
}

// ForExposedPort constructs an exposed port strategy. Alias for `NewHostPortStrategy("")`.
// This strategy waits for the first port exposed in the Docker container.
// ForExposedPort returns a host port strategy that waits for the first port
// to be exposed and bound internally the container.
func ForExposedPort() *HostPortStrategy {
return NewHostPortStrategy("")
}

// SkipInternalCheck changes the host port strategy to skip the internal check,
// which is useful when a shell is not available in the container or when the
// container doesn't bind the port internally until additional conditions are met.
func (hp *HostPortStrategy) SkipInternalCheck() *HostPortStrategy {
hp.skipInternalCheck = true

return hp
}

// WithStartupTimeout can be used to change the default startup timeout
func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *HostPortStrategy {
hp.timeout = &startupTimeout
Expand Down Expand Up @@ -130,6 +146,10 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT
return err
}

if hp.skipInternalCheck {
return nil
}

err = internalCheck(ctx, internalPort, target)
if err != nil && errors.Is(errShellNotExecutable, err) {
log.Println("Shell not executable in container, only external port check will be performed")
Expand Down Expand Up @@ -164,12 +184,11 @@ func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target
}
}
return err
} else {
_ = conn.Close()
break
}

conn.Close()
return nil
}
return nil
}

func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTarget) error {
Expand Down

0 comments on commit 4e9cb66

Please sign in to comment.