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

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


The following commit(s) were added to refs/heads/master by this push:
     new a676d999 [LIVY-1060] Livy Spark sessions fail to connect to Hive 
Metastore over SSL with keystore password error
a676d999 is described below

commit a676d999c7101ef4bb12c863536ff244e10cf420
Author: nileshrathi345 <[email protected]>
AuthorDate: Tue Aug 4 19:37:31 2026 +0530

    [LIVY-1060] Livy Spark sessions fail to connect to Hive Metastore over SSL 
with keystore password error
    
    ## What changes were proposed in this pull request?
    
    Resolves Hive Metastore SSL credential handling for Spark sessions/jobs 
submitted by Livy.
    
    - Added `enrichHiveMetastoreSslConf` in `SparkApp.prepareSparkConf` to read 
`hive.metastore.keystore.password` and `hive.metastore.truststore.password` 
from Livy’s local credential provider 
(`livy.hadoop.security.credential.provider.path`) and inject them as 
`spark.hadoop.*` properties for the Spark app.
    - Ensures YARN/K8s driver and executors get explicit HMS SSL passwords 
without needing access to the Livy host’s local JCEKS file.
    - Preserves the existing credential provider path in Spark conf (fix for 
interactive session creation regression from blanking the path).
    
    JIRA: https://issues.apache.org/jira/browse/LIVY-1060
    
    ## How was this patch tested?
    
    - Added `SparkAppSpec` unit tests for no-op, successful HMS SSL resolution, 
unrelated credentials, and unreadable provider path cases.
    - Verified credential resolution from a local JCEKS test store and no-op 
paths when provider is missing, unreadable, or has no HMS aliases.
    
    ## Was this patch authored or co-authored using generative AI tooling?
    
    No
---
 .../scala/org/apache/livy/utils/SparkApp.scala     |  56 +++++++++-
 .../scala/org/apache/livy/utils/SparkAppSpec.scala | 114 +++++++++++++++++++++
 2 files changed, 167 insertions(+), 3 deletions(-)

diff --git a/server/src/main/scala/org/apache/livy/utils/SparkApp.scala 
b/server/src/main/scala/org/apache/livy/utils/SparkApp.scala
index e424f80f..acb2b0e1 100644
--- a/server/src/main/scala/org/apache/livy/utils/SparkApp.scala
+++ b/server/src/main/scala/org/apache/livy/utils/SparkApp.scala
@@ -17,9 +17,13 @@
 
 package org.apache.livy.utils
 
+import java.io.IOException
+
 import scala.collection.JavaConverters._
 
