From f599979daaf1dc51b214600be76bb4e8bb366bc8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= <mdelapenya@gmail.com>
Date: Wed, 3 Apr 2024 10:21:07 +0200
Subject: [PATCH 1/6] chore: use "docker compose" (v2) instead of
 "docker-compose" (v1) (#2464)

* chore: migrate from docker-compose (v1) to compose (v2)

* chore: lowercase error message

* chore: lint
---
 docs/features/docker_compose.md  | 20 +++++++++----------
 modules/compose/compose.go       |  5 +++--
 modules/compose/compose_api.go   |  3 +--
 modules/compose/compose_local.go | 34 +++++++++++++++++++++++++-------
 4 files changed, 41 insertions(+), 21 deletions(-)

diff --git a/docs/features/docker_compose.md b/docs/features/docker_compose.md
index ea2a56bc19..352f2c84eb 100644
--- a/docs/features/docker_compose.md
+++ b/docs/features/docker_compose.md
@@ -7,7 +7,7 @@ This is intended to be useful on projects where Docker Compose is already used
 in dev or other environments to define services that an application may be
 dependent upon.
 
-## Using `docker-compose` directly
+## Using `docker compose` directly
 
 !!!warning
 	The minimal version of Go required to use this module is **1.21**.
@@ -16,9 +16,9 @@ dependent upon.
 go get github.com/testcontainers/testcontainers-go/modules/compose
 ```
 
-Because `docker-compose` v2 is implemented in Go it's possible for _Testcontainers for Go_ to
+Because `compose` v2 is implemented in Go it's possible for _Testcontainers for Go_ to
 use [`github.com/docker/compose`](https://github.com/docker/compose) directly and skip any process execution/_docker-compose-in-a-container_ scenario.
-The `ComposeStack` API exposes this variant of using `docker-compose` in an easy way.
+The `ComposeStack` API exposes this variant of using `docker compose` in an easy way.
 
 ### Basic examples
 
@@ -88,14 +88,14 @@ func TestSomethingElse(t *testing.T) {
 
 To interact with service containers after a stack was started it is possible to get an `*tc.DockerContainer` instance via the `ServiceContainer(...)` function.
 The function takes a **service name** (and a `context.Context`) and returns either a `*tc.DockerContainer` or an `error`.
-This is different to the previous `LocalDockerCompose` API where service containers were accessed via their **container name** e.g. `mysql_1` or `mysql-1` (depending on the version of `docker-compose`).
+This is different to the previous `LocalDockerCompose` API where service containers were accessed via their **container name** e.g. `mysql_1` or `mysql-1` (depending on the version of `docker compose`).
 
 Furthermore, there's the convenience function `Serices()` to get a list of all services **defined** by the current project.
 Note that not all of them need necessarily be correctly started as the information is based on the given compose files.
 
 ### Wait strategies
 
-Just like with regular test containers you can also apply wait strategies to `docker-compose` services.
+Just like with regular test containers you can also apply wait strategies to `docker compose` services.
 The `ComposeStack.WaitForService(...)` function allows you to apply a wait strategy to **a service by name**.
 All wait strategies are executed in parallel to both improve startup performance by not blocking too long and to fail
 early if something's wrong.
@@ -139,7 +139,7 @@ func TestSomethingWithWaiting(t *testing.T) {
 
 ### Compose environment
 
-`docker-compose` supports expansion based on environment variables.
+`docker compose` supports expansion based on environment variables.
 The `ComposeStack` supports this as well in two different variants:
 
 - `ComposeStack.WithEnv(m map[string]string) ComposeStack` to parameterize stacks from your test code
@@ -150,14 +150,14 @@ The `ComposeStack` supports this as well in two different variants:
 Also have a look at [ComposeStack](https://pkg.go.dev/github.com/testcontainers/testcontainers-go#ComposeStack) docs for
 further information.
 
-## Usage of `docker-compose` binary
+## Usage of the `docker compose` binary
 
-_Node:_ this API is deprecated and superseded by `ComposeStack` which takes advantage of `docker-compose` v2 being
+_Node:_ this API is deprecated and superseded by `ComposeStack` which takes advantage of `compose` v2 being
 implemented in Go as well by directly using the upstream project.
 
 You can override Testcontainers' default behaviour and make it use a
-docker-compose binary installed on the local machine. This will generally yield
-an experience that is closer to running docker-compose locally, with the caveat
+docker compose binary installed on the local machine. This will generally yield
+an experience that is closer to running docker compose locally, with the caveat
 that Docker Compose needs to be present on dev and CI machines.
 
 ### Examples
diff --git a/modules/compose/compose.go b/modules/compose/compose.go
index 75bf55e0aa..242caef501 100644
--- a/modules/compose/compose.go
+++ b/modules/compose/compose.go
@@ -155,11 +155,12 @@ func NewLocalDockerCompose(filePaths []string, identifier string, opts ...LocalD
 		opts[idx].ApplyToLocalCompose(dc.LocalDockerComposeOptions)
 	}
 
-	dc.Executable = "docker-compose"
+	dc.Executable = "docker"
 	if runtime.GOOS == "windows" {
-		dc.Executable = "docker-compose.exe"
+		dc.Executable = "docker.exe"
 	}
 
+	dc.composeSubcommand = "compose"
 	dc.ComposeFilePaths = filePaths
 
 	dc.absComposeFilePaths = make([]string, len(filePaths))
diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go
index b0ec87ed29..409875537c 100644
--- a/modules/compose/compose_api.go
+++ b/modules/compose/compose_api.go
@@ -34,7 +34,7 @@ func (f stackDownOptionFunc) applyToStackDown(do *api.DownOptions) {
 	f(do)
 }
 
-// RunServices is comparable to 'docker-compose run' as it only creates a subset of containers
+// RunServices is comparable to 'docker compose run' as it only creates a subset of containers
 // instead of all services defined by the project
 func RunServices(serviceNames ...string) StackUpOption {
 	return stackUpOptionFunc(func(o *stackUpOptions) {
@@ -231,7 +231,6 @@ func (d *dockerCompose) Up(ctx context.Context, opts ...StackUpOption) error {
 			Wait:    upOptions.Wait,
 		},
 	})
-
 	if err != nil {
 		return err
 	}
diff --git a/modules/compose/compose_local.go b/modules/compose/compose_local.go
index c008e2ec9d..2be2daf152 100644
--- a/modules/compose/compose_local.go
+++ b/modules/compose/compose_local.go
@@ -41,12 +41,14 @@ func (c composeVersion2) Format(parts ...string) string {
 	return strings.Join(parts, "-")
 }
 
+// Deprecated: use ComposeStack instead
 // LocalDockerCompose represents a Docker Compose execution using local binary
-// docker-compose or docker-compose.exe, depending on the underlying platform
+// docker compose or docker.exe compose, depending on the underlying platform
 type LocalDockerCompose struct {
 	ComposeVersion
 	*LocalDockerComposeOptions
 	Executable           string
+	composeSubcommand    string
 	ComposeFilePaths     []string
 	absComposeFilePaths  []string
 	Identifier           string
@@ -58,17 +60,20 @@ type LocalDockerCompose struct {
 }
 
 type (
+	// Deprecated: it will be removed in the next major release
 	// LocalDockerComposeOptions defines options applicable to LocalDockerCompose
 	LocalDockerComposeOptions struct {
 		Logger testcontainers.Logging
 	}
 
+	// Deprecated: it will be removed in the next major release
 	// LocalDockerComposeOption defines a common interface to modify LocalDockerComposeOptions
 	// These options can be passed to NewLocalDockerCompose in a variadic way to customize the returned LocalDockerCompose instance
 	LocalDockerComposeOption interface {
 		ApplyToLocalCompose(opts *LocalDockerComposeOptions)
 	}
 
+	// Deprecated: it will be removed in the next major release
 	// LocalDockerComposeOptionsFunc is a shorthand to implement the LocalDockerComposeOption interface
 	LocalDockerComposeOptionsFunc func(opts *LocalDockerComposeOptions)
 )
@@ -86,6 +91,7 @@ func WithLogger(logger testcontainers.Logging) ComposeLoggerOption {
 	}
 }
 
+// Deprecated: it will be removed in the next major release
 func (o ComposeLoggerOption) ApplyToLocalCompose(opts *LocalDockerComposeOptions) {
 	opts.Logger = o.logger
 }
@@ -94,15 +100,18 @@ func (o ComposeLoggerOption) applyToComposeStack(opts *composeStackOptions) {
 	opts.Logger = o.logger
 }
 
+// Deprecated: it will be removed in the next major release
 func (f LocalDockerComposeOptionsFunc) ApplyToLocalCompose(opts *LocalDockerComposeOptions) {
 	f(opts)
 }
 
-// Down executes docker-compose down
+// Deprecated: it will be removed in the next major release
+// Down executes docker compose down
 func (dc *LocalDockerCompose) Down() ExecError {
 	return executeCompose(dc, []string{"down", "--remove-orphans", "--volumes"})
 }
 
+// Deprecated: it will be removed in the next major release
 func (dc *LocalDockerCompose) getDockerComposeEnvironment() map[string]string {
 	environment := map[string]string{}
 
@@ -117,10 +126,12 @@ func (dc *LocalDockerCompose) getDockerComposeEnvironment() map[string]string {
 	return environment
 }
 
+// Deprecated: it will be removed in the next major release
 func (dc *LocalDockerCompose) containerNameFromServiceName(service, separator string) string {
 	return dc.Identifier + separator + service
 }
 
+// Deprecated: it will be removed in the next major release
 func (dc *LocalDockerCompose) applyStrategyToRunningContainer() error {
 	cli, err := testcontainers.NewDockerClientWithOpts(context.Background())
 	if err != nil {
@@ -163,17 +174,19 @@ func (dc *LocalDockerCompose) applyStrategyToRunningContainer() error {
 
 		err = strategy.WaitUntilReady(context.Background(), dockercontainer)
 		if err != nil {
-			return fmt.Errorf("Unable to apply wait strategy %v to service %s due to %w", strategy, k.service, err)
+			return fmt.Errorf("unable to apply wait strategy %v to service %s due to %w", strategy, k.service, err)
 		}
 	}
 	return nil
 }
 
+// Deprecated: it will be removed in the next major release
 // Invoke invokes the docker compose
 func (dc *LocalDockerCompose) Invoke() ExecError {
 	return executeCompose(dc, dc.Cmd)
 }
 
+// Deprecated: it will be removed in the next major release
 // WaitForService sets the strategy for the service that is to be waited on
 func (dc *LocalDockerCompose) WaitForService(service string, strategy wait.Strategy) DockerCompose {
 	dc.waitStrategySupplied = true
@@ -181,18 +194,21 @@ func (dc *LocalDockerCompose) WaitForService(service string, strategy wait.Strat
 	return dc
 }
 
+// Deprecated: it will be removed in the next major release
 // WithCommand assigns the command
 func (dc *LocalDockerCompose) WithCommand(cmd []string) DockerCompose {
 	dc.Cmd = cmd
 	return dc
 }
 
+// Deprecated: it will be removed in the next major release
 // WithEnv assigns the environment
 func (dc *LocalDockerCompose) WithEnv(env map[string]string) DockerCompose {
 	dc.Env = env
 	return dc
 }
 
+// Deprecated: it will be removed in the next major release
 // WithExposedService sets the strategy for the service that is to be waited on. If multiple strategies
 // are given for a single service running on different ports, both strategies will be applied on the same container
 func (dc *LocalDockerCompose) WithExposedService(service string, port int, strategy wait.Strategy) DockerCompose {
@@ -201,7 +217,8 @@ func (dc *LocalDockerCompose) WithExposedService(service string, port int, strat
 	return dc
 }
 
-// determineVersion checks which version of docker-compose is installed
+// Deprecated: it will be removed in the next major release
+// determineVersion checks which version of docker compose is installed
 // depending on the version services names are composed in a different way
 func (dc *LocalDockerCompose) determineVersion() error {
 	execErr := executeCompose(dc, []string{"version", "--short"})
@@ -232,6 +249,7 @@ func (dc *LocalDockerCompose) determineVersion() error {
 	return nil
 }
 
+// Deprecated: it will be removed in the next major release
 // validate checks if the files to be run in the compose are valid YAML files, setting up
 // references to all services in them
 func (dc *LocalDockerCompose) validate() error {
@@ -336,11 +354,12 @@ func execute(
 	}
 }
 
+// Deprecated: it will be removed in the next major release
 func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
 	if which(dc.Executable) != nil {
 		return ExecError{
 			Command: []string{dc.Executable},
-			Error:   fmt.Errorf("Local Docker Compose not found. Is %s on the PATH?", dc.Executable),
+			Error:   fmt.Errorf("Local Docker not found. Is %s on the PATH?", dc.Executable),
 		}
 	}
 
@@ -349,7 +368,8 @@ func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
 		environment[k] = v
 	}
 
-	var cmds []string
+	// initialise the command with the compose subcommand
+	cmds := []string{dc.composeSubcommand}
 	pwd := "."
 	if len(dc.absComposeFilePaths) > 0 {
 		pwd, _ = filepath.Split(dc.absComposeFilePaths[0])
@@ -367,7 +387,7 @@ func executeCompose(dc *LocalDockerCompose, args []string) ExecError {
 	if err != nil {
 		args := strings.Join(dc.Cmd, " ")
 		return ExecError{
-			Command: []string{dc.Executable},
+			Command: []string{dc.Executable, args},
 			Error:   fmt.Errorf("Local Docker compose exited abnormally whilst running %s: [%v]. %s", dc.Executable, args, err.Error()),
 		}
 	}

From d24cf912cedcb991f79f1088ce6c652e2fab7f41 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Manuel=20de=20la=20Pe=C3=B1a?= <mdelapenya@gmail.com>
Date: Wed, 3 Apr 2024 10:22:27 +0200
Subject: [PATCH 2/6] chore: more compose updates in comments

---
 docker_test.go                  | 2 +-
 modules/compose/compose_test.go | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docker_test.go b/docker_test.go
index 45e276b1ed..bc29e854d0 100644
--- a/docker_test.go
+++ b/docker_test.go
@@ -1854,7 +1854,7 @@ func TestContainerWithNoUserID(t *testing.T) {
 }
 
 func TestGetGatewayIP(t *testing.T) {
-	// When using docker-compose with DinD mode, and using host port or http wait strategy
+	// When using docker compose with DinD mode, and using host port or http wait strategy
 	// It's need to invoke GetGatewayIP for get the host
 	provider, err := providerType.GetProvider(WithLogger(TestLogger(t)))
 	if err != nil {
diff --git a/modules/compose/compose_test.go b/modules/compose/compose_test.go
index bffec7bd94..c87e204725 100644
--- a/modules/compose/compose_test.go
+++ b/modules/compose/compose_test.go
@@ -33,7 +33,7 @@ func ExampleNewLocalDockerCompose() {
 
 func ExampleLocalDockerCompose() {
 	_ = LocalDockerCompose{
-		Executable: "docker-compose",
+		Executable: "docker compose",
 		ComposeFilePaths: []string{
 			"/path/to/docker-compose.yml",
 			"/path/to/docker-compose-1.yml",

From 2d89e90485fc319beb0ad18ecd4c04e9d68b6648 Mon Sep 17 00:00:00 2001
From: Barrett Strausser <bearrito@users.noreply.github.com>
Date: Wed, 3 Apr 2024 04:29:36 -0400
Subject: [PATCH 3/6] bug:Fix AMQPS url (#2462)

* Fix AMQPS url

* Split off amqps and add failing test for client certs

* Adds certs into tests

---------

Co-authored-by: bstrausser <bstrausser@locusrobotics.com>
---
 modules/rabbitmq/rabbitmq.go      |  2 +-
 modules/rabbitmq/rabbitmq_test.go | 57 +++++++++++++++++++++++++++++++
 2 files changed, 58 insertions(+), 1 deletion(-)

diff --git a/modules/rabbitmq/rabbitmq.go b/modules/rabbitmq/rabbitmq.go
index 9fb28212e1..a6dec1a779 100644
--- a/modules/rabbitmq/rabbitmq.go
+++ b/modules/rabbitmq/rabbitmq.go
@@ -48,7 +48,7 @@ func (c *RabbitMQContainer) AmqpURL(ctx context.Context) (string, error) {
 
 // AmqpURL returns the URL for AMQPS clients.
 func (c *RabbitMQContainer) AmqpsURL(ctx context.Context) (string, error) {
-	endpoint, err := c.PortEndpoint(ctx, nat.Port(DefaultAMQPPort), "")
+	endpoint, err := c.PortEndpoint(ctx, nat.Port(DefaultAMQPSPort), "")
 	if err != nil {
 		return "", err
 	}
diff --git a/modules/rabbitmq/rabbitmq_test.go b/modules/rabbitmq/rabbitmq_test.go
index 0c85c66607..7079379421 100644
--- a/modules/rabbitmq/rabbitmq_test.go
+++ b/modules/rabbitmq/rabbitmq_test.go
@@ -2,8 +2,12 @@ package rabbitmq_test
 
 import (
 	"context"
+	"crypto/tls"
+	"crypto/x509"
 	"fmt"
 	"io"
+	"io/ioutil"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -42,6 +46,59 @@ func TestRunContainer_connectUsingAmqp(t *testing.T) {
 	}
 }
 
+func TestRunContainer_connectUsingAmqps(t *testing.T) {
+	ctx := context.Background()
+
+	sslSettings := rabbitmq.SSLSettings{
+		CACertFile:        filepath.Join("testdata", "certs", "server_ca.pem"),
+		CertFile:          filepath.Join("testdata", "certs", "server_cert.pem"),
+		KeyFile:           filepath.Join("testdata", "certs", "server_key.pem"),
+		VerificationMode:  rabbitmq.SSLVerificationModePeer,
+		FailIfNoCert:      false,
+		VerificationDepth: 1,
+	}
+
+	rabbitmqContainer, err := rabbitmq.RunContainer(ctx, rabbitmq.WithSSL(sslSettings))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	defer func() {
+		if err := rabbitmqContainer.Terminate(ctx); err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	amqpsURL, err := rabbitmqContainer.AmqpsURL(ctx)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !strings.HasPrefix(amqpsURL, "amqps") {
+		t.Fatal(fmt.Errorf("AMQPS Url should begin with `amqps`"))
+	}
+
+	certs := x509.NewCertPool()
+
+	pemData, err := ioutil.ReadFile(sslSettings.CACertFile)
+	if err != nil {
+		t.Fatal(err)
+	}
+	certs.AppendCertsFromPEM(pemData)
+
+	amqpsConnection, err := amqp.DialTLS(amqpsURL, &tls.Config{InsecureSkipVerify: false, RootCAs: certs})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if amqpsConnection.IsClosed() {
+		t.Fatal(fmt.Errorf("AMQPS Connection unexpectdely closed"))
+	}
+	if err = amqpsConnection.Close(); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func TestRunContainer_withAllSettings(t *testing.T) {
 	ctx := context.Background()
 

From db6136946fb3e513a779d98eb6c2e924c18a6ab7 Mon Sep 17 00:00:00 2001
From: Daniel Orbach <49489492+danielorbach@users.noreply.github.com>
Date: Wed, 3 Apr 2024 12:07:06 +0300
Subject: [PATCH 4/6] Upgrade neo4j module to use features from v0.29.1 of
 testcontainers-go (#2463)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Configure enterprise license using the new WithEnv option

Release v0.29.1 introduces a new option - WithEnv. This commit uses this
new option to configure the enterprise license instead of the manual
approach taken prior.

* Fix typo in WithEnv documentation

I've noticed a typo while reading about this new option.

---------

Co-authored-by: Manuel de la Peña <mdelapenya@gmail.com>
---
 docs/features/common_functional_options.md |  2 +-
 modules/neo4j/config.go                    | 12 ++++++------
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/docs/features/common_functional_options.md b/docs/features/common_functional_options.md
index 288093941c..1572bb461b 100644
--- a/docs/features/common_functional_options.md
+++ b/docs/features/common_functional_options.md
@@ -22,7 +22,7 @@ Using the `WithImageSubstitutors` options, you could define your own substitutio
 If you need to either pass additional environment variables to a container or override them, you can use `testcontainers.WithEnv` for example:
 
 ```golang
-postgres, err = postgresModule.RunContainer(ctx, testcontainers.WithEnv(map[string]string{"POSTGRES_INITDB_ARGS", "--no-sync"}))
+postgres, err = postgresModule.RunContainer(ctx, testcontainers.WithEnv(map[string]string{"POSTGRES_INITDB_ARGS": "--no-sync"}))
 ```
 
 #### WithLogConsumers
diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go
index 9d0bc70816..1b62363cb9 100644
--- a/modules/neo4j/config.go
+++ b/modules/neo4j/config.go
@@ -123,9 +123,9 @@ func formatNeo4jConfig(name string) string {
 // the commercial licence agreement of Neo4j Enterprise Edition. The license
 // agreement is available at https://neo4j.com/terms/licensing/.
 func WithAcceptCommercialLicenseAgreement() testcontainers.CustomizeRequestOption {
-	return func(req *testcontainers.GenericContainerRequest) {
-		req.Env["NEO4J_ACCEPT_LICENSE_AGREEMENT"] = "yes"
-	}
+	return testcontainers.WithEnv(map[string]string{
+		"NEO4J_ACCEPT_LICENSE_AGREEMENT": "yes",
+	})
 }
 
 // WithAcceptEvaluationLicenseAgreement sets the environment variable
@@ -134,7 +134,7 @@ func WithAcceptCommercialLicenseAgreement() testcontainers.CustomizeRequestOptio
 // agreement is available at https://neo4j.com/terms/enterprise_us/. Please
 // read the terms of the evaluation agreement before you accept.
 func WithAcceptEvaluationLicenseAgreement() testcontainers.CustomizeRequestOption {
-	return func(req *testcontainers.GenericContainerRequest) {
-		req.Env["NEO4J_ACCEPT_LICENSE_AGREEMENT"] = "eval"
-	}
+	return testcontainers.WithEnv(map[string]string{
+		"NEO4J_ACCEPT_LICENSE_AGREEMENT": "eval",
+	})
 }

From 88622f0cb1bcb38bb86de8a45df993b35c28c8e5 Mon Sep 17 00:00:00 2001
From: "Luis Gustavo S. Barreto" <gustavosbarreto@gmail.com>
Date: Wed, 3 Apr 2024 06:49:55 -0300
Subject: [PATCH 5/6] fix(exec): updates the `Multiplexed` opt to combine
 stdout and stderr (#2452)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(exec): updates the `Multiplexed` opt to combine stdout and stderr

Updates the `Multiplexed` option to combine the output of stdout and stderr,
changing the previous behavior where stderr was preferred over stdout.
Now, both streams are properly combined into a single stream.

Additionally, this commit enhance and adjust existing test cases for
the `Multiplexed` option:

* Modifies `TestExecWithMultiplexedResponse` to ensure that stdout and
  stderr are on the same stream, testing the new behavior of the
  `Multiplexed` option.
* Refactors `TestExecWithNonMultiplexedResponse` to use `stdcopy.StdCopy`
  for ensuring that `io.Reader` contains a separated stdout and stderr.

* chore: no need to pass the provider type

---------

Co-authored-by: Manuel de la Peña <mdelapenya@gmail.com>
---
 docker_exec_test.go | 59 +++++++++++++--------------------------------
 exec/processor.go   |  6 +----
 2 files changed, 18 insertions(+), 47 deletions(-)

diff --git a/docker_exec_test.go b/docker_exec_test.go
index 7e1cd4bc2f..4716c493ab 100644
--- a/docker_exec_test.go
+++ b/docker_exec_test.go
@@ -1,44 +1,16 @@
 package testcontainers
 
 import (
+	"bytes"
 	"context"
 	"io"
-	"strings"
 	"testing"
 
+	"github.com/docker/docker/pkg/stdcopy"
 	"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 TestExecWithOptions(t *testing.T) {
 	tests := []struct {
 		name string
@@ -79,7 +51,6 @@ func TestExecWithOptions(t *testing.T) {
 			}
 
 			container, err := GenericContainer(ctx, GenericContainerRequest{
-				ProviderType:     providerType,
 				ContainerRequest: req,
 				Started:          true,
 			})
@@ -106,14 +77,13 @@ func TestExecWithOptions(t *testing.T) {
 	}
 }
 
-func TestExecWithMultiplexedStderrResponse(t *testing.T) {
+func TestExecWithMultiplexedResponse(t *testing.T) {
 	ctx := context.Background()
 	req := ContainerRequest{
 		Image: nginxAlpineImage,
 	}
 
 	container, err := GenericContainer(ctx, GenericContainerRequest{
-		ProviderType:     providerType,
 		ContainerRequest: req,
 		Started:          true,
 	})
@@ -121,9 +91,9 @@ func TestExecWithMultiplexedStderrResponse(t *testing.T) {
 	require.NoError(t, err)
 	terminateContainerOnEnd(t, ctx, container)
 
-	code, reader, err := container.Exec(ctx, []string{"ls", "/non-existing-directory"}, tcexec.Multiplexed())
+	code, reader, err := container.Exec(ctx, []string{"sh", "-c", "echo stdout; echo stderr >&2"}, tcexec.Multiplexed())
 	require.NoError(t, err)
-	require.NotZero(t, code)
+	require.Zero(t, code)
 	require.NotNil(t, reader)
 
 	b, err := io.ReadAll(reader)
@@ -131,7 +101,8 @@ func TestExecWithMultiplexedStderrResponse(t *testing.T) {
 	require.NotNil(t, b)
 
 	str := string(b)
-	require.Contains(t, str, "No such file or directory")
+	require.Contains(t, str, "stdout")
+	require.Contains(t, str, "stderr")
 }
 
 func TestExecWithNonMultiplexedResponse(t *testing.T) {
@@ -141,7 +112,6 @@ func TestExecWithNonMultiplexedResponse(t *testing.T) {
 	}
 
 	container, err := GenericContainer(ctx, GenericContainerRequest{
-		ProviderType:     providerType,
 		ContainerRequest: req,
 		Started:          true,
 	})
@@ -149,15 +119,20 @@ func TestExecWithNonMultiplexedResponse(t *testing.T) {
 	require.NoError(t, err)
 	terminateContainerOnEnd(t, ctx, container)
 
-	code, reader, err := container.Exec(ctx, []string{"ls", "/usr/share/nginx"})
+	code, reader, err := container.Exec(ctx, []string{"sh", "-c", "echo stdout; echo stderr >&2"})
 	require.NoError(t, err)
 	require.Zero(t, code)
 	require.NotNil(t, reader)
 
-	b, err := io.ReadAll(reader)
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+
+	written, err := stdcopy.StdCopy(&stdout, &stderr, reader)
 	require.NoError(t, err)
-	require.NotNil(t, b)
+	require.NotZero(t, written)
+	require.NotNil(t, stdout)
+	require.NotNil(t, stderr)
 
-	str := string(b)
-	require.True(t, strings.HasSuffix(str, "html\n"))
+	require.Equal(t, stdout.String(), "stdout\n")
+	require.Equal(t, stderr.String(), "stderr\n")
 }
diff --git a/exec/processor.go b/exec/processor.go
index c4d40f1e73..c4f9e1b100 100644
--- a/exec/processor.go
+++ b/exec/processor.go
@@ -82,10 +82,6 @@ func Multiplexed() ProcessOption {
 
 		<-done
 
-		if errBuff.Bytes() != nil {
-			opts.Reader = &errBuff
-		} else {
-			opts.Reader = &outBuff
-		}
+		opts.Reader = io.MultiReader(&outBuff, &errBuff)
 	})
 }

From 697c264b1e8f9e1ebf31d6428fdf3b7b3b68b849 Mon Sep 17 00:00:00 2001
From: Adrian Cole <64215+codefromthecrypt@users.noreply.github.com>
Date: Wed, 3 Apr 2024 18:51:25 +0800
Subject: [PATCH 6/6] feat: optimizes file copies to and from containers
 (#2450)

* feat: optimizes file copies to and from containers

Signed-off-by: Adrian Cole <adrian@tetrate.io>

* drift

Signed-off-by: Adrian Cole <adrian@tetrate.io>

* go 1.21 defense

Signed-off-by: Adrian Cole <adrian@tetrate.io>

---------

Signed-off-by: Adrian Cole <adrian@tetrate.io>
---
 docker.go    | 36 ++++++++++++++++++++++++++++++------
 file.go      |  6 +++---
 file_test.go |  5 ++++-
 3 files changed, 37 insertions(+), 10 deletions(-)

diff --git a/docker.go b/docker.go
index ec703ac913..c946103556 100644
--- a/docker.go
+++ b/docker.go
@@ -11,6 +11,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"io/fs"
 	"net/url"
 	"os"
 	"path/filepath"
@@ -602,19 +603,41 @@ func (c *DockerContainer) CopyFileToContainer(ctx context.Context, hostFilePath
 		return c.CopyDirToContainer(ctx, hostFilePath, containerFilePath, fileMode)
 	}
 
-	fileContent, err := os.ReadFile(hostFilePath)
+	f, err := os.Open(hostFilePath)
 	if err != nil {
 		return err
 	}
-	return c.CopyToContainer(ctx, fileContent, containerFilePath, fileMode)
+	defer f.Close()
+
+	info, err := f.Stat()
+	if err != nil {
+		return err
+	}
+
+	// In Go 1.22 os.File is always an io.WriterTo. However, testcontainers
+	// currently allows Go 1.21, so we need to trick the compiler a little.
+	var file fs.File = f
+	return c.copyToContainer(ctx, func(tw io.Writer) error {
+		// Attempt optimized writeTo, implemented in linux
+		if wt, ok := file.(io.WriterTo); ok {
+			_, err := wt.WriteTo(tw)
+			return err
+		}
+		_, err := io.Copy(tw, f)
+		return err
+	}, info.Size(), containerFilePath, fileMode)
 }
 
 // CopyToContainer copies fileContent data to a file in container
 func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error {
-	buffer, err := tarFile(fileContent, containerFilePath, fileMode)
-	if err != nil {
+	return c.copyToContainer(ctx, func(tw io.Writer) error {
+		_, err := tw.Write(fileContent)
 		return err
-	}
+	}, int64(len(fileContent)), containerFilePath, fileMode)
+}
+
+func (c *DockerContainer) copyToContainer(ctx context.Context, fileContent func(tw io.Writer) error, fileContentSize int64, containerFilePath string, fileMode int64) error {
+	buffer, err := tarFile(containerFilePath, fileContent, fileContentSize, fileMode)
 
 	err = c.provider.client.CopyToContainer(ctx, c.ID, "/", buffer, types.CopyToContainerOptions{})
 	if err != nil {
@@ -1574,7 +1597,8 @@ func (p *DockerProvider) SaveImages(ctx context.Context, output string, images .
 		_ = imageReader.Close()
 	}()
 
-	_, err = io.Copy(outputFile, imageReader)
+	// Attempt optimized readFrom, implemented in linux
+	_, err = outputFile.ReadFrom(imageReader)
 	if err != nil {
 		return fmt.Errorf("writing images to output %w", err)
 	}
diff --git a/file.go b/file.go
index 4c1f3ee32b..a6743cc9e4 100644
--- a/file.go
+++ b/file.go
@@ -110,7 +110,7 @@ func tarDir(src string, fileMode int64) (*bytes.Buffer, error) {
 }
 
 // tarFile compress a single file using tar + gzip algorithms
-func tarFile(fileContent []byte, basePath string, fileMode int64) (*bytes.Buffer, error) {
+func tarFile(basePath string, fileContent func(tw io.Writer) error, fileContentSize int64, fileMode int64) (*bytes.Buffer, error) {
 	buffer := &bytes.Buffer{}
 
 	zr := gzip.NewWriter(buffer)
@@ -119,12 +119,12 @@ func tarFile(fileContent []byte, basePath string, fileMode int64) (*bytes.Buffer
 	hdr := &tar.Header{
 		Name: basePath,
 		Mode: fileMode,
-		Size: int64(len(fileContent)),
+		Size: fileContentSize,
 	}
 	if err := tw.WriteHeader(hdr); err != nil {
 		return buffer, err
 	}
-	if _, err := tw.Write(fileContent); err != nil {
+	if err := fileContent(tw); err != nil {
 		return buffer, err
 	}
 
diff --git a/file_test.go b/file_test.go
index c1fc9f0704..e5e660dee7 100644
--- a/file_test.go
+++ b/file_test.go
@@ -119,7 +119,10 @@ func Test_TarFile(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	buff, err := tarFile(b, "Docker.file", 0o755)
+	buff, err := tarFile("Docker.file", func(tw io.Writer) error {
+		_, err := tw.Write(b)
+		return err
+	}, int64(len(b)), 0o755)
 	if err != nil {
 		t.Fatal(err)
 	}