build: upgrade dependencies and build with go 1.21

This commit is contained in:
2023-08-21 23:04:28 +02:00
parent ddc5ee91e5
commit 3942b32843
1201 changed files with 129198 additions and 39613 deletions

View File

@@ -0,0 +1,80 @@
package wait
import (
"context"
"fmt"
"time"
)
// Implement interface
var _ Strategy = (*MultiStrategy)(nil)
var _ StrategyTimeout = (*MultiStrategy)(nil)
type MultiStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
deadline *time.Duration
// additional properties
Strategies []Strategy
}
// 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{
Strategies: strategies,
}
}
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 {
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
}

View File

@@ -1,3 +1,4 @@
//go:build !windows
// +build !windows
package wait

View File

@@ -0,0 +1,99 @@
package wait
import (
"context"
"io"
"time"
tcexec "github.com/testcontainers/testcontainers-go/exec"
)
// Implement interface
var _ Strategy = (*ExecStrategy)(nil)
var _ StrategyTimeout = (*ExecStrategy)(nil)
type ExecStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
timeout *time.Duration
cmd []string
// additional properties
ExitCodeMatcher func(exitCode int) bool
ResponseMatcher func(body io.Reader) bool
PollInterval time.Duration
}
// NewExecStrategy constructs an Exec strategy ...
func NewExecStrategy(cmd []string) *ExecStrategy {
return &ExecStrategy{
cmd: cmd,
ExitCodeMatcher: defaultExitCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
PollInterval: defaultPollInterval(),
}
}
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.timeout = &startupTimeout
return ws
}
func (ws *ExecStrategy) WithExitCodeMatcher(exitCodeMatcher func(exitCode int) bool) *ExecStrategy {
ws.ExitCodeMatcher = exitCodeMatcher
return ws
}
func (ws *ExecStrategy) WithResponseMatcher(matcher func(body io.Reader) bool) *ExecStrategy {
ws.ResponseMatcher = matcher
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *ExecStrategy) WithPollInterval(pollInterval time.Duration) *ExecStrategy {
ws.PollInterval = pollInterval
return ws
}
// ForExec is a convenience method to assign ExecStrategy
func ForExec(cmd []string) *ExecStrategy {
return NewExecStrategy(cmd)
}
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 {
case <-ctx.Done():
return ctx.Err()
case <-time.After(ws.PollInterval):
exitCode, resp, err := target.Exec(ctx, ws.cmd, tcexec.Multiplexed())
if err != nil {
return err
}
if !ws.ExitCodeMatcher(exitCode) {
continue
}
if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp) {
continue
}
return nil
}
}
}

View File

@@ -0,0 +1,89 @@
package wait
import (
"context"
"strings"
"time"
)
// 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
timeout *time.Duration
// additional properties
PollInterval time.Duration
}
// NewExitStrategy constructs with polling interval of 100 milliseconds without timeout by default
func NewExitStrategy() *ExitStrategy {
return &ExitStrategy{
PollInterval: defaultPollInterval(),
}
}
// fluent builders for each property
// 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
// WithExitTimeout can be used to change the default exit timeout
func (ws *ExitStrategy) WithExitTimeout(exitTimeout time.Duration) *ExitStrategy {
ws.timeout = &exitTimeout
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *ExitStrategy) WithPollInterval(pollInterval time.Duration) *ExitStrategy {
ws.PollInterval = pollInterval
return ws
}
// ForExit is the default construction for the fluid interface.
//
// For Example:
//
// wait.
// ForExit().
// WithPollInterval(1 * time.Second)
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) {
if ws.timeout != nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, *ws.timeout)
defer cancel()
}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
state, err := target.State(ctx)
if err != nil {
if !strings.Contains(err.Error(), "No such container") {
return err
} else {
return nil
}
}
if state.Running {
time.Sleep(ws.PollInterval)
continue
}
return nil
}
}
}

View File

@@ -0,0 +1,91 @@
package wait
import (
"context"
"time"
"github.com/docker/docker/api/types"
)
// 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
timeout *time.Duration
// additional properties
PollInterval time.Duration
}
// NewHealthStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default
func NewHealthStrategy() *HealthStrategy {
return &HealthStrategy{
PollInterval: defaultPollInterval(),
}
}
// fluent builders for each property
// 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
// WithStartupTimeout can be used to change the default startup timeout
func (ws *HealthStrategy) WithStartupTimeout(startupTimeout time.Duration) *HealthStrategy {
ws.timeout = &startupTimeout
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *HealthStrategy) WithPollInterval(pollInterval time.Duration) *HealthStrategy {
ws.PollInterval = pollInterval
return ws
}
// ForHealthCheck is the default construction for the fluid interface.
//
// For Example:
//
// wait.
// ForHealthCheck().
// WithPollInterval(1 * time.Second)
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) {
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
state, err := target.State(ctx)
if err != nil {
return err
}
if err := checkState(state); err != nil {
return err
}
if state.Health == nil || state.Health.Status != types.Healthy {
time.Sleep(ws.PollInterval)
continue
}
return nil
}
}
}

