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 wait.ForExec with response matcher #1035

Merged
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
14 changes: 5 additions & 9 deletions docs/features/wait/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@ The exec wait strategy will check the exit code of a process to be executed in t

- the command and arguments to be executed, as an array of strings.
- a function to match a specific exit code, with the default matching `0`.
- the output response matcher as a function.
- the startup timeout to be used in seconds, default is 60 seconds.
- the poll interval to be used in milliseconds, default is 100 milliseconds.

## Match an exit code
## Match an exit code and a response matcher

```golang
req := ContainerRequest{
Image: "docker.io/nginx:alpine",
WaitingFor: wait.NewExecStrategy([]string{"git", "version"}).WithExitCodeMatcher(func(exitCode int) bool {
return exitCode == 10
}),
}
```
<!--codeinclude-->
[Waiting for a command matching an exit code and response](../../../wait/exec_test.go) inside_block:waitForExecExitCodeResponse
<!--/codeinclude-->
15 changes: 14 additions & 1 deletion wait/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package wait

import (
"context"
"io"
"time"

tcexec "github.com/testcontainers/testcontainers-go/exec"
)

// Implement interface
Expand All @@ -16,6 +19,7 @@ type ExecStrategy struct {

// additional properties
ExitCodeMatcher func(exitCode int) bool
ResponseMatcher func(body io.Reader) bool
PollInterval time.Duration
}

Expand All @@ -24,6 +28,7 @@ func NewExecStrategy(cmd []string) *ExecStrategy {
return &ExecStrategy{
cmd: cmd,
ExitCodeMatcher: defaultExitCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
PollInterval: defaultPollInterval(),
}
}
Expand All @@ -43,6 +48,11 @@ func (ws *ExecStrategy) WithExitCodeMatcher(exitCodeMatcher func(exitCode int) b
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
Expand Down Expand Up @@ -72,13 +82,16 @@ func (ws *ExecStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
case <-ctx.Done():
return ctx.Err()
case <-time.After(ws.PollInterval):
exitCode, _, err := target.Exec(ctx, ws.cmd)
exitCode, resp, err := target.Exec(ctx, ws.cmd, tcexec.Multiplexed())
Copy link
Collaborator Author

@mdelapenya mdelapenya Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used multiplexed to remove the trailing bytes from the Docker protocol (garbage). See #624

if err != nil {
return err
}
if !ws.ExitCodeMatcher(exitCode) {
continue
}
if ws.ResponseMatcher != nil && !ws.ResponseMatcher(resp) {
continue
}

return nil
}
Expand Down
43 changes: 40 additions & 3 deletions wait/exec_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package wait_test

import (
"bytes"
"context"
"errors"
"io"
Expand Down Expand Up @@ -44,6 +45,7 @@ type mockExecTarget struct {
waitDuration time.Duration
successAfter time.Time
exitCode int
response string
failure error
}

Expand All @@ -66,15 +68,20 @@ func (st mockExecTarget) Logs(_ context.Context) (io.ReadCloser, error) {
func (st mockExecTarget) Exec(ctx context.Context, _ []string, options ...tcexec.ProcessOption) (int, io.Reader, error) {
time.Sleep(st.waitDuration)

var reader io.Reader
if st.response != "" {
reader = bytes.NewReader([]byte(st.response))
}

if err := ctx.Err(); err != nil {
return st.exitCode, nil, err
return st.exitCode, reader, err
}

if !st.successAfter.IsZero() && time.Now().After(st.successAfter) {
return 0, nil, st.failure
return 0, reader, st.failure
}

return st.exitCode, nil, st.failure
return st.exitCode, reader, st.failure
}

func (st mockExecTarget) State(_ context.Context) (*types.ContainerState, error) {
Expand Down Expand Up @@ -139,3 +146,33 @@ func TestExecStrategyWaitUntilReady_CustomExitCode(t *testing.T) {
t.Fatal(err)
}
}

func TestExecStrategyWaitUntilReady_CustomResponseMatcher(t *testing.T) {
// waitForExecExitCodeResponse {
dockerReq := testcontainers.ContainerRequest{
Image: "docker.io/nginx:latest",
WaitingFor: wait.ForExec([]string{"echo", "hello world!"}).
WithStartupTimeout(time.Second * 10).
WithExitCodeMatcher(func(exitCode int) bool {
return exitCode == 0
}).
WithResponseMatcher(func(body io.Reader) bool {
data, _ := io.ReadAll(body)
return bytes.Equal(data, []byte("hello world!\n"))
}),
}
// }

ctx := context.Background()
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ContainerRequest: dockerReq, Started: true})
if err != nil {
t.Error(err)
return
}
t.Cleanup(func() {
if err := container.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate container: %s", err)
}
})
// }
}