Skip to content

Commit

Permalink
feat: Add Cassandra module (testcontainers#1726)
Browse files Browse the repository at this point in the history
* Add cassandra module

* Add new line before testcontainers imports

Also revert typo fix in postgres document while its not scope of this pr

* Refactor the way for running init scripts

* fix: typo

---------

Co-authored-by: Manuel de la Peña <[email protected]>
  • Loading branch information
anilsenay and mdelapenya authored Oct 11, 2023
1 parent d2fe780 commit 1cb3391
Show file tree
Hide file tree
Showing 16 changed files with 841 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ updates:
day: sunday
open-pull-requests-limit: 3
rebase-strategy: disabled
- package-ecosystem: gomod
directory: /modules/cassandra
schedule:
interval: monthly
day: sunday
open-pull-requests-limit: 3
rebase-strategy: disabled
- package-ecosystem: gomod
directory: /modules/clickhouse
schedule:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ jobs:
matrix:
go-version: [1.20.x, 1.x]
platform: [ubuntu-latest, macos-latest]
module: [artemis, clickhouse, compose, couchbase, elasticsearch, gcloud, k3s, k6, kafka, localstack, mariadb, mongodb, mysql, nats, neo4j, postgres, pulsar, rabbitmq, redis, redpanda, vault]
module: [artemis, cassandra, clickhouse, compose, couchbase, elasticsearch, gcloud, k3s, k6, kafka, localstack, mariadb, mongodb, mysql, nats, neo4j, postgres, pulsar, rabbitmq, redis, redpanda, vault]

uses: ./.github/workflows/ci-test-go.yml
with:
go-version: ${{ matrix.go-version }}
Expand Down
4 changes: 4 additions & 0 deletions .vscode/.testcontainers-go.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
"name": "module / artemis",
"path": "../modules/artemis"
},
{
"name": "module / cassandra",
"path": "../modules/cassandra"
},
{
"name": "module / clickhouse",
"path": "../modules/clickhouse"
Expand Down
73 changes: 73 additions & 0 deletions docs/modules/cassandra.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Cassandra

Not available until the next release of testcontainers-go <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>

## Introduction

The Testcontainers module for Cassandra.

## Adding this module to your project dependencies

Please run the following command to add the Cassandra module to your Go dependencies:

```
go get github.com/testcontainers/testcontainers-go/modules/cassandra
```

## Usage example

<!--codeinclude-->
[Creating a Cassandra container](../../modules/cassandra/examples_test.go) inside_block:runCassandraContainer
<!--/codeinclude-->

## Module reference

The Cassandra module exposes one entrypoint function to create the Cassandra container, and this function receives two parameters:

```golang
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*CassandraContainer, error)
```

- `context.Context`, the Go context.
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.

### Container Options

When starting the Cassandra container, you can pass options in a variadic way to configure it.

#### Image

If you need to set a different Cassandra Docker image, you can use `testcontainers.WithImage` with a valid Docker image
for Cassandra. E.g. `testcontainers.WithImage("cassandra:4.1.3")`.

{% include "../features/common_functional_options.md" %}

#### Init Scripts

If you would like to do additional initialization in the Cassandra container, add one or more `*.cql` or `*.sh` scripts to the container request with the `WithInitScripts` function.
Those files will be copied after the container is created but before it's started under root directory.
An example of a `*.sh` script that creates a keyspace and table is shown below:
<!--codeinclude-->
[Init script content](../../modules/cassandra/testdata/init.sh)
<!--/codeinclude-->
#### Database configuration
In the case you have a custom config file for Cassandra, it's possible to copy that file into the container before it's started, using the `WithConfigFile(cfgPath string)` function.
!!!warning
You should provide a valid Cassandra configuration file, otherwise the container will fail to start.
### Container Methods
The Cassandra container exposes the following methods:
#### ConnectionHost
This method returns the host and port of the Cassandra container, using the default, `9042/tcp` port. E.g. `localhost:9042`
<!--codeinclude-->
[Get connection host](../../modules/cassandra/cassandra_test.go) inside_block:connectionHost
<!--/codeinclude-->
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ nav:
- Modules:
- modules/index.md
- modules/artemis.md
- modules/cassandra.md
- modules/clickhouse.md
- modules/couchbase.md
- modules/elasticsearch.md
Expand Down
5 changes: 5 additions & 0 deletions modules/cassandra/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
include ../../commons-test.mk

.PHONY: test
test:
$(MAKE) test-cassandra
109 changes: 109 additions & 0 deletions modules/cassandra/cassandra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cassandra

import (
"context"
"io"
"path/filepath"
"strings"

"github.com/docker/go-connections/nat"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

const (
port = nat.Port("9042/tcp")
)

// CassandraContainer represents the Cassandra container type used in the module
type CassandraContainer struct {
testcontainers.Container
}

// ConnectionHost returns the host and port of the cassandra container, using the default, native 9000 port, and
// obtaining the host and exposed port from the container
func (c *CassandraContainer) ConnectionHost(ctx context.Context) (string, error) {
host, err := c.Host(ctx)
if err != nil {
return "", err
}

port, err := c.MappedPort(ctx, port)
if err != nil {
return "", err
}

return host + ":" + port.Port(), nil
}

// WithConfigFile sets the YAML config file to be used for the cassandra container
// It will also set the "configFile" parameter to the path of the config file
// as a command line argument to the container.
func WithConfigFile(configFile string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
cf := testcontainers.ContainerFile{
HostFilePath: configFile,
ContainerFilePath: "/etc/cassandra/cassandra.yaml",
FileMode: 0o755,
}
req.Files = append(req.Files, cf)
}
}

// WithInitScripts sets the init cassandra queries to be run when the container starts
func WithInitScripts(scripts ...string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) {
var initScripts []testcontainers.ContainerFile
for _, script := range scripts {
cf := testcontainers.ContainerFile{
HostFilePath: script,
ContainerFilePath: "/" + filepath.Base(script),
FileMode: 0o755,
}
initScripts = append(initScripts, cf)

testcontainers.WithStartupCommand(initScript{File: cf.ContainerFilePath})(req)
}
req.Files = append(req.Files, initScripts...)
}
}

