dongjoon-hyun commented on code in PR #58187:
URL: https://github.com/apache/spark/pull/58187#discussion_r4300454490


##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -121,8 +121,24 @@ private[spark] class Client(
         .withMountPath(SPARK_CONF_DIR_INTERNAL)
         .endVolumeMount()
       .build()
+    // SPARK-38079: a pod template (spark.kubernetes.driver.podTemplateFile) 
may already pin
+    // spec.nodeName to a specific node. The Kubernetes API rejects creating a 
pod that has
+    // both a non-empty spec.nodeName and a non-empty spec.schedulingGates 
(added below) --
+    // "nodeName cannot be set until all schedulingGates have been cleared" -- 
since nodeName
+    // is meant to be set only once nothing (including scheduling) stands 
between the pod and
+    // that node. Translate it to an equivalent kubernetes.io/hostname 
nodeSelector entry
+    // instead, which (unlike nodeName) is created and read normally on a 
gated pod and has
+    // the same effect once the scheduler runs -- unless 
spark.kubernetes.driver.node.selector.
+    // kubernetes.io/hostname (via BasicDriverFeatureStep, applied earlier in
+    // builder.buildFromFeatures() above) already set that same key: that 
explicit user
+    // configuration always wins, so it is never overwritten by this fallback.
+    val pinnedNodeName = Option(resolvedDriverSpec.pod.pod.getSpec.getNodeName)

Review Comment:
   **Rewriting `spec.nodeName` into a `kubernetes.io/hostname` node selector 
isn't equivalent, although the comment at L129-L131 says it "has the same 
effect once the scheduler runs".** The scheduler never sees a pod with 
`nodeName`; the kubelet admits it and checks only resources, node affinity and 
selectors, ports and `NoExecute` taints. After this change the driver goes 
through the scheduler named by `spec.schedulerName`, so cordoned nodes, 
`NoSchedule` taints (already in the Known limitations), the template's pod 
(anti-)affinity and topology spread constraints, and the custom scheduler's 
queues all apply. For example, a template that pins the driver to a cordoned or 
tainted node used to run there, and now stays `Pending`, with `spark-submit` 
waiting forever when `spark.kubernetes.submission.waitAppCompletion` is on.
   
   Two smaller points:
   
   - When a `kubernetes.io/hostname` selector is already set, `nodeName` is 
dropped (L141) without any log.
   - The "Pod template properties" table in `docs/running-on-kubernetes.md`, 
"the full list of pod specifications that will be overwritten by spark", 
doesn't mention `nodeName` or `schedulingGates`.
   
   Since such a pod bypasses the scheduler anyway, could we skip the gate when 
the resolved pod has `nodeName`, and keep master's flow for that case?



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount
+        // anything on it -- until its pre-resources exist (see below). Only 
removed once
+        // that is true, right before the watch loop; see the "Remove the 
pre-resources
+        // scheduling gate" step below for why this makes an 
ownerless-resource window,
+        // and its consequences (a shutdown hook), unnecessary.
+        .addNewSchedulingGate(PRE_RESOURCES_SCHEDULING_GATE)
         .endSpec()
       .build()
     val driverPodName = resolvedDriverPod.getMetadata.getName
 
