Skip to content

Commit 8b55a3e

Browse files
Add Redpanda module (#5740)
New module uses `docker.redpanda.com/vectorized/redpanda:v22.2.1` docker image as a base giving the `--mode dev-container` flag added in that version. Co-authored-by: Sergei Egorov <[email protected]>
1 parent 20fdee2 commit 8b55a3e

File tree

11 files changed

+263
-0
lines changed

11 files changed

+263
-0
lines changed

.github/ISSUE_TEMPLATE/bug_report.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ body:
4242
- Presto
4343
- Pulsar
4444
- RabbitMQ
45+
- Redpanda
4546
- Selenium
4647
- Solr
4748
- TiDB

.github/ISSUE_TEMPLATE/enhancement.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ body:
4242
- Presto
4343
- Pulsar
4444
- RabbitMQ
45+
- Redpanda
4546
- Selenium
4647
- Solr
4748
- TiDB

.github/ISSUE_TEMPLATE/feature.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ body:
4242
- Presto
4343
- Pulsar
4444
- RabbitMQ
45+
- Redpanda
4546
- Selenium
4647
- Solr
4748
- TiDB

.github/dependabot.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ updates:
187187
schedule:
188188
interval: "monthly"
189189
open-pull-requests-limit: 10
190+
- package-ecosystem: "gradle"
191+
directory: "/modules/redpanda"
192+
schedule:
193+
interval: "monthly"
194+
open-pull-requests-limit: 10
190195
- package-ecosystem: "gradle"
191196
directory: "/modules/selenium"
192197
schedule:

.github/labeler.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
- modules/r2dbc/*
7070
"modules/rabbitmq":
7171
- modules/rabbitmq/*
72+
"modules/redpanda":
73+
- modules/redpanda/*
7274
"modules/selenium":
7375
- modules/selenium/*
7476
"modules/solr":

docs/modules/redpanda.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Redpanda
2+
3+
Testcontainers can be used to automatically instantiate and manage [Redpanda](https://redpanda.com/) containers.
4+
More precisely Testcontainers uses the official Docker images for [Redpanda](https://hub.docker.com/r/vectorized/redpanda/)
5+
6+
!!! note
7+
This module uses features provided in `docker.redpanda.com/vectorized/redpanda`.
8+
9+
## Example
10+
11+
Create a `Redpanda` to use it in your tests:
12+
<!--codeinclude-->
13+
[Creating a Redpanda](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:constructorWithVersion
14+
<!--/codeinclude-->
15+
16+
Now your tests or any other process running on your machine can get access to running Redpanda broker by using the following bootstrap server location:
17+
18+
<!--codeinclude-->
19+
[Bootstrap Servers](../../modules/redpanda/src/test/java/org/testcontainers/redpanda/RedpandaContainerTest.java) inside_block:getBootstrapServers
20+
<!--/codeinclude-->
21+
22+
## Adding this module to your project dependencies
23+
24+
Add the following dependency to your `pom.xml`/`build.gradle` file:
25+
26+
=== "Gradle"
27+
```groovy
28+
testImplementation "org.testcontainers:redpanda:{{latest_version}}"
29+
```
30+
=== "Maven"
31+
```xml
32+
<dependency>
33+
<groupId>org.testcontainers</groupId>
34+
<artifactId>redpanda</artifactId>
35+
<version>{{latest_version}}</version>
36+
<scope>test</scope>
37+
</dependency>
38+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ nav:
7878
- modules/nginx.md
7979
- modules/pulsar.md
8080
- modules/rabbitmq.md
81+
- modules/redpanda.md
8182
- modules/solr.md
8283
- modules/toxiproxy.md
8384
- modules/vault.md

modules/redpanda/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
description = "Testcontainers :: Redpanda"
2+
3+
dependencies {
4+
api project(':testcontainers')
5+
6+
testImplementation 'org.apache.kafka:kafka-clients:3.2.1'
7+
testImplementation 'org.assertj:assertj-core:3.23.1'
8+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.testcontainers.redpanda;
2+
3+
import com.github.dockerjava.api.command.InspectContainerResponse;
4+
import org.testcontainers.containers.GenericContainer;
5+
import org.testcontainers.containers.wait.strategy.Wait;
6+
import org.testcontainers.images.builder.Transferable;
7+
import org.testcontainers.utility.ComparableVersion;
8+
import org.testcontainers.utility.DockerImageName;
9+
10+
/**
11+
* Testcontainers implementation for Redpanda.
12+
*/
13+
public class RedpandaContainer extends GenericContainer<RedpandaContainer> {
14+
15+
private static final String REDPANDA_FULL_IMAGE_NAME = "docker.redpanda.com/vectorized/redpanda";
16+
17+
private static final DockerImageName REDPANDA_IMAGE = DockerImageName.parse(REDPANDA_FULL_IMAGE_NAME);
18+
19+
private static final int REDPANDA_PORT = 9092;
20+
21+
private static final String STARTER_SCRIPT = "/testcontainers_start.sh";
22+
23+
public RedpandaContainer(String image) {
24+
this(DockerImageName.parse(image));
25+
}
26+
27+
public RedpandaContainer(DockerImageName imageName) {
28+
super(imageName);
29+
imageName.assertCompatibleWith(REDPANDA_IMAGE);
30+
31+
boolean isLessThanBaseVersion = new ComparableVersion(imageName.getVersionPart()).isLessThan("v22.2.1");
32+
if (REDPANDA_FULL_IMAGE_NAME.equals(imageName.getUnversionedPart()) && isLessThanBaseVersion) {
33+
throw new IllegalArgumentException("Redpanda version must be >= v22.2.1");
34+
}
35+
36+
withExposedPorts(REDPANDA_PORT);
37+
withCreateContainerCmdModifier(cmd -> {
38+
cmd.withEntrypoint("sh");
39+
});
40+
waitingFor(Wait.forLogMessage(".*Started Kafka API server.*", 1));
41+
withCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
42+
}
43+
44+
@Override
45+
protected void containerIsStarting(InspectContainerResponse containerInfo) {
46+
super.containerIsStarting(containerInfo);
47+
48+
String command = "#!/bin/bash\n";
49+
50+
command += "/usr/bin/rpk redpanda start --mode dev-container ";
51+
command += "--kafka-addr PLAINTEXT://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 ";
52+
command += "--advertise-kafka-addr PLAINTEXT://kafka:29092,OUTSIDE://" + getHost() + ":" + getMappedPort(9092);
53+
54+
copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT);
55+
}
56+
57+
public String getBootstrapServers() {
58+
return String.format("PLAINTEXT://%s:%s", getHost(), getMappedPort(REDPANDA_PORT));
59+
}
60+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package org.testcontainers.redpanda;
2+
3+
import com.google.common.collect.ImmutableMap;
4+
import org.apache.kafka.clients.admin.AdminClient;
5+
import org.apache.kafka.clients.admin.AdminClientConfig;
6+
import org.apache.kafka.clients.admin.NewTopic;
7+
import org.apache.kafka.clients.consumer.ConsumerConfig;
8+
import org.apache.kafka.clients.consumer.ConsumerRecord;
9+
import org.apache.kafka.clients.consumer.ConsumerRecords;
10+
import org.apache.kafka.clients.consumer.KafkaConsumer;
11+
import org.apache.kafka.clients.producer.KafkaProducer;
12+
import org.apache.kafka.clients.producer.ProducerConfig;
13+
import org.apache.kafka.clients.producer.ProducerRecord;
14+
import org.apache.kafka.common.serialization.StringDeserializer;
15+
import org.apache.kafka.common.serialization.StringSerializer;
16+
import org.junit.Test;
17+
import org.rnorth.ducttape.unreliables.Unreliables;
18+
import org.testcontainers.utility.DockerImageName;
19+
20+
import java.time.Duration;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.UUID;
24+
import java.util.concurrent.TimeUnit;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
28+
import static org.assertj.core.api.Assertions.tuple;
29+
30+
public class RedpandaContainerTest {
31+
32+
private static final String REDPANDA_IMAGE = "docker.redpanda.com/vectorized/redpanda:v22.2.1";
33+
34+
private static final DockerImageName REDPANDA_DOCKER_IMAGE = DockerImageName.parse(REDPANDA_IMAGE);
35+
36+
@Test
37+
public void testUsage() throws Exception {
38+
try (RedpandaContainer container = new RedpandaContainer(REDPANDA_DOCKER_IMAGE)) {
39+
container.start();
40+
testKafkaFunctionality(container.getBootstrapServers());
41+
}
42+
}
43+
44+
@Test
45+
public void testUsageWithStringImage() throws Exception {
46+
try (
47+
// constructorWithVersion {
48+
RedpandaContainer container = new RedpandaContainer("docker.redpanda.com/vectorized/redpanda:v22.2.1")
49+
// }
50+
) {
51+
container.start();
52+
testKafkaFunctionality(
53+
// getBootstrapServers {
54+
container.getBootstrapServers()
55+
// }
56+
);
57+
}
58+
}
59+
60+
@Test
61+
public void testNotCompatibleVersion() {
62+
assertThatThrownBy(() -> new RedpandaContainer("docker.redpanda.com/vectorized/redpanda:v21.11.19"))
63+
.isInstanceOf(IllegalArgumentException.class)
64+
.hasMessageContaining("Redpanda version must be >= v22.2.1");
65+
}
66+
67+
private void testKafkaFunctionality(String bootstrapServers) throws Exception {
68+
testKafkaFunctionality(bootstrapServers, 1, 1);
69+
}
70+
71+
private void testKafkaFunctionality(String bootstrapServers, int partitions, int rf) throws Exception {
72+
try (
73+
AdminClient adminClient = AdminClient.create(
74+
ImmutableMap.of(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers)
75+
);
76+
KafkaProducer<String, String> producer = new KafkaProducer<>(
77+
ImmutableMap.of(
78+
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
79+
bootstrapServers,
80+
ProducerConfig.CLIENT_ID_CONFIG,
81+
UUID.randomUUID().toString()
82+
),
83+
new StringSerializer(),
84+
new StringSerializer()
85+
);
86+
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(
87+
ImmutableMap.of(
88+
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
89+
bootstrapServers,
90+
ConsumerConfig.GROUP_ID_CONFIG,
91+
"tc-" + UUID.randomUUID(),
92+
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,
93+
"earliest"
94+
),
95+
new StringDeserializer(),
96+
new StringDeserializer()
97+
);
98+
) {
99+
String topicName = "messages-" + UUID.randomUUID();
100+
101+
Collection<NewTopic> topics = Collections.singletonList(new NewTopic(topicName, partitions, (short) rf));
102+
adminClient.createTopics(topics).all().get(30, TimeUnit.SECONDS);
103+
104+
consumer.subscribe(Collections.singletonList(topicName));
105+
106+
producer.send(new ProducerRecord<>(topicName, "testcontainers", "rulezzz")).get();
107+
108+
Unreliables.retryUntilTrue(
109+
10,
110+
TimeUnit.SECONDS,
111+
() -> {
112+
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
113+
114+
if (records.isEmpty()) {
115+
return false;
116+
}
117+
118+
assertThat(records)
119+
.hasSize(1)
120+
.extracting(ConsumerRecord::topic, ConsumerRecord::key, ConsumerRecord::value)
121+
.containsExactly(tuple(topicName, "testcontainers", "rulezzz"));
122+
123+
return true;
124+
}
125+
);
126+
127+
consumer.unsubscribe();
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)