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

fix: do not prepend garbage in the container.Exec response #624

Merged
merged 5 commits into from Nov 16, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion container.go
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/docker/docker/pkg/archive"
"github.com/docker/go-connections/nat"

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

Expand Down Expand Up @@ -54,7 +55,7 @@ type Container interface {
State(context.Context) (*types.ContainerState, error) // returns container's running state
Networks(context.Context) ([]string, error) // get container networks
NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network
Exec(ctx context.Context, cmd []string) (int, io.Reader, error)
Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error)
ContainerIP(context.Context) (string, error) // get container ip
ContainerIPs(context.Context) ([]string, error) // get all container IPs
CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error
Expand Down
13 changes: 11 additions & 2 deletions docker.go
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/moby/term"
specs "github.com/opencontainers/image-spec/specs-go/v1"

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

Expand Down Expand Up @@ -433,7 +434,7 @@ func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]stri
return a, nil
}

func (c *DockerContainer) Exec(ctx context.Context, cmd []string) (int, io.Reader, error) {
func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) {
cli := c.provider.client
response, err := cli.ContainerExecCreate(ctx, c.ID, types.ExecConfig{
Cmd: cmd,
Expand All @@ -450,6 +451,14 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string) (int, io.Reade
return 0, nil, err
}

opt := &tcexec.ProcessOptions{
Reader: hijack.Reader,
}

for _, o := range options {
o.Apply(opt)
}

var exitCode int
for {
execResp, err := cli.ContainerExecInspect(ctx, response.ID)
Expand All @@ -465,7 +474,7 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string) (int, io.Reade
time.Sleep(100 * time.Millisecond)
}

return exitCode, hijack.Reader, nil
return exitCode, opt.Reader, nil
}

type FileFromContainer struct {
Expand Down
67 changes: 67 additions & 0 deletions docker_exec_test.go
@@ -0,0 +1,67 @@
package testcontainers

import (
"context"
"io"
"strings"
"testing"

"github.com/stretchr/testify/require"
tcexec "github.com/testcontainers/testcontainers-go/exec"
)

func TestExecWithMultiplexedResponse(t *testing.T) {
ctx := context.Background()
req := ContainerRequest{
Image: nginxAlpineImage,
}

container, err := GenericContainer(ctx, GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: req,
Started: true,
})

require.NoError(t, err)
terminateContainerOnEnd(t, ctx, container)

code, reader, err := container.Exec(ctx, []string{"ls", "/usr/share/nginx"}, tcexec.Multiplexed())
require.NoError(t, err)
require.Zero(t, code)
require.NotNil(t, reader)

b, err := io.ReadAll(reader)
require.NoError(t, err)
require.NotNil(t, b)

str := string(b)
require.Equal(t, "html\n", str)
}

func TestExecWithNonMultiplexedResponse(t *testing.T) {
ctx := context.Background()
req := ContainerRequest{
Image: nginxAlpineImage,
}

container, err := GenericContainer(ctx, GenericContainerRequest{
ProviderType: providerType,
ContainerRequest: req,
Started: true,
})

require.NoError(t, err)
terminateContainerOnEnd(t, ctx, container)

code, reader, err := container.Exec(ctx, []string{"ls", "/usr/share/nginx"})
require.NoError(t, err)
require.Zero(t, code)
require.NotNil(t, reader)

b, err := io.ReadAll(reader)
require.NoError(t, err)
require.NotNil(t, b)

str := string(b)
require.True(t, strings.HasSuffix(str, "html\n"))
}
44 changes: 44 additions & 0 deletions exec/processor.go
@@ -0,0 +1,44 @@
package exec

import (
"bytes"
"io"

"github.com/docker/docker/pkg/stdcopy"
)

// ProcessOptions defines options applicable to the reader processor
type ProcessOptions struct {
Reader io.Reader
}

// ProcessOption defines a common interface to modify the reader processor
// These options can be passed to the Exec function in a variadic way to customize the returned Reader instance
type ProcessOption interface {
Apply(opts *ProcessOptions)
}

type ProcessOptionFunc func(opts *ProcessOptions)

func (fn ProcessOptionFunc) Apply(opts *ProcessOptions) {
fn(opts)
}

func Multiplexed() ProcessOption {
return ProcessOptionFunc(func(opts *ProcessOptions) {
done := make(chan struct{})

var outBuff bytes.Buffer
var errBuff bytes.Buffer
go func() {
if _, err := stdcopy.StdCopy(&outBuff, &errBuff, opts.Reader); err != nil {
return
}
close(done)
}()

<-done

opts.Reader = &outBuff
})
}
3 changes: 2 additions & 1 deletion wait/exec_test.go
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/docker/go-connections/nat"

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

Expand Down Expand Up @@ -62,7 +63,7 @@ func (st mockExecTarget) Logs(_ context.Context) (io.ReadCloser, error) {
return nil, errors.New("not implemented")
}

func (st mockExecTarget) Exec(ctx context.Context, _ []string) (int, io.Reader, error) {
func (st mockExecTarget) Exec(ctx context.Context, _ []string, options ...tcexec.ProcessOption) (int, io.Reader, error) {
time.Sleep(st.waitDuration)

if err := ctx.Err(); err != nil {
Expand Down
3 changes: 2 additions & 1 deletion wait/exit_test.go
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
tcexec "github.com/testcontainers/testcontainers-go/exec"
)

type exitStrategyTarget struct {
Expand All @@ -30,7 +31,7 @@ func (st exitStrategyTarget) Logs(ctx context.Context) (io.ReadCloser, error) {
return nil, nil
}

func (st exitStrategyTarget) Exec(ctx context.Context, cmd []string) (int, io.Reader, error) {
func (st exitStrategyTarget) Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) {
return 0, nil, nil
}

Expand Down
3 changes: 2 additions & 1 deletion wait/log_test.go
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
tcexec "github.com/testcontainers/testcontainers-go/exec"
)

type noopStrategyTarget struct {
Expand All @@ -31,7 +32,7 @@ func (st noopStrategyTarget) Logs(ctx context.Context) (io.ReadCloser, error) {
return st.ioReaderCloser, nil
}

func (st noopStrategyTarget) Exec(ctx context.Context, cmd []string) (int, io.Reader, error) {
func (st noopStrategyTarget) Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) {
return 0, nil, nil
}
func (st noopStrategyTarget) State(ctx context.Context) (*types.ContainerState, error) {
Expand Down
3 changes: 2 additions & 1 deletion wait/wait.go
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/docker/docker/api/types"
"github.com/docker/go-connections/nat"
tcexec "github.com/testcontainers/testcontainers-go/exec"
)

type Strategy interface {
Expand All @@ -18,7 +19,7 @@ type StrategyTarget interface {
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, io.Reader, error)
Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error)
State(context.Context) (*types.ContainerState, error)
}

Expand Down