Skip to content

feat(cassandra): add ssl option cassandra #3151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from

Conversation

MitulShah1
Copy link
Contributor

@MitulShah1 MitulShah1 commented May 2, 2025

#3124

What does this PR do?

Why is it important?

Related issues

@mdelapenya Can you please review

@MitulShah1 MitulShah1 requested a review from a team as a code owner May 2, 2025 09:36
Copy link

netlify bot commented May 2, 2025

Deploy Preview for testcontainers-go ready!

Name Link
🔨 Latest commit 60622d4
🔍 Latest deploy log https://app.netlify.com/sites/testcontainers-go/deploys/681d95cdea7398000823931c
😎 Deploy Preview https://deploy-preview-3151--testcontainers-go.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@MitulShah1 MitulShah1 changed the title Add ssl option cassandra feat(cassandra): Add ssl option cassandra May 2, 2025
@mdelapenya mdelapenya changed the title feat(cassandra): Add ssl option cassandra feat(cassandra): add ssl option cassandra May 3, 2025
Copy link
Member

@mdelapenya mdelapenya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, although I added some comments about using the same approach as in the Redis module. Could you please take a look?

Cheers!

),
}

c := &CassandraContainer{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: keep this initialisation next to its usage. Seems moved here for no reasons

},
WaitingFor: wait.ForAll(
wait.ForListeningPort(port),
wait.ForExec([]string{"cqlsh", "-e", "SELECT bootstrapped FROM system.local"}).WithResponseMatcher(func(body io.Reader) bool {
data, _ := io.ReadAll(body)
return strings.Contains(string(data), "COMPLETED")
}),
}).WithStartupTimeout(2*time.Minute),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: is 2 minutes too long? Can we use a shorter duration?

Comment on lines 160 to 161
"CASSANDRA_SKIP_WAIT_FOR_GOSSIP": "1",
"CASSANDRA_START_NATIVE_TRANSPORT": "true",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: are these new env vars related to the SSL options? If not, I'd separate the changes into a new PR, if possible

}

// WithTLS is an alias for WithSSL for user convenience
func WithTLS(opts SSLOptions) testcontainers.CustomizeRequestOption {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why adding two flavours of the same option? I'd just choose one to avoid creating confusion on users

}

// Mark that SSL is enabled for later use
req.Labels = mergeMap(req.Labels, map[string]string{"testcontainers.cassandra.ssl": "true"})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I guess this is needed to notify the Run function about the SSL addition, right? I'd choose a different pattern for this, creating an options type as in the redis module. Then, you can simply generate the SSL certs on-the-fly as in that module.

@MitulShah1 MitulShah1 requested a review from mdelapenya May 7, 2025 07:05
@MitulShah1
Copy link
Contributor Author

Looking good, although I added some comments about using the same approach as in the Redis module. Could you please take a look?

Cheers!

I made changes, can you please check again?

Copy link
Contributor

@stevenh stevenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick updates, here's some suggestion which focus on simplification and reducing API surface area.

Comment on lines 15 to 16
IsTLSEnabled bool
TLSConfig *tls.Config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: make these private so there's less API surface area allowing for refactoring without causing breaking changes. Given it's typed nature we can skip the Is prefix for boolean flag too.

Suggested change
IsTLSEnabled bool
TLSConfig *tls.Config
tlsEnabled bool
tlsConfig *tls.Config

More fixes to align with this change obviously, so best to just rename in your editor.

