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 executing commands in a container with user, workDir and env #1914

Merged
merged 6 commits into from Nov 17, 2023
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
26 changes: 15 additions & 11 deletions docker.go
Expand Up @@ -466,12 +466,16 @@ func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]stri

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,
Detach: false,
AttachStdout: true,
AttachStderr: true,
})

processOptions := tcexec.NewProcessOptions(cmd)

// processing all the options in a first loop because for the multiplexed option
// we first need to have a containerExecCreateResponse
for _, o := range options {
o.Apply(processOptions)
}

response, err := cli.ContainerExecCreate(ctx, c.ID, processOptions.ExecConfig)
if err != nil {
return 0, nil, err
}
Expand All @@ -481,12 +485,12 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tce
return 0, nil, err
}

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

// second loop to process the multiplexed option, as now we have a reader
// from the created exec response.
for _, o := range options {
o.Apply(opt)
o.Apply(processOptions)
}

var exitCode int
Expand All @@ -504,7 +508,7 @@ func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tce
time.Sleep(100 * time.Millisecond)
}

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

type FileFromContainer struct {
Expand Down
67 changes: 67 additions & 0 deletions docker_exec_test.go
Expand Up @@ -39,6 +39,73 @@ func TestExecWithMultiplexedResponse(t *testing.T) {
require.Equal(t, "html\n", str)
}

func TestExecWithOptions(t *testing.T) {
tests := []struct {
name string
cmds []string
opts []tcexec.ProcessOption
want string
}{
{
name: "with user",
cmds: []string{"whoami"},
opts: []tcexec.ProcessOption{
tcexec.WithUser("nginx"),
},
want: "nginx\n",
},
{
name: "with working dir",
cmds: []string{"pwd"},
opts: []tcexec.ProcessOption{
tcexec.WithWorkingDir("/var/log/nginx"),
},
want: "/var/log/nginx\n",
},
{
name: "with env",
cmds: []string{"env"},
opts: []tcexec.ProcessOption{
tcexec.WithEnv([]string{"TEST_ENV=test"}),
},
want: "TEST_ENV=test\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(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)

// always append the multiplexed option for having the output
// in a readable format
tt.opts = append(tt.opts, tcexec.Multiplexed())

code, reader, err := container.Exec(ctx, tt.cmds, tt.opts...)
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.Contains(t, str, tt.want)
})
}
}

