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 +}