-    // setup resources before pod creation
-    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources
+    // SPARK-38079: the driver's own base config map (mounted as 
SPARK_CONF_VOLUME_DRIVER
+    // above) must also be created before the pod is schedulable, to avoid a 
"configmap ...
+    // not found" mount race between the driver pod and the config map it 
depends on.
+    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources ++ Seq(configMap)
+
+    var watch: Watch = null
+    val createdDriverPod: Pod =
+      try {
+        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()

Review Comment:
   **This creates the driver pod before every pre-resource, not only the moved 
ones, so a pre-resource that pod admission needs doesn't exist yet at the pod 
CREATE.** A scheduling gate only blocks binding; admission still runs on the 
CREATE. On master, `driverPreKubernetesResources` were applied first, which is 
what SPARK-37331 added the hook for ("they should be ready before pod creation 
... such as pod group in Volcano"), and what the `@Stable` Scaladoc of 
`KubernetesFeatureConfigStep.getAdditionalPreKubernetesResources()` still says 
("Resources would be setup/refresh before Pod creation.").
   
   For example, with the documented Volcano setup 
(`spark.kubernetes.scheduler.name=volcano` and `VolcanoFeatureStep`) on Volcano 
v1.7.0 to v1.11.x:
   
   - The driver pod carries `scheduling.k8s.io/group-name=<appId>-podgroup`, 
and Volcano's `validatepod.volcano.sh` webhook (enabled by default, CREATE, 
`failurePolicy: Fail`) calls `checkPG(pod, pgName, true)` ([admit_pod.go 
L111-L117](https://github.com/volcano-sh/volcano/blob/v1.11.0/pkg/webhooks/admission/pods/validate/admit_pod.go#L111-L117)).
   - Since the PodGroup is created only at L190, `checkPG` fails with `failed 
to get PodGroup for pod ...` 
([L148-L153](https://github.com/volcano-sh/volcano/blob/v1.11.0/pkg/webhooks/admission/pods/validate/admit_pod.go#L148-L153)),
 and every Volcano submission fails here with the "kubectl auth can-i create 
pod" hint.
   - The docs say Volcano is supported "since Spark v3.3.0 and Volcano v1.7.0". 
CI doesn't catch this because it installs Volcano v1.14.2, and the check was 
removed in v1.12.0.
   
   The same happens to a custom feature step that creates the driver's 
ServiceAccount as a pre-resource (`error looking up service account ...`). 
Could we keep applying `driverPreKubernetesResources` before the pod, as on 
master? The comment at L223 describes an order that does this and still closes 
the mount race.



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount
+        // anything on it -- until its pre-resources exist (see below). Only 
removed once
+        // that is true, right before the watch loop; see the "Remove the 
pre-resources
+        // scheduling gate" step below for why this makes an 
ownerless-resource window,
+        // and its consequences (a shutdown hook), unnecessary.
+        .addNewSchedulingGate(PRE_RESOURCES_SCHEDULING_GATE)
         .endSpec()
       .build()
     val driverPodName = resolvedDriverPod.getMetadata.getName
 
-    // setup resources before pod creation
-    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources
+    // SPARK-38079: the driver's own base config map (mounted as 
SPARK_CONF_VOLUME_DRIVER
+    // above) must also be created before the pod is schedulable, to avoid a 
"configmap ...
+    // not found" mount race between the driver pod and the config map it 
depends on.
+    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources ++ Seq(configMap)
+
+    var watch: Watch = null
+    val createdDriverPod: Pod =
+      try {
+        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()
+      } catch {
+        case NonFatal(e) =>
+          logError("Please check \"kubectl auth can-i create pod\" first. It 
should be yes.")
+          throw e
+      }
+
+    // SPARK-38079: some of the pre-resources above (e.g. the Kerberos 
keytab/delegation token
+    // secrets, the driver Kubernetes credentials secret) carry credentials. 
Now that the
+    // driver pod exists, its UID is known, so each pre-resource's owner 
reference can be set
+    // before -- and included in -- the single call that creates it. The pod 
itself is still
+    // scheduling-gated, so nothing can be scheduled against it (and so 
nothing can attempt to
+    // mount these resources) until they exist -- but they are never ownerless 
at any point
+    // after this call: either it succeeds and every pre-resource already has 
an owner
+    // reference, or it fails and none of them (that made it to the server) 
are left
+    // referencing anything, since the driver pod that would have owned them 
is deleted in the
+    // catch block below.
     try {
+      addOwnerReference(createdDriverPod, preKubernetesResources)
       kubernetesClient.resourceList(preKubernetesResources: 
_*).forceConflicts().serverSideApply()
     } catch {
       case NonFatal(e) =>
         logError("Please check \"kubectl auth can-i create [resource]\" 
first." +
           " It should be yes. And please also check your feature step 
implementation.")
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
-        throw e
-    }
-
-    var watch: Watch = null
-    var createdDriverPod: Pod = null
-    try {
-      createdDriverPod =
-        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()
-    } catch {
-      case NonFatal(e) =>
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
-        logError("Please check \"kubectl auth can-i create pod\" first. It 
should be yes.")
+        deletePodAndPreResources(createdDriverPod, preKubernetesResources)
         throw e
     }
 
-    // Refresh all pre-resources' owner references
+    // SPARK-38079: remove the pre-resources scheduling gate now that its 
pre-resources exist,
+    // letting the scheduler proceed with this pod. If this process is 
terminated abruptly
+    // before this point (e.g. Ctrl-C, SIGTERM, or a fatal JVM error), the 
driver pod is left
+    // behind still gated -- inert (kubelet cannot schedule/mount anything on 
a gated pod) and
+    // visible as `Pending`/`SchedulingGated` via any standard pod listing. 
That is a low-
+    // severity leak, in the same class Spark already accepts elsewhere for a 
process killed
+    // right after pod creation (e.g. Ctrl-C before the watch loop below 
starts), so no
+    // shutdown hook is registered to guard this window.
     try {
-      addOwnerReference(createdDriverPod, preKubernetesResources)
-      kubernetesClient.resourceList(preKubernetesResources: 
_*).forceConflicts().serverSideApply()
+      
kubernetesClient.pods().inNamespace(conf.namespace).withName(driverPodName).edit(
+        (currentPod: Pod) => new PodBuilder(currentPod)
+          .editSpec()
+            .removeMatchingFromSchedulingGates(_.getName == 
PRE_RESOURCES_SCHEDULING_GATE)

Review Comment:
   **`edit()` sends a JSON patch that pins the `resourceVersion` it just read, 
so any other write to the pod in between turns into a 409 and then a full 
teardown.** fabric8 7.9.0 clears the version only on the base copy 
([OperationSupport.java 
L380-L389](https://github.com/fabric8io/kubernetes-client/blob/v7.9.0/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java#L380-L389)),
 so the patch carries the GET's `metadata.resourceVersion`. A 409 isn't 
retried, and after a 5xx or an I/O error fabric8 resends the same body, which 
fails once the first one was applied. Either way the catch block logs the RBAC 
hint, deletes the pod and all pre-resources, and fails the submission; in the 
resend case the pod may already be bound.
   
   A vanilla cluster doesn't write to a gated pod in that window, but Kueue's 
pod integration does: its webhook adds `kueue.x-k8s.io/admission` at CREATE, 
and on admission Kueue patches the pod to remove that gate and merge node 
selectors.
   
   A strategic merge patch keyed by the gate name has no precondition and is 
idempotent, since `schedulingGates` merges by `name`:
   
   ```json
   
{"spec":{"schedulingGates":[{"$patch":"delete","name":"spark.apache.org/pre-resources"}]}}
   ```
   
   This module already patches pods with 
`PatchContext.of(PatchType.STRATEGIC_MERGE)` (e.g. 
`KubernetesClusterSchedulerBackend`, `ExecutorPodsLifecycleManager`), and 
Volcano removes its own gate the same way. Sending it through 
`resource(createdDriverPod)` also avoids the extra GET.



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount
+        // anything on it -- until its pre-resources exist (see below). Only 
removed once
+        // that is true, right before the watch loop; see the "Remove the 
pre-resources
+        // scheduling gate" step below for why this makes an 
ownerless-resource window,
+        // and its consequences (a shutdown hook), unnecessary.
+        .addNewSchedulingGate(PRE_RESOURCES_SCHEDULING_GATE)
         .endSpec()
       .build()
     val driverPodName = resolvedDriverPod.getMetadata.getName
 
-    // setup resources before pod creation
-    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources
+    // SPARK-38079: the driver's own base config map (mounted as 
SPARK_CONF_VOLUME_DRIVER
+    // above) must also be created before the pod is schedulable, to avoid a 
"configmap ...
+    // not found" mount race between the driver pod and the config map it 
depends on.
+    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources ++ Seq(configMap)
+
+    var watch: Watch = null
+    val createdDriverPod: Pod =
+      try {
+        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()
+      } catch {
+        case NonFatal(e) =>
+          logError("Please check \"kubectl auth can-i create pod\" first. It 
should be yes.")
+          throw e
+      }
+
+    // SPARK-38079: some of the pre-resources above (e.g. the Kerberos 
keytab/delegation token
+    // secrets, the driver Kubernetes credentials secret) carry credentials. 
Now that the
+    // driver pod exists, its UID is known, so each pre-resource's owner 
reference can be set
+    // before -- and included in -- the single call that creates it. The pod 
itself is still
+    // scheduling-gated, so nothing can be scheduled against it (and so 
nothing can attempt to
+    // mount these resources) until they exist -- but they are never ownerless 
at any point
+    // after this call: either it succeeds and every pre-resource already has 
an owner
+    // reference, or it fails and none of them (that made it to the server) 
are left
+    // referencing anything, since the driver pod that would have owned them 
is deleted in the
+    // catch block below.
     try {
+      addOwnerReference(createdDriverPod, preKubernetesResources)
       kubernetesClient.resourceList(preKubernetesResources: 
_*).forceConflicts().serverSideApply()
     } catch {
       case NonFatal(e) =>
         logError("Please check \"kubectl auth can-i create [resource]\" 
first." +
           " It should be yes. And please also check your feature step 
implementation.")
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
-        throw e
-    }
-
-    var watch: Watch = null
-    var createdDriverPod: Pod = null
-    try {
-      createdDriverPod =
-        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()
-    } catch {
-      case NonFatal(e) =>
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
-        logError("Please check \"kubectl auth can-i create pod\" first. It 
should be yes.")
+        deletePodAndPreResources(createdDriverPod, preKubernetesResources)
         throw e
     }
 
-    // Refresh all pre-resources' owner references
+    // SPARK-38079: remove the pre-resources scheduling gate now that its 
pre-resources exist,
+    // letting the scheduler proceed with this pod. If this process is 
terminated abruptly
+    // before this point (e.g. Ctrl-C, SIGTERM, or a fatal JVM error), the 
driver pod is left
+    // behind still gated -- inert (kubelet cannot schedule/mount anything on 
a gated pod) and
+    // visible as `Pending`/`SchedulingGated` via any standard pod listing. 
That is a low-
+    // severity leak, in the same class Spark already accepts elsewhere for a 
process killed
+    // right after pod creation (e.g. Ctrl-C before the watch loop below 
starts), so no
+    // shutdown hook is registered to guard this window.
     try {
-      addOwnerReference(createdDriverPod, preKubernetesResources)
-      kubernetesClient.resourceList(preKubernetesResources: 
_*).forceConflicts().serverSideApply()
+      
kubernetesClient.pods().inNamespace(conf.namespace).withName(driverPodName).edit(

Review Comment:
   **This makes `patch` on pods a new requirement for every submission.** 
fabric8's `edit()` is a GET followed by an HTTP PATCH 
([HasMetadataOperation.java 
L58-L61](https://github.com/fabric8io/kubernetes-client/blob/v7.9.0/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/HasMetadataOperation.java#L58-L61)),
 while master's `Client.run()` never wrote to the pod after `create()`; 
server-side apply touched only the other resources.
   
   For example, kubeflow/spark-operator runs `spark-submit` inside its 
controller with the controller's service account, and its default rules grant 
pods only `get`, `list`, `watch`, `create`, `update` and `delete` 
([`_helpers.tpl` 
L110-L121](https://github.com/kubeflow/spark-operator/blob/v2.5.2/charts/spark-operator-chart/templates/controller/_helpers.tpl#L110-L121)
 in v2.5.2, the same on master). Once its image bundles a Spark with this 
change, every submission there would create the gated pod and the 
pre-resources, get a 403 here, delete the driver pod in the catch block, and 
fail. This is the same kind of RBAC gap as the 
`OwnerReferencesPermissionEnforcement` case in [round 
1](https://github.com/apache/spark/pull/58187#discussion_r4129910475), and I 
missed it when I suggested `spec.schedulingGates`.
   
   `docs/running-on-kubernetes.md` only says "list, create, edit and delete 
pods", and `edit` isn't an RBAC verb. If we keep the gate, could we document 
`patch` on pods explicitly and mention it in the migration guide? `update` 
(PUT) would fit kubeflow's role, but it is also a new verb.



##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/submit/ClientSuite.scala:
##########
@@ -221,14 +306,216 @@ class ClientSuite extends SparkFunSuite with 
BeforeAndAfter {
     assert(secrets.nonEmpty)
     assert(configMaps.nonEmpty)
     val configMap = configMaps.head
-    assert(configMap.getMetadata.getName ===
-      KubernetesClientUtils.configMapNameDriver)
+    // SPARK-38079: configMapNameDriver is a `def` -- see its own comment -- 
so this cannot
+    // compare against a freshly-generated name (that call would return a 
different one); it
+    // can only check the name has the shape configMapNameDriver is documented 
to produce.
+    assert(configMap.getMetadata.getName.matches("spark-drv-\\S+-conf-map"))

Review Comment:
   **Now that `configMapNameDriver` returns a new name on every call, no test 
checks that the created ConfigMap is the one the pod mounts.** L271 and L329 
compare the captured pod with a pod built from its own volume name, and this 
regex (also at L561 and L621) accepts any `spark-drv-*-conf-map`. So if `run()` 
read `configMapNameDriver` again for `buildConfigMap` (L109) or for the volume 
(L147), the pod would mount a ConfigMap that is never created, which is the 
"not found" failure this PR targets, and every `ClientSuite` test would still 
pass. `assert(configMap.getMetadata.getName === gatedConfigMapName)` works in 
all three places, so the comments saying only the shape can be checked aren't 
accurate.
   
   Similarly, the `SPARK_CONF_DIR` test lost its only check of the conf 
volume's items for several files. The removed stub, 
`when(podsWithNamespace.resource(fullExpectedPod(expectedKeyToPaths)))`, made 
`run()` fail unless the volume listed all the expected files, and the test now 
checks only the ConfigMap data. A `configMap` volume with explicit `items` 
projects only those keys, so dropping one in `buildKeyToPathObjects` would go 
unnoticed. Keeping `expectedKeyToPaths` and asserting `gatedPod === 
fullExpectedPod(gatedConfigMapName, expectedKeyToPaths)` would restore it; the 
`keyToPaths` parameter of the three helpers is currently unused.



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount
+        // anything on it -- until its pre-resources exist (see below). Only 
removed once
+        // that is true, right before the watch loop; see the "Remove the 
pre-resources
+        // scheduling gate" step below for why this makes an 
ownerless-resource window,
+        // and its consequences (a shutdown hook), unnecessary.
+        .addNewSchedulingGate(PRE_RESOURCES_SCHEDULING_GATE)
         .endSpec()
       .build()
     val driverPodName = resolvedDriverPod.getMetadata.getName
 
-    // setup resources before pod creation
-    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources
+    // SPARK-38079: the driver's own base config map (mounted as 
SPARK_CONF_VOLUME_DRIVER
+    // above) must also be created before the pod is schedulable, to avoid a 
"configmap ...
+    // not found" mount race between the driver pod and the config map it 
depends on.
+    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources ++ Seq(configMap)
+
+    var watch: Watch = null
+    val createdDriverPod: Pod =
+      try {
+        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()
+      } catch {
+        case NonFatal(e) =>
+          logError("Please check \"kubectl auth can-i create pod\" first. It 
should be yes.")
+          throw e
+      }
+
+    // SPARK-38079: some of the pre-resources above (e.g. the Kerberos 
keytab/delegation token
+    // secrets, the driver Kubernetes credentials secret) carry credentials. 
Now that the
+    // driver pod exists, its UID is known, so each pre-resource's owner 
reference can be set
+    // before -- and included in -- the single call that creates it. The pod 
itself is still
+    // scheduling-gated, so nothing can be scheduled against it (and so 
nothing can attempt to
+    // mount these resources) until they exist -- but they are never ownerless 
at any point
+    // after this call: either it succeeds and every pre-resource already has 
an owner
+    // reference, or it fails and none of them (that made it to the server) 
are left
+    // referencing anything, since the driver pod that would have owned them 
is deleted in the
+    // catch block below.
     try {
+      addOwnerReference(createdDriverPod, preKubernetesResources)
       kubernetesClient.resourceList(preKubernetesResources: 
_*).forceConflicts().serverSideApply()
     } catch {
       case NonFatal(e) =>
         logError("Please check \"kubectl auth can-i create [resource]\" 
first." +
           " It should be yes. And please also check your feature step 
implementation.")
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
-        throw e
-    }
-
-    var watch: Watch = null
-    var createdDriverPod: Pod = null
-    try {
-      createdDriverPod =
-        
kubernetesClient.pods().inNamespace(conf.namespace).resource(resolvedDriverPod).create()
-    } catch {
-      case NonFatal(e) =>
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
-        logError("Please check \"kubectl auth can-i create pod\" first. It 
should be yes.")
+        deletePodAndPreResources(createdDriverPod, preKubernetesResources)
         throw e
     }
 
-    // Refresh all pre-resources' owner references
+    // SPARK-38079: remove the pre-resources scheduling gate now that its 
pre-resources exist,
+    // letting the scheduler proceed with this pod. If this process is 
terminated abruptly
+    // before this point (e.g. Ctrl-C, SIGTERM, or a fatal JVM error), the 
driver pod is left
+    // behind still gated -- inert (kubelet cannot schedule/mount anything on 
a gated pod) and
+    // visible as `Pending`/`SchedulingGated` via any standard pod listing. 
That is a low-
+    // severity leak, in the same class Spark already accepts elsewhere for a 
process killed
+    // right after pod creation (e.g. Ctrl-C before the watch loop below 
starts), so no
+    // shutdown hook is registered to guard this window.
     try {
-      addOwnerReference(createdDriverPod, preKubernetesResources)
-      kubernetesClient.resourceList(preKubernetesResources: 
_*).forceConflicts().serverSideApply()
+      
kubernetesClient.pods().inNamespace(conf.namespace).withName(driverPodName).edit(
+        (currentPod: Pod) => new PodBuilder(currentPod)
+          .editSpec()
+            .removeMatchingFromSchedulingGates(_.getName == 
PRE_RESOURCES_SCHEDULING_GATE)
+            .endSpec()
+          .build())
     } catch {
       case NonFatal(e) =>
-        kubernetesClient.pods().resource(createdDriverPod).delete()
-        kubernetesClient.resourceList(preKubernetesResources: _*).delete()
+        logError("Please check \"kubectl auth can-i patch pod\" first. It 
should be yes.")
+        deletePodAndPreResources(createdDriverPod, preKubernetesResources)
         throw e
     }
 
     // setup resources after pod creation, and refresh all resources' owner 
references
     try {
-      val otherKubernetesResources = 
resolvedDriverSpec.driverKubernetesResources ++ Seq(configMap)
+      val otherKubernetesResources = 
resolvedDriverSpec.driverKubernetesResources

Review Comment:
   **Because the gate is removed before this apply (L207-L219), the driver 
post-resources are still created after the pod can be scheduled, which is why 
the four feature steps' resources and the conf ConfigMap had to move to 
pre-resources.** If the gate is removed last instead, that move isn't needed:
   
   1. Apply `driverPreKubernetesResources` before the pod, as on master (see 
the comment at L171).
   2. Create the gated pod.
   3. Refresh the pre-resources' owner references, as on master.
   4. Apply `driverKubernetesResources ++ Seq(configMap)` with owner 
references. This is a CREATE, so there is no 
`OwnerReferencesPermissionEnforcement` issue.
   5. Remove the gate.
   
   This covers every driver resource, including the ones that stay 
post-resources here, such as the `OnDemand` driver PVC from 
`MountVolumesFeatureStep` or a ConfigMap or Secret that a user's feature step 
mounts. It also means:
   
   - The four `getAdditionalPreKubernetesResources()` overrides, L166 and the 
`HADOOP_CONF_DIR` test exclusions can be reverted.
   - The output of `KubernetesDriverBuilder` (a `@DeveloperApi`) stays the 
same, so the first Known limitation, about `apache/spark-kubernetes-operator`, 
goes away.
   - A failure after the pod exists only needs to delete the pod (except in 
step 3, as on master), so `deletePodAndPreResources` and the 
`configMapNameDriver` change aren't needed.
   
   The trade-off is that pre-resources, which are empty by default, keep 
master's ownerless window until step 3. This keeps the part of my [round-1 
suggestion](https://github.com/apache/spark/pull/58187#discussion_r4129910563) 
that matters (create the pod gated, apply the resources with owner references, 
then remove the gate) without moving the pre-resources after the pod, which I 
didn't think through then.



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientUtils.scala:
##########
@@ -57,8 +57,17 @@ object KubernetesClientUtils extends Logging {
   @Since("3.1.0")
   val configMapNameExecutor: String = 
configMapName(s"spark-exec-${KubernetesUtils.uniqueID()}")
 
+  // SPARK-38079: a `def`, not a `val` -- it is called exactly once per 
submission
+  // (Client.run(), into a local val reused for that submission), so unlike
+  // configMapNameExecutor (set up once by KubernetesClusterSchedulerBackend, 
then read
+  // independently by BasicExecutorFeatureStep for every executor, so every 
call must agree on
+  // the same name), there is no cross-call consistency requirement to 
preserve here. A `val`
+  // would instead share this name across every submission from the same JVM 
(e.g. via
+  // InProcessLauncher) for the life of the JVM, which, now that this config 
map is a
+  // pre-resource, would let a failed submission's cleanup delete-by-name a 
different,
+  // still-running submission's live config map of that same name.
   @Since("3.1.0")
-  val configMapNameDriver: String = 
configMapName(s"spark-drv-${KubernetesUtils.uniqueID()}")
+  def configMapNameDriver: String = 
configMapName(s"spark-drv-${KubernetesUtils.uniqueID()}")

Review Comment:
   **This turns a `@Since("3.1.0")` member of a `@Stable` `@DeveloperApi` 
object from one value per JVM into a new random name on every read.** Nothing 
flags it, since the accessor signature is the same, and a parameterless `def` 
with a side effect (each call advances the shared RNG) reads like a stable 
field. A caller that reads it twice, once for the ConfigMap and once for the 
volume as the old `ClientSuite` did, would now get two names and a driver stuck 
in `ContainerCreating`. No caller in Spark or 
`apache/spark-kubernetes-operator` does that today; the operator has its own 
`SparkAppDriverConf.configMapNameDriver()`.
   
   The name sharing ([round 
1](https://github.com/apache/spark/pull/58187#discussion_r4129910528), also 
SPARK-41060) can be fixed in `Client.run()` without touching this member, for 
example with 
`KubernetesClientUtils.configMapName(s"${conf.resourceNamePrefix}-driver")` as 
the other per-submission resources and the operator do, or with a local 
`spark-drv-<uniqueID>` name. A name from `resourceNamePrefix` would also let 
`ClientSuite` assert exact names again. With the order in the comment at L223, 
the cross-submission delete from round 1 can't happen anyway, so this could 
move to SPARK-41060.



##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/submit/ClientSuite.scala:
##########
@@ -221,14 +306,216 @@ class ClientSuite extends SparkFunSuite with 
BeforeAndAfter {
     assert(secrets.nonEmpty)
     assert(configMaps.nonEmpty)
     val configMap = configMaps.head
-    assert(configMap.getMetadata.getName ===
-      KubernetesClientUtils.configMapNameDriver)
+    // SPARK-38079: configMapNameDriver is a `def` -- see its own comment -- 
so this cannot
+    // compare against a freshly-generated name (that call would return a 
different one); it
+    // can only check the name has the shape configMapNameDriver is documented 
to produce.
+    assert(configMap.getMetadata.getName.matches("spark-drv-\\S+-conf-map"))
     assert(configMap.getImmutable())
     assert(configMap.getData.containsKey(SPARK_CONF_FILE_NAME))
     
assert(configMap.getData.get(SPARK_CONF_FILE_NAME).contains("conf1key=conf1value"))
     
assert(configMap.getData.get(SPARK_CONF_FILE_NAME).contains("conf2key=conf2value"))
   }
 
+  test("SPARK-38079: driver pod is created (still scheduling-gated) before its 
own config " +
+      "map, and the config map's single create call already carries the owner 
reference") {
+    val submissionClient = new Client(
+      kconf,
+      driverBuilder,
+      kubernetesClient,
+      loggingPodStatusWatcher)
+    submissionClient.run()
+    // The pod itself must be created before any pre-resource resourceList() 
call, since a
+    // pre-resource's owner reference (set below) needs the pod's UID.
+    assert(gatedPod === fullExpectedPod(gatedConfigMapName))
+    verify(namedPods).create()
+
+    // The (single) resourceList(...) call creating the driver's own config 
map must already
+    // carry the owner reference -- there is no separate, later "refresh" 
call. The pod stays
+    // scheduling-gated (so kubelet cannot attempt to mount it) for as long as 
this has not yet
+    // happened, avoiding the "configmap ... not found" mount race 
(SPARK-38079) without ever
+    // creating the config map ownerless.
+    val resourceListCall = createdResourcesArgumentCaptor.getAllValues.get(0)
+    val configMaps = resourceListCall
+      .filter(_.isInstanceOf[ConfigMap]).map(_.asInstanceOf[ConfigMap])
+    assert(configMaps.nonEmpty,
+      "the driver's own config map must be created as a pre-resource")
+    val ownerReferences = configMaps.head.getMetadata.getOwnerReferences
+    assert(ownerReferences.size() === 1)
+    assert(ownerReferences.get(0).getName === POD_NAME)
+    assert(ownerReferences.get(0).getUid === DRIVER_POD_UID)
+
+    // The scheduling gate must be removed only after the config map has been 
created.
+    
verify(namedPods).edit(ArgumentMatchers.any[java.util.function.UnaryOperator[Pod]]())
+  }
+
+  // SPARK-38079: the three failure branches in run() between pod creation and 
gate removal --
+  // covering (a) the pod creation call itself failing, (b) the pre-resource 
creation call
+  // failing after the pod exists, and (c) the gate-removal call failing after 
pre-resources
+  // exist -- each verified against real-cluster behavior 
(KubernetesClientException on
+  // AlreadyExists/Invalid/NotFound) before being written as these mock-based 
tests.
+  test("SPARK-38079: pod creation failure propagates without attempting any 
pre-resource " +
+      "creation") {
+    val podCreationFailure = new RuntimeException("simulated pod creation 
failure")
+    doThrow(podCreationFailure).when(namedPods).create()
+    val submissionClient = new Client(
+      kconf,
+      driverBuilder,
+      kubernetesClient,
+      loggingPodStatusWatcher)
+
+    val thrown = intercept[RuntimeException] {
+      submissionClient.run()
+    }
+
+    assert(thrown eq podCreationFailure)
+    verify(kubernetesClient, 
never()).resourceList(ArgumentMatchers.any[Array[HasMetadata]](): _*)
+  }
+
+  test("SPARK-38079: pre-resource creation failure (after the pod exists) 
deletes both the " +
+      "pod and the pre-resources, then propagates") {
+    val preResourceFailure = new RuntimeException("simulated pre-resource 
creation failure")
+    doThrow(preResourceFailure).when(resourceList).serverSideApply()
+    val submissionClient = new Client(
+      kconf,
+      driverBuilder,
+      kubernetesClient,
+      loggingPodStatusWatcher)
+
+    val thrown = intercept[RuntimeException] {
+      submissionClient.run()
+    }
+
+    assert(thrown eq preResourceFailure)
+    // Not just "delete() was called on some mock" -- pods().resource(...) 
must have been
+    // called with the actual created (gated) pod, i.e. the same pod object 
this cleanup path
+    // is documented to delete, and each pre-resource must be deleted 
individually (not as a
+    // single resourceList(...).delete() call, which stops at the first 
failing item).
+    verify(namedPods).create()
+    verify(podOperations).resource(podWithOwnerReference(gatedConfigMapName))
+    verify(namedPods).delete()
+    verify(namedResource).delete()

Review Comment:
   **Since every `kubernetesClient.resource(...)` returns the same 
`namedResource` and each failure test has a single pre-resource, these checks 
can't tell which objects were deleted.** For example, these changes still pass:
   
   - passing `resolvedDriverSpec.driverKubernetesResources` at L195 and L217,
   - one `Utils.tryLogNonFatalError` around the whole loop, so a 403 on an 
earlier item skips the conf ConfigMap (the [round-1 
case](https://github.com/apache/spark/pull/58187#discussion_r4129910518)),
   - `preResources.take(1)`.
   
   The pod-creation failure test also has no `never()` check on deletes, 
although a delete by name after a 409 `AlreadyExists` (with a fixed 
`spark.kubernetes.driver.pod.name`) would remove another submission's driver.
   
   The gate checks have the same issue. `withName(POD_NAME)` and 
`resource(any)` return the same `namedPods`, and the fixture has only Spark's 
gate. So calling `edit()` on `resource(resolvedDriverPod)`, which patches from 
the local copy without a GET, or a lambda that clears every gate, would also 
pass. In production, both would remove a gate that a webhook added, such as 
Kueue's, and Kueue then deletes the pod it didn't admit.
   
   Could we give the fixture a second gate that must survive, add `never()` 
delete checks to the creation failure test, and, if the per-item deletes stay 
(see the comment at L276), capture the `resource(...)` arguments and add a 
failure test with two or more pre-resources where the first delete throws?



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -209,6 +257,26 @@ private[spark] class Client(
         log"submission ID ${MDC(SUBMISSION_ID, sId)} into Kubernetes")
     }
   }
+
+  // SPARK-38079: best-effort cleanup for the two failure catch blocks between 
pod creation and
+  // gate removal. The pod and each pre-resource are deleted independently -- 
every delete call
+  // wrapped in its own Utils.tryLogNonFatalError -- so that one of them 
failing (e.g. a delete
+  // call itself hitting a permission or network error) neither masks the 
original exception
+  // the caller is about to (re)throw, nor skips the rest. In particular, 
fabric8's
+  // resourceList(...).delete() deletes the given items sequentially and stops 
at the first
+  // non-404 exception, so pre-resources are deleted one at a time here rather 
than as a single
+  // resourceList(...).delete() call, which could otherwise leave every 
pre-resource after the
+  // one that failed undeleted.
+  private def deletePodAndPreResources(pod: Pod, preResources: 
Seq[HasMetadata]): Unit = {
+    Utils.tryLogNonFatalError {
+      kubernetesClient.pods().resource(pod).delete()
+    }
+    preResources.foreach { resource =>
+      Utils.tryLogNonFatalError {
+        kubernetesClient.resource(resource).delete()

Review Comment:
   **Now that every pre-resource gets the pod's controller owner reference 
before its first write (L189), these per-item deletes duplicate garbage 
collection.** Deleting the gated pod (background propagation, and it is removed 
at once since it was never scheduled) lets GC remove every namespaced 
pre-resource that reached the server, including one whose apply response was 
lost, which an explicit DELETE can miss. The loop adds costs on the failure 
path:
   
   - an ERROR log with a stack trace per item for submitters without `delete` 
on ConfigMaps, Secrets or PodGroups (the docs only require pod permissions),
   - deletes by name of objects this submission may not own, e.g. a fixed-name 
pre-resource from a custom step whose apply failed with 422, or items that were 
never sent after the first failure,
   - one more request timeout per item when the API server is unreachable.
   
   I asked for per-item deletes in [round 
1](https://github.com/apache/spark/pull/58187#discussion_r4129910518), when 
pre-resources were created without an owner. With the owner reference set up 
front, could this be a single guarded pod delete, keeping explicit deletes only 
for cluster-scoped pre-resources, if any?



##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/submit/ClientSuite.scala:
##########
@@ -221,14 +306,216 @@ class ClientSuite extends SparkFunSuite with 
BeforeAndAfter {
     assert(secrets.nonEmpty)
     assert(configMaps.nonEmpty)
     val configMap = configMaps.head
-    assert(configMap.getMetadata.getName ===
-      KubernetesClientUtils.configMapNameDriver)
+    // SPARK-38079: configMapNameDriver is a `def` -- see its own comment -- 
so this cannot
+    // compare against a freshly-generated name (that call would return a 
different one); it
+    // can only check the name has the shape configMapNameDriver is documented 
to produce.
+    assert(configMap.getMetadata.getName.matches("spark-drv-\\S+-conf-map"))
     assert(configMap.getImmutable())
     assert(configMap.getData.containsKey(SPARK_CONF_FILE_NAME))
     
assert(configMap.getData.get(SPARK_CONF_FILE_NAME).contains("conf1key=conf1value"))
     
assert(configMap.getData.get(SPARK_CONF_FILE_NAME).contains("conf2key=conf2value"))
   }
 
+  test("SPARK-38079: driver pod is created (still scheduling-gated) before its 
own config " +
+      "map, and the config map's single create call already carries the owner 
reference") {
+    val submissionClient = new Client(
+      kconf,
+      driverBuilder,
+      kubernetesClient,
+      loggingPodStatusWatcher)
+    submissionClient.run()
+    // The pod itself must be created before any pre-resource resourceList() 
call, since a
+    // pre-resource's owner reference (set below) needs the pod's UID.
+    assert(gatedPod === fullExpectedPod(gatedConfigMapName))
+    verify(namedPods).create()
+
+    // The (single) resourceList(...) call creating the driver's own config 
map must already
+    // carry the owner reference -- there is no separate, later "refresh" 
call. The pod stays
+    // scheduling-gated (so kubelet cannot attempt to mount it) for as long as 
this has not yet
+    // happened, avoiding the "configmap ... not found" mount race 
(SPARK-38079) without ever
+    // creating the config map ownerless.
+    val resourceListCall = createdResourcesArgumentCaptor.getAllValues.get(0)
+    val configMaps = resourceListCall
+      .filter(_.isInstanceOf[ConfigMap]).map(_.asInstanceOf[ConfigMap])
+    assert(configMaps.nonEmpty,
+      "the driver's own config map must be created as a pre-resource")
+    val ownerReferences = configMaps.head.getMetadata.getOwnerReferences

Review Comment:
   **Two points from [round 
1](https://github.com/apache/spark/pull/58187#discussion_r4129910551) are still 
open here.**
   
   - `addOwnerReference` mutates the resources in place, and the captor keeps 
references to the same objects, so this reads the owner reference after `run()` 
returns. If L189 and L190 were swapped, production would create the conf 
ConfigMap and the Kerberos and credentials Secrets without owner references, 
because fabric8 copies the items in `resourceList(...)` and sends them right 
away. Yet this check and the one at L549 would still pass; only `checkPodGroup` 
in the Volcano integration test would notice, for the PodGroup alone. Recording 
`getOwnerReferences` inside a `doAnswer` on `resourceList(...)` would check the 
state at call time.
   - `ArgumentMatchers.any[Array[HasMetadata]]()` is the no-arg `any()`, which 
"no longer matches varargs" since Mockito 5.0.0 ([ArgumentMatchers.java 
L129](https://github.com/mockito/mockito/blob/v5.23.0/mockito-core/src/main/java/org/mockito/ArgumentMatchers.java#L129)):
 it matches only calls with exactly one resource. So `never()` at L371 doesn't 
see a call with zero or several resources; master's order, which calls 
`resourceList(Nil: _*)` before `create()` with this fixture, would pass it. And 
with `BUILT_KUBERNETES_SPEC_WITH_PRERES`, the `InOrder` at L286 would bind to 
the post-resource call and fail. 
`ArgumentMatchers.any(classOf[Array[HasMetadata]]): _*` matches the whole array.



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount

Review Comment:
   **A few of the new comments describe behavior the code doesn't have, or the 
earlier shutdown-hook design.**
   
   - L155 says the gate is removed "right before the watch loop", but the 
post-resources are applied in between (L221-L230). L155-L157 also point to the 
gate-removal step for the ownerless-window reasoning, which is at L178-L187, 
and L157 and L205-L206 still mention the shutdown hook.
   - L184-L187 say that on failure none of the pre-resources that reached the 
server "are left referencing anything", but L189 already set the pod's owner 
reference on them; that is why GC removes them once the pod is deleted.
   - L129-L131 say the `nodeName` fallback "has the same effect" (see the 
comment at L135).
   - The comments in the four feature steps (`HadoopConfDriverFeatureStep` 
L115-L116, `KerberosConfDriverFeatureStep` L228-L230, 
`PodTemplateConfigMapStep` L77-L78, `DriverKubernetesCredentialsFeatureStep` 
L165-L169) and in `HadoopConfDriverFeatureStepSuite` say "before the pod 
itself", but the pod is now created first (L171). "Before the pod is 
schedulable" (L163-L165) is accurate.
   
   The diff still adds 46 comment lines and 40 code lines to this file, and 71 
and 46 across the main files. Could these be trimmed to a short comment at the 
gate?



##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/submit/KubernetesDriverBuilderSuite.scala:
##########
@@ -66,6 +66,12 @@ class KubernetesDriverBuilderSuite extends PodBuilderSuite {
   test("SPARK-37331: check driver pre kubernetes resource, empty by default") {
     val sparkConf = new SparkConf(false)
       .set(Config.CONTAINER_IMAGE, "spark-driver:latest")
+      // SPARK-38079: HadoopConfDriverFeatureStep now also contributes a 
pre-resource
+      // whenever HADOOP_CONF_DIR is set in the environment the test JVM 
inherits, which
+      // would otherwise make this assertion depend on that environment 
variable. Exclude
+      // it so this test only exercises the "empty by default" case it is 
meant to check.
+      .set(Config.KUBERNETES_DRIVER_POD_EXCLUDED_FEATURE_STEPS.key,
+        "org.apache.spark.deploy.k8s.features.HadoopConfDriverFeatureStep")

Review Comment:
   **Excluding only `HadoopConfDriverFeatureStep` still leaves these size 
checks dependent on the environment.** `KerberosConfDriverFeatureStep` is 
always included, and with this PR it returns its delegation-token Secret from 
`getAdditionalPreKubernetesResources()` whenever the current UGI has tokens. 
UGI loads them from `HADOOP_TOKEN_FILE_LOCATION` (or `HADOOP_TOKEN` and 
`hadoop.token.files`), so with a valid token file in the environment, e.g. 
inside a YARN container, both tests get an extra `<prefix>-delegation-tokens` 
pre-resource and fail. Could we also exclude 
`org.apache.spark.deploy.k8s.features.KerberosConfDriverFeatureStep`? The 
config takes a comma-separated list.



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount
+        // anything on it -- until its pre-resources exist (see below). Only 
removed once
+        // that is true, right before the watch loop; see the "Remove the 
pre-resources
+        // scheduling gate" step below for why this makes an 
ownerless-resource window,
+        // and its consequences (a shutdown hook), unnecessary.
+        .addNewSchedulingGate(PRE_RESOURCES_SCHEDULING_GATE)
         .endSpec()
       .build()
     val driverPodName = resolvedDriverPod.getMetadata.getName
 
-    // setup resources before pod creation
-    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources
+    // SPARK-38079: the driver's own base config map (mounted as 
SPARK_CONF_VOLUME_DRIVER
+    // above) must also be created before the pod is schedulable, to avoid a 
"configmap ...
+    // not found" mount race between the driver pod and the config map it 
depends on.
+    val preKubernetesResources = 
resolvedDriverSpec.driverPreKubernetesResources ++ Seq(configMap)

Review Comment:
   **These changes apply to every cluster-mode submission, with no switch back 
to the previous flow and no migration guide entry.** The scheduling gate, the 
`nodeName` rewrite, the pod `patch` and the new order of pre-resources are all 
unconditional. A user hit by the cases at L171, L208 or L135 has no workaround 
other than changing the cluster or downgrading. Other Kubernetes behavior 
changes in `docs/core-migration-guide.md` come with "To restore the legacy 
behavior, you can set ...", e.g. `spark.kubernetes.executor.useDriverPodIP`.
   
   The PR description also still says the resources "are created *before* the 
driver pod" and "No behavior change for successful submissions", and that 
`VolcanoFeatureStep` is "intentionally left unchanged", while its PodGroup is 
now created after the pod too. Since the description becomes the commit 
message, could you update it, including the user-facing change section, as 
asked in [round 
1](https://github.com/apache/spark/pull/58187#discussion_r4129910571)?



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -209,6 +257,26 @@ private[spark] class Client(
         log"submission ID ${MDC(SUBMISSION_ID, sId)} into Kubernetes")
     }
   }
+
+  // SPARK-38079: best-effort cleanup for the two failure catch blocks between 
pod creation and
+  // gate removal. The pod and each pre-resource are deleted independently -- 
every delete call
+  // wrapped in its own Utils.tryLogNonFatalError -- so that one of them 
failing (e.g. a delete
+  // call itself hitting a permission or network error) neither masks the 
original exception
+  // the caller is about to (re)throw, nor skips the rest. In particular, 
fabric8's
+  // resourceList(...).delete() deletes the given items sequentially and stops 
at the first
+  // non-404 exception, so pre-resources are deleted one at a time here rather 
than as a single
+  // resourceList(...).delete() call, which could otherwise leave every 
pre-resource after the
+  // one that failed undeleted.
+  private def deletePodAndPreResources(pod: Pod, preResources: 
Seq[HasMetadata]): Unit = {
+    Utils.tryLogNonFatalError {
+      kubernetesClient.pods().resource(pod).delete()

Review Comment:
   **This isn't introduced by this PR, but the post-resource catch block 
(L226-L229) still calls 
`kubernetesClient.pods().resource(createdDriverPod).delete()` without this 
guard.** If the post-resource apply fails and that delete also fails, e.g. on a 
dropped connection or a webhook or RBAC denying the DELETE, the delete's 
exception replaces the original one, which is then neither thrown nor logged. 
Since this PR edits that block and guards the same delete here for the other 
two catch blocks, could we wrap that one in `Utils.tryLogNonFatalError` as well?



##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/submit/KubernetesClientApplication.scala:
##########
@@ -131,48 +147,80 @@ private[spark] class Client(
             .withName(configMapName)
             .endConfigMap()
           .endVolume()
+        .addToNodeSelector(
+          pinnedNodeName.map(nodeName => Map("kubernetes.io/hostname" -> 
nodeName))
+            .getOrElse(Map.empty[String, String]).asJava)
+        // SPARK-38079: holds the pod unschedulable -- so kubelet cannot 
attempt to mount
+        // anything on it -- until its pre-resources exist (see below). Only 
removed once
+        // that is true, right before the watch loop; see the "Remove the 
pre-resources
+        // scheduling gate" step below for why this makes an 
ownerless-resource window,
+        // and its consequences (a shutdown hook), unnecessary.
+        .addNewSchedulingGate(PRE_RESOURCES_SCHEDULING_GATE)

Review Comment:
   **Gating the driver may also strand it with schedulers that don't support 
scheduling gates.** For example, the YuniKorn shim before 1.7.0 creates a task 
for the gated pod right away. If it allocates before L208 removes the gate, 
which takes a few scheduling cycles and so needs a slow API server or a shorter 
`service.schedulingInterval`, the bind is rejected (`pod ... has non-empty 
.spec.schedulingGates`), the task fails, and the driver can stay `Pending` 
after the gate is removed. This is narrow, since YuniKorn 1.6.x supports 
Kubernetes only up to 1.31 and the docs now install 1.9.0, which skips gated 
pods. It may still be worth a note in the docs, together with the `patch` 
requirement.



-- 
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]

Reply via email to