1+ import base64
12import tarfile
23import time
4+ import uuid
35from io import BytesIO
46from textwrap import dedent
57
68from testcontainers .core .container import DockerContainer
79from testcontainers .core .utils import raise_for_deprecated_parameter
10+ from testcontainers .core .version import ComparableVersion
811from testcontainers .core .waiting_utils import wait_for_logs
9- from testcontainers . kafka . _redpanda import RedpandaContainer
12+ from typing_extensions import Self
1013
1114__all__ = [
1215 "KafkaContainer" ,
@@ -29,15 +32,26 @@ class KafkaContainer(DockerContainer):
2932 """
3033
3134 TC_START_SCRIPT = "/tc-start.sh"
35+ MIN_KRAFT_TAG = "7.0.0"
3236
33- def __init__ (self , image : str = "confluentinc/cp-kafka:7.6.0" , port : int = 9093 , ** kwargs ) -> None :
37+ def __init__ (
38+ self , image : str = "confluentinc/cp-kafka:7.6.0" , port : int = 9093 , ** kwargs
39+ ) -> None :
3440 raise_for_deprecated_parameter (kwargs , "port_to_expose" , "port" )
3541 super ().__init__ (image , ** kwargs )
3642 self .port = port
43+ self .kraft_enabled = False
44+ self .wait_for = r".*\[KafkaServer id=\d+\] started.*"
45+ self .boot_command = ""
46+ self .cluster_id = self ._random_uuid ()
47+ self .listeners = f"PLAINTEXT://0.0.0.0:{ self .port } ,BROKER://0.0.0.0:9092"
48+ self .security_protocol_map = "BROKER:PLAINTEXT,PLAINTEXT:PLAINTEXT"
49+
3750 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" )
51+ self .with_env ("KAFKA_LISTENERS" , self .listeners )
52+ self .with_env (
53+ "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" , self .security_protocol_map
54+ )
4155 self .with_env ("KAFKA_INTER_BROKER_LISTENER_NAME" , "BROKER" )
4256
4357 self .with_env ("KAFKA_BROKER_ID" , "1" )
@@ -46,6 +60,85 @@ def __init__(self, image: str = "confluentinc/cp-kafka:7.6.0", port: int = 9093,
4660 self .with_env ("KAFKA_LOG_FLUSH_INTERVAL_MESSAGES" , "10000000" )
4761 self .with_env ("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS" , "0" )
4862
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+ @classmethod
83+ def _random_uuid (cls ):
84+ uuid_value = uuid .uuid4 ()
85+ uuid_bytes = uuid_value .bytes
86+ base64_encoded_uuid = base64 .b64encode (uuid_bytes )
87+
88+ return base64_encoded_uuid .decode ()
89+
90+ def configure (self ):
91+ if self .kraft_enabled :
92+ self ._configure_kraft ()
93+ else :
94+ self ._configure_zookeeper ()
95+
96+ def _configure_kraft (self ) -> None :
97+ self .wait_for = r".*Kafka Server started.*"
98+
99+ self .with_env ("CLUSTER_ID" , self .cluster_id )
100+ self .with_env ("KAFKA_NODE_ID" , 1 )
101+ self .with_env (
102+ "KAFKA_LISTENER_SECURITY_PROTOCOL_MAP" ,
103+ f"{ self .security_protocol_map } ,CONTROLLER:PLAINTEXT" ,
104+ )
105+ self .with_env (
106+ "KAFKA_LISTENERS" ,
107+ f"{ self .listeners } ,CONTROLLER://0.0.0.0:9094" ,
108+ )
109+ self .with_env ("KAFKA_PROCESS_ROLES" , "broker,controller" )
110+
111+ network_alias = self ._get_network_alias ()
112+ controller_quorum_voters = f"1@{ network_alias } :9094"
113+ self .with_env ("KAFKA_CONTROLLER_QUORUM_VOTERS" , controller_quorum_voters )
114+ self .with_env ("KAFKA_CONTROLLER_LISTENER_NAMES" , "CONTROLLER" )
115+
116+ self .boot_command = f"""
117+ sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure
118+ echo 'kafka-storage format --ignore-formatted -t { self .cluster_id } -c /etc/kafka/kafka.properties' >> /etc/confluent/docker/configure
119+ """
120+
121+ def _get_network_alias (self ):
122+ if self ._network :
123+ return next (
124+ iter (
125+ self ._network_aliases
126+ or [self ._network .name or self ._kwargs .get ("network" , [])]
127+ ),
128+ None ,
129+ )
130+
131+ return "localhost"
132+
133+ def _configure_zookeeper (self ) -> None :
134+ self .boot_command = """
135+ echo 'clientPort=2181' > zookeeper.properties
136+ echo 'dataDir=/var/lib/zookeeper/data' >> zookeeper.properties
137+ echo 'dataLogDir=/var/lib/zookeeper/log' >> zookeeper.properties
138+ zookeeper-server-start zookeeper.properties &
139+ export KAFKA_ZOOKEEPER_CONNECT='localhost:2181'
140+ """
141+
49142 def get_bootstrap_server (self ) -> str :
50143 host = self .get_container_host_ip ()
51144 port = self .get_exposed_port (self .port )
@@ -59,11 +152,7 @@ def tc_start(self) -> None:
59152 dedent (
60153 f"""
61154 #!/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'
155+ { self .boot_command }
67156 export KAFKA_ADVERTISED_LISTENERS={ listeners }
68157 . /etc/confluent/docker/bash-config
69158 /etc/confluent/docker/configure
@@ -78,10 +167,11 @@ def tc_start(self) -> None:
78167 def start (self , timeout = 30 ) -> "KafkaContainer" :
79168 script = KafkaContainer .TC_START_SCRIPT
80169 command = f'sh -c "while [ ! -f { script } ]; do sleep 0.1; done; sh { script } "'
170+ self .configure ()
81171 self .with_command (command )
82172 super ().start ()
83173 self .tc_start ()
84- wait_for_logs (self , r".*\[KafkaServer id=\d+\] started.*" , timeout = timeout )
174+ wait_for_logs (self , self . wait_for , timeout = timeout )
85175 return self
86176
87177 def create_file (self , content : bytes , path : str ) -> None :
0 commit comments