This is an automated email from the ASF dual-hosted git repository.
sarutak pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 1c78b9124d5b [SPARK-57896][CORE][TEST] Add Kerberos coexistence and
per-user token integration tests
1c78b9124d5b is described below
commit 1c78b9124d5bcd2ce7c843143cf85177f30c75c3
Author: Shrirang Mhalgi <[email protected]>
AuthorDate: Sat Aug 15 07:47:30 2026 +0900
[SPARK-57896][CORE][TEST] Add Kerberos coexistence and per-user token
integration tests
### What changes were proposed in this pull request?
Add `OidcCredentialIntegrationSuite` with 13 integration tests that verify
the coexistence of the new OIDC credential propagation framework
(`UserCredentialManager`) with the existing Kerberos delegation token system
(`HadoopDelegationTokenManager`).
The tests cover:
- OIDC credential delivery via the `UserCredentialManager` callback using
the existing `FakeCredentialProvider`
- Credential refresh on token expiry with version monotonicity guarantees
- Per-user identity tokens produce credentials identically to
workload-level tokens
- Both `UserCredentialManager` and `HadoopDelegationTokenManager` active
simultaneously without interference
- Failure isolation: OIDC failure does not affect delegation tokens, and
vice versa
- `UserCredentials` serialization roundtrip (executor receive path)
- Clean shutdown lifecycle
- Feature-disabled state does not interfere with the other credential system
### Why are the changes needed?
SPARK-57896 is a sub-task of the OIDC Credential Propagation SPIP
(SPARK-57703). The SPIP's Q8 mid-term success criteria require integration
tests proving that:
- A Fake `CredentialProvider` delivers `ServiceCredential` to executors and
refreshes on expiry
- The same test passes with per-user identity tokens
- Both credential systems coexist without interference
- Existing Kerberos delegation token tests pass without modification
- These tests did not previously exist. All core SPI components are now
merged on master, making these tests implementable.
### Does this PR introduce _any_ user-facing change?
No. This PR adds only test code.
### How was this patch tested?
All 13 new tests pass in `OidcCredentialIntegrationSuite`. The existing
credential suites pass without modification:
- `UserCredentialManagerSuite`
- `NonKerberosCredentialsSuite`
- `HadoopDelegationTokenManagerSuite`
- `HadoopFSDelegationTokenProviderSuite`
### Was this patch authored or co-authored using generative AI tooling?
Yes. Co-Authored using Claude Opus 4.6
Closes #57866 from
shrirangmhalgi/SPARK-57896-oidc-kerberos-integration-tests.
Authored-by: Shrirang Mhalgi <[email protected]>
Signed-off-by: Kousuke Saruta <[email protected]>
---
.../security/OidcCredentialIntegrationSuite.scala | 595 +++++++++++++++++++++
.../spark/scheduler/TaskSetManagerSuite.scala | 45 ++
2 files changed, 640 insertions(+)
diff --git
a/core/src/test/scala/org/apache/spark/deploy/security/OidcCredentialIntegrationSuite.scala
b/core/src/test/scala/org/apache/spark/deploy/security/OidcCredentialIntegrationSuite.scala
new file mode 100644
index 000000000000..be660693b319
--- /dev/null
+++
b/core/src/test/scala/org/apache/spark/deploy/security/OidcCredentialIntegrationSuite.scala
@@ -0,0 +1,595 @@
+/*
+ * 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.spark.deploy.security
+
+import java.io.File
+import java.nio.file.Files
+import java.time.Instant
+import java.util.Optional
+import java.util.concurrent.atomic.{AtomicInteger, AtomicLong, AtomicReference}
+
+import scala.concurrent.duration._
+
+import org.apache.hadoop.conf.Configuration
+import org.apache.hadoop.io.Text
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.{mock, verify}
+import org.scalatest.concurrent.Eventually.{eventually, timeout}
+
+import org.apache.spark.{SparkConf, SparkFunSuite, VersionedCredentials}
+import org.apache.spark.deploy.SparkHadoopUtil
+import org.apache.spark.internal.config._
+import org.apache.spark.internal.config.Network.NETWORK_CRYPTO_ENABLED
+import org.apache.spark.rpc.RpcEndpointRef
+import
org.apache.spark.scheduler.cluster.CoarseGrainedClusterMessages.UpdateDelegationTokens
+import org.apache.spark.security._
+
+/**
+ * Integration tests for SPARK-57896: Kerberos coexistence and per-user token
tests.
+ *
+ * Verifies that:
+ * 1. UserCredentialManager (OIDC) delivers credentials via the update callback
+ * 2. Credential refresh works end-to-end with expiring tokens
+ * 3. Per-user identity tokens produce valid credentials identically to
workload tokens
+ * 4. Both UserCredentialManager and HadoopDelegationTokenManager can run
simultaneously
+ * without interfering with each other
+ * 5. Failure in one credential system does not affect the other
+ * 6. TaskDescription credentials are applied to the executor store with
version guard
+ */
+class OidcCredentialIntegrationSuite extends SparkFunSuite {
+
+ private var tokenFile: File = _
+
+ override def beforeEach(): Unit = {
+ super.beforeEach()
+ CredentialProviderLoader.resetForTesting()
+ tokenFile = File.createTempFile("oidc-token-", ".jwt")
+ tokenFile.deleteOnExit()
+ writeTokenFile("fake.jwt.token.workload")
+ }
+
+ override def afterEach(): Unit = {
+ try {
+ if (tokenFile != null) tokenFile.delete()
+ } finally {
+ CredentialProviderLoader.resetForTesting()
+ super.afterEach()
+ }
+ }
+
+ private def writeTokenFile(content: String): Unit = {
+ Files.writeString(tokenFile.toPath, content)
+ }
+
+ private def createOidcConf(): SparkConf = {
+ new SparkConf(loadDefaults = false)
+ .set(SECURITY_OIDC_ENABLED, true)
+ .set(SECURITY_OIDC_IDENTITY_TOKEN_FILE, tokenFile.getAbsolutePath)
+ .set(SECURITY_OIDC_RENEWAL_SAFETY_MARGIN, 5000L)
+ .set(SECURITY_OIDC_RENEWAL_MIN_INTERVAL, 1000L)
+ }
+
+ private def createUserContext(
+ principal: String = "test-user",
+ expiresInSeconds: Long = 300): UserContext = {
+ val now = Instant.now()
+ new UserContext(
+ principal,
+ "https://issuer.example.com",
+ "fake.jwt.token",
+ now,
+ now.plusSeconds(expiresInSeconds))
+ }
+
+ private def createIngestor(ctx: UserContext): TokenIngestor = {
+ new TokenIngestor {
+ override def load(): Optional[UserContext] = Optional.of(ctx)
+ }
+ }
+
+ private def createFreshExpiryIngestor(expiresInSeconds: Long): TokenIngestor
= {
+ new TokenIngestor {
+ override def load(): Optional[UserContext] =
+ Optional.of(createUserContext(expiresInSeconds = expiresInSeconds))
+ }
+ }
+
+ private def createFailingIngestor(): TokenIngestor = {
+ new TokenIngestor {
+ override def load(): Optional[UserContext] = Optional.empty()
+ }
+ }
+
+ test("OIDC credential delivery via update callback") {
+ val conf = createOidcConf()
+ val ctx = createUserContext()
+ val callbackRef = new AtomicReference[Array[Byte]]()
+ val callbackVersion = new AtomicLong(0L)
+
+ val manager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (version, bytes) => {
+ callbackVersion.set(version)
+ callbackRef.set(bytes)
+ })
+
+ try {
+ val (version, initialBytes) = manager.start()
+
+ assert(version == 1L, "Initial version should be 1")
+ assert(initialBytes != null, "Initial credentials should not be null")
+
+ val credentials =
UserCredentialManager.deserializeUserCredentials(initialBytes)
+ assert(credentials != null, "Deserialized credentials should not be
null")
+
+ val fakeCred = credentials.forScheme("fake")
+ assert(fakeCred.isPresent, "Should have credential for scheme 'fake'")
+ assert(fakeCred.get().getProperties.get("provider") == "fake",
+ "Credential should come from FakeCredentialProvider")
+ assert(!fakeCred.get().isExpired(Instant.now()),
+ "Credential should not be expired immediately after resolution")
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("credential refresh works end-to-end on expiry") {
+ val conf = createOidcConf()
+ .set(SECURITY_OIDC_RENEWAL_SAFETY_MARGIN, 2000L)
+ .set(SECURITY_OIDC_RENEWAL_MIN_INTERVAL, 500L)
+
+ val updateCount = new AtomicInteger(0)
+ val latestVersion = new AtomicLong(0L)
+
+ // Return a fresh UserContext on each load() so each renewal gets a
genuinely
+ // new expiry rather than spinning on an already-expired token.
+ val manager = new UserCredentialManager(
+ conf,
+ createFreshExpiryIngestor(expiresInSeconds = 3),
+ (version, _) => {
+ latestVersion.set(version)
+ updateCount.incrementAndGet()
+ })
+
+ try {
+ manager.start()
+ assert(updateCount.get() == 1, "Should have exactly 1 update after
start()")
+
+ eventually(timeout(15.seconds)) {
+ assert(updateCount.get() >= 2,
+ s"Expected at least 2 updates (got ${updateCount.get()}), " +
+ "indicating credential renewal occurred")
+ }
+
+ assert(latestVersion.get() >= 2L,
+ "Version should be at least 2 after renewal")
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("per-user identity token produces valid credentials") {
+ val conf = createOidcConf()
+
+ val userCtx = createUserContext(principal = "[email protected]")
+ val callbackRef = new AtomicReference[Array[Byte]]()
+
+ val manager = new UserCredentialManager(
+ conf,
+ createIngestor(userCtx),
+ (_, bytes) => callbackRef.set(bytes))
+
+ try {
+ val (_, initialBytes) = manager.start()
+
+ val credentials =
UserCredentialManager.deserializeUserCredentials(initialBytes)
+ val fakeCred = credentials.forScheme("fake")
+ assert(fakeCred.isPresent,
+ "Per-user token should produce credentials for scheme 'fake'")
+ assert(fakeCred.get().getProperties.get("provider") == "fake",
+ "Per-user credential should come from FakeCredentialProvider")
+ assert(!fakeCred.get().isExpired(Instant.now()),
+ "Per-user credential should not be expired")
+
+ // Verify the credential is identical in structure to workload token
output
+ val workloadCtx = createUserContext(principal = "workload-identity")
+ val workloadManager = new UserCredentialManager(
+ conf,
+ createIngestor(workloadCtx),
+ (_, _) => ())
+ try {
+ val (_, workloadBytes) = workloadManager.start()
+ val workloadCreds =
UserCredentialManager.deserializeUserCredentials(workloadBytes)
+ val workloadFake = workloadCreds.forScheme("fake")
+ assert(workloadFake.isPresent)
+ assert(workloadFake.get().getProperties ==
fakeCred.get().getProperties,
+ "Per-user and workload tokens should produce identical credential
properties")
+ } finally {
+ workloadManager.stop()
+ }
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("UserCredentialManager and HadoopDelegationTokenManager coexist") {
+ val hadoopConf = new Configuration()
+ val mockRef = mock(classOf[RpcEndpointRef])
+
+ val conf = createOidcConf()
+ .set(DIRECT_CREDENTIAL_PROVIDERS_ENABLED, true)
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(NETWORK_CRYPTO_ENABLED, true)
+
+ val ctx = createUserContext()
+ val oidcCallbackRef = new AtomicReference[Array[Byte]]()
+ val oidcVersion = new AtomicLong(0L)
+
+ val oidcManager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (version, bytes) => {
+ oidcVersion.set(version)
+ oidcCallbackRef.set(bytes)
+ })
+
+ val dtManager = new HadoopDelegationTokenManager(conf, hadoopConf, mockRef)
+
+ try {
+ val (oidcVer, oidcBytes) = oidcManager.start()
+ assert(oidcVer == 1L)
+ assert(oidcBytes != null)
+
+ val dtTokens = dtManager.start()
+ assert(dtTokens != null, "DT manager should produce tokens")
+
+ val oidcCreds =
UserCredentialManager.deserializeUserCredentials(oidcBytes)
+ assert(oidcCreds.forScheme("fake").isPresent,
+ "OIDC credentials should contain 'fake' scheme")
+
+ // HadoopDelegationTokenManager.start() is synchronous -- verify directly
+ val captor = ArgumentCaptor.forClass(classOf[Any])
+ verify(mockRef).send(captor.capture())
+ val msg = captor.getValue.asInstanceOf[UpdateDelegationTokens]
+ val dtCreds = SparkHadoopUtil.get.deserialize(msg.tokens)
+ assert(dtCreds.getSecretKey(new Text("test.direct.credential")) != null,
+ "DT credentials should contain test.direct.credential")
+ assert(new String(dtCreds.getSecretKey(new
Text("test.direct.credential"))) === "test-token",
+ "DT credential value should match")
+
+ assert(oidcVersion.get() == 1L, "OIDC version should remain at 1")
+ } finally {
+ oidcManager.stop()
+ dtManager.stop()
+ }
+ }
+
+ test("OIDC failure does not affect HadoopDelegationTokenManager") {
+ val hadoopConf = new Configuration()
+ val mockRef = mock(classOf[RpcEndpointRef])
+
+ val conf = createOidcConf()
+ .set(DIRECT_CREDENTIAL_PROVIDERS_ENABLED, true)
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(NETWORK_CRYPTO_ENABLED, true)
+
+ val failingOidcManager = new UserCredentialManager(
+ conf,
+ createFailingIngestor(),
+ (_, _) => ())
+
+ val dtManager = new HadoopDelegationTokenManager(conf, hadoopConf, mockRef)
+
+ try {
+ // OIDC start should fail with IllegalStateException (missing token)
+ val oidcException = intercept[IllegalStateException] {
+ failingOidcManager.start()
+ }
+ assert(oidcException.getMessage.contains(
+ "identity token file is missing or malformed"))
+
+ // DT manager should still work perfectly despite OIDC failure
+ val dtTokens = dtManager.start()
+ assert(dtTokens != null, "DT manager should succeed despite OIDC
failure")
+
+ val captor = ArgumentCaptor.forClass(classOf[Any])
+ verify(mockRef).send(captor.capture())
+ val msg = captor.getValue.asInstanceOf[UpdateDelegationTokens]
+ val dtCreds = SparkHadoopUtil.get.deserialize(msg.tokens)
+ assert(dtCreds.getSecretKey(new Text("test.direct.credential")) != null,
+ "DT credentials should be unaffected by OIDC failure")
+ } finally {
+ failingOidcManager.stop()
+ dtManager.stop()
+ }
+ }
+
+ test("DT provider failure does not affect UserCredentialManager") {
+ val hadoopConf = new Configuration()
+ val mockRef = mock(classOf[RpcEndpointRef])
+
+ val conf = createOidcConf()
+ .set(DIRECT_CREDENTIAL_PROVIDERS_ENABLED, true)
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(NETWORK_CRYPTO_ENABLED, true)
+ .set("spark.security.credentials.test-direct.enabled", "false")
+ .set("spark.security.credentials.test-noexpiry.enabled", "false")
+
+ val ctx = createUserContext()
+ val oidcCallbackRef = new AtomicReference[Array[Byte]]()
+
+ val oidcManager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (_, bytes) => oidcCallbackRef.set(bytes))
+
+ val dtManager = new HadoopDelegationTokenManager(conf, hadoopConf, mockRef)
+
+ try {
+ val dtTokens = dtManager.start()
+ assert(dtTokens == null, "DT manager should return null when all
providers fail")
+
+ val (oidcVer, oidcBytes) = oidcManager.start()
+ assert(oidcVer == 1L, "OIDC version should be 1")
+ assert(oidcBytes != null, "OIDC should produce credentials despite DT
failure")
+
+ val oidcCreds =
UserCredentialManager.deserializeUserCredentials(oidcBytes)
+ assert(oidcCreds.forScheme("fake").isPresent,
+ "OIDC credentials should be unaffected by DT failure")
+ } finally {
+ oidcManager.stop()
+ dtManager.stop()
+ }
+ }
+
+ test("deserialization idempotency preserves credential content") {
+ val conf = createOidcConf()
+ val ctx = createUserContext()
+ val serializedRef = new AtomicReference[Array[Byte]]()
+
+ val manager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (_, bytes) => serializedRef.set(bytes))
+
+ try {
+ manager.start()
+
+ val bytes = serializedRef.get()
+ assert(bytes != null && bytes.length > 0, "Serialized credentials should
be non-empty")
+
+ val creds1 = UserCredentialManager.deserializeUserCredentials(bytes)
+ val creds2 = UserCredentialManager.deserializeUserCredentials(bytes)
+
+ assert(creds1.forScheme("fake").isPresent)
+ assert(creds2.forScheme("fake").isPresent)
+ assert(creds1.forScheme("fake").get().getProperties ==
+ creds2.forScheme("fake").get().getProperties,
+ "Multiple deserializations of same bytes should produce identical
credentials")
+
+ val cred = creds1.forScheme("fake").get()
+ assert(cred.getProperties.containsKey("provider"))
+ assert(cred.getExpiresAt != null, "Credential should have an expiry set")
+ assert(!cred.isExpired(Instant.now()), "Freshly resolved credential
should not be expired")
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("case-insensitive scheme lookup in credential bundle") {
+ val conf = createOidcConf()
+ val ctx = createUserContext()
+ val serializedRef = new AtomicReference[Array[Byte]]()
+
+ val manager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (_, bytes) => serializedRef.set(bytes))
+
+ try {
+ manager.start()
+
+ val creds =
UserCredentialManager.deserializeUserCredentials(serializedRef.get())
+
+ // FakeCredentialProvider declares supportedSchemes = Set("fake",
"shared")
+ // but "shared" is ambiguous (AnotherFakeCredentialProvider also claims
it),
+ // so only "fake" auto-resolves without explicit config.
+ assert(creds.forScheme("fake").isPresent, "Should resolve 'fake' scheme")
+ assert(creds.forScheme("FAKE").isPresent,
+ "Scheme lookup should be case-insensitive")
+ assert(creds.forScheme("Fake").isPresent,
+ "Scheme lookup should be case-insensitive")
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("stop() after start() completes cleanly without exceptions") {
+ val conf = createOidcConf()
+ val ctx = createUserContext(expiresInSeconds = 60)
+
+ val manager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (_, _) => ())
+
+ manager.start()
+ manager.stop()
+
+ // Double stop should also be safe
+ manager.stop()
+ }
+
+ test("credential version is monotonically increasing across renewals") {
+ val conf = createOidcConf()
+ .set(SECURITY_OIDC_RENEWAL_SAFETY_MARGIN, 2000L)
+ .set(SECURITY_OIDC_RENEWAL_MIN_INTERVAL, 500L)
+
+ val versions = new java.util.concurrent.CopyOnWriteArrayList[Long]()
+
+ val manager = new UserCredentialManager(
+ conf,
+ createFreshExpiryIngestor(expiresInSeconds = 3),
+ (version, _) => versions.add(version))
+
+ try {
+ manager.start()
+
+ eventually(timeout(15.seconds)) {
+ assert(versions.size() >= 3,
+ s"Expected at least 3 callbacks (got ${versions.size()})")
+ }
+
+ val versionList = new java.util.ArrayList(versions)
+ for (i <- 1 until versionList.size()) {
+ assert(versionList.get(i) > versionList.get(i - 1),
+ s"Version ${versionList.get(i)} should be > ${versionList.get(i -
1)} " +
+ s"at index $i (full list: $versionList)")
+ }
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("every renewal callback provides non-null non-empty credentials") {
+ val conf = createOidcConf()
+ .set(SECURITY_OIDC_RENEWAL_SAFETY_MARGIN, 2000L)
+ .set(SECURITY_OIDC_RENEWAL_MIN_INTERVAL, 500L)
+
+ val allBytes = new java.util.concurrent.CopyOnWriteArrayList[Array[Byte]]()
+
+ val manager = new UserCredentialManager(
+ conf,
+ createFreshExpiryIngestor(expiresInSeconds = 3),
+ (_, bytes) => allBytes.add(bytes))
+
+ try {
+ manager.start()
+
+ eventually(timeout(15.seconds)) {
+ assert(allBytes.size() >= 2,
+ s"Expected at least 2 callbacks (got ${allBytes.size()})")
+ }
+
+ val it = allBytes.iterator()
+ while (it.hasNext) {
+ val bytes = it.next()
+ assert(bytes != null, "Callback bytes should never be null")
+ assert(bytes.length > 0, "Callback bytes should never be empty")
+ val creds = UserCredentialManager.deserializeUserCredentials(bytes)
+ assert(creds.forScheme("fake").isPresent,
+ "Every renewed credential bundle should contain 'fake' scheme")
+ }
+ } finally {
+ manager.stop()
+ }
+ }
+
+ test("OIDC disabled does not interfere with DT manager") {
+ val hadoopConf = new Configuration()
+ val mockRef = mock(classOf[RpcEndpointRef])
+
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SECURITY_OIDC_ENABLED, false)
+ .set(DIRECT_CREDENTIAL_PROVIDERS_ENABLED, true)
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(NETWORK_CRYPTO_ENABLED, true)
+
+ val oidcManager = UserCredentialManager.create(conf, (_, _) => ())
+ assert(oidcManager.isEmpty, "OIDC manager should not be created when
disabled")
+
+ val dtManager = new HadoopDelegationTokenManager(conf, hadoopConf, mockRef)
+ try {
+ val dtTokens = dtManager.start()
+ assert(dtTokens != null, "DT manager should work when OIDC is disabled")
+
+ val captor = ArgumentCaptor.forClass(classOf[Any])
+ verify(mockRef).send(captor.capture())
+ val msg = captor.getValue.asInstanceOf[UpdateDelegationTokens]
+ val dtCreds = SparkHadoopUtil.get.deserialize(msg.tokens)
+ assert(dtCreds.getSecretKey(new Text("test.direct.credential")) != null)
+ } finally {
+ dtManager.stop()
+ }
+ }
+
+ test("credential serialization roundtrip through VersionedCredentials
store") {
+ val conf = createOidcConf()
+ val ctx = createUserContext()
+
+ val manager = new UserCredentialManager(
+ conf,
+ createIngestor(ctx),
+ (_, _) => ())
+
+ try {
+ val (version, credentialBytes) = manager.start()
+ assert(version == 1L)
+
+ // Set up the credential store exactly as CoarseGrainedSchedulerBackend
does
+ val store = new AtomicReference[VersionedCredentials]()
+ VersionedCredentials.updateIfNewer(store, version, credentialBytes)
+
+ // Exercise the EXACT expression from TaskSetManager.scala line 607:
+ // Option(env.userCredentials.get()).map(vc => (vc.version, vc.bytes))
+ // This is what TaskSetManager reads when constructing TaskDescription.
+ val credentialTuple: Option[(Long, Array[Byte])] =
+ Option(store.get()).map(vc => (vc.version, vc.bytes))
+
+ assert(credentialTuple.isDefined,
+ "Credential store should produce Some when credentials are set")
+ assert(credentialTuple.get._1 == 1L,
+ "TaskDescription should carry version 1")
+ assert(credentialTuple.get._2 === credentialBytes,
+ "TaskDescription should carry the credential bytes from the store")
+
+ // Verify the bytes are valid credentials end-to-end
+ val creds =
UserCredentialManager.deserializeUserCredentials(credentialTuple.get._2)
+ assert(creds.forScheme("fake").isPresent,
+ "Credentials from TaskDescription should contain 'fake' scheme")
+
+ // Update the store to version 2 (simulating a renewal)
+ val v2Bytes = UserCredentialManager.serializeUserCredentials(
+ new UserCredentials(java.util.Map.of("fake",
+ new ServiceCredential(java.util.Map.of("provider", "fake-v2"),
+ Instant.now().plusSeconds(300)))))
+ VersionedCredentials.updateIfNewer(store, 2L, v2Bytes)
+
+ // Read again -- same expression as TaskSetManager line 607
+ val credentialTupleV2: Option[(Long, Array[Byte])] =
+ Option(store.get()).map(vc => (vc.version, vc.bytes))
+
+ assert(credentialTupleV2.get._1 == 2L,
+ "After renewal, TaskDescription should carry version 2")
+
+ val credsV2 =
UserCredentialManager.deserializeUserCredentials(credentialTupleV2.get._2)
+ assert(credsV2.forScheme("fake").get().getProperties.get("provider") ==
"fake-v2",
+ "TaskDescription should carry renewed credentials")
+
+ // Verify empty store produces None (no credentials yet scenario)
+ val emptyStore = new AtomicReference[VersionedCredentials]()
+ val emptyTuple: Option[(Long, Array[Byte])] =
+ Option(emptyStore.get()).map(vc => (vc.version, vc.bytes))
+ assert(emptyTuple.isEmpty,
+ "Empty store should produce None for TaskDescription.userCredentials")
+ } finally {
+ manager.stop()
+ }
+ }
+}
diff --git
a/core/src/test/scala/org/apache/spark/scheduler/TaskSetManagerSuite.scala
b/core/src/test/scala/org/apache/spark/scheduler/TaskSetManagerSuite.scala
index 5fc7c842c54b..94f4ca251c6b 100644
--- a/core/src/test/scala/org/apache/spark/scheduler/TaskSetManagerSuite.scala
+++ b/core/src/test/scala/org/apache/spark/scheduler/TaskSetManagerSuite.scala
@@ -3245,6 +3245,51 @@ class TaskSetManagerSuite
}
}
+ test("resourceOffer attaches current userCredentials to TaskDescription") {
+ sc = new SparkContext("local", "test")
+ sched = new FakeTaskScheduler(sc, ("exec1", "host1"))
+ val taskSet = FakeTask.createTaskSet(3)
+ val clock = new ManualClock()
+ val manager = new TaskSetManager(sched, taskSet, MAX_TASK_FAILURES, clock
= clock)
+
+ try {
+ // Initially no credentials in SparkEnv store
+ assert(SparkEnv.get.userCredentials.get() == null)
+ val taskOpt1 = manager.resourceOffer("exec1", "host1",
TaskLocality.ANY)._1
+ assert(taskOpt1.isDefined)
+ assert(taskOpt1.get.userCredentials.isEmpty,
+ "TaskDescription should have None when credential store is empty")
+
+ // Set credentials to version 1
+ val v1Bytes = Array[Byte](10, 20, 30, 40, 50)
+ VersionedCredentials.updateIfNewer(SparkEnv.get.userCredentials, 1L,
v1Bytes)
+
+ // Offer another task from the same set -- should carry v1
+ val taskOpt2 = manager.resourceOffer("exec1", "host1",
TaskLocality.ANY)._1
+ assert(taskOpt2.isDefined)
+ assert(taskOpt2.get.userCredentials.isDefined,
+ "TaskDescription should carry credentials after store is populated")
+ assert(taskOpt2.get.userCredentials.get._1 === 1L,
+ "TaskDescription should carry version 1")
+ assert(taskOpt2.get.userCredentials.get._2 === v1Bytes,
+ "TaskDescription should carry the v1 credential bytes")
+
+ // Update store to version 2
+ val v2Bytes = Array[Byte](50, 60, 70, 80, 90)
+ VersionedCredentials.updateIfNewer(SparkEnv.get.userCredentials, 2L,
v2Bytes)
+
+ // Offer another task -- should carry v2
+ val taskOpt3 = manager.resourceOffer("exec1", "host1",
TaskLocality.ANY)._1
+ assert(taskOpt3.isDefined)
+ assert(taskOpt3.get.userCredentials.get._1 === 2L,
+ "After renewal, TaskDescription should carry version 2")
+ assert(taskOpt3.get.userCredentials.get._2 === v2Bytes,
+ "TaskDescription should carry the v2 credential bytes")
+ } finally {
+ SparkEnv.get.userCredentials.set(null)
+ }
+ }
+
}
class FakeLongTasks(stageId: Int, partitionId: Int) extends FakeTask(stageId,
partitionId) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]