// RunContainer creates an instance of the Cassandra container type
func RunContainer(ctx context.Context, opts ...testcontainers.ContainerCustomizer) (*CassandraContainer, error) {
req := testcontainers.ContainerRequest{
Image: "cassandra:4.1.3",
ExposedPorts: []string{string(port)},
Env: map[string]string{
"CASSANDRA_SNITCH": "GossipingPropertyFileSnitch",
"JVM_OPTS": "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0",
"HEAP_NEWSIZE": "128M",
"MAX_HEAP_SIZE": "1024M",
"CASSANDRA_ENDPOINT_SNITCH": "GossipingPropertyFileSnitch",
"CASSANDRA_DC": "datacenter1",
},
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")
}),
),
}

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

for _, opt := range opts {
opt.Customize(&genericContainerReq)
}

container, err := testcontainers.GenericContainer(ctx, genericContainerReq)
if err != nil {
return nil, err
}

return &CassandraContainer{Container: container}, nil
}
148 changes: 148 additions & 0 deletions modules/cassandra/cassandra_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package cassandra

import (
"context"
"path/filepath"
"testing"

"github.com/gocql/gocql"
"github.com/stretchr/testify/assert"
)

type Test struct {
Id uint64
Name string
}

func TestCassandra(t *testing.T) {
ctx := context.Background()

container, err := RunContainer(ctx)
if err != nil {
t.Fatal(err)
}

// Clean up the container after the test is complete
t.Cleanup(func() {
assert.NoError(t, container.Terminate(ctx))
})

// connectionString {
connectionHost, err := container.ConnectionHost(ctx)
// }
assert.NoError(t, err)

cluster := gocql.NewCluster(connectionHost)
session, err := cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()

// perform assertions
err = session.Query("CREATE KEYSPACE test_keyspace WITH REPLICATION = {'class' : 'SimpleStrategy', 'replication_factor' : 1}").Exec()
assert.NoError(t, err)
err = session.Query("CREATE TABLE test_keyspace.test_table (id int PRIMARY KEY, name text)").Exec()
assert.NoError(t, err)

err = session.Query("INSERT INTO test_keyspace.test_table (id, name) VALUES (1, 'NAME')").Exec()
assert.NoError(t, err)

var test Test
err = session.Query("SELECT id, name FROM test_keyspace.test_table WHERE id=1").Scan(&test.Id, &test.Name)
assert.NoError(t, err)
assert.Equal(t, Test{Id: 1, Name: "NAME"}, test)
}

func TestCassandraWithConfigFile(t *testing.T) {
ctx := context.Background()

container, err := RunContainer(ctx, WithConfigFile(filepath.Join("testdata", "config.yaml")))
if err != nil {
t.Fatal(err)
}

// Clean up the container after the test is complete
t.Cleanup(func() {
assert.NoError(t, container.Terminate(ctx))
})

connectionHost, err := container.ConnectionHost(ctx)
assert.NoError(t, err)

cluster := gocql.NewCluster(connectionHost)
session, err := cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()

var result string
err = session.Query("SELECT cluster_name FROM system.local").Scan(&result)
assert.NoError(t, err)
assert.Equal(t, "My Cluster", result)
}

func TestCassandraWithInitScripts(t *testing.T) {
t.Run("with init cql script", func(t *testing.T) {
ctx := context.Background()

// withInitScripts {
container, err := RunContainer(ctx, WithInitScripts(filepath.Join("testdata", "init.cql")))
// }
if err != nil {
t.Fatal(err)
}

// Clean up the container after the test is complete
t.Cleanup(func() {
assert.NoError(t, container.Terminate(ctx))
})

// connectionHost {
connectionHost, err := container.ConnectionHost(ctx)
// }
assert.NoError(t, err)

cluster := gocql.NewCluster(connectionHost)
session, err := cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()

var test Test
err = session.Query("SELECT id, name FROM test_keyspace.test_table WHERE id=1").Scan(&test.Id, &test.Name)
assert.NoError(t, err)
assert.Equal(t, Test{Id: 1, Name: "NAME"}, test)
})

t.Run("with init bash script", func(t *testing.T) {
ctx := context.Background()

container, err := RunContainer(ctx, WithInitScripts(filepath.Join("testdata", "init.sh")))
if err != nil {
t.Fatal(err)
}

// Clean up the container after the test is complete
t.Cleanup(func() {
assert.NoError(t, container.Terminate(ctx))
})

connectionHost, err := container.ConnectionHost(ctx)
assert.NoError(t, err)

cluster := gocql.NewCluster(connectionHost)
session, err := cluster.CreateSession()
if err != nil {
t.Fatal(err)
}
defer session.Close()

var test Test
err = session.Query("SELECT id, name FROM init_sh_keyspace.test_table WHERE id=1").Scan(&test.Id, &test.Name)
assert.NoError(t, err)
assert.Equal(t, Test{Id: 1, Name: "NAME"}, test)
})
}
Loading

0 comments on commit 1cb3391

Please sign in to comment.