2020-03-01 16:06:34 +00:00
|
|
|
package wait
|
|
|
|
|
|
|
|
import (
|
2023-08-21 21:04:28 +00:00
|
|
|
"bytes"
|
2020-03-01 16:06:34 +00:00
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2021-01-17 18:00:46 +00:00
|
|
|
"io"
|
2020-03-01 16:06:34 +00:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2023-08-21 21:04:28 +00:00
|
|
|
"net/url"
|
2020-03-01 16:06:34 +00:00
|
|
|
"strconv"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/docker/go-connections/nat"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Implement interface
|
|
|
|
var _ Strategy = (*HTTPStrategy)(nil)
|
2023-08-21 21:04:28 +00:00
|
|
|
var _ StrategyTimeout = (*HTTPStrategy)(nil)
|
2020-03-01 16:06:34 +00:00
|
|
|
|
|
|
|
type HTTPStrategy struct {
|
|
|
|
// all Strategies should have a startupTimeout to avoid waiting infinitely
|
2023-08-21 21:04:28 +00:00
|
|
|
timeout *time.Duration
|
2020-03-01 16:06:34 +00:00
|
|
|
|
|
|
|
// additional properties
|
|
|
|
Port nat.Port
|
|
|
|
Path string
|
|
|
|
StatusCodeMatcher func(status int) bool
|
2021-01-17 18:00:46 +00:00
|
|
|
ResponseMatcher func(body io.Reader) bool
|
2020-03-01 16:06:34 +00:00
|
|
|
UseTLS bool
|
|
|
|
AllowInsecure bool
|
2021-01-17 18:00:46 +00:00
|
|
|
TLSConfig *tls.Config // TLS config for HTTPS
|
|
|
|
Method string // http method
|
|
|
|
Body io.Reader // http request body
|
|
|
|
PollInterval time.Duration
|
2023-08-21 21:04:28 +00:00
|
|
|
UserInfo *url.Userinfo
|
2020-03-01 16:06:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
|
|
|
|
func NewHTTPStrategy(path string) *HTTPStrategy {
|
|
|
|
return &HTTPStrategy{
|
2023-08-21 21:04:28 +00:00
|
|
|
Port: "",
|
2020-03-01 16:06:34 +00:00
|
|
|
Path: path,
|
|
|
|
StatusCodeMatcher: defaultStatusCodeMatcher,
|
2021-01-17 18:00:46 +00:00
|
|
|
ResponseMatcher: func(body io.Reader) bool { return true },
|
2020-03-01 16:06:34 +00:00
|
|
|
UseTLS: false,
|
2021-01-17 18:00:46 +00:00
|
|
|
TLSConfig: nil,
|
|
|
|
Method: http.MethodGet,
|
|
|
|
Body: nil,
|
|
|
|
PollInterval: defaultPollInterval(),
|
2023-08-21 21:04:28 +00:00
|
|
|
UserInfo: nil,
|
2020-03-01 16:06:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-08-21 21:04:28 +00:00
|
|
|
// WithStartupTimeout can be used to change the default startup timeout
|
|
|
|
func (ws *HTTPStrategy) WithStartupTimeout(timeout time.Duration) *HTTPStrategy {
|
|
|
|
ws.timeout = &timeout
|
2020-03-01 16:06:34 +00:00
|
|
|
return ws
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-01-17 18:00:46 +00:00
|
|
|
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 {
|
2020-03-01 16:06:34 +00:00
|
|
|
ws.UseTLS = useTLS
|
2021-01-17 18:00:46 +00:00
|
|
|
if useTLS && len(tlsconf) > 0 {
|
|
|
|
ws.TLSConfig = tlsconf[0]
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
return ws
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ws *HTTPStrategy) WithAllowInsecure(allowInsecure bool) *HTTPStrategy {
|
|
|
|
ws.AllowInsecure = allowInsecure
|
|
|
|
return ws
|
|
|
|
}
|
|
|
|
|
2021-01-17 18:00:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-08-21 21:04:28 +00:00
|
|
|
func (ws *HTTPStrategy) WithBasicAuth(username, password string) *HTTPStrategy {
|
|
|
|
ws.UserInfo = url.UserPassword(username, password)
|
|
|
|
return ws
|
|
|
|
}
|
|
|
|
|
2021-01-17 18:00:46 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-03-01 16:06:34 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2023-08-21 21:04:28 +00:00
|
|
|
func (ws *HTTPStrategy) Timeout() *time.Duration {
|
|
|
|
return ws.timeout
|
|
|
|
}
|
|
|
|
|
2020-03-01 16:06:34 +00:00
|
|
|
// WaitUntilReady implements Strategy.WaitUntilReady
|
|
|
|
func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarget) (err error) {
|
2023-08-21 21:04:28 +00:00
|
|
|
timeout := defaultStartupTimeout()
|
|
|
|
if ws.timeout != nil {
|
|
|
|
timeout = *ws.timeout
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
|
|
|
defer cancel()
|
2020-03-01 16:06:34 +00:00
|
|
|
|
|
|
|
ipAddress, err := target.Host(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-08-21 21:04:28 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
|
2023-08-21 21:04:28 +00:00
|
|
|
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")
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
}
|
|
|
|
|
2021-01-17 18:00:46 +00:00
|
|
|
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
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
|
2021-01-17 18:00:46 +00:00
|
|
|
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,
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
|
|
|
|
var proto string
|
|
|
|
if ws.UseTLS {
|
|
|
|
proto = "https"
|
2021-01-17 18:00:46 +00:00
|
|
|
if ws.AllowInsecure {
|
|
|
|
if ws.TLSConfig == nil {
|
|
|
|
tripper.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
|
|
} else {
|
|
|
|
ws.TLSConfig.InsecureSkipVerify = true
|
|
|
|
}
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
} else {
|
|
|
|
proto = "http"
|
|
|
|
}
|
|
|
|
|
2021-01-17 18:00:46 +00:00
|
|
|
client := http.Client{Transport: tripper, Timeout: time.Second}
|
2023-08-21 21:04:28 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
2021-01-17 18:00:46 +00:00
|
|
|
return ctx.Err()
|
|
|
|
case <-time.After(ws.PollInterval):
|
2023-08-21 21:04:28 +00:00
|
|
|
if err := checkTarget(ctx, target); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, ws.Method, endpoint.String(), bytes.NewReader(body))
|
2021-01-17 18:00:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-03-01 16:06:34 +00:00
|
|
|
resp, err := client.Do(req)
|
2021-01-17 18:00:46 +00:00
|
|
|
if err != nil {
|
2020-03-01 16:06:34 +00:00
|
|
|
continue
|
|
|
|
}
|
2021-01-17 18:00:46 +00:00
|
|
|
if ws.StatusCodeMatcher != nil && !ws.StatusCodeMatcher(resp.StatusCode) {
|
2023-08-21 21:04:28 +00:00
|
|
|
_ = resp.Body.Close()
|
2021-01-17 18:00:46 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp.Body) {
|
2023-08-21 21:04:28 +00:00
|
|
|
_ = resp.Body.Close()
|
2021-01-17 18:00:46 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if err := resp.Body.Close(); err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return nil
|
2020-03-01 16:06:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|