From c4db8a65a767e74d4d7cceb40004ebc145fdb91c Mon Sep 17 00:00:00 2001 From: Arnel Robles Date: Mon, 21 Sep 2026 22:30:15 +0800 Subject: [PATCH 1/2] fix(Kafka): Drop the trailing comma from advertised listeners Without additional advertised listeners the startup script emitted KAFKA_ADVERTISED_LISTENERS with an empty final element. Kafka 4.x, which Confluent Platform 8.x runs on, rejects that instead of ignoring it, so kafka-storage format failed and the container exited before the broker started. Kafka 3.x tolerated it, which is why this only surfaces on 8.x. Builds the list and joins it, skipping empty entries, in both vendors. --- src/Testcontainers.Kafka/ApacheConfiguration.cs | 7 +++++-- .../ConfluentConfiguration.cs | 7 +++++-- tests/Testcontainers.Kafka.Tests/Dockerfile | 3 ++- .../KafkaContainerTest.cs | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Testcontainers.Kafka/ApacheConfiguration.cs b/src/Testcontainers.Kafka/ApacheConfiguration.cs index 3e8d68868..0482fed66 100644 --- a/src/Testcontainers.Kafka/ApacheConfiguration.cs +++ b/src/Testcontainers.Kafka/ApacheConfiguration.cs @@ -50,12 +50,15 @@ public void Validate(KafkaConfiguration resourceConfiguration) /// public string CreateStartupScript(KafkaConfiguration resourceConfiguration, KafkaContainer container) { - var additionalAdvertisedListeners = string.Join(",", container.AdvertisedListeners ?? Array.Empty()); + var additionalAdvertisedListeners = container.AdvertisedListeners ?? Array.Empty(); var startupScript = new StringWriter(); startupScript.NewLine = "\n"; startupScript.WriteLine("#!/bin/bash"); - startupScript.WriteLine("export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort) + ",BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort + "," + additionalAdvertisedListeners); + var advertisedListeners = new[] { "PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort), "BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort } + .Concat(additionalAdvertisedListeners.Where(listener => !string.IsNullOrEmpty(listener))); + + startupScript.WriteLine("export KAFKA_ADVERTISED_LISTENERS=" + string.Join(",", advertisedListeners)); startupScript.WriteLine("exec /etc/kafka/docker/run"); return startupScript.ToString(); } diff --git a/src/Testcontainers.Kafka/ConfluentConfiguration.cs b/src/Testcontainers.Kafka/ConfluentConfiguration.cs index f2e584710..f453ccb11 100644 --- a/src/Testcontainers.Kafka/ConfluentConfiguration.cs +++ b/src/Testcontainers.Kafka/ConfluentConfiguration.cs @@ -49,7 +49,7 @@ public void Validate(KafkaConfiguration resourceConfiguration) /// public string CreateStartupScript(KafkaConfiguration resourceConfiguration, KafkaContainer container) { - var additionalAdvertisedListeners = string.Join(",", container.AdvertisedListeners ?? Array.Empty()); + var additionalAdvertisedListeners = container.AdvertisedListeners ?? Array.Empty(); var isZooKeeperConsensus = resourceConfiguration.ConsensusProtocol == ConsensusProtocol.ZooKeeper; @@ -68,7 +68,10 @@ public string CreateStartupScript(KafkaConfiguration resourceConfiguration, Kafk startupScript.WriteLine("zookeeper-server-start zookeeper.properties &"); } - startupScript.WriteLine("export KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort) + ",BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort + "," + additionalAdvertisedListeners); + var advertisedListeners = new[] { "PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort), "BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort } + .Concat(additionalAdvertisedListeners.Where(listener => !string.IsNullOrEmpty(listener))); + + startupScript.WriteLine("export KAFKA_ADVERTISED_LISTENERS=" + string.Join(",", advertisedListeners)); startupScript.WriteLine("exec /etc/confluent/docker/run"); return startupScript.ToString(); } diff --git a/tests/Testcontainers.Kafka.Tests/Dockerfile b/tests/Testcontainers.Kafka.Tests/Dockerfile index 78378f63b..d714d896d 100644 --- a/tests/Testcontainers.Kafka.Tests/Dockerfile +++ b/tests/Testcontainers.Kafka.Tests/Dockerfile @@ -1,3 +1,4 @@ FROM confluentinc/cp-kafka:7.5.12@sha256:997883c60e6f1207fb5f779c7a8fdae57312a17f446fd5ea3eb8b73f52d4a1cf FROM apache/kafka:4.1.1@sha256:0bc1bb2478f45b6cea78864df86acdc11e8df2c5172477819a4d12942cbe5d40 AS apache-v4_1_1 -FROM apache/kafka-native:4.1.1@sha256:b8faca36f5385f90e46861ea0b2f532ebed86e9b2868b717c75a67f6fda0c7ce AS apache-native-v4_1_1 \ No newline at end of file +FROM apache/kafka-native:4.1.1@sha256:b8faca36f5385f90e46861ea0b2f532ebed86e9b2868b717c75a67f6fda0c7ce AS apache-native-v4_1_1 +FROM confluentinc/cp-kafka:8.2.3@sha256:7c675640c2e917085e03f40a61b9dcc2dc362bc1ae41b4a9991658ce349c56ba AS confluent-v8_2_3 \ No newline at end of file diff --git a/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs b/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs index a46d07da6..33a17cde2 100644 --- a/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs +++ b/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs @@ -96,6 +96,23 @@ public KafkaZooKeeperConfiguration() } } + /// + /// Confluent Platform 8.x runs on Kafka 4.x, which rejects an empty element in + /// advertised.listeners instead of ignoring it. Without additional advertised + /// listeners the startup script used to emit a trailing comma, so storage formatting + /// failed before the broker started. + /// + [UsedImplicitly] + public sealed class ConfluentKafkaV8Configuration : KafkaContainerTest + { + public ConfluentKafkaV8Configuration() + : base(new KafkaBuilder(TestSession.GetImageFromDockerfile(stage: "confluent-v8_2_3")) + .WithKRaft() + .Build()) + { + } + } + [UsedImplicitly] public sealed class ApacheKafkaConfiguration : KafkaContainerTest { From 1371ae1de97d3b97cf4d450534f2e4331593bf0a Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:45:42 +0200 Subject: [PATCH 2/2] chore: Build advertised listeners as a list --- src/Testcontainers.Kafka/ApacheConfiguration.cs | 8 ++++---- src/Testcontainers.Kafka/ConfluentConfiguration.cs | 8 ++++---- tests/Testcontainers.Kafka.Tests/Dockerfile | 4 ++-- .../Testcontainers.Kafka.Tests/KafkaContainerTest.cs | 11 ++++------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Testcontainers.Kafka/ApacheConfiguration.cs b/src/Testcontainers.Kafka/ApacheConfiguration.cs index 0482fed66..de4629c20 100644 --- a/src/Testcontainers.Kafka/ApacheConfiguration.cs +++ b/src/Testcontainers.Kafka/ApacheConfiguration.cs @@ -50,14 +50,14 @@ public void Validate(KafkaConfiguration resourceConfiguration) /// public string CreateStartupScript(KafkaConfiguration resourceConfiguration, KafkaContainer container) { - var additionalAdvertisedListeners = container.AdvertisedListeners ?? Array.Empty(); + var advertisedListeners = new List(); + advertisedListeners.Add("PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort)); + advertisedListeners.Add("BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort); + advertisedListeners.AddRange(container.AdvertisedListeners ?? Array.Empty()); var startupScript = new StringWriter(); startupScript.NewLine = "\n"; startupScript.WriteLine("#!/bin/bash"); - var advertisedListeners = new[] { "PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort), "BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort } - .Concat(additionalAdvertisedListeners.Where(listener => !string.IsNullOrEmpty(listener))); - startupScript.WriteLine("export KAFKA_ADVERTISED_LISTENERS=" + string.Join(",", advertisedListeners)); startupScript.WriteLine("exec /etc/kafka/docker/run"); return startupScript.ToString(); diff --git a/src/Testcontainers.Kafka/ConfluentConfiguration.cs b/src/Testcontainers.Kafka/ConfluentConfiguration.cs index f453ccb11..27c63f98c 100644 --- a/src/Testcontainers.Kafka/ConfluentConfiguration.cs +++ b/src/Testcontainers.Kafka/ConfluentConfiguration.cs @@ -49,7 +49,10 @@ public void Validate(KafkaConfiguration resourceConfiguration) /// public string CreateStartupScript(KafkaConfiguration resourceConfiguration, KafkaContainer container) { - var additionalAdvertisedListeners = container.AdvertisedListeners ?? Array.Empty(); + var advertisedListeners = new List(); + advertisedListeners.Add("PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort)); + advertisedListeners.Add("BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort); + advertisedListeners.AddRange(container.AdvertisedListeners ?? Array.Empty()); var isZooKeeperConsensus = resourceConfiguration.ConsensusProtocol == ConsensusProtocol.ZooKeeper; @@ -68,9 +71,6 @@ public string CreateStartupScript(KafkaConfiguration resourceConfiguration, Kafk startupScript.WriteLine("zookeeper-server-start zookeeper.properties &"); } - var advertisedListeners = new[] { "PLAINTEXT://" + container.Hostname + ":" + container.GetMappedPublicPort(KafkaBuilder.KafkaPort), "BROKER://" + container.IpAddress + ":" + KafkaBuilder.BrokerPort } - .Concat(additionalAdvertisedListeners.Where(listener => !string.IsNullOrEmpty(listener))); - startupScript.WriteLine("export KAFKA_ADVERTISED_LISTENERS=" + string.Join(",", advertisedListeners)); startupScript.WriteLine("exec /etc/confluent/docker/run"); return startupScript.ToString(); diff --git a/tests/Testcontainers.Kafka.Tests/Dockerfile b/tests/Testcontainers.Kafka.Tests/Dockerfile index d714d896d..a62f8f15c 100644 --- a/tests/Testcontainers.Kafka.Tests/Dockerfile +++ b/tests/Testcontainers.Kafka.Tests/Dockerfile @@ -1,4 +1,4 @@ FROM confluentinc/cp-kafka:7.5.12@sha256:997883c60e6f1207fb5f779c7a8fdae57312a17f446fd5ea3eb8b73f52d4a1cf +FROM confluentinc/cp-kafka:8.2.4@sha256:58e544adba6a825389ab56dad5f8a1c72d9bbcdcf077e275473f1aff22e2cd76 AS confluent-v8_2_4 FROM apache/kafka:4.1.1@sha256:0bc1bb2478f45b6cea78864df86acdc11e8df2c5172477819a4d12942cbe5d40 AS apache-v4_1_1 -FROM apache/kafka-native:4.1.1@sha256:b8faca36f5385f90e46861ea0b2f532ebed86e9b2868b717c75a67f6fda0c7ce AS apache-native-v4_1_1 -FROM confluentinc/cp-kafka:8.2.3@sha256:7c675640c2e917085e03f40a61b9dcc2dc362bc1ae41b4a9991658ce349c56ba AS confluent-v8_2_3 \ No newline at end of file +FROM apache/kafka-native:4.1.1@sha256:b8faca36f5385f90e46861ea0b2f532ebed86e9b2868b717c75a67f6fda0c7ce AS apache-native-v4_1_1 \ No newline at end of file diff --git a/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs b/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs index 33a17cde2..b1ce060f3 100644 --- a/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs +++ b/tests/Testcontainers.Kafka.Tests/KafkaContainerTest.cs @@ -96,17 +96,14 @@ public KafkaZooKeeperConfiguration() } } - /// - /// Confluent Platform 8.x runs on Kafka 4.x, which rejects an empty element in - /// advertised.listeners instead of ignoring it. Without additional advertised - /// listeners the startup script used to emit a trailing comma, so storage formatting - /// failed before the broker started. - /// + // Confluent Platform 8.x runs on Kafka 4.x, which rejects empty elements in + // `advertised.listeners`. A trailing comma made the container exit on startup: + // https://github.com/testcontainers/testcontainers-dotnet/issues/1771. [UsedImplicitly] public sealed class ConfluentKafkaV8Configuration : KafkaContainerTest { public ConfluentKafkaV8Configuration() - : base(new KafkaBuilder(TestSession.GetImageFromDockerfile(stage: "confluent-v8_2_3")) + : base(new KafkaBuilder(TestSession.GetImageFromDockerfile(stage: "confluent-v8_2_4")) .WithKRaft() .Build()) {