-import org.apache.livy.LivyConf
+import org.apache.hadoop.conf.Configuration
+
+import org.apache.livy.{LivyConf, Logging}
 
 object AppInfo {
   val DRIVER_LOG_URL_NAME = "driverLogUrl"
@@ -54,7 +58,7 @@ trait SparkAppListener {
 /**
  * Provide factory methods for SparkApp.
  */
-object SparkApp {
+object SparkApp extends Logging {
   private val SPARK_YARN_TAG_KEY = "spark.yarn.tags"
 
   object State extends Enumeration {
@@ -73,7 +77,7 @@ object SparkApp {
       uniqueAppTag: String,
       livyConf: LivyConf,
       sparkConf: Map[String, String]): Map[String, String] = {
-    if (livyConf.isRunningOnYarn()) {
+    val baseConf = if (livyConf.isRunningOnYarn()) {
       val userYarnTags = sparkConf.get(SPARK_YARN_TAG_KEY).map("," + 
_).getOrElse("")
       val mergedYarnTags = uniqueAppTag + userYarnTags
       sparkConf ++ Map(
@@ -89,6 +93,52 @@ object SparkApp {
     } else {
       sparkConf
     }
+    enrichHiveMetastoreSslConf(baseConf, livyConf)
+  }
+
+  /**
+   * Reads HMS SSL passwords from LivyConf's local JCEKS provider and injects 
them as
+   * spark.hadoop.* properties for YARN/K8s drivers and executors. The 
incoming Spark
+   * conf (including any user-supplied credential provider path) is preserved; 
only
+   * HMS SSL properties are merged via `conf ++ sslProps`.
+   *
+   * No-op if no credential provider path is configured, or if it resolves to
+   * unrelated (non-HMS) secrets; in either case the original conf is returned
+   * untouched so any user-supplied provider path (e.g. an HDFS-backed one used
+   * for other credentials) is preserved.
+   */
+  private def enrichHiveMetastoreSslConf(
+      conf: Map[String, String],
+      livyConf: LivyConf): Map[String, String] = {
+    val credentialProviderPath = 
livyConf.get(LivyConf.HADOOP_CREDENTIAL_PROVIDER_PATH)
+    if (credentialProviderPath == null || credentialProviderPath.isEmpty) {
+      return conf
+    }
+    val hadoopConf = new Configuration()
+    hadoopConf.set("hadoop.security.credential.provider.path", 
credentialProviderPath)
+    val hmsSslKeys = Seq("hive.metastore.keystore.password", 
"hive.metastore.truststore.password")
+    val sslProps =
+      try {
+        hmsSslKeys.flatMap { key =>
+          Option(hadoopConf.getPassword(key)).map(_.mkString) match {
+            case Some(password) if password.nonEmpty => 
Some(s"spark.hadoop.$key" -> password)
+            case _ => None
+          }
+        }.toMap
+      } catch {
+        case e: IOException =>
+          warn(s"Could not resolve Hive Metastore SSL credentials from 
provider path " +
+            s"$credentialProviderPath: ${e.getMessage}")
+          Map.empty[String, String]
+      }
+
+    if (sslProps.isEmpty) {
+      // Nothing HMS-related found (or resolution failed); leave the user's
+      // provider path config exactly as it was.
+      conf
+    } else {
+      conf ++ sslProps
+    }
   }
 
   /**
diff --git a/server/src/test/scala/org/apache/livy/utils/SparkAppSpec.scala 
b/server/src/test/scala/org/apache/livy/utils/SparkAppSpec.scala
new file mode 100644
index 00000000..94026029
--- /dev/null
+++ b/server/src/test/scala/org/apache/livy/utils/SparkAppSpec.scala
@@ -0,0 +1,114 @@
+/*
+ * 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.livy.utils
+
+import java.nio.file.{Files, Path}
+
+import scala.collection.JavaConverters._
+
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.security.alias.CredentialProviderFactory
+import org.scalatest.FunSpec
+
+import org.apache.livy.{LivyBaseUnitTestSuite, LivyConf}
+
+class SparkAppSpec extends FunSpec with LivyBaseUnitTestSuite {
+
+  private val providerPathKey = 
"spark.hadoop.hadoop.security.credential.provider.path"
+  private val truststorePasswordKey = 
"spark.hadoop.hive.metastore.truststore.password"
+
+  private def deleteRecursively(path: Path): Unit = {
+    if (Files.exists(path)) {
+      
Files.walk(path).iterator().asScala.toSeq.reverse.foreach(Files.deleteIfExists)
+    }
+  }
+
+  private def withCredentialStore(secrets: Map[String, String])
+      (f: LivyConf => Unit): Unit = {
+    val jceksDir = Files.createTempDirectory("livy-cred-test")
+    val jceksPath = jceksDir.resolve("credentials.jceks")
+    val providerPath = s"jceks://file${jceksPath.toAbsolutePath}"
+
+    val hadoopConf = new Configuration()
+    hadoopConf.set("hadoop.security.credential.provider.path", providerPath)
+    val provider = CredentialProviderFactory.getProviders(hadoopConf).get(0)
+    secrets.foreach { case (alias, value) =>
+      provider.createCredentialEntry(alias, value.toCharArray)
+    }
+    provider.flush()
+
+    val livyConf = new LivyConf(false)
+    livyConf.set(LivyConf.HADOOP_CREDENTIAL_PROVIDER_PATH, providerPath)
+    try {
+      f(livyConf)
+    } finally {
+      deleteRecursively(jceksDir)
+    }
+  }
+
+  describe("SparkApp.prepareSparkConf") {
+    it("should leave conf unchanged when no credential provider path is 
configured") {
+      val livyConf = new LivyConf(false)
+      livyConf.set(LivyConf.LIVY_SPARK_MASTER, "local")
+      val inputConf = Map("spark.app.name" -> "test-app")
+
+      val result = SparkApp.prepareSparkConf("livy-test-tag", livyConf, 
inputConf)
+
+      assert(result === inputConf)
+    }
+
+    it("should resolve HMS SSL credentials locally and preserve the provider 
path") {
+      withCredentialStore(Map("hive.metastore.truststore.password" -> 
"trust-secret")) { livyConf =>
+        livyConf.set(LivyConf.LIVY_SPARK_MASTER, "local")
+        val inputConf = Map(
+          "spark.app.name" -> "test-app",
+          providerPathKey -> "jceks://file/local-only-path")
+
+        val result = SparkApp.prepareSparkConf("livy-test-tag", livyConf, 
inputConf)
+
+        assert(result(truststorePasswordKey) === "trust-secret")
+        assert(result(providerPathKey) === "jceks://file/local-only-path")  // 
preserved
+        assert(result("spark.app.name") === "test-app")
+      }
+    }
+
+    it("should leave conf unchanged when JCEKS has no HMS SSL aliases") {
+      withCredentialStore(Map("livy.keystore.password" -> "livy-secret")) { 
livyConf =>
+        livyConf.set(LivyConf.LIVY_SPARK_MASTER, "local")
+        val inputConf = Map(
+          "spark.app.name" -> "test-app",
+          providerPathKey -> "jceks://file/user-supplied")
+
+        val result = SparkApp.prepareSparkConf("livy-test-tag", livyConf, 
inputConf)
+
+        assert(result === inputConf)
+      }
+    }
+
+    it("should leave conf unchanged when credential provider path cannot be 
read") {
+      val livyConf = new LivyConf(false)
+      livyConf.set(LivyConf.LIVY_SPARK_MASTER, "local")
+      livyConf.set(LivyConf.HADOOP_CREDENTIAL_PROVIDER_PATH, 
"jceks://file/does-not-exist.jceks")
+      val inputConf = Map("spark.app.name" -> "test-app")
+
+      val result = SparkApp.prepareSparkConf("livy-test-tag", livyConf, 
inputConf)
+
+      assert(result === inputConf)
+    }
+  }
+}

Reply via email to