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

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new fda09b7a70f Support Kafka config providers in realtime table configs 
(#19202)
fda09b7a70f is described below

commit fda09b7a70f43be91319a37f00f846960d03c8a8
Author: Goutam Adwant <[email protected]>
AuthorDate: Tue Aug 18 11:48:15 2026 -0700

    Support Kafka config providers in realtime table configs (#19202)
---
 .../KafkaPartitionLevelConnectionHandler.java      |  33 +++----
 .../KafkaPartitionLevelConnectionHandlerTest.java  |   7 ++
 .../KafkaPartitionLevelConnectionHandler.java      |  33 +++----
 .../KafkaPartitionLevelConnectionHandlerTest.java  |   7 ++
 .../plugin/stream/kafka/KafkaConfigUtils.java      | 104 +++++++++++++++++++++
 .../pinot/plugin/stream/kafka/KafkaSSLUtils.java   |  22 +++++
 .../stream/kafka/KafkaConfigProviderTestUtils.java |  92 ++++++++++++++++++
 .../plugin/stream/kafka/KafkaConfigUtilsTest.java  |  97 +++++++++++++++++++
 .../plugin/stream/kafka/KafkaSSLUtilsTest.java     |  72 +++++++++++++-
 .../org/apache/pinot/spi/config/ConfigUtils.java   |   6 +-
 .../apache/pinot/spi/config/ConfigUtilsTest.java   |  29 ++++++
 11 files changed, 460 insertions(+), 42 deletions(-)

diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/main/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandler.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/main/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandler.java
index eeb41b80146..18228339543 100644
--- 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/main/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandler.java
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/main/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandler.java
@@ -38,6 +38,7 @@ import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.serialization.BytesDeserializer;
 import org.apache.kafka.common.utils.Bytes;
 import org.apache.pinot.plugin.stream.kafka.KafkaAdminClientManager;
+import org.apache.pinot.plugin.stream.kafka.KafkaConfigUtils;
 import org.apache.pinot.plugin.stream.kafka.KafkaPartitionLevelStreamConfig;
 import org.apache.pinot.plugin.stream.kafka.KafkaSSLUtils;
 import org.apache.pinot.spi.stream.StreamConfig;
@@ -98,28 +99,14 @@ public abstract class KafkaPartitionLevelConnectionHandler {
     return consumerProp;
   }
 
-  /// Filter properties to only include the specified Kafka configurations.
-  /// This prevents "was supplied but isn't a known config" warnings from 
Kafka clients.
-  ///
-  /// @param props The properties to filter
-  /// @param validConfigNames The set of valid configuration names for the 
target Kafka client
-  /// @return A new Properties object containing only the valid configurations
-  private Properties filterKafkaProperties(Properties props, Set<String> 
validConfigNames) {
-    Properties filteredProps = new Properties();
-    for (String key : props.stringPropertyNames()) {
-      if (validConfigNames.contains(key)) {
-        filteredProps.put(key, props.get(key));
-      }
-    }
-    return filteredProps;
-  }
-
   private Consumer<Bytes, Bytes> createConsumer(Properties consumerProp, 
RetryPolicy retryPolicy) {
+    Properties filteredConsumerProp =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES);
     AtomicReference<Consumer<Bytes, Bytes>> consumer = new AtomicReference<>();
     try {
       retryPolicy.attempt(() -> {
         try {
-          consumer.set(new KafkaConsumer<>(filterKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES)));
+          consumer.set(new KafkaConsumer<>(filteredConsumerProp));
           return true;
         } catch (Exception e) {
           LOGGER.warn("Caught exception while creating Kafka consumer, 
retrying.", e);
@@ -135,11 +122,15 @@ public abstract class 
KafkaPartitionLevelConnectionHandler {
 
   @VisibleForTesting
   protected Consumer<Bytes, Bytes> createConsumer(Properties consumerProp) {
-    return retry(() -> new KafkaConsumer<>(filterKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES)), 5);
+    Properties filteredConsumerProp =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES);
+    return retry(() -> new KafkaConsumer<>(filteredConsumerProp), 5);
   }
 
   protected AdminClient createAdminClient() {
-    return retry(() -> AdminClient.create(filterKafkaProperties(_consumerProp, 
ADMIN_CLIENT_CONFIG_NAMES)), 5);
+    Properties filteredAdminClientProp =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(_consumerProp, 
ADMIN_CLIENT_CONFIG_NAMES);
+    return retry(() -> AdminClient.create(filteredAdminClientProp), 5);
   }
 
   /// Gets or creates a reusable admin client instance. The admin client is 
lazily initialized
@@ -166,7 +157,9 @@ public abstract class KafkaPartitionLevelConnectionHandler {
       synchronized (this) {
         ref = _sharedAdminClientRef;
         if (ref == null) {
-          ref = 
KafkaAdminClientManager.getInstance().getOrCreateAdminClient(_consumerProp);
+          Properties filteredAdminClientProp =
+              KafkaConfigUtils.filterAndValidateKafkaProperties(_consumerProp, 
ADMIN_CLIENT_CONFIG_NAMES);
+          ref = 
KafkaAdminClientManager.getInstance().getOrCreateAdminClient(filteredAdminClientProp);
           _sharedAdminClientRef = ref;
         }
       }
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/test/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandlerTest.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/test/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandlerTest.java
index 1abe7681c76..407b534588f 100644
--- 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/test/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandlerTest.java
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/test/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConnectionHandlerTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.plugin.stream.kafka30;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.pinot.plugin.stream.kafka.KafkaConfigProviderTestUtils;
 import org.apache.pinot.spi.stream.StreamConfig;
 import org.testng.annotations.Test;
 
@@ -44,6 +45,12 @@ public class KafkaPartitionLevelConnectionHandlerTest {
     return new StreamConfig("testTable_REALTIME", streamConfigMap);
   }
 
+  @Test
+  public void testConfigProviderReferencesReachKafkaClients()
+      throws Exception {
+    
KafkaConfigProviderTestUtils.assertConfigProviderReferencesReachKafkaClients();
+  }
+
   @Test
   public void testSharedAdminClientReference() {
     StreamConfig streamConfig = createTestStreamConfig();
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandler.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandler.java
index e9a015181b3..213c3b67f12 100644
--- 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandler.java
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandler.java
@@ -38,6 +38,7 @@ import org.apache.kafka.common.TopicPartition;
 import org.apache.kafka.common.serialization.BytesDeserializer;
 import org.apache.kafka.common.utils.Bytes;
 import org.apache.pinot.plugin.stream.kafka.KafkaAdminClientManager;
+import org.apache.pinot.plugin.stream.kafka.KafkaConfigUtils;
 import org.apache.pinot.plugin.stream.kafka.KafkaPartitionLevelStreamConfig;
 import org.apache.pinot.plugin.stream.kafka.KafkaSSLUtils;
 import org.apache.pinot.spi.stream.StreamConfig;
@@ -98,28 +99,14 @@ public abstract class KafkaPartitionLevelConnectionHandler {
     return consumerProp;
   }
 
-  /// Filter properties to only include the specified Kafka configurations.
-  /// This prevents "was supplied but isn't a known config" warnings from 
Kafka clients.
-  ///
-  /// @param props The properties to filter
-  /// @param validConfigNames The set of valid configuration names for the 
target Kafka client
-  /// @return A new Properties object containing only the valid configurations
-  private Properties filterKafkaProperties(Properties props, Set<String> 
validConfigNames) {
-    Properties filteredProps = new Properties();
-    for (String key : props.stringPropertyNames()) {
-      if (validConfigNames.contains(key)) {
-        filteredProps.put(key, props.get(key));
-      }
-    }
-    return filteredProps;
-  }
-
   private Consumer<Bytes, Bytes> createConsumer(Properties consumerProp, 
RetryPolicy retryPolicy) {
+    Properties filteredConsumerProp =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES);
     AtomicReference<Consumer<Bytes, Bytes>> consumer = new AtomicReference<>();
     try {
       retryPolicy.attempt(() -> {
         try {
-          consumer.set(new KafkaConsumer<>(filterKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES)));
+          consumer.set(new KafkaConsumer<>(filteredConsumerProp));
           return true;
         } catch (Exception e) {
           LOGGER.warn("Caught exception while creating Kafka consumer, 
retrying.", e);
@@ -135,11 +122,15 @@ public abstract class 
KafkaPartitionLevelConnectionHandler {
 
   @VisibleForTesting
   protected Consumer<Bytes, Bytes> createConsumer(Properties consumerProp) {
-    return retry(() -> new KafkaConsumer<>(filterKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES)), 5);
+    Properties filteredConsumerProp =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(consumerProp, 
CONSUMER_CONFIG_NAMES);
+    return retry(() -> new KafkaConsumer<>(filteredConsumerProp), 5);
   }
 
   protected AdminClient createAdminClient() {
-    return retry(() -> AdminClient.create(filterKafkaProperties(_consumerProp, 
ADMIN_CLIENT_CONFIG_NAMES)), 5);
+    Properties filteredAdminClientProp =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(_consumerProp, 
ADMIN_CLIENT_CONFIG_NAMES);
+    return retry(() -> AdminClient.create(filteredAdminClientProp), 5);
   }
 
   /// Gets or creates a reusable admin client instance. The admin client is 
lazily initialized
@@ -166,7 +157,9 @@ public abstract class KafkaPartitionLevelConnectionHandler {
       synchronized (this) {
         ref = _sharedAdminClientRef;
         if (ref == null) {
-          ref = 
KafkaAdminClientManager.getInstance().getOrCreateAdminClient(_consumerProp);
+          Properties filteredAdminClientProp =
+              KafkaConfigUtils.filterAndValidateKafkaProperties(_consumerProp, 
ADMIN_CLIENT_CONFIG_NAMES);
+          ref = 
KafkaAdminClientManager.getInstance().getOrCreateAdminClient(filteredAdminClientProp);
           _sharedAdminClientRef = ref;
         }
       }
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/test/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandlerTest.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/test/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandlerTest.java
index a7a2ac9ee7a..01fc2734075 100644
--- 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/test/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandlerTest.java
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/test/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConnectionHandlerTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.plugin.stream.kafka40;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.pinot.plugin.stream.kafka.KafkaConfigProviderTestUtils;
 import org.apache.pinot.spi.stream.StreamConfig;
 import org.testng.annotations.Test;
 
@@ -44,6 +45,12 @@ public class KafkaPartitionLevelConnectionHandlerTest {
     return new StreamConfig("testTable_REALTIME", streamConfigMap);
   }
 
+  @Test
+  public void testConfigProviderReferencesReachKafkaClients()
+      throws Exception {
+    
KafkaConfigProviderTestUtils.assertConfigProviderReferencesReachKafkaClients();
+  }
+
   @Test
   public void testSharedAdminClientReference() {
     StreamConfig streamConfig = createTestStreamConfig();
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigUtils.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigUtils.java
new file mode 100644
index 00000000000..e3456506ce9
--- /dev/null
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigUtils.java
@@ -0,0 +1,104 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.plugin.stream.kafka;
+
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Matcher;
+import org.apache.kafka.common.config.AbstractConfig;
+import org.apache.kafka.common.config.ConfigException;
+import org.apache.kafka.common.config.ConfigTransformer;
+import org.apache.kafka.common.config.provider.FileConfigProvider;
+
+
+/// Shared helpers for preparing Kafka client configuration. The helpers are 
stateless and thread-safe.
+public class KafkaConfigUtils {
+  private static final String CONFIG_PROVIDERS_PREFIX = 
AbstractConfig.CONFIG_PROVIDERS_CONFIG + ".";
+  private static final String CONFIG_PROVIDER_CLASS_SUFFIX = ".class";
+  private static final String CONFIG_PROVIDER_PARAM_PREFIX = ".param.";
+
+  private KafkaConfigUtils() {
+  }
+
+  /// Filters properties to the target Kafka client's known configurations and 
Kafka's dynamic ConfigProvider
+  /// namespace. Kafka may still report the provider keys themselves as 
unknown after using them for resolution.
+  ///
+  /// @param properties properties to filter
+  /// @param validConfigNames configuration names recognized by the target 
Kafka client
+  /// @return a new Properties object containing the client and ConfigProvider 
settings
+  /// @throws ConfigException if a referenced provider is undeclared, has no 
class, or is a FileConfigProvider
+  /// without an allowed-path restriction
+  public static Properties filterAndValidateKafkaProperties(Properties 
properties, Set<String> validConfigNames) {
+    Properties filteredProperties = new Properties();
+    for (String key : properties.stringPropertyNames()) {
+      if (validConfigNames.contains(key) || 
key.equals(AbstractConfig.CONFIG_PROVIDERS_CONFIG)
+          || key.startsWith(CONFIG_PROVIDERS_PREFIX)) {
+        filteredProperties.put(key, properties.get(key));
+      }
+    }
+    validateConfigProviderReferences(filteredProperties);
+    return filteredProperties;
+  }
+
+  private static void validateConfigProviderReferences(Properties properties) {
+    Set<String> configuredProviders = getConfiguredProviders(properties);
+    for (String key : properties.stringPropertyNames()) {
+      Matcher matcher = 
ConfigTransformer.DEFAULT_PATTERN.matcher(properties.getProperty(key));
+      while (matcher.find()) {
+        String provider = matcher.group(1);
+        if (!configuredProviders.contains(provider)) {
+          throw new ConfigException("Kafka ConfigProvider alias '" + provider 
+ "' referenced by '" + key
+              + "' is not listed in '" + 
AbstractConfig.CONFIG_PROVIDERS_CONFIG + "'");
+        }
+        validateProviderConfiguration(properties, provider, key);
+      }
+    }
+  }
+
+  private static Set<String> getConfiguredProviders(Properties properties) {
+    Set<String> configuredProviders = new HashSet<>();
+    String providers = 
properties.getProperty(AbstractConfig.CONFIG_PROVIDERS_CONFIG, "");
+    for (String provider : providers.split(",")) {
+      String trimmedProvider = provider.trim();
+      if (!trimmedProvider.isEmpty()) {
+        configuredProviders.add(trimmedProvider);
+      }
+    }
+    return configuredProviders;
+  }
+
+  private static void validateProviderConfiguration(Properties properties, 
String provider, String referencingKey) {
+    String providerPrefix = CONFIG_PROVIDERS_PREFIX + provider;
+    String providerClassKey = providerPrefix + CONFIG_PROVIDER_CLASS_SUFFIX;
+    String providerClass = properties.getProperty(providerClassKey);
+    if (providerClass == null || providerClass.trim().isEmpty()) {
+      throw new ConfigException("Kafka ConfigProvider alias '" + provider + "' 
referenced by '" + referencingKey
+          + "' does not define '" + providerClassKey + "'");
+    }
+    if (providerClass.equals(FileConfigProvider.class.getName())) {
+      String allowedPathsKey = providerPrefix + CONFIG_PROVIDER_PARAM_PREFIX + 
FileConfigProvider.ALLOWED_PATHS_CONFIG;
+      String allowedPaths = properties.getProperty(allowedPathsKey);
+      if (allowedPaths == null || allowedPaths.trim().isEmpty()) {
+        throw new ConfigException("Kafka FileConfigProvider alias '" + 
provider + "' referenced by '" + referencingKey
+            + "' must define '" + allowedPathsKey + "'");
+      }
+    }
+  }
+}
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtils.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtils.java
index 92e4a5d322f..19895e0fe55 100644
--- 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtils.java
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/main/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtils.java
@@ -39,6 +39,7 @@ import java.util.Arrays;
 import java.util.Base64;
 import java.util.Properties;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.kafka.common.config.ConfigTransformer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -81,6 +82,17 @@ public class KafkaSSLUtils {
     String trustStoreLocation = 
consumerProps.getProperty(SSL_TRUSTSTORE_LOCATION);
     String trustStorePassword = 
consumerProps.getProperty(SSL_TRUSTSTORE_PASSWORD);
     String serverCertificate = 
consumerProps.getProperty(STREAM_KAFKA_SSL_SERVER_CERTIFICATE);
+    if (StringUtils.isNotEmpty(serverCertificate)) {
+      validateAutoSslProperties(consumerProps, SSL_TRUSTSTORE_LOCATION, 
SSL_TRUSTSTORE_PASSWORD,
+          STREAM_KAFKA_SSL_SERVER_CERTIFICATE, 
STREAM_KAFKA_SSL_CERTIFICATE_TYPE, SSL_TRUSTSTORE_TYPE);
+
+      String clientCertificate = 
consumerProps.getProperty(STREAM_KAFKA_SSL_CLIENT_CERTIFICATE);
+      if (StringUtils.isNotEmpty(clientCertificate)) {
+        validateAutoSslProperties(consumerProps, SSL_KEYSTORE_LOCATION, 
SSL_KEYSTORE_PASSWORD, SSL_KEY_PASSWORD,
+            STREAM_KAFKA_SSL_CLIENT_CERTIFICATE, STREAM_KAFKA_SSL_CLIENT_KEY,
+            STREAM_KAFKA_SSL_CLIENT_KEY_ALGORITHM, SSL_KEYSTORE_TYPE);
+      }
+    }
     if (StringUtils.isAnyEmpty(trustStoreLocation, trustStorePassword, 
serverCertificate)) {
       LOGGER.info("Skipping auto SSL server validation since it's not 
configured.");
       return;
@@ -108,6 +120,16 @@ public class KafkaSSLUtils {
     }
   }
 
+  private static void validateAutoSslProperties(Properties consumerProps, 
String... propertyNames) {
+    for (String propertyName : propertyNames) {
+      String value = consumerProps.getProperty(propertyName);
+      if (value != null && 
ConfigTransformer.DEFAULT_PATTERN.matcher(value).find()) {
+        throw new IllegalArgumentException("Kafka ConfigProvider references 
are not supported for '" + propertyName
+            + "' when Pinot auto-generates Kafka SSL stores; use a prebuilt 
keystore or truststore file instead");
+      }
+    }
+  }
+
   @VisibleForTesting
   static void initTrustStore(Properties consumerProps) {
     Path trustStorePath = getTrustStorePath(consumerProps);
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigProviderTestUtils.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigProviderTestUtils.java
new file mode 100644
index 00000000000..0a2ebcff219
--- /dev/null
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigProviderTestUtils.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.plugin.stream.kafka;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.kafka.clients.admin.AdminClientConfig;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.common.config.AbstractConfig;
+import org.apache.kafka.common.config.SslConfigs;
+import org.apache.kafka.common.config.provider.FileConfigProvider;
+import org.apache.kafka.common.serialization.BytesDeserializer;
+import org.apache.pinot.spi.config.ConfigUtils;
+import org.apache.pinot.spi.config.table.IndexingConfig;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+
+
+/// Test helper that verifies ConfigProvider resolution against the Kafka 
client version on the caller's classpath.
+public class KafkaConfigProviderTestUtils {
+  private KafkaConfigProviderTestUtils() {
+  }
+
+  public static void assertConfigProviderReferencesReachKafkaClients()
+      throws Exception {
+    Path providerFile = Files.createTempFile("kafka-config-provider", 
".properties");
+    try {
+      Files.writeString(providerFile, "keystore.password=test-password\n");
+
+      String passwordReference = "${file:" + providerFile + 
":keystore.password}";
+      Map<String, String> streamConfigs = new HashMap<>();
+      streamConfigs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, 
"localhost:9092");
+      streamConfigs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
BytesDeserializer.class.getName());
+      streamConfigs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
BytesDeserializer.class.getName());
+      streamConfigs.put(AbstractConfig.CONFIG_PROVIDERS_CONFIG, "file");
+      streamConfigs.put("config.providers.file.class", 
FileConfigProvider.class.getName());
+      streamConfigs.put("config.providers.file.param.allowed.paths", 
providerFile.getParent().toString());
+      streamConfigs.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, "$" + 
passwordReference);
+      streamConfigs.put("streamType", "kafka");
+
+      IndexingConfig indexingConfig = new IndexingConfig();
+      indexingConfig.setStreamConfigs(streamConfigs);
+      IndexingConfig resolvedIndexingConfig =
+          ConfigUtils.applyConfigWithEnvVariablesAndSystemProperties(Map.of(), 
indexingConfig);
+      Properties properties = new Properties();
+      properties.putAll(resolvedIndexingConfig.getStreamConfigs());
+      
assertEquals(properties.getProperty(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG), 
passwordReference);
+
+      Properties consumerProperties =
+          KafkaConfigUtils.filterAndValidateKafkaProperties(properties, 
ConsumerConfig.configNames());
+      assertProviderProperties(consumerProperties, providerFile);
+      assertEquals(new 
ConsumerConfig(consumerProperties).getPassword(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG).value(),
+          "test-password");
+
+      Properties adminProperties =
+          KafkaConfigUtils.filterAndValidateKafkaProperties(properties, 
AdminClientConfig.configNames());
+      assertProviderProperties(adminProperties, providerFile);
+      assertEquals(new 
AdminClientConfig(adminProperties).getPassword(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG).value(),
+          "test-password");
+    } finally {
+      Files.deleteIfExists(providerFile);
+    }
+  }
+
+  private static void assertProviderProperties(Properties properties, Path 
providerFile) {
+    
assertEquals(properties.getProperty(AbstractConfig.CONFIG_PROVIDERS_CONFIG), 
"file");
+    assertEquals(properties.getProperty("config.providers.file.class"), 
FileConfigProvider.class.getName());
+    
assertEquals(properties.getProperty("config.providers.file.param.allowed.paths"),
+        providerFile.getParent().toString());
+    assertFalse(properties.containsKey("streamType"));
+  }
+}
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigUtilsTest.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigUtilsTest.java
new file mode 100644
index 00000000000..8d884979a72
--- /dev/null
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaConfigUtilsTest.java
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.plugin.stream.kafka;
+
+import java.util.Properties;
+import java.util.Set;
+import org.apache.kafka.common.config.AbstractConfig;
+import org.apache.kafka.common.config.ConfigException;
+import org.apache.kafka.common.config.provider.FileConfigProvider;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.expectThrows;
+
+
+public class KafkaConfigUtilsTest {
+
+  @Test
+  public void testFilterKafkaPropertiesPreservesConfigProviders() {
+    Properties properties = createFileProviderProperties();
+    properties.setProperty("bootstrap.servers", "localhost:9092");
+    properties.setProperty("streamType", "kafka");
+
+    Properties filteredProperties =
+        KafkaConfigUtils.filterAndValidateKafkaProperties(properties,
+            Set.of("bootstrap.servers", "ssl.keystore.password"));
+
+    assertEquals(filteredProperties.getProperty("bootstrap.servers"), 
"localhost:9092");
+    
assertEquals(filteredProperties.getProperty(AbstractConfig.CONFIG_PROVIDERS_CONFIG),
 "file");
+    
assertEquals(filteredProperties.getProperty("config.providers.file.class"), 
FileConfigProvider.class.getName());
+    
assertEquals(filteredProperties.getProperty("config.providers.file.param.allowed.paths"),
 "/vault/secrets");
+    assertFalse(filteredProperties.containsKey("streamType"));
+  }
+
+  @Test
+  public void testFilterKafkaPropertiesRejectsUndeclaredProvider() {
+    Properties properties = new Properties();
+    properties.setProperty("ssl.keystore.password", 
"${file:/vault/secrets/kafka.properties:password}");
+
+    ConfigException exception = expectThrows(ConfigException.class,
+        () -> KafkaConfigUtils.filterAndValidateKafkaProperties(properties, 
Set.of("ssl.keystore.password")));
+
+    assertEquals(exception.getMessage(), "Kafka ConfigProvider alias 'file' 
referenced by 'ssl.keystore.password' "
+        + "is not listed in 'config.providers'");
+  }
+
+  @Test
+  public void testFilterKafkaPropertiesRejectsProviderWithoutClass() {
+    Properties properties = new Properties();
+    properties.setProperty(AbstractConfig.CONFIG_PROVIDERS_CONFIG, "file");
+    properties.setProperty("ssl.keystore.password", 
"${file:/vault/secrets/kafka.properties:password}");
+
+    ConfigException exception = expectThrows(ConfigException.class,
+        () -> KafkaConfigUtils.filterAndValidateKafkaProperties(properties, 
Set.of("ssl.keystore.password")));
+
+    assertEquals(exception.getMessage(), "Kafka ConfigProvider alias 'file' 
referenced by 'ssl.keystore.password' "
+        + "does not define 'config.providers.file.class'");
+  }
+
+  @Test
+  public void testFilterKafkaPropertiesRequiresAllowedPathsForFileProvider() {
+    Properties properties = createFileProviderProperties();
+    properties.remove("config.providers.file.param.allowed.paths");
+
+    ConfigException exception = expectThrows(ConfigException.class,
+        () -> KafkaConfigUtils.filterAndValidateKafkaProperties(properties, 
Set.of("ssl.keystore.password")));
+
+    assertEquals(exception.getMessage(), "Kafka FileConfigProvider alias 
'file' referenced by "
+        + "'ssl.keystore.password' must define 
'config.providers.file.param.allowed.paths'");
+  }
+
+  private static Properties createFileProviderProperties() {
+    Properties properties = new Properties();
+    properties.setProperty(AbstractConfig.CONFIG_PROVIDERS_CONFIG, "file");
+    properties.setProperty("config.providers.file.class", 
FileConfigProvider.class.getName());
+    properties.setProperty("config.providers.file.param.allowed.paths", 
"/vault/secrets");
+    properties.setProperty("ssl.keystore.password", 
"${file:/vault/secrets/kafka.properties:password}");
+    return properties;
+  }
+}
diff --git 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtilsTest.java
 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtilsTest.java
index 64d2505ae0d..a9d7bf83979 100644
--- 
a/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtilsTest.java
+++ 
b/pinot-plugins/pinot-stream-ingestion/pinot-kafka-base/src/test/java/org/apache/pinot/plugin/stream/kafka/KafkaSSLUtilsTest.java
@@ -34,6 +34,7 @@ import java.security.SecureRandom;
 import java.security.Security;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.Enumeration;
 import java.util.Properties;
@@ -141,6 +142,76 @@ public class KafkaSSLUtilsTest {
     validateTrustStoreCertificateCount(1);
   }
 
+  @Test
+  public void testInitSSLRejectsConfigProviderTrustStorePassword() {
+    Properties consumerProps = new Properties();
+    consumerProps.setProperty("ssl.truststore.location", _trustStorePath);
+    consumerProps.setProperty("ssl.truststore.password", 
"${file:/vault/secrets/kafka.properties:password}");
+    consumerProps.setProperty("stream.kafka.ssl.server.certificate", 
"certificate");
+
+    IllegalArgumentException exception =
+        Assert.expectThrows(IllegalArgumentException.class, () -> 
KafkaSSLUtils.initSSL(consumerProps));
+
+    Assert.assertEquals(exception.getMessage(), "Kafka ConfigProvider 
references are not supported for "
+        + "'ssl.truststore.password' when Pinot auto-generates Kafka SSL 
stores; use a prebuilt keystore or "
+        + "truststore file instead");
+  }
+
+  @Test
+  public void testInitSSLRejectsConfigProviderKeyStorePassword()
+      throws CertificateException, NoSuchAlgorithmException, 
OperatorCreationException, NoSuchProviderException {
+    Properties consumerProps = new Properties();
+    setTrustStoreProps(consumerProps);
+    setKeyStoreProps(consumerProps);
+    consumerProps.setProperty("ssl.keystore.password", 
"${file:/vault/secrets/kafka.properties:password}");
+
+    IllegalArgumentException exception =
+        Assert.expectThrows(IllegalArgumentException.class, () -> 
KafkaSSLUtils.initSSL(consumerProps));
+
+    Assert.assertEquals(exception.getMessage(), "Kafka ConfigProvider 
references are not supported for "
+        + "'ssl.keystore.password' when Pinot auto-generates Kafka SSL stores; 
use a prebuilt keystore or truststore "
+        + "file instead");
+  }
+
+  @Test
+  public void testInitSSLRejectsConfigProviderKeyPassword()
+      throws CertificateException, NoSuchAlgorithmException, 
OperatorCreationException, NoSuchProviderException {
+    Properties consumerProps = new Properties();
+    setTrustStoreProps(consumerProps);
+    setKeyStoreProps(consumerProps);
+    consumerProps.setProperty("ssl.key.password", 
"${file:/vault/secrets/kafka.properties:password}");
+
+    IllegalArgumentException exception =
+        Assert.expectThrows(IllegalArgumentException.class, () -> 
KafkaSSLUtils.initSSL(consumerProps));
+
+    Assert.assertEquals(exception.getMessage(), "Kafka ConfigProvider 
references are not supported for "
+        + "'ssl.key.password' when Pinot auto-generates Kafka SSL stores; use 
a prebuilt keystore or truststore file "
+        + "instead");
+  }
+
+  @Test
+  public void testInitSSLRejectsConfigProviderBeforeRenewingExistingStore()
+      throws CertificateException, NoSuchAlgorithmException, 
OperatorCreationException, NoSuchProviderException,
+             IOException {
+    Properties consumerProps = new Properties();
+    setTrustStoreProps(consumerProps);
+    KafkaSSLUtils.initSSL(consumerProps);
+    byte[] originalTrustStore = Files.readAllBytes(Paths.get(_trustStorePath));
+
+    setTrustStoreProps(consumerProps);
+    setKeyStoreProps(consumerProps);
+    consumerProps.setProperty("stream.kafka.ssl.client.key.algorithm",
+        "${file:/vault/secrets/kafka.properties:algorithm}");
+
+    IllegalArgumentException exception =
+        Assert.expectThrows(IllegalArgumentException.class, () -> 
KafkaSSLUtils.initSSL(consumerProps));
+
+    Assert.assertEquals(exception.getMessage(), "Kafka ConfigProvider 
references are not supported for "
+        + "'stream.kafka.ssl.client.key.algorithm' when Pinot auto-generates 
Kafka SSL stores; use a prebuilt "
+        + "keystore or truststore file instead");
+    
Assert.assertTrue(Arrays.equals(Files.readAllBytes(Paths.get(_trustStorePath)), 
originalTrustStore));
+  }
+
   @Test (expectedExceptions = java.io.FileNotFoundException.class)
   public void testInitSSLKeyStoreOnly()
       throws CertificateException, NoSuchAlgorithmException, 
OperatorCreationException, NoSuchProviderException,
@@ -163,7 +234,6 @@ public class KafkaSSLUtilsTest {
     setTrustStoreProps(consumerProps);
     setKeyStoreProps(consumerProps);
     KafkaSSLUtils.initSSL(consumerProps);
-
     // renew the truststore and keystore
     setTrustStoreProps(consumerProps);
     setKeyStoreProps(consumerProps);
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/config/ConfigUtils.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/config/ConfigUtils.java
index db24f09e41d..cfa4523477e 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/config/ConfigUtils.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/ConfigUtils.java
@@ -48,7 +48,8 @@ public class ConfigUtils {
     return applyConfigWithEnvVariablesAndSystemProperties(combinedMap, config);
   }
 
-  /// Apply a map of config to any given BaseJsonConfig with templates.
+  /// Apply a map of config to any given BaseJsonConfig with templates. Prefix 
a template with an additional '$'
+  /// (for example, `$${name:value}`) to preserve it as a literal 
`${name:value}` for downstream processing.
   ///
   /// @return Config with the configs applied.
   public static <T extends BaseJsonConfig> T 
applyConfigWithEnvVariablesAndSystemProperties(
@@ -92,6 +93,9 @@ public class ConfigUtils {
         break;
       case STRING:
         final String field = jsonNode.asText();
+        if (field.startsWith("$${") && field.endsWith("}")) {
+          return JsonNodeFactory.instance.textNode(field.substring(1));
+        }
         if (field.startsWith("${") && field.endsWith("}")) {
           String[] envVarSplits = field.substring(2, field.length() - 
1).split(":", 2);
           String envVarKey = envVarSplits[0];
diff --git 
a/pinot-spi/src/test/java/org/apache/pinot/spi/config/ConfigUtilsTest.java 
b/pinot-spi/src/test/java/org/apache/pinot/spi/config/ConfigUtilsTest.java
index 586def3ca9f..7ab927c0013 100644
--- a/pinot-spi/src/test/java/org/apache/pinot/spi/config/ConfigUtilsTest.java
+++ b/pinot-spi/src/test/java/org/apache/pinot/spi/config/ConfigUtilsTest.java
@@ -56,6 +56,35 @@ public class ConfigUtilsTest {
     System.clearProperty("AWS_SECRET_KEY");
   }
 
+  @Test
+  public void testEscapedConfigReferenceIsPreserved() {
+    IndexingConfig indexingConfig = new IndexingConfig();
+    Map<String, String> streamConfigMap = new HashMap<>();
+    streamConfigMap.put("ssl.keystore.password", 
"$${file:/vault/secrets/kafka.properties:keystore.password}");
+    streamConfigMap.put("ssl.truststore.password", 
"${PINOT_KAFKA_TRUSTSTORE_PASSWORD:fallback}");
+    indexingConfig.setStreamConfigs(streamConfigMap);
+
+    IndexingConfig resolvedConfig =
+        ConfigUtils.applyConfigWithEnvVariablesAndSystemProperties(Map.of(), 
indexingConfig);
+
+    
assertEquals(resolvedConfig.getStreamConfigs().get("ssl.keystore.password"),
+        "${file:/vault/secrets/kafka.properties:keystore.password}");
+    
assertEquals(resolvedConfig.getStreamConfigs().get("ssl.truststore.password"), 
"fallback");
+  }
+
+  @Test
+  public void testEscapedAndUnescapedConfigReferences() {
+    IndexingConfig indexingConfig = new IndexingConfig();
+    indexingConfig.setStreamConfigs(
+        Map.of("escaped", "$${file:fallback}", "unescaped", 
"${file:fallback}"));
+
+    IndexingConfig resolvedConfig =
+        ConfigUtils.applyConfigWithEnvVariablesAndSystemProperties(Map.of(), 
indexingConfig);
+
+    assertEquals(resolvedConfig.getStreamConfigs().get("escaped"), 
"${file:fallback}");
+    assertEquals(resolvedConfig.getStreamConfigs().get("unescaped"), 
"fallback");
+  }
+
   private void testIndexingWithConfig(Map<String, String> configOverride) {
     IndexingConfig indexingConfig = new IndexingConfig();
     indexingConfig.setLoadMode("${LOAD_MODE}");


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

Reply via email to