func TestExecWithNonMultiplexedResponse(t *testing.T) {
ctx := context.Background()
req := ContainerRequest{
Expand Down
5 changes: 4 additions & 1 deletion docs/features/common_functional_options.md
Expand Up @@ -24,7 +24,10 @@ Testcontainers exposes the `WithStartupCommand(e ...Executable)` option to run a
!!!info
To better understand how this feature works, please read the [Create containers: Lifecycle Hooks](/features/creating_container/#lifecycle-hooks) documentation.

It also exports an `Executable` interface, defining one single method: `AsCommand()`, which returns a slice of strings to represent the command and positional arguments to be executed in the container.
It also exports an `Executable` interface, defining the following methods:

- `AsCommand()`, which returns a slice of strings to represent the command and positional arguments to be executed in the container;
- `Options()`, which returns the slice of functional options with the Docker's ExecConfigs used to create the command in the container (the working directory, environment variables, user executing the command, etc) and the possible output format (Multiplexed).

You could use this feature to run a custom script, or to run a command that is not supported by the module right after the container is started.

Expand Down
45 changes: 44 additions & 1 deletion exec/processor.go
Expand Up @@ -4,12 +4,30 @@ import (
"bytes"
"io"

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

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

// NewProcessOptions returns a new ProcessOptions instance
// with the given command and default options:
// - detach: false
// - attach stdout: true
// - attach stderr: true
func NewProcessOptions(cmd []string) *ProcessOptions {
return &ProcessOptions{
ExecConfig: types.ExecConfig{
Cmd: cmd,
Detach: false,
AttachStdout: true,
AttachStderr: true,
},
}
}

// ProcessOption defines a common interface to modify the reader processor
Expand All @@ -24,8 +42,33 @@ func (fn ProcessOptionFunc) Apply(opts *ProcessOptions) {
fn(opts)
}

func WithUser(user string) ProcessOption {
return ProcessOptionFunc(func(opts *ProcessOptions) {
opts.ExecConfig.User = user
})
}

func WithWorkingDir(workingDir string) ProcessOption {
return ProcessOptionFunc(func(opts *ProcessOptions) {
opts.ExecConfig.WorkingDir = workingDir
})
}

func WithEnv(env []string) ProcessOption {
return ProcessOptionFunc(func(opts *ProcessOptions) {
opts.ExecConfig.Env = env
})
}

func Multiplexed() ProcessOption {
return ProcessOptionFunc(func(opts *ProcessOptions) {
// returning fast to bypass those options with a nil reader,
// which could be the case when other options are used
// to configure the exec creation.
if opts.Reader == nil {
return
}

done := make(chan struct{})

var outBuff bytes.Buffer
Expand Down
3 changes: 3 additions & 0 deletions modules/cassandra/executable.go
Expand Up @@ -2,9 +2,12 @@ package cassandra

import (
"strings"

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

type initScript struct {
testcontainers.ExecOptions
File string
}

Expand Down
5 changes: 4 additions & 1 deletion modules/rabbitmq/examples_test.go
Expand Up @@ -130,7 +130,10 @@ func ExampleRunContainer_withPlugins() {
testcontainers.WithImage("rabbitmq:3.7.25-management-alpine"),
// Multiple test implementations of the Executable interface, specific to RabbitMQ, exist in the types_test.go file.
// Please refer to them for more examples.
testcontainers.WithStartupCommand(testcontainers.RawCommand{"rabbitmq_shovel"}, testcontainers.RawCommand{"rabbitmq_random_exchange"}),
testcontainers.WithStartupCommand(
testcontainers.NewRawCommand([]string{"rabbitmq_shovel"}),
testcontainers.NewRawCommand([]string{"rabbitmq_random_exchange"}),
),
)
if err != nil {
panic(err)
Expand Down
2 changes: 1 addition & 1 deletion modules/rabbitmq/rabbitmq_test.go
Expand Up @@ -124,7 +124,7 @@ func TestRunContainer_withAllSettings(t *testing.T) {
}),
// }
// enablePlugins {
testcontainers.WithStartupCommand(Plugin("rabbitmq_shovel"), Plugin("rabbitmq_random_exchange")),
testcontainers.WithStartupCommand(Plugin{Name: "rabbitmq_shovel"}, Plugin{Name: "rabbitmq_random_exchange"}),
// }
)
if err != nil {
Expand Down
19 changes: 17 additions & 2 deletions modules/rabbitmq/types_test.go
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"strings"

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

// The following structs are added as a demonstration for the RabbitMQ management API therefore,
Expand All @@ -15,6 +17,7 @@ import (
// --------- Bindings ---------

type Binding struct {
testcontainers.ExecOptions
VHost string
Source string
Destination string
Expand Down Expand Up @@ -72,6 +75,7 @@ func (b Binding) AsCommand() []string {
// --------- Exchange ---------

type Exchange struct {
testcontainers.ExecOptions
Name string
VHost string
Type string
Expand Down Expand Up @@ -117,6 +121,7 @@ func (e Exchange) AsCommand() []string {
// --------- OperatorPolicy ---------

type OperatorPolicy struct {
testcontainers.ExecOptions
Name string
Pattern string
Definition map[string]interface{}
Expand Down Expand Up @@ -151,6 +156,7 @@ func (op OperatorPolicy) AsCommand() []string {
// --------- Parameter ---------

type Parameter struct {
testcontainers.ExecOptions
Component string
Name string
Value string
Expand All @@ -176,6 +182,7 @@ func (p Parameter) AsCommand() []string {
// --------- Permission ---------

type Permission struct {
testcontainers.ExecOptions
VHost string
User string
Configure string
Expand Down Expand Up @@ -205,17 +212,21 @@ func (p Permission) AsCommand() []string {

// --------- Plugin ---------

type Plugin string
type Plugin struct {
testcontainers.ExecOptions
Name string
}

func (p Plugin) AsCommand() []string {
return []string{"rabbitmq-plugins", "enable", string(p)}
return []string{"rabbitmq-plugins", "enable", p.Name}
}

// --------- Plugin ---------

// --------- Policy ---------

type Policy struct {
testcontainers.ExecOptions
VHost string
Name string
Pattern string
Expand Down Expand Up @@ -257,6 +268,7 @@ func (p Policy) AsCommand() []string {
// --------- Queue ---------

type Queue struct {
testcontainers.ExecOptions
Name string
VHost string
AutoDelete bool
Expand Down Expand Up @@ -297,6 +309,7 @@ func (q Queue) AsCommand() []string {
// --------- User ---------

type User struct {
testcontainers.ExecOptions
Name string
Password string
Tags []string
Expand Down Expand Up @@ -325,6 +338,7 @@ func (u User) AsCommand() []string {
// --------- Virtual Hosts --------

type VirtualHost struct {
testcontainers.ExecOptions
Name string
Tracing bool
}
Expand All @@ -340,6 +354,7 @@ func (v VirtualHost) AsCommand() []string {
}

type VirtualHostLimit struct {
testcontainers.ExecOptions
VHost string
Name string
Value int
Expand Down