diff --git a/docs/modules/redis.md b/docs/modules/redis.md
index 58993fc181..e0c18bbaf8 100644
--- a/docs/modules/redis.md
+++ b/docs/modules/redis.md
@@ -73,10 +73,12 @@ In the case you have a custom config file for Redis, it's possible to copy that
#### WithTLS
+- Since testcontainers-go :material-tag: v0.37.0
+
In the case you want to enable TLS for the Redis container, you can use the `WithTLS()` option. This options enables TLS on the `6379/tcp` port and uses a secure URL (e.g. `rediss://host:port`).
!!!info
- In case you want to use Non-mutual TLS (i.e. client authentication is not required), you can customize the CMD arguments by using the `WithCmdArgs` option. E.g. `WithCmdArgs("--tls-auth-clients no")`.
+ In case you want to use Non-mutual TLS (i.e. client authentication is not required), you can customize the CMD arguments by using the `WithCmdArgs` option. E.g. `WithCmdArgs("--tls-auth-clients", "no")`.
The module automatically generates three certificates, a CA certificate, a client certificate and a Redis certificate. Please use the `TLSConfig()` container method to get the TLS configuration and use it to configure the Redis client. See more details in the [TLSConfig](#tlsconfig) section.
@@ -94,6 +96,8 @@ If the container is started with TLS enabled, the connection string is `rediss:/
#### TLSConfig
+- Since testcontainers-go :material-tag: v0.37.0
+
This method returns the TLS configuration for the Redis container, nil if TLS is not enabled.
diff --git a/docs/modules/valkey.md b/docs/modules/valkey.md
index 81c84b4509..c31d6ac545 100644
--- a/docs/modules/valkey.md
+++ b/docs/modules/valkey.md
@@ -66,6 +66,17 @@ You can easily set the valkey logging level. E.g. `WithLogLevel(LogLevelDebug)`.
In the case you have a custom config file for Valkey, it's possible to copy that file into the container before it's started. E.g. `WithConfigFile(filepath.Join("testdata", "valkey.conf"))`.
+#### WithTLS
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+In the case you want to enable TLS for the Valkey container, you can use the `WithTLS()` option. This options enables TLS on the `6379/tcp` port and uses a secure URL (e.g. `rediss://host:port`).
+
+!!!info
+ In case you want to use Non-mutual TLS (i.e. client authentication is not required), you can customize the CMD arguments by using the `WithCmdArgs` option. E.g. `WithCmdArgs("--tls-auth-clients", "no")`.
+
+The module automatically generates three certificates, a CA certificate, a client certificate and a Valkey certificate. Please use the `TLSConfig()` container method to get the TLS configuration and use it to configure the Valkey client. See more details in the [TLSConfig](#tlsconfig) section.
+
### Container Methods
The Valkey container exposes the following methods:
@@ -78,4 +89,16 @@ This method returns the connection string to connect to the Valkey container, us
[Get connection string](../../modules/valkey/valkey_test.go) inside_block:connectionString
-
\ No newline at end of file
+
+
+#### TLSConfig
+
+- Not available until the next release of testcontainers-go :material-tag: main
+
+This method returns the TLS configuration for the Valkey container, nil if TLS is not enabled.
+
+
+[Get TLS config](../../modules/valkey/valkey_test.go) inside_block:tlsConfig
+
+
+In the above example, the options are used to configure a Valkey client with TLS enabled.
diff --git a/modules/valkey/examples_test.go b/modules/valkey/examples_test.go
index c700e8d3f3..192e488ba5 100644
--- a/modules/valkey/examples_test.go
+++ b/modules/valkey/examples_test.go
@@ -6,19 +6,21 @@ import (
"log"
"path/filepath"
+ "github.com/valkey-io/valkey-go"
+
"github.com/testcontainers/testcontainers-go"
- "github.com/testcontainers/testcontainers-go/modules/valkey"
+ tcvalkey "github.com/testcontainers/testcontainers-go/modules/valkey"
)
func ExampleRun() {
// runValkeyContainer {
ctx := context.Background()
- valkeyContainer, err := valkey.Run(ctx,
+ valkeyContainer, err := tcvalkey.Run(ctx,
"valkey/valkey:7.2.5",
- valkey.WithSnapshotting(10, 1),
- valkey.WithLogLevel(valkey.LogLevelVerbose),
- valkey.WithConfigFile(filepath.Join("testdata", "valkey7.conf")),
+ tcvalkey.WithSnapshotting(10, 1),
+ tcvalkey.WithLogLevel(tcvalkey.LogLevelVerbose),
+ tcvalkey.WithConfigFile(filepath.Join("testdata", "valkey7.conf")),
)
defer func() {
if err := testcontainers.TerminateContainer(valkeyContainer); err != nil {
@@ -42,3 +44,63 @@ func ExampleRun() {
// Output:
// true
}
+
+func ExampleRun_withTLS() {
+ ctx := context.Background()
+
+ valkeyContainer, err := tcvalkey.Run(ctx,
+ "valkey/valkey:7.2.5",
+ tcvalkey.WithSnapshotting(10, 1),
+ tcvalkey.WithLogLevel(tcvalkey.LogLevelVerbose),
+ tcvalkey.WithTLS(),
+ )
+ defer func() {
+ if err := testcontainers.TerminateContainer(valkeyContainer); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ if valkeyContainer.TLSConfig() == nil {
+ log.Println("TLS is not enabled")
+ return
+ }
+
+ uri, err := valkeyContainer.ConnectionString(ctx)
+ if err != nil {
+ log.Printf("failed to get connection string: %s", err)
+ return
+ }
+
+ // You will likely want to wrap your Valkey package of choice in an
+ // interface to aid in unit testing and limit lock-in throughout your
+ // codebase but that's out of scope for this example
+ options, err := valkey.ParseURL(uri)
+ if err != nil {
+ log.Printf("failed to parse connection string: %s", err)
+ return
+ }
+
+ options.TLSConfig = valkeyContainer.TLSConfig()
+
+ client, err := valkey.NewClient(options)
+ if err != nil {
+ log.Printf("failed to create valkey client: %s", err)
+ return
+ }
+ defer func() {
+ err := flushValkey(ctx, client)
+ if err != nil {
+ log.Printf("failed to flush valkey: %s", err)
+ }
+ }()
+
+ resp := client.Do(ctx, client.B().Ping().Build().Pin())
+ fmt.Println(resp.String())
+
+ // Output:
+ // {"Message":{"Value":"PONG","Type":"simple string"}}
+}
diff --git a/modules/valkey/go.mod b/modules/valkey/go.mod
index 46316ec2c7..ac739c0b2f 100644
--- a/modules/valkey/go.mod
+++ b/modules/valkey/go.mod
@@ -6,9 +6,10 @@ toolchain go1.23.6
require (
github.com/google/uuid v1.6.0
+ github.com/mdelapenya/tlscert v0.2.0
github.com/stretchr/testify v1.10.0
github.com/testcontainers/testcontainers-go v0.37.0
- github.com/valkey-io/valkey-go v1.0.41
+ github.com/valkey-io/valkey-go v1.0.59
)
replace github.com/testcontainers/testcontainers-go => ../..
@@ -60,7 +61,6 @@ require (
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
- golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
diff --git a/modules/valkey/go.sum b/modules/valkey/go.sum
index e01fdc3ed5..dcd01783ad 100644
--- a/modules/valkey/go.sum
+++ b/modules/valkey/go.sum
@@ -60,6 +60,8 @@ github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tA
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
+github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
@@ -74,8 +76,8 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
-github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
-github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
+github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
+github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -102,8 +104,8 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
-github.com/valkey-io/valkey-go v1.0.41 h1:pWgh9MP24Vl0ANZ0KxEMwB/LHvTUKwlm2SPuWIrSlFw=
-github.com/valkey-io/valkey-go v1.0.41/go.mod h1:LXqAbjygRuA1YRocojTslAGx2dQB4p8feaseGviWka4=
+github.com/valkey-io/valkey-go v1.0.59 h1:W67Z0UY+Qqk3k8NKkFCFlM3X4yQUniixl7dSJAch2Qo=
+github.com/valkey-io/valkey-go v1.0.59/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
diff --git a/modules/valkey/options.go b/modules/valkey/options.go
new file mode 100644
index 0000000000..78f0f7170d
--- /dev/null
+++ b/modules/valkey/options.go
@@ -0,0 +1,82 @@
+package valkey
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net"
+
+ "github.com/mdelapenya/tlscert"
+
+ "github.com/testcontainers/testcontainers-go"
+)
+
+type options struct {
+ tlsEnabled bool
+ tlsConfig *tls.Config
+}
+
+// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface.
+var _ testcontainers.ContainerCustomizer = (Option)(nil)
+
+// Option is an option for the Redpanda container.
+type Option func(*options) error
+
+// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
+func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
+ // NOOP to satisfy interface.
+ return nil
+}
+
+// WithTLS sets the TLS configuration for the redis container, setting
+// the 6380/tcp port to listen on for TLS connections and using a secure URL (rediss://).
+func WithTLS() Option {
+ return func(o *options) error {
+ o.tlsEnabled = true
+ return nil
+ }
+}
+
+// createTLSCerts creates a CA certificate, a client certificate and a Valkey certificate.
+func createTLSCerts() (caCert *tlscert.Certificate, clientCert *tlscert.Certificate, valkeyCert *tlscert.Certificate, err error) {
+ // ips is the extra list of IPs to include in the certificates.
+ // It's used to allow the client and Valkey certificates to be used in the same host
+ // when the tests are run using a remote docker daemon.
+ ips := []net.IP{net.ParseIP("127.0.0.1")}
+
+ // Generate CA certificate
+ caCert, err = tlscert.SelfSignedFromRequestE(tlscert.Request{
+ Host: "localhost",
+ IPAddresses: ips,
+ Name: "ca",
+ SubjectCommonName: "ca",
+ IsCA: true,
+ })
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("generate CA certificate: %w", err)
+ }
+
+ // Generate client certificate
+ clientCert, err = tlscert.SelfSignedFromRequestE(tlscert.Request{
+ Host: "localhost",
+ Name: "Redis Client",
+ SubjectCommonName: "localhost",
+ IPAddresses: ips,
+ Parent: caCert,
+ })
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("generate client certificate: %w", err)
+ }
+
+ // Generate Valkey certificate
+ valkeyCert, err = tlscert.SelfSignedFromRequestE(tlscert.Request{
+ Host: "localhost",
+ IPAddresses: ips,
+ Name: "Valkey Server",
+ Parent: caCert,
+ })
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("generate Valkey certificate: %w", err)
+ }
+
+ return caCert, clientCert, valkeyCert, nil
+}
diff --git a/modules/valkey/valkey.go b/modules/valkey/valkey.go
index 26de4458a8..48257c04ff 100644
--- a/modules/valkey/valkey.go
+++ b/modules/valkey/valkey.go
@@ -1,9 +1,13 @@
package valkey
import (
+ "bytes"
"context"
+ "crypto/tls"
"fmt"
"strconv"
+ "strings"
+ "time"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
@@ -12,6 +16,7 @@ import (
// ValkeyContainer represents the Valkey container type used in the module
type ValkeyContainer struct {
testcontainers.Container
+ settings options
}
// valkeyServerProcess is the name of the valkey server process
@@ -20,6 +25,9 @@ const valkeyServerProcess = "valkey-server"
type LogLevel string
const (
+ // valkeyPort is the port for the Valkey connection
+ valkeyPort = "6379/tcp"
+
// LogLevelDebug is the debug log level
LogLevelDebug LogLevel = "debug"
// LogLevelVerbose is the verbose log level
@@ -32,7 +40,7 @@ const (
// ConnectionString returns the connection string for the Valkey container
func (c *ValkeyContainer) ConnectionString(ctx context.Context) (string, error) {
- mappedPort, err := c.MappedPort(ctx, "6379/tcp")
+ mappedPort, err := c.MappedPort(ctx, valkeyPort)
if err != nil {
return "", err
}
@@ -42,10 +50,20 @@ func (c *ValkeyContainer) ConnectionString(ctx context.Context) (string, error)
return "", err
}
- uri := fmt.Sprintf("redis://%s:%s", hostIP, mappedPort.Port())
+ schema := "redis"
+ if c.settings.tlsEnabled {
+ schema = "rediss"
+ }
+
+ uri := fmt.Sprintf("%s://%s:%s", schema, hostIP, mappedPort.Port())
return uri, nil
}
+// TLSConfig returns the TLS configuration for the Valkey container, nil if TLS is not enabled.
+func (c *ValkeyContainer) TLSConfig() *tls.Config {
+ return c.settings.tlsConfig
+}
+
// Deprecated: use Run instead
// RunContainer creates an instance of the Valkey container type
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*ValkeyContainer, error) {
@@ -56,8 +74,7 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*ValkeyContainer, error) {
req := testcontainers.ContainerRequest{
Image: img,
- ExposedPorts: []string{"6379/tcp"},
- WaitingFor: wait.ForListeningPort("6379/tcp"),
+ ExposedPorts: []string{valkeyPort},
}
genericContainerReq := testcontainers.GenericContainerRequest{
@@ -65,7 +82,77 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
Started: true,
}
+ var settings options
for _, opt := range opts {
+ if opt, ok := opt.(Option); ok {
+ if err := opt(&settings); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ tcOpts := []testcontainers.ContainerCustomizer{}
+
+ waitStrategies := []wait.Strategy{
+ wait.ForListeningPort(valkeyPort).WithStartupTimeout(time.Second * 10),
+ wait.ForLog("* Ready to accept connections"),
+ }
+
+ if settings.tlsEnabled {
+ // wait for the TLS port to be available
+ waitStrategies = append(waitStrategies, wait.ForListeningPort(valkeyPort).WithStartupTimeout(time.Second*10))
+
+ // Generate TLS certificates in the fly and add them to the container before it starts.
+ // Update the CMD to use the TLS certificates.
+ caCert, clientCert, serverCert, err := createTLSCerts()
+ if err != nil {
+ return nil, fmt.Errorf("create tls certs: %w", err)
+ }
+
+ // Update the CMD to use the TLS certificates.
+ cmds := []string{
+ "--tls-port", strings.Replace(valkeyPort, "/tcp", "", 1),
+ // Disable the default port, as described in https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/#running-manually
+ "--port", "0",
+ "--tls-cert-file", "/tls/server.crt",
+ "--tls-key-file", "/tls/server.key",
+ "--tls-ca-cert-file", "/tls/ca.crt",
+ "--tls-auth-clients", "yes",
+ }
+
+ tcOpts = append(tcOpts, testcontainers.WithCmdArgs(cmds...)) // Append the default CMD with the TLS certificates.
+ tcOpts = append(tcOpts, testcontainers.WithFiles(
+ testcontainers.ContainerFile{
+ Reader: bytes.NewReader(caCert.Bytes),
+ ContainerFilePath: "/tls/ca.crt",
+ FileMode: 0o644,
+ },
+ testcontainers.ContainerFile{
+ Reader: bytes.NewReader(serverCert.Bytes),
+ ContainerFilePath: "/tls/server.crt",
+ FileMode: 0o644,
+ },
+ testcontainers.ContainerFile{
+ Reader: bytes.NewReader(serverCert.KeyBytes),
+ ContainerFilePath: "/tls/server.key",
+ FileMode: 0o644,
+ }))
+
+ settings.tlsConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ RootCAs: caCert.TLSConfig().RootCAs,
+ Certificates: clientCert.TLSConfig().Certificates,
+ ServerName: "localhost", // Match the server cert's common name
+ }
+ }
+
+ tcOpts = append(tcOpts, testcontainers.WithWaitStrategy(waitStrategies...))
+
+ // Append the customizers passed to the Run function.
+ tcOpts = append(tcOpts, opts...)
+
+ // Apply the testcontainers customizers.
+ for _, opt := range tcOpts {
if err := opt.Customize(&genericContainerReq); err != nil {
return nil, err
}
@@ -74,7 +161,7 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
container, err := testcontainers.GenericContainer(ctx, genericContainerReq)
var c *ValkeyContainer
if container != nil {
- c = &ValkeyContainer{Container: container}
+ c = &ValkeyContainer{Container: container, settings: settings}
}
if err != nil {
diff --git a/modules/valkey/valkey_test.go b/modules/valkey/valkey_test.go
index 44412afa4a..ef8eead8cc 100644
--- a/modules/valkey/valkey_test.go
+++ b/modules/valkey/valkey_test.go
@@ -80,6 +80,26 @@ func TestValkeyWithSnapshotting(t *testing.T) {
assertSetsGets(t, ctx, valkeyContainer, 10)
}
+func TestRedisWithTLS(t *testing.T) {
+ ctx := context.Background()
+
+ t.Run("mtls-disabled", func(t *testing.T) {
+ valkeyContainer, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5", tcvalkey.WithTLS())
+ testcontainers.CleanupContainer(t, valkeyContainer)
+ require.NoError(t, err)
+
+ assertSetsGets(t, ctx, valkeyContainer, 1)
+ })
+
+ t.Run("mtls-enabled", func(t *testing.T) {
+ valkeyContainer, err := tcvalkey.Run(ctx, "valkey/valkey:7.2.5", tcvalkey.WithTLS(), testcontainers.WithCmdArgs("--tls-auth-clients", "no"))
+ testcontainers.CleanupContainer(t, valkeyContainer)
+ require.NoError(t, err)
+
+ assertSetsGets(t, ctx, valkeyContainer, 1)
+ })
+}
+
func assertSetsGets(t *testing.T, ctx context.Context, valkeyContainer *tcvalkey.ValkeyContainer, keyCount int) {
t.Helper()
// connectionString {
@@ -93,6 +113,10 @@ func assertSetsGets(t *testing.T, ctx context.Context, valkeyContainer *tcvalkey
options, err := valkey.ParseURL(uri)
require.NoError(t, err)
+ // tlsConfig {
+ options.TLSConfig = valkeyContainer.TLSConfig()
+ // }
+
client, err := valkey.NewClient(options)
require.NoError(t, err)
defer func(t *testing.T, ctx context.Context, client *valkey.Client) {