View File

@@ -2,31 +2,37 @@ package wait
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"time"
"github.com/pkg/errors"
"github.com/docker/go-connections/nat"
)
// Implement interface
var _ Strategy = (*HostPortStrategy)(nil)
var _ StrategyTimeout = (*HostPortStrategy)(nil)
var errShellNotExecutable = errors.New("/bin/sh command not executable")
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
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(),
Port: port,
PollInterval: defaultPollInterval(),
}
}
@@ -40,70 +46,150 @@ 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.
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
}
// 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
}
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 {
return
}
port, err := target.MappedPort(ctx, hp.Port)
if err != nil {
var waitInterval = hp.PollInterval
internalPort := hp.Port
if internalPort == "" {
var ports nat.PortMap
ports, err = target.Ports(ctx)
if err != nil {
return
}
if len(ports) > 0 {
for p := range ports {
internalPort = p
break
}
}
}
if internalPort == "" {
err = fmt.Errorf("no port to wait for")
return
}
var port nat.Port
port, err = target.MappedPort(ctx, internalPort)
var i = 0
for port == "" {
i++
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-time.After(waitInterval):
if err := checkTarget(ctx, target); err != nil {
return err
}
port, err = target.MappedPort(ctx, internalPort)
if err != nil {
fmt.Printf("(%d) [%s] %s\n", i, port, err)
}
}
}
if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil {
return err
}
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")
} else {
return err
}
return nil
}
func externalCheck(ctx context.Context, ipAddress string, port nat.Port, target StrategyTarget, waitInterval time.Duration) error {
proto := port.Proto()
portNumber := port.Int()
portString := strconv.Itoa(portNumber)
//external check
dialer := net.Dialer{}
address := net.JoinHostPort(ipAddress, portString)
for {
if err := checkTarget(ctx, target); err != nil {
return err
}
conn, err := dialer.DialContext(ctx, proto, address)
if err != nil {
if v, ok := err.(*net.OpError); ok {
if v2, ok := (v.Err).(*os.SyscallError); ok {
if isConnRefusedErr(v2.Err) {
time.Sleep(100 * time.Millisecond)
time.Sleep(waitInterval)
continue
}
}
}
return err
} else {
conn.Close()
_ = conn.Close()
break
}
}
return nil
}
//internal check
command := buildInternalCheckCommand(hp.Port.Int())
func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTarget) error {
command := buildInternalCheckCommand(internalPort.Int())
for {
if ctx.Err() != nil {
return ctx.Err()
}
exitCode, err := target.Exec(ctx, []string{"/bin/sh", "-c", command})
if err := checkTarget(ctx, target); err != nil {
return err
}
exitCode, _, err := target.Exec(ctx, []string{"/bin/sh", "-c", command})
if err != nil {
return errors.Wrapf(err, "host port waiting failed")
return fmt.Errorf("%w, host port waiting failed", err)
}
if exitCode == 0 {
break
} else if exitCode == 126 {
return errors.New("/bin/sh command not executable")
return errShellNotExecutable
}
}
return nil
}

View File

