diff --git a/Pipfile.lock b/Pipfile.lock
index cc75f7cb9d..c3d3100d53 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -548,11 +548,12 @@
},
"urllib3": {
"hashes": [
- "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
- "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
+ "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
+ "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
],
+ "index": "pypi",
"markers": "python_version >= '3.8'",
- "version": "==2.2.1"
+ "version": "==2.2.2"
},
"watchdog": {
"hashes": [
diff --git a/docs/features/configuration.md b/docs/features/configuration.md
index c5be4c47a6..0f29bbc36c 100644
--- a/docs/features/configuration.md
+++ b/docs/features/configuration.md
@@ -49,7 +49,7 @@ Please read more about customizing images in the [Image name substitution](image
1. Ryuk must be started as a privileged container. For that, you can set the `TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED` **environment variable**, or the `ryuk.container.privileged` **property** to `true`.
1. If your environment already implements automatic cleanup of containers after the execution,
but does not allow starting privileged containers, you can turn off the Ryuk container by setting
-`TESTCONTAINERS_RYUK_DISABLED` **environment variable** to `true`.
+`TESTCONTAINERS_RYUK_DISABLED` **environment variable** , or the `ryuk.disabled` **property** to `true`.
1. You can specify the connection timeout for Ryuk by setting the `TESTCONTAINERS_RYUK_CONNECTION_TIMEOUT` **environment variable**, or the `ryuk.connection.timeout` **property**. The default value is 1 minute.
1. You can specify the reconnection timeout for Ryuk by setting the `TESTCONTAINERS_RYUK_RECONNECTION_TIMEOUT` **environment variable**, or the `ryuk.reconnection.timeout` **property**. The default value is 10 seconds.
1. You can configure Ryuk to run in verbose mode by setting any of the `ryuk.verbose` **property** or the `TESTCONTAINERS_RYUK_VERBOSE` **environment variable**. The default value is `false`.
diff --git a/docs/modules/nats.md b/docs/modules/nats.md
index 9d561bd39b..72bd7b4fc5 100644
--- a/docs/modules/nats.md
+++ b/docs/modules/nats.md
@@ -44,6 +44,8 @@ for NATS. E.g. `testcontainers.WithImage("nats:2.9")`.
#### Set username and password
+- Since testcontainers-go :material-tag: v0.24.0
+
If you need to set different credentials, you can use `WithUsername` and `WithPassword`
options. By default, the username, the password are not set. To establish the connection with the NATS container:
@@ -51,15 +53,45 @@ options. By default, the username, the password are not set. To establish the co
[Connect using the credentials](../../modules/nats/examples_test.go) inside_block:natsConnect
+#### Cmd Arguments
+
+- Since testcontainers-go :material-tag: v0.24.0
+
+It's possible to pass extra arguments to the NATS container using the `testcontainers.WithArgument` option. E.g. `nats.WithArgument("cluster_name", "c1")`.
+These arguments are passed to the NATS server when it starts, as part of the command line arguments of the entrypoint.
+
+!!! note
+ Arguments do not need to be prefixed with `--`: the NATS container will add them automatically.
+
+
+[Passing arguments](../../modules/nats/examples_test.go) inside_block:withArguments
+
+
### Container Methods
The NATS container exposes the following methods:
#### ConnectionString
+- Since testcontainers-go :material-tag: v0.24.0
+
This method returns the connection string to connect to the NATS container, using the default `4222` port.
It's possible to pass extra parameters to the connection string, in a variadic way.
[Get connection string](../../modules/nats/nats_test.go) inside_block:connectionString
+
+#### MustConnectionString
+
+- Since testcontainers-go :material-tag: v0.30.0
+
+Exactly like `ConnectionString`, but it panics if an error occurs, returning just a string.
+
+## Examples
+
+### NATS Cluster
+
+
+[NATS Cluster](../../modules/nats/examples_test.go) inside_block:cluster
+
\ No newline at end of file
diff --git a/modules/compose/compose_api.go b/modules/compose/compose_api.go
index 5bdffaf7c8..8af2d38e7f 100644
--- a/modules/compose/compose_api.go
+++ b/modules/compose/compose_api.go
@@ -22,8 +22,8 @@ import (
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"
- testcontainers "github.com/testcontainers/testcontainers-go"
- wait "github.com/testcontainers/testcontainers-go/wait"
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
)
type stackUpOptionFunc func(s *stackUpOptions)
@@ -149,7 +149,7 @@ func (r ComposeStackReaders) applyToComposeStack(o *composeStackOptions) error {
o.temporaryPaths[f[i]] = true
}
- o.Paths = f
+ o.Paths = append(o.Paths, f...)
return nil
}
@@ -157,7 +157,7 @@ func (r ComposeStackReaders) applyToComposeStack(o *composeStackOptions) error {
type ComposeStackFiles []string
func (f ComposeStackFiles) applyToComposeStack(o *composeStackOptions) error {
- o.Paths = f
+ o.Paths = append(o.Paths, f...)
return nil
}
diff --git a/modules/compose/compose_api_test.go b/modules/compose/compose_api_test.go
index bd895c3b3a..e4dff06cfd 100644
--- a/modules/compose/compose_api_test.go
+++ b/modules/compose/compose_api_test.go
@@ -472,6 +472,53 @@ services:
require.Nil(t, f, "File should be removed")
}
+func TestDockerComposeAPIWithStackReaderAndComposeFile(t *testing.T) {
+ identifier := testNameHash(t.Name())
+ simple, _ := RenderComposeSimple(t)
+ composeContent := `version: '3.7'
+services:
+ api-postgres:
+ image: docker.io/postgres:14
+ environment:
+ POSTGRES_PASSWORD: s3cr3t
+`
+
+ compose, err := NewDockerComposeWith(
+ identifier,
+ WithStackFiles(simple),
+ WithStackReaders(strings.NewReader(composeContent)),
+ )
+ require.NoError(t, err, "NewDockerCompose()")
+
+ t.Cleanup(func() {
+ require.NoError(t, compose.Down(context.Background(), RemoveOrphans(true), RemoveImagesLocal), "compose.Down()")
+ })
+
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+
+ err = compose.
+ WithEnv(map[string]string{
+ "bar": "BAR",
+ "foo": "FOO",
+ }).
+ Up(ctx, Wait(true))
+ require.NoError(t, err, "compose.Up()")
+
+ serviceNames := compose.Services()
+
+ assert.Len(t, serviceNames, 2)
+ assert.Contains(t, serviceNames, "api-nginx")
+ assert.Contains(t, serviceNames, "api-postgres")
+
+ present := map[string]string{
+ "bar": "BAR",
+ "foo": "FOO",
+ }
+ absent := map[string]string{}
+ assertContainerEnvironmentVariables(t, identifier.String(), "api-nginx", present, absent)
+}
+
func TestDockerComposeAPIWithEnvironment(t *testing.T) {
identifier := testNameHash(t.Name())
diff --git a/modules/nats/examples_test.go b/modules/nats/examples_test.go
index fd95691cc4..63e1c2b956 100644
--- a/modules/nats/examples_test.go
+++ b/modules/nats/examples_test.go
@@ -4,11 +4,13 @@ import (
"context"
"fmt"
"log"
+ "time"
natsgo "github.com/nats-io/nats.go"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/nats"
+ "github.com/testcontainers/testcontainers-go/network"
)
func ExampleRunContainer() {
@@ -74,3 +76,156 @@ func ExampleRunContainer_connectWithCredentials() {
// Output:
// true
}
+
+func ExampleRunContainer_cluster() {
+ ctx := context.Background()
+
+ nwr, err := network.New(ctx)
+ if err != nil {
+ log.Fatalf("failed to create network: %s", err)
+ }
+
+ // withArguments {
+ natsContainer1, err := nats.RunContainer(ctx,
+ network.WithNetwork([]string{"nats1"}, nwr),
+ nats.WithArgument("name", "nats1"),
+ nats.WithArgument("cluster_name", "c1"),
+ nats.WithArgument("cluster", "nats://nats1:6222"),
+ nats.WithArgument("routes", "nats://nats1:6222,nats://nats2:6222,nats://nats3:6222"),
+ nats.WithArgument("http_port", "8222"),
+ )
+ // }
+ if err != nil {
+ log.Fatalf("failed to start container: %s", err)
+ }
+ // Clean up the container
+ defer func() {
+ if err := natsContainer1.Terminate(ctx); err != nil {
+ log.Fatalf("failed to terminate container: %s", err)
+ }
+ }()
+
+ natsContainer2, err := nats.RunContainer(ctx,
+ network.WithNetwork([]string{"nats2"}, nwr),
+ nats.WithArgument("name", "nats2"),
+ nats.WithArgument("cluster_name", "c1"),
+ nats.WithArgument("cluster", "nats://nats2:6222"),
+ nats.WithArgument("routes", "nats://nats1:6222,nats://nats2:6222,nats://nats3:6222"),
+ nats.WithArgument("http_port", "8222"),
+ )
+ if err != nil {
+ log.Fatalf("failed to start container: %s", err) // nolint:gocritic
+ }
+ // Clean up the container
+ defer func() {
+ if err := natsContainer2.Terminate(ctx); err != nil {
+ log.Fatalf("failed to terminate container: %s", err)
+ }
+ }()
+
+ natsContainer3, err := nats.RunContainer(ctx,
+ network.WithNetwork([]string{"nats3"}, nwr),
+ nats.WithArgument("name", "nats3"),
+ nats.WithArgument("cluster_name", "c1"),
+ nats.WithArgument("cluster", "nats://nats3:6222"),
+ nats.WithArgument("routes", "nats://nats1:6222,nats://nats2:6222,nats://nats3:6222"),
+ nats.WithArgument("http_port", "8222"),
+ )
+ if err != nil {
+ log.Fatalf("failed to start container: %s", err) // nolint:gocritic
+ }
+ defer func() {
+ if err := natsContainer3.Terminate(ctx); err != nil {
+ log.Fatalf("failed to terminate container: %s", err)
+ }
+ }()
+
+ // cluster URL
+ servers := natsContainer1.MustConnectionString(ctx) + "," + natsContainer2.MustConnectionString(ctx) + "," + natsContainer3.MustConnectionString(ctx)
+
+ nc, err := natsgo.Connect(servers, natsgo.MaxReconnects(5), natsgo.ReconnectWait(2*time.Second))
+ if err != nil {
+ log.Fatalf("connecting to nats container failed:\n\t%v\n", err) // nolint:gocritic
+ }
+
+ {
+ // Simple Publisher
+ err = nc.Publish("foo", []byte("Hello World"))
+ if err != nil {
+ log.Fatalf("failed to publish message: %s", err) // nolint:gocritic
+ }
+ }
+
+ {
+ // Channel subscriber
+ ch := make(chan *natsgo.Msg, 64)
+ sub, err := nc.ChanSubscribe("channel", ch)
+ if err != nil {
+ log.Fatalf("failed to subscribe to message: %s", err) // nolint:gocritic
+ }
+
+ // Request
+ err = nc.Publish("channel", []byte("Hello NATS Cluster!"))
+ if err != nil {
+ log.Fatalf("failed to publish message: %s", err) // nolint:gocritic
+ }
+
+ msg := <-ch
+ fmt.Println(string(msg.Data))
+
+ err = sub.Unsubscribe()
+ if err != nil {
+ log.Fatalf("failed to unsubscribe: %s", err) // nolint:gocritic
+ }
+
+ err = sub.Drain()
+ if err != nil {
+ log.Fatalf("failed to drain: %s", err) // nolint:gocritic
+ }
+ }
+
+ {
+ // Responding to a request message
+ sub, err := nc.Subscribe("request", func(m *natsgo.Msg) {
+ err1 := m.Respond([]byte("answer is 42"))
+ if err1 != nil {
+ log.Fatalf("failed to respond to message: %s", err1) // nolint:gocritic
+ }
+ })
+ if err != nil {
+ log.Fatalf("failed to subscribe to message: %s", err) // nolint:gocritic
+ }
+
+ // Request
+ msg, err := nc.Request("request", []byte("what is the answer?"), 1*time.Second)
+ if err != nil {
+ log.Fatalf("failed to send request: %s", err) // nolint:gocritic
+ }
+
+ fmt.Println(string(msg.Data))
+
+ err = sub.Unsubscribe()
+ if err != nil {
+ log.Fatalf("failed to unsubscribe: %s", err) // nolint:gocritic
+ }
+
+ err = sub.Drain()
+ if err != nil {
+ log.Fatalf("failed to drain: %s", err) // nolint:gocritic
+ }
+ }
+
+ // Drain connection (Preferred for responders)
+ // Close() not needed if this is called.
+ err = nc.Drain()
+ if err != nil {
+ log.Fatalf("failed to drain connection: %s", err) // nolint:gocritic
+ }
+
+ // Close connection
+ nc.Close()
+
+ // Output:
+ // Hello NATS Cluster!
+ // answer is 42
+}