dongjoon-hyun commented on code in PR #58343:
URL: https://github.com/apache/spark/pull/58343#discussion_r4017631650
##########
docs/core-migration-guide.md:
##########
@@ -42,6 +42,8 @@ license: |
- Since Spark 4.3, a positive `spark.executor.pyspark.memory` allocation that
is too small to give each concurrent task slot at least 1 MiB fails the Python
task with an error instead of silently running the workers without any memory
limit. Setting `spark.executor.pyspark.memory=0` still disables the limit. To
restore a working memory limit, increase `spark.executor.pyspark.memory` or
reduce the executor's concurrent task capacity.
+- Since Spark 4.3, an executor pod template that names a service account in
`serviceAccountName` keeps it: Spark applies
`spark.kubernetes.authenticate.executor.serviceAccountName`, or the driver's
account as a fallback, only when the template names no account in either
`serviceAccount` or `serviceAccountName`. Earlier versions decided by reading
the deprecated `serviceAccount` field alone, so a template that named the
account in `serviceAccountName` had it overwritten. Spark logs a warning when
`spark.kubernetes.authenticate.executor.serviceAccountName` named an account
the template displaced.
Review Comment:
Two wording notes on this entry, separate from the version question.
1. The empty-value case is a user-facing change of its own, but this text
does not show it. A template with `serviceAccount: ""` and no
`serviceAccountName` used to be returned as-is, so the executors ended up on
the namespace's `default` account. Now `podServiceAccount` treats `""` as
unset, so they run as
`spark.kubernetes.authenticate.executor.serviceAccountName` or, failing that,
the driver's account. The driver's account is usually the one our RBAC example
binds to `edit` (`kubectl create clusterrolebinding spark-role
--clusterrole=edit ...` in `running-on-kubernetes.md`), so this can widen what
executors are allowed to do. The PR description lists it as the second change,
but "names no account" does not tell a reader that an empty value counts. Could
we spell it out here?
2. "had it overwritten" holds only when
`spark.kubernetes.authenticate.executor.serviceAccountName` or
`spark.kubernetes.authenticate.driver.serviceAccountName` was set. With
neither, `buildPodWithServiceAccount(None, pod)` returned the pod unchanged, so
the template's `serviceAccountName` was already kept. Stating that condition
would spare users who set neither from auditing their templates.
##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/features/ExecutorKubernetesCredentialsFeatureStep.scala:
##########
@@ -18,24 +18,60 @@ package org.apache.spark.deploy.k8s.features
import org.apache.spark.deploy.k8s.{KubernetesConf, SparkPod}
import
org.apache.spark.deploy.k8s.Config.{KUBERNETES_DRIVER_SERVICE_ACCOUNT_NAME,
KUBERNETES_EXECUTOR_SERVICE_ACCOUNT_NAME}
-import org.apache.spark.deploy.k8s.KubernetesUtils.buildPodWithServiceAccount
+import
org.apache.spark.deploy.k8s.KubernetesUtils.{buildPodWithServiceAccount,
podServiceAccount}
+import org.apache.spark.internal.Logging
+import org.apache.spark.internal.LogKeys.{CONFIG, SERVICE_ACCOUNT_NAME, VALUE}
private[spark] class ExecutorKubernetesCredentialsFeatureStep(kubernetesConf:
KubernetesConf)
- extends KubernetesFeatureConfigStep {
+ extends KubernetesFeatureConfigStep with Logging {
private lazy val driverServiceAccount =
kubernetesConf.get(KUBERNETES_DRIVER_SERVICE_ACCOUNT_NAME)
private lazy val executorServiceAccount =
kubernetesConf.get(KUBERNETES_EXECUTOR_SERVICE_ACCOUNT_NAME)
override def configurePod(pod: SparkPod): SparkPod = {
- pod.copy(
- // if not setup by the pod template, fallback to the executor's sa,
- // if executor's sa is not setup, the last option is driver's sa.
- pod = if (Option(pod.pod.getSpec.getServiceAccount).isEmpty) {
- buildPodWithServiceAccount(executorServiceAccount
- .orElse(driverServiceAccount), pod).getOrElse(pod.pod)
- } else {
- pod.pod
- })
+ podServiceAccount(pod) match {
+ // The pod template's account takes precedence, so the pod goes back as
it came.
+ case Some(templateAccount) =>
+ reportAccountNotApplied(templateAccount)
+ pod
Review Comment:
Returning the pod untouched leaves whichever field the template did not set
as `null` for the rest of the chain. User feature steps from
`spark.kubernetes.executor.pod.featureSteps` run after this step in
`KubernetesExecutorBuilder`, so with a template naming only
`serviceAccountName` and a configured account, such a step reading
`getServiceAccount` used to see the configured account and now sees `null`.
The API server's `SetDefaults_PodSpec` writes the same value into both
fields anyway, so how about mirroring that here?
```scala
case Some(templateAccount) =>
reportAccountNotApplied(templateAccount)
pod.copy(pod = buildPodWithServiceAccount(Some(templateAccount),
pod).getOrElse(pod.pod))
```
That keeps the spec handed to later steps consistent with what the API
server stores. The first new test would then assert the account in both fields
instead of `null`. Take or leave.
##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/features/ExecutorKubernetesCredentialsFeatureStepSuite.scala:
##########
@@ -52,18 +57,155 @@ class ExecutorKubernetesCredentialsFeatureStepSuite
extends SparkFunSuite with B
assertSAName("executor-name", spec)
}
+ test("SPARK-58910: keep the service account named by the executor pod
template") {
Review Comment:
All the new tests feed a `PodBuilder`-built pod straight into the step, so
the path the bug actually takes (template -> `KubernetesExecutorBuilder` ->
`BasicExecutorFeatureStep` -> this step) is not pinned. The template in
`PodBuilderSuite.podWithSupportedFeatures` still uses only the deprecated
`withServiceAccount`, with no account configured, so the builder suites pass on
`master` and here alike. Since `mockKubernetesClient(pod)` already takes a
custom pod, one case in `KubernetesExecutorBuilderSuite` with a template naming
only `serviceAccountName` and
`spark.kubernetes.authenticate.executor.serviceAccountName` set would cover it
end to end.
##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/features/ExecutorKubernetesCredentialsFeatureStepSuite.scala:
##########
@@ -52,18 +57,155 @@ class ExecutorKubernetesCredentialsFeatureStepSuite
extends SparkFunSuite with B
assertSAName("executor-name", spec)
}
+ test("SPARK-58910: keep the service account named by the executor pod
template") {
+ // Either spelling means the template already picked an account, so the
configured one must not
+ // replace it. The field the template left alone stays null: the step
hands the pod back as it
+ // came rather than mirroring the account into both fields. Varying the
configuration alongside
+ // the spelling keeps the driver fallback covered here too.
+ Seq(
+ (EXECUTOR_SA_CONF, podWithAccount(serviceAccountName =
Some("template-name")),
+ "template-name", null),
+ (DRIVER_SA_CONF, podWithAccount(serviceAccount = Some("template-name")),
+ null, "template-name")
+ ).foreach { case (conf, templatePod, expectedName, expectedAlias) =>
+ val spec = evaluateStep(templatePod, new SparkConf(false).set(conf,
"configured-name"))
+ assert(spec.getServiceAccountName === expectedName, s"via $conf")
+ assert(spec.getServiceAccount === expectedAlias, s"via $conf")
+ }
+ }
+
+ test("SPARK-58910: an empty service account name in the template counts as
unset") {
+ baseConf.set(KUBERNETES_EXECUTOR_SERVICE_ACCOUNT_NAME, "executor-name")
+ // SetDefaults_PodSpec keys off the name being empty rather than null, and
an empty alias copies
+ // up as an empty name, so neither leaves the pod with an account.
+ Seq(
+ podWithAccount(serviceAccountName = Some("")),
+ podWithAccount(serviceAccount = Some(""))
+ ).foreach(templatePod => assertSAName("executor-name",
evaluateStep(templatePod)))
+ }
+
+ test("SPARK-58910: warn when the template displaces the executor
configuration") {
Review Comment:
Minor: none of the shapes in the new tests names different accounts in the
two fields, which is the one case where the step has to use the right field
before comparing. For example, `executor.serviceAccountName=a` with a template
of `serviceAccount: a`, `serviceAccountName: b` should warn once naming `b`,
while `executor.serviceAccountName=b` with the same template should stay
silent. The driver suite pins `serviceAccountName` winning, but only through
the driver step's message, so a regression in this step's comparison would stay
green here.
##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/features/ExecutorKubernetesCredentialsFeatureStepSuite.scala:
##########
@@ -52,18 +57,155 @@ class ExecutorKubernetesCredentialsFeatureStepSuite
extends SparkFunSuite with B
assertSAName("executor-name", spec)
}
+ test("SPARK-58910: keep the service account named by the executor pod
template") {
+ // Either spelling means the template already picked an account, so the
configured one must not
+ // replace it. The field the template left alone stays null: the step
hands the pod back as it
+ // came rather than mirroring the account into both fields. Varying the
configuration alongside
+ // the spelling keeps the driver fallback covered here too.
+ Seq(
+ (EXECUTOR_SA_CONF, podWithAccount(serviceAccountName =
Some("template-name")),
+ "template-name", null),
+ (DRIVER_SA_CONF, podWithAccount(serviceAccount = Some("template-name")),
+ null, "template-name")
+ ).foreach { case (conf, templatePod, expectedName, expectedAlias) =>
+ val spec = evaluateStep(templatePod, new SparkConf(false).set(conf,
"configured-name"))
+ assert(spec.getServiceAccountName === expectedName, s"via $conf")
+ assert(spec.getServiceAccount === expectedAlias, s"via $conf")
+ }
+ }
+
+ test("SPARK-58910: an empty service account name in the template counts as
unset") {
+ baseConf.set(KUBERNETES_EXECUTOR_SERVICE_ACCOUNT_NAME, "executor-name")
+ // SetDefaults_PodSpec keys off the name being empty rather than null, and
an empty alias copies
+ // up as an empty name, so neither leaves the pod with an account.
+ Seq(
+ podWithAccount(serviceAccountName = Some("")),
+ podWithAccount(serviceAccount = Some(""))
+ ).foreach(templatePod => assertSAName("executor-name",
evaluateStep(templatePod)))
+ }
+
+ test("SPARK-58910: warn when the template displaces the executor
configuration") {
+ // Both spellings have to warn, since the account can be named in either
field. With both
+ // configurations set the message must still name the executor one,
because that is the account
+ // the step would otherwise have applied.
+ Seq(
+ Seq(EXECUTOR_SA_CONF -> "configured-name") ->
+ podWithAccount(serviceAccountName = Some("template-name")),
+ Seq(EXECUTOR_SA_CONF -> "configured-name") ->
+ podWithAccount(serviceAccount = Some("template-name")),
+ Seq(EXECUTOR_SA_CONF -> "configured-name", DRIVER_SA_CONF ->
"driver-name") ->
+ podWithAccount(serviceAccountName = Some("template-name"))
+ ).foreach { case (confs, templatePod) =>
+ val appender = runWith(confs, templatePod)
+ val warnings = messagesAt(appender, Level.WARN)
+ assert(warnings.size === 1, s"expected one warning for $confs, got:
$warnings")
+ // And nothing besides: splitting the report into two independent
statements would add an
+ // INFO about the driver fallback for anyone who set both configurations.
Review Comment:
This still describes the driver fallback as INFO, but it is `logDebug` now.
```suggestion
// And nothing besides: splitting the report into two independent
statements would add a
// DEBUG line about the driver fallback for anyone who set both
configurations.
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]