Skip to content

Commit

Permalink
feat: support for executing commands in a container with user, workDi…
Browse files Browse the repository at this point in the history
…r and env (#1914)

* feat: support for executing commands in a container with user, workDir and env

* fix: lint

* break: update Executable interface to accept ProcessOptions

* fix: update old name

* docs: improve docs about process options
  • Loading branch information
mdelapenya committed Nov 17, 2023
1 parent 68d5f89 commit 4e6fbdc
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 33 deletions.
26 changes: 15 additions & 11 deletions docker.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 4e6fbdc

Please sign in to comment.