diff options
| author | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-05-11 21:12:57 -0600 |
| commit | 60440f90dca28e99a31dd328c5f6d5dc0f9b6a2e (patch) | |
| tree | 2f54adf55086516f162f0a55a5347e6b25f7f176 /vendor/github.com/testcontainers/testcontainers-go/wait | |
| parent | 05ca9b8d3a9c7203a3a3b590beaa400900bd9007 (diff) | |
chore: vendor go dependencies
Diffstat (limited to 'vendor/github.com/testcontainers/testcontainers-go/wait')
15 files changed, 1806 insertions, 0 deletions
diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/all.go b/vendor/github.com/testcontainers/testcontainers-go/wait/all.go new file mode 100644 index 0000000..fb7eb4e --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/all.go @@ -0,0 +1,82 @@ +package wait + +import ( + "context" + "errors" + "time" +) + +// Implement interface +var ( + _ Strategy = (*MultiStrategy)(nil) + _ 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 errors.New("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 +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/errors.go b/vendor/github.com/testcontainers/testcontainers-go/wait/errors.go new file mode 100644 index 0000000..3e3919a --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/errors.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package wait + +import ( + "errors" + "syscall" +) + +func isConnRefusedErr(err error) bool { + return errors.Is(err, syscall.ECONNREFUSED) +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/errors_windows.go b/vendor/github.com/testcontainers/testcontainers-go/wait/errors_windows.go new file mode 100644 index 0000000..3ae346d --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/errors_windows.go @@ -0,0 +1,9 @@ +package wait + +import ( + "golang.org/x/sys/windows" +) + +func isConnRefusedErr(err error) bool { + return err == windows.WSAECONNREFUSED +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/exec.go b/vendor/github.com/testcontainers/testcontainers-go/wait/exec.go new file mode 100644 index 0000000..72987c3 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/exec.go @@ -0,0 +1,107 @@ +package wait + +import ( + "context" + "io" + "time" + + tcexec "github.com/testcontainers/testcontainers-go/exec" +) + +// Implement interface +var ( + _ Strategy = (*ExecStrategy)(nil) + _ 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(_ 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) WithExitCode(exitCode int) *ExecStrategy { + return ws.WithExitCodeMatcher(func(actualCode int) bool { + return actualCode == exitCode + }) +} + +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 + } + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/exit.go b/vendor/github.com/testcontainers/testcontainers-go/wait/exit.go new file mode 100644 index 0000000..670c8e2 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/exit.go @@ -0,0 +1,89 @@ +package wait + +import ( + "context" + "strings" + "time" +) + +// Implement interface +var ( + _ Strategy = (*ExitStrategy)(nil) + _ 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) 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 + } + return nil + } + if state.Running { + time.Sleep(ws.PollInterval) + continue + } + return nil + } + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/file.go b/vendor/github.com/testcontainers/testcontainers-go/wait/file.go new file mode 100644 index 0000000..d9cab7a --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/file.go @@ -0,0 +1,112 @@ +package wait + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/docker/docker/errdefs" +) + +var ( + _ Strategy = (*FileStrategy)(nil) + _ StrategyTimeout = (*FileStrategy)(nil) +) + +// FileStrategy waits for a file to exist in the container. +type FileStrategy struct { + timeout *time.Duration + file string + pollInterval time.Duration + matcher func(io.Reader) error +} + +// NewFileStrategy constructs an FileStrategy strategy. +func NewFileStrategy(file string) *FileStrategy { + return &FileStrategy{ + file: file, + pollInterval: defaultPollInterval(), + } +} + +// WithStartupTimeout can be used to change the default startup timeout +func (ws *FileStrategy) WithStartupTimeout(startupTimeout time.Duration) *FileStrategy { + ws.timeout = &startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *FileStrategy) WithPollInterval(pollInterval time.Duration) *FileStrategy { + ws.pollInterval = pollInterval + return ws +} + +// WithMatcher can be used to consume the file content. +// The matcher can return an errdefs.ErrNotFound to indicate that the file is not ready. +// Any other error will be considered a failure. +// Default: nil, will only wait for the file to exist. +func (ws *FileStrategy) WithMatcher(matcher func(io.Reader) error) *FileStrategy { + ws.matcher = matcher + return ws +} + +// ForFile is a convenience method to assign FileStrategy +func ForFile(file string) *FileStrategy { + return NewFileStrategy(file) +} + +// Timeout returns the timeout for the strategy +func (ws *FileStrategy) Timeout() *time.Duration { + return ws.timeout +} + +// WaitUntilReady waits until the file exists in the container and copies it to the target. +func (ws *FileStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + timer := time.NewTicker(ws.pollInterval) + defer timer.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + if err := ws.matchFile(ctx, target); err != nil { + if errdefs.IsNotFound(err) { + // Not found, continue polling. + continue + } + + return fmt.Errorf("copy from container: %w", err) + } + return nil + } + } +} + +// matchFile tries to copy the file from the container and match it. +func (ws *FileStrategy) matchFile(ctx context.Context, target StrategyTarget) error { + rc, err := target.CopyFileFromContainer(ctx, ws.file) + if err != nil { + return fmt.Errorf("copy from container: %w", err) + } + defer rc.Close() + + if ws.matcher == nil { + // No matcher, just check if the file exists. + return nil + } + + if err = ws.matcher(rc); err != nil { + return fmt.Errorf("matcher: %w", err) + } + + return nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/health.go b/vendor/github.com/testcontainers/testcontainers-go/wait/health.go new file mode 100644 index 0000000..06a9ad1 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/health.go @@ -0,0 +1,92 @@ +package wait + +import ( + "context" + "time" + + "github.com/docker/docker/api/types" +) + +// Implement interface +var ( + _ Strategy = (*HealthStrategy)(nil) + _ 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) 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 + } + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go b/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go new file mode 100644 index 0000000..2070bf1 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/host_port.go @@ -0,0 +1,245 @@ +package wait + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "strconv" + "time" + + "github.com/docker/go-connections/nat" + + "github.com/testcontainers/testcontainers-go/log" +) + +const ( + exitEaccess = 126 // container cmd can't be invoked (permission denied) + exitCmdNotFound = 127 // container cmd not found/does not exist or invalid bind-mount +) + +// Implement interface +var ( + _ Strategy = (*HostPortStrategy)(nil) + _ StrategyTimeout = (*HostPortStrategy)(nil) +) + +var ( + errShellNotExecutable = errors.New("/bin/sh command not executable") + errShellNotFound = errors.New("/bin/sh command not found") +) + +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 + 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 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, + 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 + +// 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 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 + 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) error { + 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 err + } + + waitInterval := hp.PollInterval + + internalPort := hp.Port + if internalPort == "" { + inspect, err := target.Inspect(ctx) + if err != nil { + return err + } + + for port := range inspect.NetworkSettings.Ports { + if internalPort == "" || port.Int() < internalPort.Int() { + internalPort = port + } + } + } + + if internalPort == "" { + return errors.New("no port to wait for") + } + + var port nat.Port + port, err = target.MappedPort(ctx, internalPort) + i := 0 + + for port == "" { + i++ + + select { + case <-ctx.Done(): + return fmt.Errorf("mapped port: retries: %d, port: %q, last err: %w, ctx err: %w", i, port, err, ctx.Err()) + case <-time.After(waitInterval): + if err := checkTarget(ctx, target); err != nil { + return fmt.Errorf("check target: retries: %d, port: %q, last err: %w", i, port, err) + } + port, err = target.MappedPort(ctx, internalPort) + if err != nil { + log.Printf("mapped port: retries: %d, port: %q, err: %s\n", i, port, err) + } + } + } + + if err := externalCheck(ctx, ipAddress, port, target, waitInterval); err != nil { + return fmt.Errorf("external check: %w", err) + } + + if hp.skipInternalCheck { + return nil + } + + if err = internalCheck(ctx, internalPort, target); err != nil { + switch { + case errors.Is(err, errShellNotExecutable): + log.Printf("Shell not executable in container, only external port validated") + return nil + case errors.Is(err, errShellNotFound): + log.Printf("Shell not found in container") + return nil + default: + return fmt.Errorf("internal check: %w", 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) + + dialer := net.Dialer{} + address := net.JoinHostPort(ipAddress, portString) + for i := 0; ; i++ { + if err := checkTarget(ctx, target); err != nil { + return fmt.Errorf("check target: retries: %d address: %s: %w", i, address, err) + } + conn, err := dialer.DialContext(ctx, proto, address) + if err != nil { + var v *net.OpError + if errors.As(err, &v) { + var v2 *os.SyscallError + if errors.As(v.Err, &v2) { + if isConnRefusedErr(v2.Err) { + time.Sleep(waitInterval) + continue + } + } + } + return fmt.Errorf("dial: %w", err) + } + + conn.Close() + return nil + } +} + +func internalCheck(ctx context.Context, internalPort nat.Port, target StrategyTarget) error { + command := buildInternalCheckCommand(internalPort.Int()) + for { + if ctx.Err() != nil { + return ctx.Err() + } + if err := checkTarget(ctx, target); err != nil { + return err + } + exitCode, _, err := target.Exec(ctx, []string{"/bin/sh", "-c", command}) + if err != nil { + return fmt.Errorf("%w, host port waiting failed", err) + } + + // Docker has an issue which override exit code 127 to 126 due to: + // https://github.com/moby/moby/issues/45795 + // Handle both to ensure compatibility with Docker and Podman for now. + switch exitCode { + case 0: + return nil + case exitEaccess: + return errShellNotExecutable + case exitCmdNotFound: + return errShellNotFound + } + } +} + +func buildInternalCheckCommand(internalPort int) string { + command := `( + cat /proc/net/tcp* | awk '{print $2}' | grep -i :%04x || + nc -vz -w 1 localhost %d || + /bin/sh -c '</dev/tcp/localhost/%d' + ) + ` + return "true && " + fmt.Sprintf(command, internalPort, internalPort, internalPort) +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/http.go b/vendor/github.com/testcontainers/testcontainers-go/wait/http.go new file mode 100644 index 0000000..2c7c655 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/http.go @@ -0,0 +1,338 @@ +package wait + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/docker/go-connections/nat" +) + +// Implement interface +var ( + _ Strategy = (*HTTPStrategy)(nil) + _ StrategyTimeout = (*HTTPStrategy)(nil) +) + +type HTTPStrategy struct { + // all Strategies should have a startupTimeout to avoid waiting infinitely + 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 + Headers map[string]string + ResponseHeadersMatcher func(headers http.Header) bool + PollInterval time.Duration + UserInfo *url.Userinfo + ForceIPv4LocalHost bool +} + +// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200 +func NewHTTPStrategy(path string) *HTTPStrategy { + return &HTTPStrategy{ + Port: "", + Path: path, + StatusCodeMatcher: defaultStatusCodeMatcher, + ResponseMatcher: func(_ io.Reader) bool { return true }, + UseTLS: false, + TLSConfig: nil, + Method: http.MethodGet, + Body: nil, + Headers: map[string]string{}, + ResponseHeadersMatcher: func(_ http.Header) bool { return true }, + PollInterval: defaultPollInterval(), + UserInfo: nil, + } +} + +func defaultStatusCodeMatcher(status int) bool { + return status == http.StatusOK +} + +// 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 *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy { + ws.timeout = &timeout + return ws +} + +// WithPort set the port to wait for. +// Default is the lowest numbered port. +func (ws *HTTPStrategy) WithPort(port nat.Port) *HTTPStrategy { + ws.Port = port + return ws +} + +func (ws *HTTPStrategy) WithStatusCodeMatcher(statusCodeMatcher func(status int) bool) *HTTPStrategy { + ws.StatusCodeMatcher = statusCodeMatcher + return ws +} + +func (ws *HTTPStrategy) WithResponseMatcher(matcher func(body io.Reader) bool) *HTTPStrategy { + ws.ResponseMatcher = matcher + return ws +} + +func (ws *HTTPStrategy) WithTLS(useTLS bool, tlsconf ...*tls.Config) *HTTPStrategy { + ws.UseTLS = useTLS + if useTLS && len(tlsconf) > 0 { + ws.TLSConfig = tlsconf[0] + } + return ws +} + +func (ws *HTTPStrategy) WithAllowInsecure(allowInsecure bool) *HTTPStrategy { + ws.AllowInsecure = allowInsecure + return ws +} + +func (ws *HTTPStrategy) WithMethod(method string) *HTTPStrategy { + ws.Method = method + return ws +} + +func (ws *HTTPStrategy) WithBody(reqdata io.Reader) *HTTPStrategy { + ws.Body = reqdata + return ws +} + +func (ws *HTTPStrategy) WithHeaders(headers map[string]string) *HTTPStrategy { + ws.Headers = headers + return ws +} + +func (ws *HTTPStrategy) WithResponseHeadersMatcher(matcher func(http.Header) bool) *HTTPStrategy { + ws.ResponseHeadersMatcher = matcher + 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 + 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 { + 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) error { + 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 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 == "" { + // We wait one polling interval before we grab the ports + // otherwise they might not be bound yet on startup. + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(ws.PollInterval): + // Port should now be bound so just continue. + } + + if err := checkTarget(ctx, target); err != nil { + return err + } + + inspect, err := target.Inspect(ctx) + if err != nil { + return err + } + + // Find the lowest numbered exposed tcp port. + var lowestPort nat.Port + var hostPort string + for port, bindings := range inspect.NetworkSettings.Ports { + if len(bindings) == 0 || port.Proto() != "tcp" { + continue + } + + if lowestPort == "" || port.Int() < lowestPort.Int() { + lowestPort = port + hostPort = bindings[0].HostPort + } + } + + if lowestPort == "" { + return errors.New("No exposed tcp ports or mapped ports - cannot wait for status") + } + + mappedPort, _ = nat.NewPort(lowestPort.Proto(), hostPort) + } else { + mappedPort, err = target.MappedPort(ctx, ws.Port) + + for mappedPort == "" { + select { + case <-ctx.Done(): + return fmt.Errorf("%w: %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 { + case http.MethodGet, http.MethodHead, http.MethodPost, + http.MethodPut, http.MethodPatch, http.MethodDelete, + http.MethodConnect, http.MethodOptions, http.MethodTrace: + default: + if ws.Method != "" { + return fmt.Errorf("invalid http method %q", ws.Method) + } + ws.Method = http.MethodGet + } + + tripper := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: ws.TLSConfig, + } + + var proto string + if ws.UseTLS { + proto = "https" + if ws.AllowInsecure { + if ws.TLSConfig == nil { + tripper.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } else { + ws.TLSConfig.InsecureSkipVerify = true + } + } + } else { + proto = "http" + } + + client := http.Client{Transport: tripper, Timeout: time.Second} + address := net.JoinHostPort(ipAddress, strconv.Itoa(mappedPort.Int())) + + endpoint, err := url.Parse(ws.Path) + if err != nil { + return err + } + endpoint.Scheme = proto + endpoint.Host = address + + 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 err + } + } + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(ws.PollInterval): + 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 + } + + for k, v := range ws.Headers { + req.Header.Set(k, v) + } + + resp, err := client.Do(req) + if err != nil { + 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 ws.ResponseHeadersMatcher != nil && !ws.ResponseHeadersMatcher(resp.Header) { + _ = resp.Body.Close() + continue + } + if err := resp.Body.Close(); err != nil { + continue + } + return nil + } + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/log.go b/vendor/github.com/testcontainers/testcontainers-go/wait/log.go new file mode 100644 index 0000000..41c96e3 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/log.go @@ -0,0 +1,214 @@ +package wait + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "regexp" + "time" +) + +// Implement interface +var ( + _ Strategy = (*LogStrategy)(nil) + _ StrategyTimeout = (*LogStrategy)(nil) +) + +// PermanentError is a special error that will stop the wait and return an error. +type PermanentError struct { + err error +} + +// Error implements the error interface. +func (e *PermanentError) Error() string { + return e.err.Error() +} + +// NewPermanentError creates a new PermanentError. +func NewPermanentError(err error) *PermanentError { + return &PermanentError{err: err} +} + +// 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 + timeout *time.Duration + + // additional properties + Log string + IsRegexp bool + Occurrence int + PollInterval time.Duration + + // check is the function that will be called to check if the log entry is present. + check func([]byte) error + + // submatchCallback is a callback that will be called with the sub matches of the regexp. + submatchCallback func(pattern string, matches [][][]byte) error + + // re is the optional compiled regexp. + re *regexp.Regexp + + // log byte slice version of [LogStrategy.Log] used for count checks. + log []byte +} + +// NewLogStrategy constructs with polling interval of 100 milliseconds and startup timeout of 60 seconds by default +func NewLogStrategy(log string) *LogStrategy { + return &LogStrategy{ + Log: log, + IsRegexp: false, + Occurrence: 1, + 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 + +// AsRegexp can be used to change the default behavior of the log strategy to use regexp instead of plain text +func (ws *LogStrategy) AsRegexp() *LogStrategy { + ws.IsRegexp = true + return ws +} + +// Submatch configures a function that will be called with the result of +// [regexp.Regexp.FindAllSubmatch], allowing the caller to process the results. +// If the callback returns nil, the strategy will be considered successful. +// Returning a [PermanentError] will stop the wait and return an error, otherwise +// it will retry until the timeout is reached. +// [LogStrategy.Occurrence] is ignored if this option is set. +func (ws *LogStrategy) Submatch(callback func(pattern string, matches [][][]byte) error) *LogStrategy { + ws.submatchCallback = callback + + return ws +} + +// WithStartupTimeout can be used to change the default startup timeout +func (ws *LogStrategy) WithStartupTimeout(timeout time.Duration) *LogStrategy { + ws.timeout = &timeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds +func (ws *LogStrategy) WithPollInterval(pollInterval time.Duration) *LogStrategy { + ws.PollInterval = pollInterval + return ws +} + +func (ws *LogStrategy) WithOccurrence(o int) *LogStrategy { + // the number of occurrence needs to be positive + if o <= 0 { + o = 1 + } + ws.Occurrence = o + return ws +} + +// ForLog is the default construction for the fluid interface. +// +// For Example: +// +// 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) error { + timeout := defaultStartupTimeout() + if ws.timeout != nil { + timeout = *ws.timeout + } + + switch { + case ws.submatchCallback != nil: + ws.re = regexp.MustCompile(ws.Log) + ws.check = ws.checkSubmatch + case ws.IsRegexp: + ws.re = regexp.MustCompile(ws.Log) + ws.check = ws.checkRegexp + default: + ws.log = []byte(ws.Log) + ws.check = ws.checkCount + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var lastLen int + var lastError error + for { + select { + case <-ctx.Done(): + return errors.Join(lastError, ctx.Err()) + default: + checkErr := checkTarget(ctx, target) + + reader, err := target.Logs(ctx) + if err != nil { + // TODO: fix as this will wait for timeout if the logs are not available. + time.Sleep(ws.PollInterval) + continue + } + + b, err := io.ReadAll(reader) + if err != nil { + // TODO: fix as this will wait for timeout if the logs are not readable. + time.Sleep(ws.PollInterval) + continue + } + + if lastLen == len(b) && checkErr != nil { + // Log length hasn't changed so we're not making progress. + return checkErr + } + + if err := ws.check(b); err != nil { + var errPermanent *PermanentError + if errors.As(err, &errPermanent) { + return err + } + + lastError = err + lastLen = len(b) + time.Sleep(ws.PollInterval) + continue + } + + return nil + } + } +} + +// checkCount checks if the log entry is present in the logs using a string count. +func (ws *LogStrategy) checkCount(b []byte) error { + if count := bytes.Count(b, ws.log); count < ws.Occurrence { + return fmt.Errorf("%q matched %d times, expected %d", ws.Log, count, ws.Occurrence) + } + + return nil +} + +// checkRegexp checks if the log entry is present in the logs using a regexp count. +func (ws *LogStrategy) checkRegexp(b []byte) error { + if matches := ws.re.FindAll(b, -1); len(matches) < ws.Occurrence { + return fmt.Errorf("`%s` matched %d times, expected %d", ws.Log, len(matches), ws.Occurrence) + } + + return nil +} + +// checkSubmatch checks if the log entry is present in the logs using a regexp sub match callback. +func (ws *LogStrategy) checkSubmatch(b []byte) error { + return ws.submatchCallback(ws.Log, ws.re.FindAllSubmatch(b, -1)) +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go b/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go new file mode 100644 index 0000000..c47d83d --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/nop.go @@ -0,0 +1,81 @@ +package wait + +import ( + "context" + "io" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + + "github.com/testcontainers/testcontainers-go/exec" +) + +var ( + _ Strategy = (*NopStrategy)(nil) + _ 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 container.State +} + +func (st NopStrategyTarget) Host(_ context.Context) (string, error) { + return "", nil +} + +func (st NopStrategyTarget) Inspect(_ context.Context) (*container.InspectResponse, error) { + return nil, nil +} + +// Deprecated: use Inspect instead +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) (*container.State, error) { + return &st.ContainerState, nil +} + +func (st NopStrategyTarget) CopyFileFromContainer(context.Context, string) (io.ReadCloser, error) { + return st.ReaderCloser, nil +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go b/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go new file mode 100644 index 0000000..1d09eda --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/sql.go @@ -0,0 +1,118 @@ +package wait + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/docker/go-connections/nat" +) + +var ( + _ Strategy = (*waitForSQL)(nil) + _ 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 { + timeout *time.Duration + + URL func(host string, port nat.Port) string + Driver string + Port nat.Port + startupTimeout time.Duration + PollInterval time.Duration + query string +} + +// 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 +func (w *waitForSQL) WithPollInterval(pollInterval time.Duration) *waitForSQL { + w.PollInterval = pollInterval + return w +} + +// 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) error { + 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 err + } + + ticker := time.NewTicker(w.PollInterval) + defer ticker.Stop() + + var port nat.Port + port, err = target.MappedPort(ctx, w.Port) + + for port == "" { + select { + case <-ctx.Done(): + return fmt.Errorf("%w: %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(host, port)) + if err != nil { + return fmt.Errorf("sql.Open: %w", err) + } + defer db.Close() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + if err := checkTarget(ctx, target); err != nil { + return err + } + if _, err := db.ExecContext(ctx, w.query); err != nil { + continue + } + return nil + } + } +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/tls.go b/vendor/github.com/testcontainers/testcontainers-go/wait/tls.go new file mode 100644 index 0000000..ab904b2 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/tls.go @@ -0,0 +1,167 @@ +package wait + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "time" +) + +// Validate we implement interface. +var _ Strategy = (*TLSStrategy)(nil) + +// TLSStrategy is a strategy for handling TLS. +type TLSStrategy struct { + // General Settings. + timeout *time.Duration + pollInterval time.Duration + + // Custom Settings. + certFiles *x509KeyPair + rootFiles []string + + // State. + tlsConfig *tls.Config +} + +// x509KeyPair is a pair of certificate and key files. +type x509KeyPair struct { + certPEMFile string + keyPEMFile string +} + +// ForTLSCert returns a CertStrategy that will add a Certificate to the [tls.Config] +// constructed from PEM formatted certificate key file pair in the container. +func ForTLSCert(certPEMFile, keyPEMFile string) *TLSStrategy { + return &TLSStrategy{ + certFiles: &x509KeyPair{ + certPEMFile: certPEMFile, + keyPEMFile: keyPEMFile, + }, + tlsConfig: &tls.Config{}, + pollInterval: defaultPollInterval(), + } +} + +// ForTLSRootCAs returns a CertStrategy that sets the root CAs for the [tls.Config] +// using the given PEM formatted files from the container. +func ForTLSRootCAs(pemFiles ...string) *TLSStrategy { + return &TLSStrategy{ + rootFiles: pemFiles, + tlsConfig: &tls.Config{}, + pollInterval: defaultPollInterval(), + } +} + +// WithRootCAs sets the root CAs for the [tls.Config] using the given files from +// the container. +func (ws *TLSStrategy) WithRootCAs(files ...string) *TLSStrategy { + ws.rootFiles = files + return ws +} + +// WithCert sets the [tls.Config] Certificates using the given files from the container. +func (ws *TLSStrategy) WithCert(certPEMFile, keyPEMFile string) *TLSStrategy { + ws.certFiles = &x509KeyPair{ + certPEMFile: certPEMFile, + keyPEMFile: keyPEMFile, + } + return ws +} + +// WithServerName sets the server for the [tls.Config]. +func (ws *TLSStrategy) WithServerName(serverName string) *TLSStrategy { + ws.tlsConfig.ServerName = serverName + return ws +} + +// WithStartupTimeout can be used to change the default startup timeout. +func (ws *TLSStrategy) WithStartupTimeout(startupTimeout time.Duration) *TLSStrategy { + ws.timeout = &startupTimeout + return ws +} + +// WithPollInterval can be used to override the default polling interval of 100 milliseconds. +func (ws *TLSStrategy) WithPollInterval(pollInterval time.Duration) *TLSStrategy { + ws.pollInterval = pollInterval + return ws +} + +// TLSConfig returns the TLS config once the strategy is ready. +// If the strategy is nil, it returns nil. +func (ws *TLSStrategy) TLSConfig() *tls.Config { + if ws == nil { + return nil + } + + return ws.tlsConfig +} + +// WaitUntilReady implements the [Strategy] interface. +// It waits for the CA, client cert and key files to be available in the container and +// uses them to setup the TLS config. +func (ws *TLSStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) error { + size := len(ws.rootFiles) + if ws.certFiles != nil { + size += 2 + } + strategies := make([]Strategy, 0, size) + for _, file := range ws.rootFiles { + strategies = append(strategies, + ForFile(file).WithMatcher(func(r io.Reader) error { + buf, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("read CA cert file %q: %w", file, err) + } + + if ws.tlsConfig.RootCAs == nil { + ws.tlsConfig.RootCAs = x509.NewCertPool() + } + + if !ws.tlsConfig.RootCAs.AppendCertsFromPEM(buf) { + return fmt.Errorf("invalid CA cert file %q", file) + } + + return nil + }).WithPollInterval(ws.pollInterval), + ) + } + + if ws.certFiles != nil { + var certPEMBlock []byte + strategies = append(strategies, + ForFile(ws.certFiles.certPEMFile).WithMatcher(func(r io.Reader) error { + var err error + if certPEMBlock, err = io.ReadAll(r); err != nil { + return fmt.Errorf("read certificate cert %q: %w", ws.certFiles.certPEMFile, err) + } + + return nil + }).WithPollInterval(ws.pollInterval), + ForFile(ws.certFiles.keyPEMFile).WithMatcher(func(r io.Reader) error { + keyPEMBlock, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("read certificate key %q: %w", ws.certFiles.keyPEMFile, err) + } + + cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) + if err != nil { + return fmt.Errorf("x509 key pair %q %q: %w", ws.certFiles.certPEMFile, ws.certFiles.keyPEMFile, err) + } + + ws.tlsConfig.Certificates = []tls.Certificate{cert} + + return nil + }).WithPollInterval(ws.pollInterval), + ) + } + + strategy := ForAll(strategies...) + if ws.timeout != nil { + strategy.WithStartupTimeout(*ws.timeout) + } + + return strategy.WaitUntilReady(ctx, target) +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go b/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go new file mode 100644 index 0000000..ca5a7db --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/wait.go @@ -0,0 +1,65 @@ +package wait + +import ( + "context" + "errors" + "fmt" + "io" + "time" + + "github.com/docker/docker/api/types/container" + "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) + Inspect(context.Context) (*container.InspectResponse, error) + Ports(ctx context.Context) (nat.PortMap, error) // Deprecated: use Inspect instead + MappedPort(context.Context, nat.Port) (nat.Port, error) + Logs(context.Context) (io.ReadCloser, error) + Exec(context.Context, []string, ...exec.ProcessOption) (int, io.Reader, error) + State(context.Context) (*container.State, error) + CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error) +} + +func checkTarget(ctx context.Context, target StrategyTarget) error { + state, err := target.State(ctx) + if err != nil { + return fmt.Errorf("get state: %w", err) + } + + return checkState(state) +} + +func checkState(state *container.State) 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 { + return 60 * time.Second +} + +func defaultPollInterval() time.Duration { + return 100 * time.Millisecond +} diff --git a/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go b/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go new file mode 100644 index 0000000..4685e50 --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/wait/walk.go @@ -0,0 +1,74 @@ +package wait + +import ( + "errors" +) + +var ( + // VisitStop is used as a return value from [VisitFunc] to stop the walk. + // It is not returned as an error by any function. + VisitStop = errors.New("stop the walk") + + // VisitRemove is used as a return value from [VisitFunc] to have the current node removed. + // It is not returned as an error by any function. + VisitRemove = errors.New("remove this strategy") +) + +// VisitFunc is a function that visits a strategy node. +// If it returns [VisitStop], the walk stops. +// If it returns [VisitRemove], the current node is removed. +type VisitFunc func(root Strategy) error + +// Walk walks the strategies tree and calls the visit function for each node. +func Walk(root *Strategy, visit VisitFunc) error { + if root == nil { + return errors.New("root strategy is nil") + } + + if err := walk(root, visit); err != nil { + if errors.Is(err, VisitRemove) || errors.Is(err, VisitStop) { + return nil + } + return err + } + + return nil +} + +// walk walks the strategies tree and calls the visit function for each node. +// It returns an error if the visit function returns an error. +func walk(root *Strategy, visit VisitFunc) error { + if *root == nil { + // No strategy. + return nil + } + + // Allow the visit function to customize the behaviour of the walk before visiting the children. + if err := visit(*root); err != nil { + if errors.Is(err, VisitRemove) { + *root = nil + } + + return err + } + + if s, ok := (*root).(*MultiStrategy); ok { + var i int + for range s.Strategies { + if err := walk(&s.Strategies[i], visit); err != nil { + if errors.Is(err, VisitRemove) { + s.Strategies = append(s.Strategies[:i], s.Strategies[i+1:]...) + if errors.Is(err, VisitStop) { + return VisitStop + } + continue + } + + return err + } + i++ + } + } + + return nil +} |
