This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-connectors-kafka.git


The following commit(s) were added to refs/heads/main by this push:
     new 6b970f1e test with Kafka 4 server as well as Kafka 3 (#646)
6b970f1e is described below

commit 6b970f1e2ed5766287fa8eff73ac8f65a3d5f2af
Author: PJ Fanning <[email protected]>
AuthorDate: Tue Sep 1 10:09:35 2026 +0100

    test with Kafka 4 server as well as Kafka 3 (#646)
    
    * test with Kafka 4 server as well as Kafka 3
    
    Motivation:
    The testkit could start Confluent Platform 8.x (Kafka 4) images only far
    enough to skip the ZooKeeper container; the broker itself was still
    configured in ZooKeeper mode, which Kafka 4 no longer supports, so the
    container never started. CI also only tested against a Kafka 3 server.
    
    Modification:
    - PekkoConnectorsKafkaContainer detects 8.x image tags and configures the
      broker in KRaft mode (CLUSTER_ID, node id, process roles, controller
      listener and quorum voters) and runs /etc/confluent/docker/ensure so the
      KRaft storage is formatted; ZooKeeper mode is kept for older images.
    - KafkaContainerCluster decides ZooKeeper vs KRaft from the actual Kafka
      image tag (instead of a system property that did not reach forked test
      JVMs when only the env var was set), shares one cluster id and controller
      quorum across brokers, and uses --bootstrap-server for the readiness
      check producer since Kafka 4 removed --broker-list.
    - withBrokerNum now always applies the broker-<n> network alias (previously
      broker number 1 never got its alias), which KRaft quorum voters rely on.
    - DEFAULT_CONFLUENT_PLATFORM_VERSION falls back to the
      CONFLUENT_PLATFORM_VERSION env var so CI can select the server version.
    - CI runs the test job and the integration tests against both Confluent
      7.9.2 (Kafka 3) and 8.3.1 (Kafka 4).
    
    Result:
    Tests run against a Kafka 4 server in CI while Kafka 3 server coverage is
    retained. Testkit users can test against Confluent Platform 8.x images via
    CONFLUENT_PLATFORM_VERSION or the testcontainers settings.
    
    Tests:
    - CONFLUENT_PLATFORM_VERSION=8.3.1 sbt "tests/testOnly 
org.apache.pekko.kafka.scaladsl.TimestampSpec" (KRaft broker + schema registry, 
passed)
    - CONFLUENT_PLATFORM_VERSION=8.3.1 sbt "tests/testOnly 
org.apache.pekko.kafka.scaladsl.ReconnectSpec" (broker stop/start under KRaft, 
passed)
    - sbt "tests/testOnly org.apache.pekko.kafka.scaladsl.TimestampSpec" 
(default 7.9.2 ZooKeeper mode, passed)
    - sbt testkit/mimaReportBinaryIssues (passed)
    - multi-broker int-tests left to the new CI matrix
    
    References:
    Fixes #301
    
    * fix duplicate broker network alias breaking multi-broker KRaft quorum
    
    Motivation:
    The 3-broker KRaft integration tests failed in CI: the constructor-default
    broker-1 network alias was never removed when the broker number changed,
    because testcontainers' getNetworkAliases() returns a copy of the alias
    list, so remove() mutated a throwaway list. Every broker container then
    also carried the broker-1 alias, and Docker DNS resolved broker-1
    round-robin across all brokers (visible in the CI container logs), which
    broke the controller quorum and the failover tests.
    
    Modification:
    withBrokerNum builds the new alias list and applies it via
    setNetworkAliases, which writes through to the container definition.
    
    Result:
    Each broker container carries exactly its own broker-<n> alias.
    
    Tests:
    - standalone check instantiating brokers 0..2: each container ends up
      with exactly its own broker-<n> alias
    - multi-broker KRaft covered by the CI integration-test matrix (8.3.1)
    
    References:
    Refs #301
