diff --git a/docs/features/wait/exec.md b/docs/features/wait/exec.md new file mode 100644 index 0000000000..11c0474bfa --- /dev/null +++ b/docs/features/wait/exec.md @@ -0,0 +1,19 @@ +# Exec Wait Strategy + +The exec wait strategy will check the exit code of a process to be executed in the container, and allows to set the following conditions: + +- the command and arguments to be executed, as an array of strings. +- a function to match a specific exit code, with the default matching `0`. +- the startup timeout to be used in seconds, default is 60 seconds. +- the poll interval to be used in milliseconds, default is 100 milliseconds. + +## Match an exit code + +```golang +req := ContainerRequest{ + Image: "docker.io/nginx:alpine", + WaitingFor: wait.NewExecStrategy([]string{"git", "version"}).WithExitCodeMatcher(func(exitCode int) bool { + return exitCode == 10 + }), +} +``` diff --git a/docs/features/wait/exit.md b/docs/features/wait/exit.md new file mode 100644 index 0000000000..3487cb2d21 --- /dev/null +++ b/docs/features/wait/exit.md @@ -0,0 +1,15 @@ +# Exit Wait strategy + +The exit wait strategy will check that the container is not in the running state, and allows to set the following conditions: + +- the exit timeout in seconds, default is `0`. +- the poll interval to be used in milliseconds, default is 100 milliseconds. + +## Match an exit code + +```golang +req := ContainerRequest{ + Image: "docker.io/alpine:latest", + WaitingFor: wait.ForExit(), +} +``` diff --git a/docs/features/wait/health.md b/docs/features/wait/health.md new file mode 100644 index 0000000000..d4756f47bd --- /dev/null +++ b/docs/features/wait/health.md @@ -0,0 +1,13 @@ +# Health Wait strategy + +The health wait strategy will check that the container is in the healthy state and allows to set the following conditions: + +- the startup timeout to be used in seconds, default is 60 seconds. +- the poll interval to be used in milliseconds, default is 100 milliseconds. + +```golang +req := ContainerRequest{ + Image: "docker.io/alpine:latest", + WaitingFor: wait.ForHealthCheck(), +} +``` diff --git a/docs/features/wait/host_port.md b/docs/features/wait/host_port.md new file mode 100644 index 0000000000..5d52f99cc1 --- /dev/null +++ b/docs/features/wait/host_port.md @@ -0,0 +1,41 @@ +# HostPort Wait strategy + +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. +- the startup timeout to be used, default is 60 seconds. +- the poll interval to be used, default is 100 milliseconds. + +Variations on the HostPort wait strategy are supported, including: + +## Listening port in the container + +```golang +req := ContainerRequest{ + Image: "docker.io/nginx:alpine", + ExposedPorts: []string{"80/tcp"}, + WaitingFor: wait.ForListeningPort("80/tcp"), +} +``` + +## First exposed port in the container + +The wait strategy will use the first exposed port from the container configuration. + +```golang +req := ContainerRequest{ + Image: "docker.io/nginx:alpine", + WaitingFor: wait.ForExposedPort(), +} +``` + +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. + +```golang +req := ContainerRequest{ + Image: "docker.io/nginx:alpine", + 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 new file mode 100644 index 0000000000..91d5aefd3e --- /dev/null +++ b/docs/features/wait/http.md @@ -0,0 +1,61 @@ +# HTTP(S) Wait strategy + +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. +- the path to be used. +- the HTTP method to be used. +- the HTTP request body to be sent. +- the HTTP status code matcher as a function. +- the HTTP response matcher as a function. +- the TLS config to be used for HTTPS. +- the startup timeout to be used in seconds, default is 60 seconds. +- the poll interval to be used in milliseconds, default is 100 milliseconds. + +Variations on the HTTP wait strategy are supported, including: + +## Match an HTTP method + +```golang +req := ContainerRequest{ + Image: "docker.io/nginx:alpine", + ExposedPorts: []string{"8086/tcp"}, + WaitingFor: wait.ForHTTP("/ping").WithMethod(http.MethodPost).WithBody(bytes.NewReader([]byte("ping"))), + } +``` + +## Match an HTTP status code + +```golang +req := ContainerRequest{ + Image: "docker.io/nginx:alpine", + ExposedPorts: []string{"8086/tcp"}, + WaitingFor: wait.ForHTTP("/ping").WithPort("8086/tcp").WithStatusCodeMatcher( + func(status int) bool { + return status == http.StatusNoContent + }, + ), + } +``` + +## Match an HTTPS status code and a response matcher + +```golang +req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ + Context: workdir + "/testdata", + }, + ExposedPorts: []string{"80/tcp"}, + WaitingFor: wait.NewHTTPStrategy("/ping"). + WithStartupTimeout(time.Second * 10).WithPort("80/tcp"). + WithResponseMatcher(func(body io.Reader) bool { + data, _ := ioutil.ReadAll(body) + return bytes.Equal(data, []byte("pong")) + }). + WithStatusCodeMatcher(func(status int) bool { + i++ // always fail the first try in order to force the polling loop to be re-run + return i > 1 && status == 200 + }). + WithMethod(http.MethodPost).WithBody(bytes.NewReader([]byte("ping"))), +} +``` diff --git a/docs/features/wait/introduction.md b/docs/features/wait/introduction.md new file mode 100644 index 0000000000..59423fa153 --- /dev/null +++ b/docs/features/wait/introduction.md @@ -0,0 +1,26 @@ +# Wait Strategies + +There are scenarios where your tests need the external services they rely on to reach a specific state that is particularly useful for testing. This is generally approximated as 'Can we talk to this container over the network?' or 'Let's wait until the container is running an reaches certain state'. + +Testcontainers-go comes with the concept of `wait strategy`, which allows your tests to actually wait for the most useful conditions to be met, before continuing with their execution. These wait strategies are implemented in the `wait` package. + +Below you can find a list of the available wait strategies that you can use: + +- [Exec](./exec.md) +- [Exit](./exit.md) +- [Health](./health.md) +- [HostPort](./host_port.md) +- [HTTP](./http.md) +- [Log](./log.md) +- [Multi](./multi.md) +- [SQL](./sql.md) + +## Startup timeout and Poll interval + +When defining a wait strategy, it should define a way to set the startup timeout to avoid waiting infinitely. For that, Testcontainers-go creates a cancel context with 60 seconds defined as timeout. + +If the default 60s timeout is not sufficient, it can be updated with the `WithStartupTimeout(startupTimeout time.Duration)` function. + +Besides that, it's possible to define a poll interval, which will actually stop 100 milliseconds the test execution. + +If the default 100 milliseconds poll interval is not sufficient, it can be updated with the `WithPollInterval(pollInterval time.Duration)` function. diff --git a/docs/features/wait/log.md b/docs/features/wait/log.md new file mode 100644 index 0000000000..2825423498 --- /dev/null +++ b/docs/features/wait/log.md @@ -0,0 +1,20 @@ +# Log Wait strategy + +The Log wait strategy will check if a string occurs in the container logs for a desired number of times, and allows to set the following conditions: + +- the string to be waited for in the container log. +- the number of occurrences of the string to wait for, default is `1`. +- the startup timeout to be used in seconds, default is 60 seconds. +- the poll interval to be used in milliseconds, default is 100 milliseconds. + +```golang +req := ContainerRequest{ + Image: "docker.io/mysql:latest", + ExposedPorts: []string{"3306/tcp", "33060/tcp"}, + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": "password", + "MYSQL_DATABASE": "database", + }, + WaitingFor: wait.ForLog("port: 3306 MySQL Community Server - GPL"), +} +``` diff --git a/docs/features/wait/multi.md b/docs/features/wait/multi.md new file mode 100644 index 0000000000..0511d0d449 --- /dev/null +++ b/docs/features/wait/multi.md @@ -0,0 +1,20 @@ +# Multi Wait strategy + +The Multi wait strategy will hold a list of wait strategies, in order to wait for all of them. It's possible to set the following conditions: + +- the startup timeout to be used in seconds, default is 60 seconds. + +```golang +req := ContainerRequest{ + Image: "docker.io/mysql:latest", + ExposedPorts: []string{"3306/tcp", "33060/tcp"}, + Env: map[string]string{ + "MYSQL_ROOT_PASSWORD": "password", + "MYSQL_DATABASE": "database", + }, + WaitingFor: wait.ForAll( + wait.ForLog("port: 3306 MySQL Community Server - GPL"), + wait.ForListeningPort("3306/tcp"), + ).WithStartupTimeout(10*time.Second), +} +``` diff --git a/docs/features/wait/sql.md b/docs/features/wait/sql.md new file mode 100644 index 0000000000..5e261cf6c6 --- /dev/null +++ b/docs/features/wait/sql.md @@ -0,0 +1,22 @@ +# SQL Wait strategy + +The SQL wait strategy will check the result of a SQL query executed in a container representing a SQL database, and allows to set the following conditions: + +- the SQL query to be used, default is `SELECT 1`. +- the port to be used. +- the database driver to be used, as a string. +- the URL of the database to be used, as a function returning the URL string. +- the startup timeout to be used in seconds, default is 60 seconds. +- the poll interval to be used in milliseconds, default is 100 milliseconds. + +```golang +req := ContainerRequest{ + Image: "postgres:14.1-alpine", + ExposedPorts: []string{port}, + Cmd: []string{"postgres", "-c", "fsync=off"}, + Env: env, + WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL). + WithStartupTimeout(time.Second * 5). + WithQuery("SELECT 10"), +} +``` diff --git a/mkdocs.yml b/mkdocs.yml index 9ff8f892ac..0d1a7574ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,16 @@ nav: - features/override_container_command.md - features/copy_file.md - features/using_podman.md + - Wait Strategies: + - Introduction: features/wait/introduction.md + - Exec: features/wait/exec.md + - Exit: features/wait/exit.md + - Health: features/wait/health.md + - HostPort: features/wait/host_port.md + - HTTP: features/wait/http.md + - Log: features/wait/log.md + - Multi: features/wait/multi.md + - SQL: features/wait/sql.md - Examples: - examples/cockroachdb.md - examples/nginx.md @@ -46,4 +56,4 @@ nav: - contributing_docs.md - Getting help: getting_help.md extra: - latest_version: 0.13.0 + latest_version: 0.14.0 diff --git a/wait/host_port.go b/wait/host_port.go index 98be362901..53de3405ba 100644 --- a/wait/host_port.go +++ b/wait/host_port.go @@ -21,6 +21,7 @@ type HostPortStrategy struct { Port nat.Port // all WaitStrategies should have a startupTimeout to avoid waiting infinitely startupTimeout time.Duration + PollInterval time.Duration } // NewHostPortStrategy constructs a default host port strategy @@ -28,6 +29,7 @@ func NewHostPortStrategy(port nat.Port) *HostPortStrategy { return &HostPortStrategy{ Port: port, startupTimeout: defaultStartupTimeout(), + PollInterval: defaultPollInterval(), } } @@ -52,6 +54,12 @@ func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *Ho return hp } +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (hp *HostPortStrategy) WithPollInterval(pollInterval time.Duration) *HostPortStrategy { + hp.PollInterval = pollInterval + return hp +} + // WaitUntilReady implements Strategy.WaitUntilReady func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { // limit context to startupTimeout @@ -63,7 +71,7 @@ func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyT return } - var waitInterval = 100 * time.Millisecond + var waitInterval = hp.PollInterval internalPort := hp.Port if internalPort == "" { diff --git a/wait/sql.go b/wait/sql.go index 09f5fd7590..ff305fd8f2 100644 --- a/wait/sql.go +++ b/wait/sql.go @@ -33,8 +33,14 @@ type waitForSql struct { } //Timeout sets the maximum waiting time for the strategy after which it'll give up and return an error +// Deprecated: Use WithStartupTimeout func (w *waitForSql) Timeout(duration time.Duration) *waitForSql { - w.startupTimeout = duration + return w.WithStartupTimeout(duration) +} + +// WithStartupTimeout can be used to change the default startup timeout +func (w *waitForSql) WithStartupTimeout(startupTimeout time.Duration) *waitForSql { + w.startupTimeout = startupTimeout return w }