3
3
from io import BytesIO
4
4
from textwrap import dedent
5
5
6
+ from typing_extensions import Self
7
+
6
8
from testcontainers .core .container import DockerContainer
7
9
from testcontainers .core .utils import raise_for_deprecated_parameter
10
+ from testcontainers .core .version import ComparableVersion
8
11
from testcontainers .core .waiting_utils import wait_for_logs
9
12
from testcontainers .kafka ._redpanda import RedpandaContainer
10
13
@@ -26,18 +29,29 @@ class KafkaContainer(DockerContainer):
26
29
27
30
>>> with KafkaContainer() as kafka:
28
31
... connection = kafka.get_bootstrap_server()
32
+
33
+ # Using KRaft protocol
34
+ >>> with KafkaContainer().with_kraft() as kafka:
35
+ ... connection = kafka.get_bootstrap_server()
29
36
"""
30
37
31
38
TC_START_SCRIPT = "/tc-start.sh"
39
+ MIN_KRAFT_TAG = "7.0.0"
32
40
33
41
def __init__ (self , image : str = "confluentinc/cp-kafka:7.6.0" , port : int = 9093 , ** kwargs ) -> None :
34
42
raise_for_deprecated_parameter (kwargs , "port_to_expose" , "port" )
35
43
super ().__init__ (image , ** kwargs )
36
44
self .port = port
45
+ self .kraft_enabled = False
46
+ self .wait_for = r".*\[KafkaServer id=\d+\] started.*"
47
+ self .boot_command = ""
48
+ self .cluster_id = "MkU3OEVBNTcwNTJENDM2Qk"
49
+ self .listeners = f"PLAINTEXT://0.0.0.0:{ self .port } ,BROKER://0.0.0.0:9092"
50
+ self .security_protocol_map = "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
51
+
37
52
self .with_exposed_ports (self .port )
38
- listeners = f"PLAINTEXT://0.0.0.0:{ self .port } ,BROKER://0.0.0.0:9092"
39
- self .with_env ("KAFKA_LISTENERS" , listeners )
40
- self .with_env ("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" , "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT" )
53
+ self .with_env ("KAFKA_LISTENERS" , self .listeners )
54
+ self .with_env ("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" , self .security_protocol_map )
41
55
self .with_env ("KAFKA_INTER_BROKER_LISTENER_NAME" , "BROKER" )
42
56
43
57
self .with_env ("KAFKA_BROKER_ID" , "1" )
@@ -46,6 +60,74 @@ def __init__(self, image: str = "confluentinc/cp-kafka:7.6.0", port: int = 9093,
46
60
self .with_env ("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES" , "10000000" )
47
61
self .with_env ("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS" , "0" )
48
62
63
+ def with_kraft (self ) -> Self :
64
+ self ._verify_min_kraft_version ()
65
+ self .kraft_enabled = True
66
+ return self
67
+
68
+ def _verify_min_kraft_version (self ):
69
+ actual_version = self .image .split (":" )[- 1 ]
70
+
71
+ if ComparableVersion (actual_version ) < self .MIN_KRAFT_TAG :
72
+ raise ValueError (
73
+ f"Provided Confluent Platform's version { actual_version } "
74
+ f"is not supported in Kraft mode"
75
+ f" (must be { self .MIN_KRAFT_TAG } or above)"
76
+ )
77
+
78
+ def with_cluster_id (self , cluster_id : str ) -> Self :
79
+ self .cluster_id = cluster_id
80
+ return self
81
+
82
+ def configure (self ):
83
+ if self .kraft_enabled :
84
+ self ._configure_kraft ()
85
+ else :
86
+ self ._configure_zookeeper ()
87
+
88
+ def _configure_kraft (self ) -> None :
89
+ self .wait_for = r".*Kafka Server started.*"
90
+
91
+ self .with_env ("CLUSTER_ID" , self .cluster_id )
92
+ self .with_env ("KAFKA_NODE_ID" , 1 )
93
+ self .with_env (
94
+ "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" ,
95
+ f"{ self .security_protocol_map } ,CONTROLLER:PLAINTEXT" ,
96
+ )
97
+ self .with_env (
98
+ "KAFKA_LISTENERS" ,
99
+ f"{ self .listeners } ,CONTROLLER://0.0.0.0:9094" ,
100
+ )
101
+ self .with_env ("KAFKA_PROCESS_ROLES" , "broker,controller" )
102
+
103
+ network_alias = self ._get_network_alias ()
104
+ controller_quorum_voters = f"1@{ network_alias } :9094"
105
+ self .with_env ("KAFKA_CONTROLLER_QUORUM_VOTERS" , controller_quorum_voters )
106
+ self .with_env ("KAFKA_CONTROLLER_LISTENER_NAMES" , "CONTROLLER" )
107
+
108
+ self .boot_command = f"""
109
+ sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure
110
+ echo 'kafka-storage format --ignore-formatted -t { self .cluster_id } -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure
111
+ """
112
+
113
+ def _get_network_alias (self ):
114
+ if self ._network :
115
+ return next (
116
+ iter (self ._network_aliases or [self ._network .name or self ._kwargs .get ("network" , [])]),
117
+ None ,
118
+ )
119
+
120
+ return "localhost"
121
+
122
+ def _configure_zookeeper (self ) -> None :
123
+ self .boot_command = """
124
+ echo 'clientPort=2181' > zookeeper.properties
125
+ echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties
126
+ echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties
127
+ zookeeper-server-start zookeeper.properties &
128
+ export KAFKA_ZOOKEEPER_CONNECT='localhost:2181'
129
+ """
130
+
49
131
def get_bootstrap_server (self ) -> str :
50
132
host = self .get_container_host_ip ()
51
133
port = self .get_exposed_port (self .port )
@@ -59,11 +141,7 @@ def tc_start(self) -> None:
59
141
dedent (
60
142
f"""
61
143
#!/bin/bash
62
- echo 'clientPort=2181' > zookeeper.properties
63
- echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties
64
- echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties
65
- zookeeper-server-start zookeeper.properties &
66
- export KAFKA_ZOOKEEPER_CONNECT='localhost:2181'
144
+ { self .boot_command }
67
145
export KAFKA_ADVERTISED_LISTENERS={ listeners }
68
146
. /etc/confluent/docker/bash-config
69
147
/etc/confluent/docker/configure
@@ -78,10 +156,11 @@ def tc_start(self) -> None:
78
156
def start (self , timeout = 30 ) -> "KafkaContainer" :
79
157
script = KafkaContainer .TC_START_SCRIPT
80
158
command = f'sh -c "while [ ! -f { script } ]; do sleep 0.1; done; sh { script } "'
159
+ self .configure ()
81
160
self .with_command (command )
82
161
super ().start ()
83
162
self .tc_start ()
84
- wait_for_logs (self , r".*\[KafkaServer id=\d+\] started.*" , timeout = timeout )
163
+ wait_for_logs (self , self . wait_for , timeout = timeout )
85
164
return self
86
165
87
166
def create_file (self , content : bytes , path : str ) -> None :
0 commit comments