diff --git a/docs/features/wait/multi.md b/docs/features/wait/multi.md index 9c9d694222..d8713d3864 100644 --- a/docs/features/wait/multi.md +++ b/docs/features/wait/multi.md @@ -1,8 +1,11 @@ # 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 Multi wait strategy holds a list of wait strategies. The execution of each strategy is first added, first executed. -- the startup timeout to be used in seconds, default is 60 seconds. +Available Options: + +- `WithDeadline` - the deadline for when all strategies must complete by, default is none. +- `WithStartupTimeoutDefault` - the startup timeout default to be used for each Strategy if not defined in seconds, default is 60 seconds. ```golang req := ContainerRequest{ @@ -12,9 +15,11 @@ req := ContainerRequest{ "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), + wait.ForAll( + wait.ForLog("port: 3306 MySQL Community Server - GPL"), // Timeout: 120s (from ForAll.WithStartupTimeoutDefault) + wait.ForExposedPort().WithStartupTimeout(180*time.Second), // Timeout: 180s + wait.ForListeningPort("3306/tcp").WithStartupTimeout(10*time.Second), // Timeout: 10s + ).WithStartupTimeoutDefault(120*time.Second). // Applies default StartupTimeout when not explictly defined + WithDeadline(360*time.Second) // Applies deadline for all Wait Strategies } ``` diff --git a/e2e/container_test.go b/e2e/container_test.go index 3be69f315d..e1888c142b 100644 --- a/e2e/container_test.go +++ b/e2e/container_test.go @@ -36,7 +36,7 @@ func TestContainerWithWaitForSQL(t *testing.T) { Cmd: []string{"postgres", "-c", "fsync=off"}, Env: env, WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL). - Timeout(time.Second * 5), + WithStartupTimeout(time.Second * 5), } container, err := GenericContainer(ctx, GenericContainerRequest{ ContainerRequest: req, @@ -55,7 +55,7 @@ func TestContainerWithWaitForSQL(t *testing.T) { Cmd: []string{"postgres", "-c", "fsync=off"}, Env: env, WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL). - Timeout(time.Second * 5). + WithStartupTimeout(time.Second * 5). WithQuery("SELECT 10"), } container, err := GenericContainer(ctx, GenericContainerRequest{ @@ -75,7 +75,7 @@ func TestContainerWithWaitForSQL(t *testing.T) { Cmd: []string{"postgres", "-c", "fsync=off"}, Env: env, WaitingFor: wait.ForSQL(nat.Port(port), "postgres", dbURL). - Timeout(time.Second * 5). + WithStartupTimeout(time.Second * 5). WithQuery("SELECT 'a' from b"), } container, err := GenericContainer(ctx, GenericContainerRequest{ diff --git a/wait/all.go b/wait/all.go index 468a5a1932..18531cbf54 100644 --- a/wait/all.go +++ b/wait/all.go @@ -8,40 +8,73 @@ import ( // Implement interface var _ Strategy = (*MultiStrategy)(nil) +var _ StrategyTimeout = (*MultiStrategy)(nil) type MultiStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration + timeout *time.Duration + deadline *time.Duration // additional properties Strategies []Strategy } -func (ms *MultiStrategy) WithStartupTimeout(startupTimeout time.Duration) *MultiStrategy { - ms.startupTimeout = startupTimeout +// WithStartupTimeoutDefault sets the default timeout for all inner wait strategies +func (ms *MultiStrategy) WithStartupTimeoutDefault(timeout time.Duration) *MultiStrategy { + ms.timeout = &timeout + return ms +} + +// WithStartupTimeout sets a time.Duration which limits all wait strategies +// +// Deprecated: use WithDeadline +func (ms *MultiStrategy) WithStartupTimeout(timeout time.Duration) Strategy { + return ms.WithDeadline(timeout) +} + +// WithDeadline sets a time.Duration which limits all wait strategies +func (ms *MultiStrategy) WithDeadline(deadline time.Duration) *MultiStrategy { + ms.deadline = &deadline return ms } func ForAll(strategies ...Strategy) *MultiStrategy { return &MultiStrategy{ - startupTimeout: defaultStartupTimeout(), - Strategies: strategies, + Strategies: strategies, } } -func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - ctx, cancelContext := context.WithTimeout(ctx, ms.startupTimeout) - defer cancelContext() +func (ms *MultiStrategy) Timeout() *time.Duration { + return ms.timeout +} + +func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + var cancel context.CancelFunc + if ms.deadline != nil { + ctx, cancel = context.WithTimeout(ctx, *ms.deadline) + defer cancel() + } if len(ms.Strategies) == 0 { return fmt.Errorf("no wait strategy supplied") } for _, strategy := range ms.Strategies { - err := strategy.WaitUntilReady(ctx, target) + strategyCtx := ctx + + // Set default Timeout when strategy implements StrategyTimeout + if st, ok := strategy.(StrategyTimeout); ok { + if ms.Timeout() != nil && st.Timeout() == nil { + strategyCtx, cancel = context.WithTimeout(ctx, *ms.Timeout()) + defer cancel() + } + } + + err := strategy.WaitUntilReady(strategyCtx, target) if err != nil { return err } } + return nil } diff --git a/wait/all_test.go b/wait/all_test.go new file mode 100644 index 0000000000..770a54f32c --- /dev/null +++ b/wait/all_test.go @@ -0,0 +1,121 @@ +package wait + +import ( + "bytes" + "context" + "errors" + "io" + "testing" + "time" +) + +func TestMultiStrategy_WaitUntilReady(t *testing.T) { + t.Parallel() + type args struct { + ctx context.Context + target StrategyTarget + } + tests := []struct { + name string + strategy Strategy + args args + wantErr bool + }{ + { + name: "returns error when no WaitStrategies are passed", + strategy: ForAll(), + args: args{ + ctx: context.Background(), + target: NopStrategyTarget{}, + }, + wantErr: true, + }, + { + name: "returns WaitStrategy error", + strategy: ForAll( + ForNop( + func(ctx context.Context, target StrategyTarget) error { + return errors.New("intentional failure") + }, + ), + ), + args: args{ + ctx: context.Background(), + target: NopStrategyTarget{}, + }, + wantErr: true, + }, + { + name: "WithDeadline sets context Deadline for WaitStrategy", + strategy: ForAll( + ForNop( + func(ctx context.Context, target StrategyTarget) error { + if _, set := ctx.Deadline(); !set { + return errors.New("expected context.Deadline to be set") + } + return nil + }, + ), + ForLog("docker"), + ).WithDeadline(1 * time.Second), + args: args{ + ctx: context.Background(), + target: NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))), + }, + }, + wantErr: false, + }, + { + name: "WithStartupTimeoutDefault skips setting context.Deadline when WaitStrategy.Timeout is defined", + strategy: ForAll( + ForNop( + func(ctx context.Context, target StrategyTarget) error { + if _, set := ctx.Deadline(); set { + return errors.New("expected context.Deadline not to be set") + } + return nil + }, + ).WithStartupTimeout(2*time.Second), + ForLog("docker"), + ).WithStartupTimeoutDefault(1 * time.Second), + args: args{ + ctx: context.Background(), + target: NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))), + }, + }, + wantErr: false, + }, + { + name: "WithStartupTimeoutDefault sets context.Deadline for nil WaitStrategy.Timeout", + strategy: ForAll( + ForNop( + func(ctx context.Context, target StrategyTarget) error { + if _, set := ctx.Deadline(); !set { + return errors.New("expected context.Deadline to be set") + } + return nil + }, + ), + ForLog("docker"), + ).WithStartupTimeoutDefault(1 * time.Second), + args: args{ + ctx: context.Background(), + target: NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))), + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if err := tt.strategy.WaitUntilReady(tt.args.ctx, tt.args.target); (err != nil) != tt.wantErr { + t.Errorf("ForAll.WaitUntilReady() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} diff --git a/wait/exec.go b/wait/exec.go index c034b3c166..556b79d932 100644 --- a/wait/exec.go +++ b/wait/exec.go @@ -7,11 +7,12 @@ import ( // Implement interface var _ Strategy = (*ExecStrategy)(nil) +var _ StrategyTimeout = (*ExecStrategy)(nil) type ExecStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration - cmd []string + timeout *time.Duration + cmd []string // additional properties ExitCodeMatcher func(exitCode int) bool @@ -21,7 +22,6 @@ type ExecStrategy struct { // NewExecStrategy constructs an Exec strategy ... func NewExecStrategy(cmd []string) *ExecStrategy { return &ExecStrategy{ - startupTimeout: defaultStartupTimeout(), cmd: cmd, ExitCodeMatcher: defaultExitCodeMatcher, PollInterval: defaultPollInterval(), @@ -32,8 +32,9 @@ func defaultExitCodeMatcher(exitCode int) bool { return exitCode == 0 } +// WithStartupTimeout can be used to change the default startup timeout func (ws *ExecStrategy) WithStartupTimeout(startupTimeout time.Duration) *ExecStrategy { - ws.startupTimeout = startupTimeout + ws.timeout = &startupTimeout return ws } @@ -53,10 +54,18 @@ func ForExec(cmd []string) *ExecStrategy { return NewExecStrategy(cmd) } -func (ws ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { - // limit context to startupTimeout - ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout) - defer cancelContext() +func (ws *ExecStrategy) Timeout() *time.Duration { + return ws.timeout +} + +func (ws *ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() for { select { diff --git a/wait/exit.go b/wait/exit.go index 528a2306be..8ff59fe0ae 100644 --- a/wait/exit.go +++ b/wait/exit.go @@ -8,11 +8,12 @@ import ( // Implement interface var _ Strategy = (*ExitStrategy)(nil) +var _ StrategyTimeout = (*ExitStrategy)(nil) // ExitStrategy will wait until container exit type ExitStrategy struct { // all Strategies should have a timeout to avoid waiting infinitely - exitTimeout time.Duration + timeout *time.Duration // additional properties PollInterval time.Duration @@ -32,7 +33,7 @@ func NewExitStrategy() *ExitStrategy { // WithExitTimeout can be used to change the default exit timeout func (ws *ExitStrategy) WithExitTimeout(exitTimeout time.Duration) *ExitStrategy { - ws.exitTimeout = exitTimeout + ws.timeout = &exitTimeout return ws } @@ -53,13 +54,16 @@ func ForExit() *ExitStrategy { return NewExitStrategy() } +func (ws *ExitStrategy) Timeout() *time.Duration { + return ws.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (ws *ExitStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to exitTimeout - if ws.exitTimeout > 0 { - var cancelContext context.CancelFunc - ctx, cancelContext = context.WithTimeout(ctx, ws.exitTimeout) - defer cancelContext() + if ws.timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *ws.timeout) + defer cancel() } for { diff --git a/wait/health.go b/wait/health.go index 9ff6635867..00130d8485 100644 --- a/wait/health.go +++ b/wait/health.go @@ -7,11 +7,12 @@ import ( // Implement interface var _ Strategy = (*HealthStrategy)(nil) +var _ StrategyTimeout = (*HealthStrategy)(nil) // HealthStrategy will wait until the container becomes healthy type HealthStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration + timeout *time.Duration // additional properties PollInterval time.Duration @@ -20,8 +21,7 @@ type HealthStrategy struct { // NewHealthStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default func NewHealthStrategy() *HealthStrategy { return &HealthStrategy{ - startupTimeout: defaultStartupTimeout(), - PollInterval: defaultPollInterval(), + PollInterval: defaultPollInterval(), } } @@ -32,7 +32,7 @@ func NewHealthStrategy() *HealthStrategy { // WithStartupTimeout can be used to change the default startup timeout func (ws *HealthStrategy) WithStartupTimeout(startupTimeout time.Duration) *HealthStrategy { - ws.startupTimeout = startupTimeout + ws.timeout = &startupTimeout return ws } @@ -53,11 +53,19 @@ func ForHealthCheck() *HealthStrategy { return NewHealthStrategy() } +func (ws *HealthStrategy) Timeout() *time.Duration { + return ws.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (ws *HealthStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to exitTimeout - ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout) - defer cancelContext() + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() for { select { diff --git a/wait/host_port.go b/wait/host_port.go index 53de3405ba..be317813e2 100644 --- a/wait/host_port.go +++ b/wait/host_port.go @@ -14,22 +14,22 @@ import ( // Implement interface var _ Strategy = (*HostPortStrategy)(nil) +var _ StrategyTimeout = (*HostPortStrategy)(nil) type HostPortStrategy struct { // Port is a string containing port number and protocol in the format "80/tcp" // which Port nat.Port // all WaitStrategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration - PollInterval time.Duration + timeout *time.Duration + PollInterval time.Duration } // NewHostPortStrategy constructs a default host port strategy func NewHostPortStrategy(port nat.Port) *HostPortStrategy { return &HostPortStrategy{ - Port: port, - startupTimeout: defaultStartupTimeout(), - PollInterval: defaultPollInterval(), + Port: port, + PollInterval: defaultPollInterval(), } } @@ -49,8 +49,9 @@ func ForExposedPort() *HostPortStrategy { return NewHostPortStrategy("") } +// WithStartupTimeout can be used to change the default startup timeout func (hp *HostPortStrategy) WithStartupTimeout(startupTimeout time.Duration) *HostPortStrategy { - hp.startupTimeout = startupTimeout + hp.timeout = &startupTimeout return hp } @@ -60,11 +61,19 @@ func (hp *HostPortStrategy) WithPollInterval(pollInterval time.Duration) *HostPo return hp } +func (hp *HostPortStrategy) Timeout() *time.Duration { + return hp.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (hp *HostPortStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to startupTimeout - ctx, cancelContext := context.WithTimeout(ctx, hp.startupTimeout) - defer cancelContext() + timeout := defaultStartupTimeout() + if hp.timeout != nil { + timeout = *hp.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() ipAddress, err := target.Host(ctx) if err != nil { diff --git a/wait/http.go b/wait/http.go index 206f51afb3..60dec1004b 100644 --- a/wait/http.go +++ b/wait/http.go @@ -17,10 +17,11 @@ import ( // Implement interface var _ Strategy = (*HTTPStrategy)(nil) +var _ StrategyTimeout = (*HTTPStrategy)(nil) type HTTPStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration + timeout *time.Duration // additional properties Port nat.Port @@ -38,7 +39,6 @@ type HTTPStrategy struct { // NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200 func NewHTTPStrategy(path string) *HTTPStrategy { return &HTTPStrategy{ - startupTimeout: defaultStartupTimeout(), Port: "80/tcp", Path: path, StatusCodeMatcher: defaultStatusCodeMatcher, @@ -59,8 +59,9 @@ func defaultStatusCodeMatcher(status int) bool { // 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 -func (ws *HTTPStrategy) WithStartupTimeout(startupTimeout time.Duration) *HTTPStrategy { - ws.startupTimeout = startupTimeout +// WithStartupTimeout can be used to change the default startup timeout +func (ws *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy { + ws.timeout = &timeout return ws } @@ -114,11 +115,19 @@ func ForHTTP(path string) *HTTPStrategy { return NewHTTPStrategy(path) } +func (ws *HTTPStrategy) Timeout() *time.Duration { + return ws.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to startupTimeout - ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout) - defer cancelContext() + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() ipAddress, err := target.Host(ctx) if err != nil { diff --git a/wait/log.go b/wait/log.go index aa3d711475..3e735cb801 100644 --- a/wait/log.go +++ b/wait/log.go @@ -9,11 +9,12 @@ import ( // Implement interface var _ Strategy = (*LogStrategy)(nil) +var _ StrategyTimeout = (*LogStrategy)(nil) // LogStrategy will wait until a given log entry shows up in the docker logs type LogStrategy struct { // all Strategies should have a startupTimeout to avoid waiting infinitely - startupTimeout time.Duration + timeout *time.Duration // additional properties Log string @@ -24,12 +25,10 @@ type LogStrategy struct { // NewLogStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default func NewLogStrategy(log string) *LogStrategy { return &LogStrategy{ - startupTimeout: defaultStartupTimeout(), - Log: log, - Occurrence: 1, - PollInterval: defaultPollInterval(), + Log: log, + Occurrence: 1, + PollInterval: defaultPollInterval(), } - } // fluent builders for each property @@ -37,8 +36,8 @@ func NewLogStrategy(log string) *LogStrategy { // this is true for all properties, even the "shared" ones like startupTimeout // WithStartupTimeout can be used to change the default startup timeout -func (ws *LogStrategy) WithStartupTimeout(startupTimeout time.Duration) *LogStrategy { - ws.startupTimeout = startupTimeout +func (ws *LogStrategy) WithStartupTimeout(timeout time.Duration) *LogStrategy { + ws.timeout = &timeout return ws } @@ -68,11 +67,19 @@ func ForLog(log string) *LogStrategy { return NewLogStrategy(log) } +func (ws *LogStrategy) Timeout() *time.Duration { + return ws.timeout +} + // WaitUntilReady implements Strategy.WaitUntilReady func (ws *LogStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - // limit context to startupTimeout - ctx, cancelContext := context.WithTimeout(ctx, ws.startupTimeout) - defer cancelContext() + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() LOOP: for { diff --git a/wait/log_test.go b/wait/log_test.go index bb902d0c44..f078b5a95b 100644 --- a/wait/log_test.go +++ b/wait/log_test.go @@ -6,42 +6,11 @@ import ( "io" "testing" "time" - - "github.com/docker/docker/api/types" - "github.com/docker/go-connections/nat" - tcexec "github.com/testcontainers/testcontainers-go/exec" ) -type noopStrategyTarget struct { - ioReaderCloser io.ReadCloser -} - -func (st noopStrategyTarget) Host(ctx context.Context) (string, error) { - return "", nil -} - -func (st noopStrategyTarget) Ports(ctx context.Context) (nat.PortMap, error) { - return nil, nil -} - -func (st noopStrategyTarget) MappedPort(ctx context.Context, n nat.Port) (nat.Port, error) { - return n, nil -} - -func (st noopStrategyTarget) Logs(ctx context.Context) (io.ReadCloser, error) { - return st.ioReaderCloser, nil -} - -func (st noopStrategyTarget) Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) { - return 0, nil, nil -} -func (st noopStrategyTarget) State(ctx context.Context) (*types.ContainerState, error) { - return nil, nil -} - func TestWaitForLog(t *testing.T) { - target := noopStrategyTarget{ - ioReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))), + target := NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("docker"))), } wg := NewLogStrategy("docker").WithStartupTimeout(100 * time.Microsecond) err := wg.WaitUntilReady(context.Background(), target) @@ -51,8 +20,8 @@ func TestWaitForLog(t *testing.T) { } func TestWaitWithExactNumberOfOccurrences(t *testing.T) { - target := noopStrategyTarget{ - ioReaderCloser: io.NopCloser(bytes.NewReader([]byte("kubernetes\r\ndocker\n\rdocker"))), + target := NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("kubernetes\r\ndocker\n\rdocker"))), } wg := NewLogStrategy("docker"). WithStartupTimeout(100 * time.Microsecond). @@ -64,8 +33,8 @@ func TestWaitWithExactNumberOfOccurrences(t *testing.T) { } func TestWaitWithExactNumberOfOccurrencesButItWillNeverHappen(t *testing.T) { - target := noopStrategyTarget{ - ioReaderCloser: io.NopCloser(bytes.NewReader([]byte("kubernetes\r\ndocker"))), + target := NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("kubernetes\r\ndocker"))), } wg := NewLogStrategy("containerd"). WithStartupTimeout(100 * time.Microsecond). @@ -77,8 +46,8 @@ func TestWaitWithExactNumberOfOccurrencesButItWillNeverHappen(t *testing.T) { } func TestWaitShouldFailWithExactNumberOfOccurrences(t *testing.T) { - target := noopStrategyTarget{ - ioReaderCloser: io.NopCloser(bytes.NewReader([]byte("kubernetes\r\ndocker"))), + target := NopStrategyTarget{ + ReaderCloser: io.NopCloser(bytes.NewReader([]byte("kubernetes\r\ndocker"))), } wg := NewLogStrategy("docker"). WithStartupTimeout(100 * time.Microsecond). diff --git a/wait/nop.go b/wait/nop.go new file mode 100644 index 0000000000..a16feb8787 --- /dev/null +++ b/wait/nop.go @@ -0,0 +1,69 @@ +package wait + +import ( + "context" + "io" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/go-connections/nat" + "github.com/testcontainers/testcontainers-go/exec" +) + +var _ Strategy = (*NopStrategy)(nil) +var _ StrategyTimeout = (*NopStrategy)(nil) + +type NopStrategy struct { + timeout *time.Duration + waitUntilReady func(context.Context, StrategyTarget) error +} + +func ForNop( + waitUntilReady func(context.Context, StrategyTarget) error, +) *NopStrategy { + return &NopStrategy{ + waitUntilReady: waitUntilReady, + } +} + +func (ws *NopStrategy) Timeout() *time.Duration { + return ws.timeout +} + +func (ws *NopStrategy) WithStartupTimeout(timeout time.Duration) *NopStrategy { + ws.timeout = &timeout + return ws +} + +func (ws *NopStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + return ws.waitUntilReady(ctx, target) +} + +type NopStrategyTarget struct { + ReaderCloser io.ReadCloser + ContainerState types.ContainerState +} + +func (st NopStrategyTarget) Host(_ context.Context) (string, error) { + return "", nil +} + +func (st NopStrategyTarget) Ports(_ context.Context) (nat.PortMap, error) { + return nil, nil +} + +func (st NopStrategyTarget) MappedPort(_ context.Context, n nat.Port) (nat.Port, error) { + return n, nil +} + +func (st NopStrategyTarget) Logs(_ context.Context) (io.ReadCloser, error) { + return st.ReaderCloser, nil +} + +func (st NopStrategyTarget) Exec(_ context.Context, _ []string, _ ...exec.ProcessOption) (int, io.Reader, error) { + return 0, nil, nil +} + +func (st NopStrategyTarget) State(_ context.Context) (*types.ContainerState, error) { + return &st.ContainerState, nil +} diff --git a/wait/sql.go b/wait/sql.go index c18203ab0a..184d804ce1 100644 --- a/wait/sql.go +++ b/wait/sql.go @@ -9,6 +9,9 @@ import ( "github.com/docker/go-connections/nat" ) +var _ Strategy = (*waitForSql)(nil) +var _ StrategyTimeout = (*waitForSql)(nil) + const defaultForSqlQuery = "SELECT 1" // ForSQL constructs a new waitForSql strategy for the given driver @@ -24,6 +27,8 @@ func ForSQL(port nat.Port, driver string, url func(host string, port nat.Port) s } type waitForSql struct { + timeout *time.Duration + URL func(host string, port nat.Port) string Driver string Port nat.Port @@ -32,15 +37,9 @@ type waitForSql struct { query string } -// 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 { - 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 +func (w *waitForSql) WithStartupTimeout(timeout time.Duration) *waitForSql { + w.timeout = &timeout return w } @@ -56,11 +55,20 @@ func (w *waitForSql) WithQuery(query string) *waitForSql { return w } +func (w *waitForSql) Timeout() *time.Duration { + return w.timeout +} + // WaitUntilReady repeatedly tries to run "SELECT 1" or user defined query on the given port using sql and driver. // // If it doesn't succeed until the timeout value which defaults to 60 seconds, it will return an error. func (w *waitForSql) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) { - ctx, cancel := context.WithTimeout(ctx, w.startupTimeout) + timeout := defaultStartupTimeout() + if w.timeout != nil { + timeout = *w.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() host, err := target.Host(ctx) diff --git a/wait/wait.go b/wait/wait.go index de6f581136..dae00c46ed 100644 --- a/wait/wait.go +++ b/wait/wait.go @@ -7,19 +7,25 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/go-connections/nat" - tcexec "github.com/testcontainers/testcontainers-go/exec" + "github.com/testcontainers/testcontainers-go/exec" ) +// Strategy defines the basic interface for a Wait Strategy type Strategy interface { WaitUntilReady(context.Context, StrategyTarget) error } +// StrategyTimeout allows MultiStrategy to configure a Strategy's Timeout +type StrategyTimeout interface { + Timeout() *time.Duration +} + type StrategyTarget interface { Host(context.Context) (string, error) Ports(ctx context.Context) (nat.PortMap, error) MappedPort(context.Context, nat.Port) (nat.Port, error) Logs(context.Context) (io.ReadCloser, error) - Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) + Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error) State(context.Context) (*types.ContainerState, error) }