---
 .github/workflows/check-build-test.yml             |  19 +++-
 .../testkit/internal/KafkaContainerCluster.java    |  36 ++++++-
 .../internal/PekkoConnectorsKafkaContainer.java    | 109 +++++++++++++++++----
 testkit/src/main/resources/reference.conf          |   2 +
 .../pekko/kafka/scaladsl/RetentionPeriodSpec.scala |   2 +-
 5 files changed, 143 insertions(+), 25 deletions(-)

diff --git a/.github/workflows/check-build-test.yml 
b/.github/workflows/check-build-test.yml
index f6848013..7c1adeb6 100644
--- a/.github/workflows/check-build-test.yml
+++ b/.github/workflows/check-build-test.yml
@@ -113,9 +113,11 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - { java-version: 17,  scala-version: scala213, sbt-opts: '' }
-          - { java-version: 17,  scala-version: scala3, sbt-opts: '' }
-          - { java-version: 17,  scala-version: next, sbt-opts: '' }
+          # confluent-version 7.x runs a Kafka 3 server, 8.x runs a Kafka 4 
server
+          - { java-version: 17,  scala-version: scala213, sbt-opts: '', 
confluent-version: '7.9.2' }
+          - { java-version: 17,  scala-version: scala213, sbt-opts: '', 
confluent-version: '8.3.1' }
+          - { java-version: 17,  scala-version: scala3, sbt-opts: '', 
confluent-version: '7.9.2' }
+          - { java-version: 17,  scala-version: next, sbt-opts: '', 
confluent-version: '7.9.2' }
     steps:
       - name: Checkout
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
@@ -139,6 +141,8 @@ jobs:
         uses: sbt/setup-sbt@c7d2d6258b4bd0d3ec5129e6b3453199d3c79729 # v1.5.8
 
       - name: Run tests with Scala ${{ matrix.scala-version }} and Java ${{ 
matrix.java-version }}
+        env:
+          CONFLUENT_PLATFORM_VERSION: ${{ matrix.confluent-version }}
         run: |-
           scala_version="$(bash .github/scripts/resolve-scala-version.sh "${{ 
matrix.scala-version }}")"
           sbt "++${scala_version}! test" ${{ matrix.sbt-opts }}
@@ -148,9 +152,14 @@ jobs:
         run: find . -name "*.log" -exec ./scripts/cat-log.sh {} \;
 
   integration-test:
-    name: Integration tests
+    name: Integration tests (Confluent ${{ matrix.confluent-version }})
     needs: [check-code-style, check-code-compilation, check-docs]
     runs-on: ubuntu-22.04
+    strategy:
+      fail-fast: false
+      matrix:
+        # confluent-version 7.x runs a Kafka 3 server, 8.x runs a Kafka 4 
server
+        confluent-version: ['7.9.2', '8.3.1']
     steps:
       - name: Checkout
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 
v7.0.1
@@ -174,6 +183,8 @@ jobs:
         uses: sbt/setup-sbt@c7d2d6258b4bd0d3ec5129e6b3453199d3c79729 # v1.5.8
 
       - name: Run multi-broker and long running integration tests
+        env:
+          CONFLUENT_PLATFORM_VERSION: ${{ matrix.confluent-version }}
         run: sbt "int-tests/test"
 
       - name: Print logs on failure
diff --git 
a/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/KafkaContainerCluster.java
 