Comment on lines +44 to +45
// GenerateJKSKeystore generates a JKS keystore with a self-signed cert using keytool, and extracts the public cert for Go client trust.
func GenerateJKSKeystore() (keystorePath, certPath string, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: make private, again less API surface area to maintain. Also needs to use go not an external tool which might not be present on all platforms. See other modules on how they generate keys, hopefully that's compatible with cassandra.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevenh making this function private, i need to change in options_test.go, its ok?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either that or make a seperate test file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options_test.go is created by me only but i cannot access that function as testcases are depend on it, do you want me to make this function private and remove options_test.go?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, having integration and unit tests are both good. In go its not a problem to use the main package for tests, as long as you're conscious about the seperation.

However can we should first see if we can eliminate this external dependency. You can see a similar example here: https://github.com/testcontainers/testcontainers-go/blob/main/modules/redis/options.go#L86 which uses https://pkg.go.dev/github.com/mdelapenya/tlscert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you're suggesting we either minimize SSL-related dependencies or simply support SSL with minimal setup. I always try to follow the existing repository codebase and coding style as much possible.

Initially, I tried using only tlscert, but it has some limitations. If we choose to use tlscert, we can eliminate the keytool dependency, but we'd then need to rely on openssl.

So, we have two options:

Use keytool as we currently do.

Follow an approach similar to Cassandra's Go driver, where certificates are added in a testdata folder and used for SSL testing, as seen in the official Go driver.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tlscert would be the preferred option as it's what a lot of the other modules use.

If it needs changes that should be quick and easy as that module is written by @mdelapenya one of this repositories maintainers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know but it can handle this cassandra cert generation? @mdelapenya ? i already tried that let me know if ok to use openssl then i will make change to tlscert.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, let's use it for consistency. If there is a missing feature in that library, let's open an issue in there and work on fix and release it

Cheers!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Quick Look and it seems like it can use standard pen formatted certs so it should just work

@MitulShah1 MitulShah1 requested a review from stevenh May 7, 2025 10:36
Copy link
Contributor

@stevenh stevenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few bugs introduced here I think, the big question is eliminating the use of external dependency keytool

Comment on lines +44 to +45
// GenerateJKSKeystore generates a JKS keystore with a self-signed cert using keytool, and extracts the public cert for Go client trust.
func GenerateJKSKeystore() (keystorePath, certPath string, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you, having integration and unit tests are both good. In go its not a problem to use the main package for tests, as long as you're conscious about the seperation.

However can we should first see if we can eliminate this external dependency. You can see a similar example here: https://github.com/testcontainers/testcontainers-go/blob/main/modules/redis/options.go#L86 which uses https://pkg.go.dev/github.com/mdelapenya/tlscert

@@ -14,28 +16,35 @@ import (
)

const (
port = nat.Port("9042/tcp")
port = nat.Port("9042/tcp")
securePort = nat.Port("9142/tcp") // Common port for SSL/TLS connections
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: do we need to two ports or can it just be one which is either SSL or not?

Comment on lines +118 to +124
waitStrategies := []wait.Strategy{
wait.ForListeningPort(port),
wait.ForExec([]string{"cqlsh", "-e", "SELECT bootstrapped FROM system.local"}).WithResponseMatcher(func(body io.Reader) bool {
data, _ := io.ReadAll(body)
return strings.Contains(string(data), "COMPLETED")
}).WithStartupTimeout(1 * time.Minute),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdelapenya looks like we have an issue with WithWaitStrategy, it sets vs appending to WaitFor preventing easy initialisation here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I think the use case here would be appending. I can submit a fix now


// Apply wait strategy using the correct method
if err := testcontainers.WithWaitStrategy(wait.ForAll(waitStrategies...)).Customize(&genericContainerReq); err != nil {
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: wrap the error here

Suggested change
return nil, err
return nil, fmt.Errorf("with wait strategy: %w", err)

Comment on lines +149 to +150
// TLSConfig returns the TLS configuration for the Cassandra container, nil if TLS is not enabled.
func (c *CassandraContainer) TLSConfig() *tls.Config {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdelapenya quite a common pattern in modules, we should consider returning an error instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the pointer to the config and the error, right? I think we changed the tlscert library precisely for that, so good to me to change it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, these container methods, have them return an error if the config is nil vs just returning nil as the caller should know if they enabled TLS or not.

func (c *CassandraContainer) TLSConfig() (*tls.Config, error) {
	if c.settings.tlsConfig == nil {
		return nil, errors.New("tls not enabled")
	}
	return c.settings.tlsConfig.Config, nil
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, the recently added methods for Redis and Valkey. For Valkey, we are in the same release cycle, we can change it without BC, for Redis, it's a BC

Comment on lines +31 to +32
func (o Option) Customize(req *testcontainers.GenericContainerRequest) error {
return o(req, &Options{})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug: while this will run there options won't be accessible, so should still be just a noop.

We're looking to refactor how this works in a future version to avoid this, but that will be a bit.

}

genericContainerReq := testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
}

var settings Options
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug: settings is never set, you still need to check if you have an Option in the for loop and process it to ensure it set correctly.

If your tests are passing they shouldn't so might need a fix there too.

Comment on lines +15 to +16
// TLSConfig represents the TLS configuration for Cassandra
type TLSConfig struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: are the paths needed? If not we can just return *tls.Config. I suspect the answer is dependent on eliminating the need for keytool

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants