Skip to content

Commit 75fb701

Browse files
MitulShah1Mitul Shahmdelapenya
authored
feat(cassandra): add ssl option cassandra (#3151)
* Added SSL Option * Passed tests * Implemented SSL Support * Implemented SSL Support * Implemented SSL Support * Revert go.mod and go.sum * Revert go.mod and go.sum * Golint fix * Golint fix * Govet fix * reduce wait time, revert container place and removed WithTLS options * Added SSL With option * Added SSL With option * Devide Run function n sub function to reduce complexity * make Options private * removed setupTls function and moved to WithSSL Options * Fix Sec warning InsecureSkipVerify * Fix lint * added cassandra ssl option * removed InsecureSkipVerify variable * added mising docs and modify TlsConfig method * doc changes --------- Co-authored-by: Mitul Shah <mitul.shah@zoodmall.com> Co-authored-by: Manuel de la Peña <mdelapenya@gmail.com>
1 parent c75e56c commit 75fb701

File tree

9 files changed

+530
-2
lines changed

9 files changed

+530
-2
lines changed

docs/modules/cassandra.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ In the case you have a custom config file for Cassandra, it's possible to copy t
7070
!!!warning
7171
You should provide a valid Cassandra configuration file, otherwise the container will fail to start.
7272

73+
#### WithTLS
74+
75+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
76+
77+
If you need to enable TLS/SSL encryption for client connections, you can use the `cassandra.WithTLS()` option.
78+
79+
When enabled, the container will:
80+
- Generate self-signed certificates automatically
81+
- Configure Cassandra to use client encryption
82+
- Expose the SSL port (9142)
83+
84+
Use the `TLSConfig()` method on the returned container to get the `*tls.Config` for client connections. The method returns an error if TLS was not enabled via `WithTLS()`.
85+
86+
<!--codeinclude-->
87+
[Creating a Cassandra container with TLS](../../modules/cassandra/examples_test.go) inside_block:runCassandraContainerWithTLS
88+
[Getting TLS connection configuration](../../modules/cassandra/examples_test.go) inside_block:getTLSConnectionHost
89+
<!--/codeinclude-->
90+
7391
{% include "../features/common_functional_options_list.md" %}
7492

7593
### Container Methods
@@ -85,3 +103,13 @@ This method returns the host and port of the Cassandra container, using the defa
85103
<!--codeinclude-->
86104
[Get connection host](../../modules/cassandra/cassandra_test.go) inside_block:connectionHost
87105
<!--/codeinclude-->
106+
107+
#### TLSConfig
108+
109+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
110+
111+
This method returns the TLS configuration for secure connections to the Cassandra container. It can only be used when the container was created with the `WithTLS()` option. Returns an error if TLS was not enabled.
112+
113+
<!--codeinclude-->
114+
[Get TLS config](../../modules/cassandra/cassandra_test.go) inside_block:tlsConfig
115+
<!--/codeinclude-->

modules/cassandra/cassandra.go

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package cassandra
22

33
import (
4+
"bytes"
45
"context"
6+
"crypto/tls"
7+
_ "embed"
8+
"errors"
59
"fmt"
610
"io"
711
"path/filepath"
@@ -12,20 +16,37 @@ import (
1216
)
1317

1418
const (
15-
port = "9042/tcp"
19+
port = "9042/tcp"
20+
sslPort = "9142/tcp"
1621
)
1722

23+
//go:embed testdata/cassandra-ssl.yaml
24+
var sslConfigYAML []byte
25+
1826
// CassandraContainer represents the Cassandra container type used in the module
1927
type CassandraContainer struct {
2028
testcontainers.Container
29+
settings options
2130
}
2231

2332
// ConnectionHost returns the host and port of the cassandra container, using the default, native 9042 port, and
2433
// obtaining the host and exposed port from the container
2534
func (c *CassandraContainer) ConnectionHost(ctx context.Context) (string, error) {
35+
if c.settings.tlsEnabled {
36+
return c.PortEndpoint(ctx, sslPort, "")
37+
}
2638
return c.PortEndpoint(ctx, port, "")
2739
}
2840

41+
// TLSConfig returns the TLS configuration for secure client connections.
42+
// Returns an error if TLS is not enabled on the container.
43+
func (c *CassandraContainer) TLSConfig() (*tls.Config, error) {
44+
if !c.settings.tlsEnabled {
45+
return nil, errors.New("TLS is not enabled on this container")
46+
}
47+
return c.settings.tlsConfig, nil
48+
}
49+
2950
// WithConfigFile sets the YAML config file to be used for the cassandra container
3051
// It will also set the "configFile" parameter to the path of the config file
3152
// as a command line argument to the container.
@@ -71,6 +92,16 @@ func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomize
7192

7293
// Run creates an instance of the Cassandra container type
7394
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*CassandraContainer, error) {
95+
// Process custom options to extract settings
96+
settings := defaultOptions()
97+
for _, opt := range opts {
98+
if opt, ok := opt.(Option); ok {
99+
if err := opt(&settings); err != nil {
100+
return nil, fmt.Errorf("apply option: %w", err)
101+
}
102+
}
103+
}
104+
74105
moduleOpts := []testcontainers.ContainerCustomizer{
75106
testcontainers.WithExposedPorts(port),
76107
testcontainers.WithEnv(map[string]string{
@@ -90,12 +121,55 @@ func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustom
90121
),
91122
}
92123

124+
// Configure TLS if enabled
125+
if settings.tlsEnabled {
126+
tlsCerts, err := createTLSCerts()
127+
if err != nil {
128+
return nil, fmt.Errorf("create TLS certs: %w", err)
129+
}
130+
131+
// Store the TLS config for client connections
132+
settings.tlsConfig = tlsCerts.TLSConfig
133+
134+
// Add SSL port and configure networking for SSL
135+
// We need CASSANDRA_BROADCAST_RPC_ADDRESS when CASSANDRA_RPC_ADDRESS is 0.0.0.0
136+
moduleOpts = append(moduleOpts,
137+
testcontainers.WithExposedPorts(sslPort),
138+
testcontainers.WithEnv(map[string]string{
139+
"CASSANDRA_BROADCAST_RPC_ADDRESS": "127.0.0.1",
140+
}),
141+
)
142+
143+
// Mount the SSL config and keystore
144+
moduleOpts = append(moduleOpts,
145+
testcontainers.WithFiles(
146+
testcontainers.ContainerFile{
147+
Reader: bytes.NewReader(sslConfigYAML),
148+
ContainerFilePath: "/etc/cassandra/cassandra.yaml",
149+
FileMode: 0o644,
150+
},
151+
testcontainers.ContainerFile{
152+
Reader: bytes.NewReader(tlsCerts.KeystoreBytes),
153+
ContainerFilePath: "/etc/cassandra/certs/keystore.p12",
154+
FileMode: 0o644,
155+
},
156+
),
157+
)
158+
159+
// Update wait strategy to also wait for SSL port
160+
moduleOpts = append(moduleOpts,
161+
testcontainers.WithAdditionalWaitStrategy(
162+
wait.ForListeningPort(sslPort),
163+
),
164+
)
165+
}
166+
93167
moduleOpts = append(moduleOpts, opts...)
94168

95169
ctr, err := testcontainers.Run(ctx, img, moduleOpts...)
96170
var c *CassandraContainer
97171
if ctr != nil {
98-
c = &CassandraContainer{Container: ctr}
172+
c = &CassandraContainer{Container: ctr, settings: settings}
99173
}
100174

101175
if err != nil {

modules/cassandra/cassandra_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,57 @@ func TestCassandraWithInitScripts(t *testing.T) {
117117
require.Equal(t, Test{ID: 1, Name: "NAME"}, test)
118118
})
119119
}
120+
121+
func TestCassandraWithTLS(t *testing.T) {
122+
ctx := context.Background()
123+
124+
// withTLS {
125+
ctr, err := cassandra.Run(ctx, "cassandra:4.1.3", cassandra.WithTLS())
126+
// }
127+
testcontainers.CleanupContainer(t, ctr)
128+
require.NoError(t, err)
129+
130+
// tlsConfig {
131+
// Verify TLS config is available
132+
tlsConfig, err := ctr.TLSConfig()
133+
require.NoError(t, err)
134+
// }
135+
require.NotNil(t, tlsConfig, "TLSConfig should not be nil when TLS is enabled")
136+
137+
// Get SSL connection host
138+
// connectionHostSSL {
139+
connectionHost, err := ctr.ConnectionHost(ctx)
140+
// }
141+
require.NoError(t, err)
142+
143+
// Create cluster with TLS
144+
cluster := gocql.NewCluster(connectionHost)
145+
cluster.SslOpts = &gocql.SslOptions{
146+
Config: tlsConfig,
147+
}
148+
149+
session, err := cluster.CreateSession()
150+
require.NoError(t, err)
151+
defer session.Close()
152+
153+
// Verify connection works by querying system table
154+
var clusterName string
155+
err = session.Query("SELECT cluster_name FROM system.local").Scan(&clusterName)
156+
require.NoError(t, err)
157+
require.Equal(t, "Test Cluster", clusterName)
158+
159+
// Test data operations over TLS
160+
err = session.Query("CREATE KEYSPACE tls_test_keyspace WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1}").Exec()
161+
require.NoError(t, err)
162+
163+
err = session.Query("CREATE TABLE tls_test_keyspace.test_table (id int PRIMARY KEY, name text)").Exec()
164+
require.NoError(t, err)
165+
166+
err = session.Query("INSERT INTO tls_test_keyspace.test_table (id, name) VALUES (1, 'TLS_TEST')").Exec()
167+
require.NoError(t, err)
168+
169+
var test Test
170+
err = session.Query("SELECT id, name FROM tls_test_keyspace.test_table WHERE id=1").Scan(&test.ID, &test.Name)
171+
require.NoError(t, err)
172+
require.Equal(t, Test{ID: 1, Name: "TLS_TEST"}, test)
173+
}

modules/cassandra/examples_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,71 @@ func ExampleRun() {
6767
// true
6868
// 4.1.3
6969
}
70+
71+
func ExampleRun_withTLS() {
72+
// runCassandraContainerWithTLS {
73+
ctx := context.Background()
74+
75+
cassandraContainer, err := cassandra.Run(ctx,
76+
"cassandra:4.1.3",
77+
cassandra.WithTLS(),
78+
)
79+
defer func() {
80+
if err := testcontainers.TerminateContainer(cassandraContainer); err != nil {
81+
log.Printf("failed to terminate container: %s", err)
82+
}
83+
}()
84+
if err != nil {
85+
log.Printf("failed to start container: %s", err)
86+
return
87+
}
88+
// }
89+
90+
state, err := cassandraContainer.State(ctx)
91+
if err != nil {
92+
log.Printf("failed to get container state: %s", err)
93+
return
94+
}
95+
96+
fmt.Println(state.Running)
97+
98+
// getTLSConnectionHost {
99+
connectionHost, err := cassandraContainer.ConnectionHost(ctx)
100+
if err != nil {
101+
log.Printf("failed to get SSL connection host: %s", err)
102+
return
103+
}
104+
105+
// Get TLS config for secure connection
106+
tlsConfig, err := cassandraContainer.TLSConfig()
107+
if err != nil {
108+
log.Printf("failed to get TLS config: %s", err)
109+
return
110+
}
111+
// }
112+
113+
cluster := gocql.NewCluster(connectionHost)
114+
cluster.SslOpts = &gocql.SslOptions{
115+
Config: tlsConfig,
116+
}
117+
118+
session, err := cluster.CreateSession()
119+
if err != nil {
120+
log.Printf("failed to create session: %s", err)
121+
return
122+
}
123+
defer session.Close()
124+
125+
var version string
126+
err = session.Query("SELECT release_version FROM system.local").Scan(&version)
127+
if err != nil {
128+
log.Printf("failed to query: %s", err)
129+
return
130+
}
131+
132+
fmt.Println(version)
133+
134+
// Output:
135+
// true
136+
// 4.1.3
137+
}

modules/cassandra/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ toolchain go1.24.7
66

77
require (
88
github.com/gocql/gocql v1.6.0
9+
github.com/mdelapenya/tlscert v0.2.0
910
github.com/stretchr/testify v1.11.1
1011
github.com/testcontainers/testcontainers-go v0.40.0
12+
software.sslmate.com/src/go-pkcs12 v0.6.0
1113
)
1214

1315
require (

modules/cassandra/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
7272
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
7373
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
7474
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
75+
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
76+
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
7577
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
7678
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
7779
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
@@ -174,3 +176,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
174176
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
175177
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
176178
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
179+
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
180+
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

modules/cassandra/options.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package cassandra
2+
3+
import (
4+
"crypto/tls"
5+
6+
"github.com/testcontainers/testcontainers-go"
7+
)
8+
9+
// options holds the configuration settings for the Cassandra container.
10+
type options struct {
11+
tlsEnabled bool
12+
tlsConfig *tls.Config
13+
}
14+
15+
// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface.
16+
var _ testcontainers.ContainerCustomizer = (Option)(nil)
17+
18+
// Option is an option for the Cassandra container.
19+
type Option func(*options) error
20+
21+
// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
22+
func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
23+
// NOOP to satisfy interface.
24+
return nil
25+
}
26+
27+
// defaultOptions returns the default options for the Cassandra container.
28+
func defaultOptions() options {
29+
return options{
30+
tlsEnabled: false,
31+
tlsConfig: nil,
32+
}
33+
}
34+
35+
// WithTLS enables TLS/SSL on the Cassandra container.
36+
// When enabled, the container will:
37+
// - Generate self-signed certificates
38+
// - Configure Cassandra to use client encryption
39+
// - Expose the SSL port (9142)
40+
//
41+
// Use TLSConfig() on the returned container to get the *tls.Config for client connections.
42+
func WithTLS() Option {
43+
return func(o *options) error {
44+
o.tlsEnabled = true
45+
return nil
46+
}
47+
}

0 commit comments

Comments
 (0)