Skip to content

Commit d873a9b

Browse files
committed
feat: support executor pod template path
1 parent 843837b commit d873a9b

File tree

8 files changed

+75
-0
lines changed

8 files changed

+75
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ and the CLI. Here is a list of the available environment variables:
186186
| SPARK_ON_K8S_SPARK_EXECUTOR_LABELS | The labels to use for the executor pods | {} |
187187
| SPARK_ON_K8S_SPARK_DRIVER_ANNOTATIONS | The annotations to use for the driver pod | {} |
188188
| SPARK_ON_K8S_SPARK_EXECUTOR_ANNOTATIONS | The annotations to use for the executor pods | {} |
189+
| SPARK_ON_K8S_EXECUTOR_POD_TEMPLATE_PATH | The path to the executor pod template | |
189190

190191

191192
## Examples

spark_on_k8s/airflow/operators.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def __init__(
126126
driver_annotations: dict[str, str] | None = None,
127127
executor_annotations: dict[str, str] | None = None,
128128
driver_tolerations: list[k8s.V1Toleration] | None = None,
129+
executor_pod_template_path: str | None = None,
129130
kubernetes_conn_id: str = "kubernetes_default",
130131
poll_interval: int = 10,
131132
deferrable: bool = False,
@@ -158,6 +159,7 @@ def __init__(
158159
self.driver_annotations = driver_annotations
159160
self.executor_annotations = executor_annotations
160161
self.driver_tolerations = driver_tolerations
162+
self.executor_pod_template_path = executor_pod_template_path
161163
self.kubernetes_conn_id = kubernetes_conn_id
162164
self.poll_interval = poll_interval
163165
self.deferrable = deferrable
@@ -254,6 +256,7 @@ def execute(self, context):
254256
driver_annotations=self.driver_annotations,
255257
executor_annotations=self.executor_annotations,
256258
driver_tolerations=self.driver_tolerations,
259+
executor_pod_template_path=self.executor_pod_template_path,
257260
)
258261
if self.app_waiter == "no_wait":
259262
return

spark_on_k8s/cli/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
executor_memory_overhead_option,
2727
executor_min_instances_option,
2828
executor_node_selector_option,
29+
executor_pod_template_path_option,
2930
force_option,
3031
image_pull_policy_option,
3132
logs_option,
@@ -129,6 +130,7 @@ def wait(app_id: str, namespace: str):
129130
executor_labels_option,
130131
driver_annotations_option,
131132
executor_annotations_option,
133+
executor_pod_template_path_option,
132134
],
133135
help="Submit a Spark application.",
134136
)
@@ -163,6 +165,7 @@ def submit(
163165
executor_labels: dict[str, str],
164166
driver_annotations: dict[str, str],
165167
executor_annotations: dict[str, str],
168+
executor_pod_template_path: str,
166169
):
167170
from spark_on_k8s.client import ExecutorInstances, PodResources, SparkOnK8S
168171

@@ -203,4 +206,5 @@ def submit(
203206
executor_labels=executor_labels,
204207
driver_annotations=driver_annotations,
205208
executor_annotations=executor_annotations,
209+
executor_pod_template_path=executor_pod_template_path,
206210
)

spark_on_k8s/cli/options.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,10 @@ def validate_list_option(ctx, param, value):
254254
show_default=True,
255255
help="Annotations for the executor in key=value format. Can be repeated.",
256256
)
257+
executor_pod_template_path_option = click.Option(
258+
("--executor-pod-template-path", "executor_pod_template_path"),
259+
type=str,
260+
default=Configuration.SPARK_ON_K8S_EXECUTOR_POD_TEMPLATE_PATH,
261+
show_default=True,
262+
help="The path to the executor pod template file.",
263+
)

