Skip to content

Commit c6d7202

Browse files
authored
Create KafkaContainerDef (#7748)
Apply `ContainerDef` to `KafkaContainer`
1 parent 1dba8d1 commit c6d7202

File tree

2 files changed

+128
-95
lines changed

2 files changed

+128
-95
lines changed

modules/kafka/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@ dependencies {
77
testImplementation 'org.assertj:assertj-core:3.24.2'
88
testImplementation 'com.google.guava:guava:23.0'
99
}
10+
11+
tasks.japicmp {
12+
methodExcludes = [
13+
"org.testcontainers.containers.KafkaContainer#configureKraft()",
14+
"org.testcontainers.containers.KafkaContainer#configureZookeeper()"
15+
]
16+
}

modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java

Lines changed: 121 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,8 @@ public class KafkaContainer extends GenericContainer<KafkaContainer> {
4848

4949
private boolean kraftEnabled = false;
5050

51-
private String clusterId = DEFAULT_CLUSTER_ID;
52-
5351
private static final String PROTOCOL_PREFIX = "TC";
5452

55-
private final Set<Supplier<String>> listeners = new HashSet<>();
56-
5753
/**
5854
* @deprecated use {@link #KafkaContainer(DockerImageName)} instead
5955
*/
@@ -73,38 +69,32 @@ public KafkaContainer(String confluentPlatformVersion) {
7369
public KafkaContainer(final DockerImageName dockerImageName) {
7470
super(dockerImageName);
7571
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
72+
}
7673

77-
withEnv("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER");
78-
79-
withEnv("KAFKA_BROKER_ID", "1");
80-
withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
81-
withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", DEFAULT_INTERNAL_TOPIC_RF);
82-
withEnv("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
83-
withEnv("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", DEFAULT_INTERNAL_TOPIC_RF);
84-
withEnv("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", Long.MAX_VALUE + "");
85-
withEnv("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0");
86-
87-
withExposedPorts(KAFKA_PORT);
74+
@Override
75+
KafkaContainerDef createContainerDef() {
76+
return new KafkaContainerDef();
77+
}
8878

89-
withCreateContainerCmdModifier(cmd -> {
90-
cmd.withEntrypoint("sh");
91-
});
92-
withCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
79+
@Override
80+
KafkaContainerDef getContainerDef() {
81+
return (KafkaContainerDef) super.getContainerDef();
9382
}
9483

9584
public KafkaContainer withEmbeddedZookeeper() {
9685
if (this.kraftEnabled) {
9786
throw new IllegalStateException("Cannot configure Zookeeper when using Kraft mode");
9887
}
99-
externalZookeeperConnect = null;
88+
getContainerDef().withEmbeddedZookeeper();
10089
return self();
10190
}
10291

10392
public KafkaContainer withExternalZookeeper(String connectString) {
10493
if (this.kraftEnabled) {
10594
throw new IllegalStateException("Cannot configure Zookeeper when using Kraft mode");
10695
}
107-
externalZookeeperConnect = connectString;
96+
this.externalZookeeperConnect = connectString;
97+
getContainerDef().withZookeeper(connectString);
10898
return self();
10999
}
110100

@@ -114,6 +104,7 @@ public KafkaContainer withKraft() {
114104
}
115105
verifyMinKraftVersion();
116106
this.kraftEnabled = true;
107+
getContainerDef().withRaft();
117108
return self();
118109
}
119110

@@ -137,7 +128,7 @@ private boolean isLessThanCP740() {
137128

138129
public KafkaContainer withClusterId(String clusterId) {
139130
Objects.requireNonNull(clusterId, "clusterId cannot be null");
140-
this.clusterId = clusterId;
131+
getContainerDef().withClusterId(clusterId);
141132
return self();
142133
}
143134

@@ -147,77 +138,10 @@ public String getBootstrapServers() {
147138

148139
@Override
149140
protected void configure() {
150-
// Use two listeners with different names, it will force Kafka to communicate with itself via internal
151-
// listener when KAFKA_INTER_BROKER_LISTENER_NAME is set, otherwise Kafka will try to use the advertised listener
152-
Set<String> listeners = new HashSet<>();
153-
listeners.add("PLAINTEXT://0.0.0.0:" + KAFKA_PORT);
154-
listeners.add("BROKER://0.0.0.0:9092");
155-
156-
Set<String> listenerSecurityProtocolMap = new HashSet<>();
157-
listenerSecurityProtocolMap.add("BROKER:PLAINTEXT");
158-
listenerSecurityProtocolMap.add("PLAINTEXT:PLAINTEXT");
141+
getContainerDef().resolveListeners();
159142

160-
List<Supplier<String>> listenersToTransform = new ArrayList<>(this.listeners);
161-
for (int i = 0; i < listenersToTransform.size(); i++) {
162-
Supplier<String> listenerSupplier = listenersToTransform.get(i);
163-
String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i);
164-
String listener = listenerSupplier.get();
165-
String listenerPort = listener.split(":")[1];
166-
String listenerProtocol = String.format("%s://0.0.0.0:%s", protocol, listenerPort);
167-
String protocolMap = String.format("%s:PLAINTEXT", protocol);
168-
listeners.add(listenerProtocol);
169-
listenerSecurityProtocolMap.add(protocolMap);
170-
171-
String host = listener.split(":")[0];
172-
withNetworkAliases(host);
173-
}
174-
175-
String kafkaListeners = String.join(",", listeners);
176-
String kafkaListenerSecurityProtocolMap = String.join(",", listenerSecurityProtocolMap);
177-
178-
withEnv("KAFKA_LISTENERS", kafkaListeners);
179-
withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", kafkaListenerSecurityProtocolMap);
180-
181-
if (this.kraftEnabled) {
182-
waitingFor(Wait.forLogMessage(".*Transitioning from RECOVERY to RUNNING.*", 1));
183-
configureKraft();
184-
} else {
185-
waitingFor(Wait.forLogMessage(".*\\[KafkaServer id=\\d+\\] started.*", 1));
186-
configureZookeeper();
187-
}
188-
}
189-
190-
protected void configureKraft() {
191-
//CP 7.4.0
192-
getEnvMap().computeIfAbsent("CLUSTER_ID", key -> clusterId);
193-
getEnvMap().computeIfAbsent("KAFKA_NODE_ID", key -> getEnvMap().get("KAFKA_BROKER_ID"));
194-
withEnv(
195-
"KAFKA_LISTENER_SECURITY_PROTOCOL_MAP",
196-
String.format("%s,CONTROLLER:PLAINTEXT", getEnvMap().get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP"))
197-
);
198-
withEnv("KAFKA_LISTENERS", String.format("%s,CONTROLLER://0.0.0.0:9094", getEnvMap().get("KAFKA_LISTENERS")));
199-
200-
withEnv("KAFKA_PROCESS_ROLES", "broker,controller");
201-
getEnvMap()
202-
.computeIfAbsent(
203-
"KAFKA_CONTROLLER_QUORUM_VOTERS",
204-
key -> {
205-
return String.format(
206-
"%s@%s:9094",
207-
getEnvMap().get("KAFKA_NODE_ID"),
208-
getNetwork() != null ? getNetworkAliases().get(0) : "localhost"
209-
);
210-
}
211-
);
212-
withEnv("KAFKA_CONTROLLER_LISTENER_NAMES", "CONTROLLER");
213-
}
214-
215-
protected void configureZookeeper() {
216-
if (externalZookeeperConnect != null) {
217-
withEnv("KAFKA_ZOOKEEPER_CONNECT", externalZookeeperConnect);
218-
} else {
219-
addExposedPort(ZOOKEEPER_PORT);
220-
withEnv("KAFKA_ZOOKEEPER_CONNECT", "localhost:" + ZOOKEEPER_PORT);
143+
if (!this.kraftEnabled && this.externalZookeeperConnect == null) {
144+
getContainerDef().withEmbeddedZookeeper();
221145
}
222146
}
223147

@@ -229,7 +153,7 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) {
229153
advertisedListeners.add(getBootstrapServers());
230154
advertisedListeners.add(brokerAdvertisedListener(containerInfo));
231155

232-
List<Supplier<String>> listenersToTransform = new ArrayList<>(this.listeners);
156+
List<Supplier<String>> listenersToTransform = new ArrayList<>(getContainerDef().listeners);
233157
for (int i = 0; i < listenersToTransform.size(); i++) {
234158
Supplier<String> listenerSupplier = listenersToTransform.get(i);
235159
String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i);
@@ -265,7 +189,7 @@ protected String commandKraft() {
265189
String command = "sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure\n";
266190
command +=
267191
"echo 'kafka-storage format --ignore-formatted -t \"" +
268-
this.clusterId +
192+
getContainerDef().getEnvVars().get("CLUSTER_ID") +
269193
"\" -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure\n";
270194
return command;
271195
}
@@ -299,11 +223,113 @@ protected String commandZookeeper() {
299223
* @return this {@link KafkaContainer} instance
300224
*/
301225
public KafkaContainer withListener(Supplier<String> listenerSupplier) {
302-
this.listeners.add(listenerSupplier);
226+
getContainerDef().withListener(listenerSupplier);
303227
return this;
304228
}
305229

306230
protected String brokerAdvertisedListener(InspectContainerResponse containerInfo) {
307231
return String.format("BROKER://%s:%s", containerInfo.getConfig().getHostName(), "9092");
308232
}
233+
234+
private static class KafkaContainerDef extends ContainerDef {
235+
236+
private final Set<Supplier<String>> listeners = new HashSet<>();
237+
238+
private String clusterId = DEFAULT_CLUSTER_ID;
239+
240+
KafkaContainerDef() {
241+
// Use two listeners with different names, it will force Kafka to communicate with itself via internal
242+
// listener when KAFKA_INTER_BROKER_LISTENER_NAME is set, otherwise Kafka will try to use the advertised listener
243+
addEnvVar("KAFKA_LISTENERS", "PLAINTEXT://0.0.0.0:" + KAFKA_PORT + ",BROKER://0.0.0.0:9092");
244+
addEnvVar("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT");
245+
addEnvVar("KAFKA_INTER_BROKER_LISTENER_NAME", "BROKER");
246+
247+
addEnvVar("KAFKA_BROKER_ID", "1");
248+
addEnvVar("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
249+
addEnvVar("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", DEFAULT_INTERNAL_TOPIC_RF);
250+
addEnvVar("KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR", DEFAULT_INTERNAL_TOPIC_RF);
251+
addEnvVar("KAFKA_TRANSACTION_STATE_LOG_MIN_ISR", DEFAULT_INTERNAL_TOPIC_RF);
252+
addEnvVar("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES", Long.MAX_VALUE + "");
253+
addEnvVar("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0");
254+
255+
addExposedTcpPort(KAFKA_PORT);
256+
257+
setEntrypoint("sh");
258+
setCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
259+
260+
setWaitStrategy(Wait.forLogMessage(".*\\[KafkaServer id=\\d+\\] started.*", 1));
261+
}
262+
263+
private void resolveListeners() {
264+
Set<String> additionalKafkaListeners = new HashSet<>();
265+
Set<String> additionalListenerSecurityProtocolMap = new HashSet<>();
266+
267+
List<Supplier<String>> listenersToTransform = new ArrayList<>(this.listeners);
268+
for (int i = 0; i < listenersToTransform.size(); i++) {
269+
Supplier<String> listenerSupplier = listenersToTransform.get(i);
270+
String protocol = String.format("%s-%d", PROTOCOL_PREFIX, i);
271+
String listener = listenerSupplier.get();
272+
String listenerPort = listener.split(":")[1];
273+
String listenerProtocol = String.format("%s://0.0.0.0:%s", protocol, listenerPort);
274+
String protocolMap = String.format("%s:PLAINTEXT", protocol);
275+
additionalKafkaListeners.add(listenerProtocol);
276+
additionalListenerSecurityProtocolMap.add(protocolMap);
277+
278+
String host = listener.split(":")[0];
279+
addNetworkAlias(host);
280+
}
281+
282+
String kafkaListeners = String.join(",", additionalKafkaListeners);
283+
String kafkaListenerSecurityProtocolMap = String.join(",", additionalListenerSecurityProtocolMap);
284+
285+
this.envVars.computeIfPresent("KAFKA_LISTENERS", (k, v) -> String.join(",", v, kafkaListeners));
286+
this.envVars.computeIfPresent(
287+
"KAFKA_LISTENER_SECURITY_PROTOCOL_MAP",
288+
(k, v) -> String.join(",", v, kafkaListenerSecurityProtocolMap)
289+
);
290+
}
291+
292+
void withListener(Supplier<String> listenerSupplier) {
293+
this.listeners.add(listenerSupplier);
294+
}
295+
296+
void withEmbeddedZookeeper() {
297+
addExposedTcpPort(ZOOKEEPER_PORT);
298+
addEnvVar("KAFKA_ZOOKEEPER_CONNECT", "localhost:" + ZOOKEEPER_PORT);
299+
}
300+
301+
void withZookeeper(String connectionString) {
302+
addEnvVar("KAFKA_ZOOKEEPER_CONNECT", connectionString);
303+
}
304+
305+
void withClusterId(String clusterId) {
306+
this.clusterId = clusterId;
307+
}
308+
309+
void withRaft() {
310+
this.envVars.computeIfAbsent("CLUSTER_ID", key -> clusterId);
311+
this.envVars.computeIfAbsent("KAFKA_NODE_ID", key -> getEnvVars().get("KAFKA_BROKER_ID"));
312+
addEnvVar(
313+
"KAFKA_LISTENER_SECURITY_PROTOCOL_MAP",
314+
String.format("%s,CONTROLLER:PLAINTEXT", getEnvVars().get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP"))
315+
);
316+
addEnvVar(
317+
"KAFKA_LISTENERS",
318+
String.format("%s,CONTROLLER://0.0.0.0:9094", getEnvVars().get("KAFKA_LISTENERS"))
319+
);
320+
addEnvVar("KAFKA_PROCESS_ROLES", "broker,controller");
321+
322+
String firstNetworkAlias = getNetworkAliases().stream().findFirst().orElse(null);
323+
String networkAlias = getNetwork() != null ? firstNetworkAlias : "localhost";
324+
String controllerQuorumVoters = String.format(
325+
"%s@%s:9094",
326+
getEnvVars().get("KAFKA_NODE_ID"),
327+
networkAlias
328+
);
329+
this.envVars.computeIfAbsent("KAFKA_CONTROLLER_QUORUM_VOTERS", key -> controllerQuorumVoters);
330+
addEnvVar("KAFKA_CONTROLLER_LISTENER_NAMES", "CONTROLLER");
331+
332+
setWaitStrategy(Wait.forLogMessage(".*Transitioning from RECOVERY to RUNNING.*", 1));
333+
}
334+
}
309335
}

0 commit comments

Comments
 (0)