@@ -1,6 +1,7 @@
package wait
import (
"bytes"
"context"
"crypto/tls"
"errors"
@@ -8,6 +9,7 @@ import (
"io"
"net"
"net/http"
"net/url"
"strconv"
"time"
@@ -16,10 +18,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
@@ -32,13 +35,13 @@ type HTTPStrategy struct {
Method string // http method
Body io.Reader // http request body
PollInterval time.Duration
UserInfo *url.Userinfo
}
// 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",
Port: "",
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
@@ -47,6 +50,7 @@ func NewHTTPStrategy(path string) *HTTPStrategy {
Method: http.MethodGet,
Body: nil,
PollInterval: defaultPollInterval(),
UserInfo: nil,
}
}
@@ -58,8 +62,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
}
@@ -101,6 +106,11 @@ func (ws *HTTPStrategy) WithBody(reqdata io.Reader) *HTTPStrategy {
return ws
}
func (ws *HTTPStrategy) WithBasicAuth(username, password string) *HTTPStrategy {
ws.UserInfo = url.UserPassword(username, password)
return ws
}
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (ws *HTTPStrategy) WithPollInterval(pollInterval time.Duration) *HTTPStrategy {
ws.PollInterval = pollInterval
@@ -113,24 +123,71 @@ 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 {
return
}
port, err := target.MappedPort(ctx, ws.Port)
if err != nil {
return
}
var mappedPort nat.Port
if ws.Port == "" {
ports, err := target.Ports(ctx)
for err != nil {
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-time.After(ws.PollInterval):
if err := checkTarget(ctx, target); err != nil {
return err
}
if port.Proto() != "tcp" {
return errors.New("Cannot use HTTP client on non-TCP ports")
ports, err = target.Ports(ctx)
}
}
for k, bindings := range ports {
if len(bindings) == 0 || k.Proto() != "tcp" {
continue
}
mappedPort, _ = nat.NewPort(k.Proto(), bindings[0].HostPort)
break
}
if mappedPort == "" {
return errors.New("No exposed tcp ports or mapped ports - cannot wait for status")
}
} else {
mappedPort, err = target.MappedPort(ctx, ws.Port)
for mappedPort == "" {
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-time.After(ws.PollInterval):
if err := checkTarget(ctx, target); err != nil {
return err
}
mappedPort, err = target.MappedPort(ctx, ws.Port)
}
}
if mappedPort.Proto() != "tcp" {
return errors.New("Cannot use HTTP client on non-TCP ports")
}
}
switch ws.Method {
@@ -174,15 +231,36 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
}
client := http.Client{Transport: tripper, Timeout: time.Second}
address := net.JoinHostPort(ipAddress, strconv.Itoa(port.Int()))
endpoint := fmt.Sprintf("%s://%s%s", proto, address, ws.Path)
address := net.JoinHostPort(ipAddress, strconv.Itoa(mappedPort.Int()))
endpoint := url.URL{
Scheme: proto,
Host: address,
Path: ws.Path,
}
if ws.UserInfo != nil {
endpoint.User = ws.UserInfo
}
// cache the body into a byte-slice so that it can be iterated over multiple times
var body []byte
if ws.Body != nil {
body, err = io.ReadAll(ws.Body)
if err != nil {
return
}
}
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(ws.PollInterval):
req, err := http.NewRequestWithContext(ctx, ws.Method, endpoint, ws.Body)
if err := checkTarget(ctx, target); err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, ws.Method, endpoint.String(), bytes.NewReader(body))
if err != nil {
return err
}
@@ -191,9 +269,11 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
continue
}
if ws.StatusCodeMatcher != nil && !ws.StatusCodeMatcher(resp.StatusCode) {
_ = resp.Body.Close()
continue
}
if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp.Body) {
_ = resp.Body.Close()
continue
}
if err := resp.Body.Close(); err != nil {

View File

@@ -2,18 +2,19 @@ package wait
import (
"context"
"io/ioutil"
"io"
"strings"
"time"
)
// 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
@@ -21,15 +22,13 @@ type LogStrategy struct {
PollInterval time.Duration
}
// NewLogStrategy constructs a HTTP strategy waiting on port 80 and status code 200
// 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
}
@@ -49,7 +48,7 @@ func (ws *LogStrategy) WithPollInterval(pollInterval time.Duration) *LogStrategy
}
func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy {
// the number of occurence needs to be positive
// the number of occurrence needs to be positive
if o <= 0 {
o = 1
}
@@ -60,19 +59,29 @@ func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy {
// ForLog is the default construction for the fluid interface.
//
// For Example:
// wait.
// ForLog("some text").
// WithPollInterval(1 * time.Second)
//
// wait.
// ForLog("some text").
// WithPollInterval(1 * time.Second)
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()
currentOccurence := 0
timeout := defaultStartupTimeout()
if ws.timeout != nil {
timeout = *ws.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
length := 0
LOOP:
for {
@@ -80,20 +89,27 @@ LOOP:
case <-ctx.Done():
return ctx.Err()
default:
reader, err := target.Logs(ctx)
checkErr := checkTarget(ctx, target)
reader, err := target.Logs(ctx)
if err != nil {
time.Sleep(ws.PollInterval)
continue
}
b, err := ioutil.ReadAll(reader)
b, err := io.ReadAll(reader)
if err != nil {
time.Sleep(ws.PollInterval)
continue
}
logs := string(b)
if strings.Contains(logs, ws.Log) {
currentOccurence++
if ws.Occurrence == 0 || currentOccurence >= ws.Occurrence-1 {
break LOOP
}
if length == len(logs) && checkErr != nil {
return checkErr
} else if strings.Count(logs, ws.Log) >= ws.Occurrence {
break LOOP
} else {
length = len(logs)
time.Sleep(ws.PollInterval)
continue
}

View File

@@ -1,47 +0,0 @@
package wait
import (
"context"
"fmt"
"time"
)
// Implement interface
var _ Strategy = (*MultiStrategy)(nil)
type MultiStrategy struct {
// all Strategies should have a startupTimeout to avoid waiting infinitely
startupTimeout time.Duration
// additional properties
Strategies []Strategy
}
func (ms *MultiStrategy) WithStartupTimeout(startupTimeout time.Duration) *MultiStrategy {
ms.startupTimeout = startupTimeout
return ms
}
func ForAll(strategies ...Strategy) *MultiStrategy {
return &MultiStrategy{
startupTimeout: defaultStartupTimeout(),
Strategies: strategies,
}
}
func (ms *MultiStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
ctx, cancelContext := context.WithTimeout(ctx, ms.startupTimeout)
defer cancelContext()
if len(ms.Strategies) == 0 {
return fmt.Errorf("no wait strategy supplied")
}
for _, strategy := range ms.Strategies {
err := strategy.WaitUntilReady(ctx, target)
if err != nil {
return err
}
}
return nil
}

View File

@@ -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
}

View File

@@ -9,52 +9,92 @@ import (
"github.com/docker/go-connections/nat"
)
//ForSQL constructs a new waitForSql strategy for the given driver
func ForSQL(port nat.Port, driver string, url func(nat.Port) string) *waitForSql {
var _ Strategy = (*waitForSql)(nil)
var _ StrategyTimeout = (*waitForSql)(nil)
const defaultForSqlQuery = "SELECT 1"
// ForSQL constructs a new waitForSql strategy for the given driver
func ForSQL(port nat.Port, driver string, url func(host string, port nat.Port) string) *waitForSql {
return &waitForSql{
Port: port,
URL: url,
Driver: driver,
startupTimeout: defaultStartupTimeout(),
PollInterval: defaultPollInterval(),
query: defaultForSqlQuery,
}
}
type waitForSql struct {
URL func(port nat.Port) string
timeout *time.Duration
URL func(host string, port nat.Port) string
Driver string
Port nat.Port
startupTimeout time.Duration
PollInterval time.Duration
query string
}
//Timeout sets the maximum waiting time for the strategy after which it'll give up and return an error
func (w *waitForSql) Timeout(duration time.Duration) *waitForSql {
w.startupTimeout = duration
// WithStartupTimeout can be used to change the default startup timeout
func (w *waitForSql) WithStartupTimeout(timeout time.Duration) *waitForSql {
w.timeout = &timeout
return w
}
//WithPollInterval can be used to override the default polling interval of 100 milliseconds
// WithPollInterval can be used to override the default polling interval of 100 milliseconds
func (w *waitForSql) WithPollInterval(pollInterval time.Duration) *waitForSql {
w.PollInterval = pollInterval
return w
}
//WaitUntilReady repeatedly tries to run "SELECT 1" query on the given port using sql and driver.
// If the it doesn't succeed until the timeout value which defaults to 60 seconds, it will return an error
// WithQuery can be used to override the default query used in the strategy.
func (w *waitForSql) WithQuery(query string) *waitForSql {
w.query = query
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)
if err != nil {
return
}
ticker := time.NewTicker(w.PollInterval)
defer ticker.Stop()
port, err := target.MappedPort(ctx, w.Port)
if err != nil {
return fmt.Errorf("target.MappedPort: %v", err)
var port nat.Port
port, err = target.MappedPort(ctx, w.Port)
for port == "" {
select {
case <-ctx.Done():
return fmt.Errorf("%s:%w", ctx.Err(), err)
case <-ticker.C:
if err := checkTarget(ctx, target); err != nil {
return err
}
port, err = target.MappedPort(ctx, w.Port)
}
}
db, err := sql.Open(w.Driver, w.URL(port))
db, err := sql.Open(w.Driver, w.URL(host, port))
if err != nil {
return fmt.Errorf("sql.Open: %v", err)
}
@@ -64,8 +104,10 @@ func (w *waitForSql) WaitUntilReady(ctx context.Context, target StrategyTarget)
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
if _, err := db.ExecContext(ctx, "SELECT 1"); err != nil {
if err := checkTarget(ctx, target); err != nil {
return err
}
if _, err := db.ExecContext(ctx, w.query); err != nil {
continue
}
return nil

View File

@@ -2,21 +2,55 @@ package wait
import (
"context"
"errors"
"fmt"
"io"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
"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) (int, error)
Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error)
State(context.Context) (*types.ContainerState, error)
}
func checkTarget(ctx context.Context, target StrategyTarget) error {
state, err := target.State(ctx)
if err != nil {
return err
}
return checkState(state)
}
func checkState(state *types.ContainerState) error {
switch {
case state.Running:
return nil
case state.OOMKilled:
return errors.New("container crashed with out-of-memory (OOMKilled)")
case state.Status == "exited":
return fmt.Errorf("container exited with code %d", state.ExitCode)
default:
return fmt.Errorf("unexpected container status %q", state.Status)
}
}
func defaultStartupTimeout() time.Duration {