Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for waiting for response headers #2349

Merged
merged 1 commit into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/features/wait/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The HTTP wait strategy will check the result of an HTTP(S) request against the c
- the HTTP request body to be sent.
- the HTTP status code matcher as a function.
- the HTTP response matcher as a function.
- the HTTP headers to be used.
- the HTTP response headers matcher as a function.
- the TLS config to be used for HTTPS.
- the startup timeout to be used in seconds, default is 60 seconds.
- the poll interval to be used in milliseconds, default is 100 milliseconds.
Expand Down Expand Up @@ -41,3 +43,9 @@ Variations on the HTTP wait strategy are supported, including:
<!--codeinclude-->
[Waiting for an HTTP endpoint matching an HTTP status code](../../../wait/http_test.go) inside_block:waitForHTTPStatusCode
<!--/codeinclude-->

## Match for HTTP response headers

<!--codeinclude-->
[Waiting for an HTTP endpoint matching an HTTP response header](../../../wait/http_test.go) inside_block:waitForHTTPHeaders
<!--/codeinclude-->
67 changes: 45 additions & 22 deletions wait/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,37 @@ type HTTPStrategy struct {
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
PollInterval time.Duration
UserInfo *url.Userinfo
ForceIPv4LocalHost bool
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(body io.Reader) bool { return true },
UseTLS: false,
TLSConfig: nil,
Method: http.MethodGet,
Body: nil,
PollInterval: defaultPollInterval(),
UserInfo: nil,
Port: "",
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
UseTLS: false,
TLSConfig: nil,
Method: http.MethodGet,
Body: nil,
Headers: map[string]string{},
ResponseHeadersMatcher: func(headers http.Header) bool { return true },
PollInterval: defaultPollInterval(),
UserInfo: nil,
}
}

Expand Down Expand Up @@ -110,6 +114,16 @@ func (ws *HTTPStrategy) WithBody(reqdata io.Reader) *HTTPStrategy {
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
Expand Down Expand Up @@ -281,6 +295,11 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
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
Expand All @@ -293,6 +312,10 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
_ = resp.Body.Close()
continue
}
if ws.ResponseHeadersMatcher != nil && !ws.ResponseHeadersMatcher(resp.Header) {
_ = resp.Body.Close()
continue
}
if err := resp.Body.Close(); err != nil {
continue
}
Expand Down
57 changes: 57 additions & 0 deletions wait/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,63 @@ func ExampleHTTPStrategy() {
// true
}

func ExampleHTTPStrategy_WithHeaders() {
capath := filepath.Join("testdata", "root.pem")
cafile, err := os.ReadFile(capath)
if err != nil {
log.Fatalf("can't load ca file: %v", err)
}

certpool := x509.NewCertPool()
if !certpool.AppendCertsFromPEM(cafile) {
log.Fatalf("the ca file isn't valid")
}

ctx := context.Background()

// waitForHTTPHeaders {
tlsconfig := &tls.Config{RootCAs: certpool, ServerName: "testcontainer.go.test"}
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: "testdata",
},
ExposedPorts: []string{"6443/tcp"},
WaitingFor: wait.ForHTTP("/headers").
WithTLS(true, tlsconfig).
WithPort("6443/tcp").
WithHeaders(map[string]string{"X-request-header": "value"}).
WithResponseHeadersMatcher(func(headers http.Header) bool {
return headers.Get("X-response-header") == "value"
},
),
}
// }

c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

defer func() {
if err := c.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err)
}
}()

state, err := c.State(ctx)
if err != nil {
log.Fatalf("failed to get container state: %s", err) // nolint:gocritic
}

fmt.Println(state.Running)

// Output:
// true

}
func ExampleHTTPStrategy_WithPort() {
// waitForHTTPWithPort {
ctx := context.Background()
Expand Down
11 changes: 11 additions & 0 deletions wait/testdata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ func main() {
w.WriteHeader(http.StatusUnauthorized)
})

mux.HandleFunc("/headers", func(w http.ResponseWriter, req *http.Request) {
h := req.Header.Get("X-request-header")
if h != "" {
w.Header().Add("X-response-header", h)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("headers"))
} else {
w.WriteHeader(http.StatusBadRequest)
}
})

mux.HandleFunc("/ping", func(w http.ResponseWriter, req *http.Request) {
data, _ := io.ReadAll(req.Body)
if bytes.Equal(data, []byte("ping")) {
Expand Down
Loading