spark_on_k8s/client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def submit_app(
130130
driver_labels: dict[str, str] | ArgNotSet = NOTSET,
131131
executor_labels: dict[str, str] | ArgNotSet = NOTSET,
132132
driver_tolerations: list[k8s.V1Toleration] | ArgNotSet = NOTSET,
133+
executor_pod_template_path: str | ArgNotSet = NOTSET,
133134
) -> str:
134135
"""Submit a Spark app to Kubernetes
135136
@@ -168,6 +169,7 @@ def submit_app(
168169
driver_node_selector: Node selector for the driver
169170
executor_node_selector: Node selector for the executors
170171
driver_tolerations: List of tolerations for the driver
172+
executor_pod_template_path: Path to the executor pod template file
171173
172174
Returns:
173175
Name of the Spark application pod
@@ -262,6 +264,8 @@ def submit_app(
262264
executor_labels = {}
263265
if driver_tolerations is NOTSET or driver_tolerations is None:
264266
driver_tolerations = []
267+
if executor_pod_template_path is NOTSET or executor_pod_template_path is None:
268+
executor_pod_template_path = Configuration.SPARK_ON_K8S_EXECUTOR_POD_TEMPLATE_PATH
265269

266270
spark_conf = spark_conf or {}
267271
main_class_parameters = app_arguments or []
@@ -313,6 +317,8 @@ def submit_app(
313317
basic_conf.update(self._executor_labels(labels=executor_labels))
314318
if executor_annotations:
315319
basic_conf.update(self._executor_annotations(annotations=executor_annotations))
320+
if executor_pod_template_path:
321+
basic_conf.update(self._executor_pod_template_path(executor_pod_template_path))
316322
driver_command_args = ["driver", "--master", "k8s://https://kubernetes.default.svc.cluster.local:443"]
317323
if class_name:
318324
driver_command_args.extend(["--class", class_name])
@@ -607,3 +613,19 @@ def _executor_annotations(
607613
if not annotations:
608614
return {}
609615
return {f"spark.kubernetes.executor.annotation.{key}": value for key, value in annotations.items()}
616+
617+
@staticmethod
618+
def _executor_pod_template_path(
619+
executor_pod_template_path: str | None,
620+
) -> dict[str, str]:
621+
"""Spark configuration to set the executor pod template file
622+
623+
Args:
624+
executor_pod_template_path: Path to the executor pod template file
625+
626+
Returns:
627+
Spark configuration dictionary
628+
"""
629+
if not executor_pod_template_path:
630+
return {}
631+
return {"spark.kubernetes.executor.podTemplateFile": executor_pod_template_path}

spark_on_k8s/utils/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class Configuration:
7070
SPARK_ON_K8S_SPARK_EXECUTOR_ANNOTATIONS = json.loads(
7171
getenv("SPARK_ON_K8S_SPARK_EXECUTOR_ANNOTATIONS", "{}")
7272
)
73+
SPARK_ON_K8S_EXECUTOR_POD_TEMPLATE_PATH = getenv("SPARK_ON_K8S_EXECUTOR_POD_TEMPLATE_PATH", None)
7374
try:
7475
from kubernetes_asyncio import client as async_k8s
7576

tests/airflow/test_operators.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_execute(self, mock_submit_app):
4141
driver_annotations={"annotation1": "value1"},
4242
executor_annotations={"annotation2": "value2"},
4343
driver_tolerations=test_tolerations,
44+
executor_pod_template_path="s3a://bucket/executor.yml",
4445
)
4546
spark_app_task.execute(None)
4647
mock_submit_app.assert_called_once_with(
@@ -69,6 +70,7 @@ def test_execute(self, mock_submit_app):
6970
driver_annotations={"annotation1": "value1"},
7071
executor_annotations={"annotation2": "value2"},
7172
driver_tolerations=test_tolerations,
73+
executor_pod_template_path="s3a://bucket/executor.yml",
7274
)
7375

7476
@mock.patch("spark_on_k8s.client.SparkOnK8S.submit_app")
@@ -158,4 +160,5 @@ def test_rendering_templates(self, mock_submit_app):
158160
driver_annotations=None,
159161
executor_annotations=None,
160162
driver_tolerations=None,
163+
executor_pod_template_path=None,
161164
)

tests/test_spark_client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,3 +803,37 @@ def test_submit_app_with_tolerations(
803803
effect="NoExecute",
804804
),
805805
]
806+
807+
@mock.patch("spark_on_k8s.k8s.sync_client.KubernetesClientManager.create_client")
808+
@mock.patch("kubernetes.client.api.core_v1_api.CoreV1Api.create_namespaced_pod")
809+
@mock.patch("kubernetes.client.api.core_v1_api.CoreV1Api.create_namespaced_service")
810+
@freeze_time(FAKE_TIME)
811+
def test_submit_app_with_executor_pod_template_path(
812+
self, mock_create_namespaced_service, mock_create_namespaced_pod, mock_create_client
813+
):
814+
"""Test the method submit_app"""
815+
816+
spark_client = SparkOnK8S()
817+
spark_client.submit_app(
818+
image="pyspark-job",
819+
app_path="local:///opt/spark/work-dir/job.py",
820+
namespace="spark",
821+
service_account="spark",
822+
app_name="pyspark-job-example",
823+
app_arguments=["100000"],
824+
app_waiter="no_wait",
825+
image_pull_policy="Never",
826+
ui_reverse_proxy=True,
827+
driver_resources=PodResources(cpu=1, memory=2048, memory_overhead=1024),
828+
executor_instances=ExecutorInstances(min=2, max=5, initial=5),
829+
executor_pod_template_path="s3a://bucket/executor.yml",
830+
)
831+
832+
created_pod = mock_create_namespaced_pod.call_args[1]["body"]
833+
arguments = created_pod.spec.containers[0].args
834+
executor_config = {
835+
conf.split("=")[0]: conf.split("=")[1]
836+
for conf in arguments
837+
if conf.startswith("spark.kubernetes.executor")
838+
}
839+
assert executor_config.get("spark.kubernetes.executor.podTemplateFile") == "s3a://bucket/executor.yml"

0 commit comments

Comments
 (0)