b/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/KafkaContainerCluster.java
index 0bd1cf46..b96bf6ad 100644
--- 
a/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/KafkaContainerCluster.java
+++ 
b/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/KafkaContainerCluster.java
@@ -55,6 +55,8 @@ public class KafkaContainerCluster implements Startable {
   private static final String READINESS_CHECK_SCRIPT = 
"/testcontainers_readiness_check.sh";
   private static final String READINESS_CHECK_TOPIC = 
"ready-kafka-container-cluster";
   private static final Version BOOTSTRAP_PARAM_MIN_VERSION = new 
Version("5.2.0");
+  // Confluent Platform 8.x images run Kafka 4, which supports KRaft mode only 
(no ZooKeeper)
+  private static final Version KRAFT_MIN_VERSION = new Version("8");
 
   private final Logger log = LoggerFactory.getLogger(getClass());
   private final Version kafkaImageTag;
@@ -111,7 +113,8 @@ public class KafkaContainerCluster implements Startable {
     this.network = Network.newNetwork();
     this.schemaRegistryImage = schemaRegistryImage;
 
-    if 
(!PekkoConnectorsKafkaContainer.DEFAULT_CONFLUENT_PLATFORM_VERSION.startsWith("8."))
 {
+    boolean useKraft = this.kafkaImageTag.compareTo(KRAFT_MIN_VERSION) >= 0;
+    if (!useKraft) {
       this.zookeeper =
           Optional.of(
               new GenericContainer(zooKeeperImage)
@@ -122,6 +125,18 @@ public class KafkaContainerCluster implements Startable {
                       
String.valueOf(PekkoConnectorsKafkaContainer.ZOOKEEPER_PORT)));
     }
 
+    // all KRaft nodes must agree on the controller quorum and share one 
cluster id
+    String controllerQuorumVoters =
+        IntStream.range(0, this.brokersNum)
+            .mapToObj(
+                brokerNum ->
+                    "%s@broker-%s:%s"
+                        .formatted(
+                            brokerNum,
+                            brokerNum,
+                            
PekkoConnectorsKafkaContainer.KAFKA_CONTROLLER_PORT))
+            .collect(Collectors.joining(","));
+
     this.brokers =
         IntStream.range(0, this.brokersNum)
             .mapToObj(
@@ -131,7 +146,6 @@ public class KafkaContainerCluster implements Startable {
                           .withNetwork(this.network)
                           .withBrokerNum(brokerNum)
                           .withRemoteJmxService()
-                          .withEnv("KAFKA_BROKER_ID", brokerNum + "")
                           .withEnv("KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR", 
internalTopicsRf + "")
                           .withEnv("KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS", 
internalTopicsRf + "")
                           .withEnv(
@@ -144,6 +158,11 @@ public class KafkaContainerCluster implements Startable {
                             .dependsOn(this.zookeeper.get())
                             .withExternalZookeeper(
                                 "zookeeper:" + 
PekkoConnectorsKafkaContainer.ZOOKEEPER_PORT);
+                  } else if (useKraft) {
+                    container =
+                        container
+                            .withEnv("CLUSTER_ID", 
PekkoConnectorsKafkaContainer.DEFAULT_CLUSTER_ID)
+                            .withEnv("KAFKA_CONTROLLER_QUORUM_VOTERS", 
controllerQuorumVoters);
                   }
                   return container;
                 })
@@ -320,7 +339,9 @@ public class KafkaContainerCluster implements Startable {
             + " \n";
     command += "MESSAGE=\"`date -u`\" \n";
     command +=
-        "echo \"$MESSAGE\" | kafka-console-producer --broker-list 
localhost:9092 --topic "
+        "echo \"$MESSAGE\" | kafka-console-producer "
+            + consoleProducerConnectParam()
+            + " --topic "
             + READINESS_CHECK_TOPIC
             + " --producer-property acks=all \n";
     command +=
@@ -340,6 +361,15 @@ public class KafkaContainerCluster implements Startable {
     }
   }
 
+  private String consoleProducerConnectParam() {
+    // Kafka 4 removed the deprecated --broker-list option
+    if (this.kafkaImageTag.compareTo(KRAFT_MIN_VERSION) >= 0) {
+      return "--bootstrap-server localhost:9092";
+    } else {
+      return "--broker-list localhost:9092";
+    }
+  }
+
   private Boolean runReadinessCheck(GenericContainer c) {
     try {
       Container.ExecResult result = c.execInContainer("sh", "-c", 
READINESS_CHECK_SCRIPT);
diff --git 
a/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/PekkoConnectorsKafkaContainer.java
 
b/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/PekkoConnectorsKafkaContainer.java
index c5be7232..ffb9ba08 100644
--- 
a/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/PekkoConnectorsKafkaContainer.java
+++ 
b/testkit/src/main/java/org/apache/pekko/kafka/testkit/internal/PekkoConnectorsKafkaContainer.java
@@ -42,7 +42,9 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
 
   // Align these confluent platform constants with 
testkit/src/main/resources/reference.conf
   public static final String DEFAULT_CONFLUENT_PLATFORM_VERSION =
-      System.getProperty("CONFLUENT_PLATFORM_VERSION", "7.9.2");
+      System.getProperty(
+          "CONFLUENT_PLATFORM_VERSION",
+          System.getenv().getOrDefault("CONFLUENT_PLATFORM_VERSION", "7.9.2"));
 
   public static final DockerImageName DEFAULT_ZOOKEEPER_IMAGE_NAME =
       DockerImageName.parse("confluentinc/cp-zookeeper")
@@ -57,6 +59,11 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
 
   public static final int ZOOKEEPER_PORT = 2181;
 
+  public static final int KAFKA_CONTROLLER_PORT = 9094;
+
+  // matches the default cluster id used by testcontainers' KafkaContainer
+  public static final String DEFAULT_CLUSTER_ID = "4L6g3nShT-eMCtK--X86sw";
+
   private static final int PORT_NOT_ASSIGNED = -1;
 
   protected String externalZookeeperConnect = null;
@@ -70,6 +77,8 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
 
   private boolean enableRemoteJmxService = false;
 
+  private final boolean useKraft;
+
   public PekkoConnectorsKafkaContainer() {
     this(DEFAULT_KAFKA_IMAGE_NAME);
   }
@@ -77,6 +86,9 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
   public PekkoConnectorsKafkaContainer(final DockerImageName dockerImageName) {
     super(dockerImageName);
 
+    // Kafka 4 (Confluent Platform 8.x) has no ZooKeeper support, brokers must 
run in KRaft mode
+    this.useKraft = requiresKraftMode(dockerImageName.getVersionPart());
+
     super.withNetwork(Network.SHARED);
 
     withExposedPorts(KAFKA_PORT);
@@ -100,6 +112,23 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
     withEnv("KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS", "0");
   }
 
+  private static boolean requiresKraftMode(String imageVersionPart) {
+    if (imageVersionPart == null) {
+      return false;
+    }
+    int dotIndex = imageVersionPart.indexOf('.');
+    String major = dotIndex > 0 ? imageVersionPart.substring(0, dotIndex) : 
imageVersionPart;
+    try {
+      return Integer.parseInt(major) >= 8;
+    } catch (NumberFormatException e) {
+      return false;
+    }
+  }
+
+  public boolean usesKraftMode() {
+    return useKraft;
+  }
+
   @Override
   public PekkoConnectorsKafkaContainer withNetwork(Network network) {
     useImplicitNetwork = false;
@@ -107,12 +136,16 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
   }
 
   public PekkoConnectorsKafkaContainer withBrokerNum(int brokerNum) {
-    if (brokerNum != this.brokerNum) {
-      this.brokerNum = brokerNum;
-      return super.withNetworkAliases("broker-" + this.brokerNum)
-          .withEnv("KAFKA_BROKER_ID", "" + this.brokerNum);
+    // getNetworkAliases() returns a copy, so build the new alias list and set 
it explicitly
+    // to make sure the alias of the previous broker number is dropped
+    List<String> aliases = getNetworkAliases();
+    aliases.remove("broker-" + this.brokerNum);
+    this.brokerNum = brokerNum;
+    if (!aliases.contains("broker-" + brokerNum)) {
+      aliases.add("broker-" + brokerNum);
     }
-    return this;
+    setNetworkAliases(aliases);
+    return withEnv("KAFKA_BROKER_ID", "" + this.brokerNum);
   }
 
   @Override
@@ -157,6 +190,11 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
   }
 
   public PekkoConnectorsKafkaContainer withExternalZookeeper(String 
connectString) {
+    if (useKraft) {
+      throw new IllegalStateException(
+          "ZooKeeper is not supported with Kafka 4 (Confluent Platform 8.x) 
images, "
+              + "the broker runs in KRaft mode");
+    }
     externalZookeeperConnect = connectString;
     return self();
   }
@@ -188,7 +226,9 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
         "-c",
         "while [ ! -f " + START_STOP_SCRIPT + " ]; do sleep 0.1; done; " + 
START_STOP_SCRIPT);
 
-    if (externalZookeeperConnect == null) {
+    if (useKraft) {
+      configureKraft();
+    } else if (externalZookeeperConnect == null) {
       addExposedPort(ZOOKEEPER_PORT);
     }
     if (enableRemoteJmxService) {
@@ -198,6 +238,34 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
     super.doStart();
   }
 
+  private void configureKraft() {
+    if (!getEnvMap().containsKey("CLUSTER_ID")) {
+      withEnv("CLUSTER_ID", DEFAULT_CLUSTER_ID);
+    }
+    // KRaft uses node.id instead of broker.id
+    getEnvMap().remove("KAFKA_BROKER_ID");
+    withEnv("KAFKA_NODE_ID", brokerNum + "");
+    withEnv("KAFKA_PROCESS_ROLES", "broker,controller");
+    withEnv("KAFKA_CONTROLLER_LISTENER_NAMES", "CONTROLLER");
+    String listeners = getEnvMap().get("KAFKA_LISTENERS");
+    if (listeners != null && !listeners.contains("CONTROLLER://")) {
+      withEnv("KAFKA_LISTENERS", listeners + ",CONTROLLER://0.0.0.0:" + 
KAFKA_CONTROLLER_PORT);
+    }
+    String protocolMap = 
getEnvMap().get("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP");
+    if (protocolMap != null && !protocolMap.contains("CONTROLLER:")) {
+      withEnv("KAFKA_LISTENER_SECURITY_PROTOCOL_MAP", protocolMap + 
",CONTROLLER:PLAINTEXT");
+    }
+    if (!getEnvMap().containsKey("KAFKA_CONTROLLER_QUORUM_VOTERS")) {
+      // a single node quorum, KafkaContainerCluster sets the full quorum for 
multi-broker setups
+      String host =
+          getNetwork() != null
+              ? getNetworkAliases().stream().findFirst().orElse("localhost")
+              : "localhost";
+      withEnv(
+          "KAFKA_CONTROLLER_QUORUM_VOTERS", brokerNum + "@" + host + ":" + 
KAFKA_CONTROLLER_PORT);
+    }
+  }
+
   @Override
   protected void containerIsStarting(InspectContainerResponse containerInfo, 
boolean reused) {
     try {
@@ -213,15 +281,18 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
       }
 
       String command = "#!/bin/bash\n";
-      final String zookeeperConnect;
-      if (externalZookeeperConnect != null) {
-        zookeeperConnect = externalZookeeperConnect;
-      } else {
-        zookeeperConnect = "localhost:" + ZOOKEEPER_PORT;
-        command += "echo 'clientPort=" + ZOOKEEPER_PORT + "' > 
zookeeper.properties\n";
-        command += "echo 'dataDir=/var/lib/zookeeper/data' >> 
zookeeper.properties\n";
-        command += "echo 'dataLogDir=/var/lib/zookeeper/log' >> 
zookeeper.properties\n";
-        command += "zookeeper-server-start zookeeper.properties &\n";
+      if (!useKraft) {
+        final String zookeeperConnect;
+        if (externalZookeeperConnect != null) {
+          zookeeperConnect = externalZookeeperConnect;
+        } else {
+          zookeeperConnect = "localhost:" + ZOOKEEPER_PORT;
+          command += "echo 'clientPort=" + ZOOKEEPER_PORT + "' > 
zookeeper.properties\n";
+          command += "echo 'dataDir=/var/lib/zookeeper/data' >> 
zookeeper.properties\n";
+          command += "echo 'dataLogDir=/var/lib/zookeeper/log' >> 
zookeeper.properties\n";
+          command += "zookeeper-server-start zookeeper.properties &\n";
+        }
+        command += "export KAFKA_ZOOKEEPER_CONNECT='" + zookeeperConnect + 
"'\n";
       }
 
       List<String> internalIps =
@@ -229,7 +300,6 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
               .map(ContainerNetwork::getIpAddress)
               .toList();
 
-      command += "export KAFKA_ZOOKEEPER_CONNECT='" + zookeeperConnect + "'\n";
       command +=
           "export KAFKA_ADVERTISED_LISTENERS='"
               + Stream.concat(
@@ -249,6 +319,11 @@ public class PekkoConnectorsKafkaContainer extends 
GenericContainer<PekkoConnect
 
       command += ". /etc/confluent/docker/bash-config \n";
       command += "/etc/confluent/docker/configure \n";
+      if (useKraft) {
+        // formats the KRaft storage directory with the provided CLUSTER_ID 
(no-op when
+        // already formatted, so the broker can be stopped and started again)
+        command += "/etc/confluent/docker/ensure \n";
+      }
       command += "/etc/confluent/docker/launch \n";
 
       copyFileToContainer(
diff --git a/testkit/src/main/resources/reference.conf 
b/testkit/src/main/resources/reference.conf
index c0bc91fb..1cb5fdcc 100644
--- a/testkit/src/main/resources/reference.conf
+++ b/testkit/src/main/resources/reference.conf
@@ -21,6 +21,8 @@ pekko.kafka.testkit.testcontainers {
   # we recommend using Confluent Platform docker images and using the same 
version across all images
   # Confluent publishes images on DockerHub: 
https://hub.docker.com/r/confluentinc/cp-kafka/tags
   # Kafka versions in Confluent Platform: 
https://docs.confluent.io/current/installation/versions-interoperability.html
+  # with Confluent Platform 8.x images (Kafka 4) the brokers run in KRaft mode 
and no ZooKeeper
+  # container is started, so the zookeeper-image settings are ignored
   zookeeper-image = "confluentinc/cp-zookeeper"
   zookeeper-image-tag = 
${pekko.kafka.testkit.testcontainers.confluent-platform-version}
   kafka-image = "confluentinc/cp-kafka"
diff --git 
a/tests/src/test/scala/org/apache/pekko/kafka/scaladsl/RetentionPeriodSpec.scala
 
b/tests/src/test/scala/org/apache/pekko/kafka/scaladsl/RetentionPeriodSpec.scala
index 2dfe0de7..b1ce993e 100644
--- 
a/tests/src/test/scala/org/apache/pekko/kafka/scaladsl/RetentionPeriodSpec.scala
+++ 
b/tests/src/test/scala/org/apache/pekko/kafka/scaladsl/RetentionPeriodSpec.scala
@@ -32,7 +32,7 @@ import scala.jdk.CollectionConverters._
 class RetentionPeriodSpec extends SpecBase with 
TestcontainersKafkaPerClassLike {
   // 
https://docs.confluent.io/current/installation/versions-interoperability.html
   private final val confluentPlatformVersion =
-    System.getProperty("CONFLUENT_PLATFORM_VERSION", "7.9.2")
+    
pekko.kafka.testkit.internal.PekkoConnectorsKafkaContainer.DEFAULT_CONFLUENT_PLATFORM_VERSION
 
   override val testcontainersSettings = 
KafkaTestkitTestcontainersSettings(system)
     .withKafkaImageTag(confluentPlatformVersion)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to