apache-airflow-providers-cncf-kubernetes 8.0.1__py3-none-any.whl → 8.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of apache-airflow-providers-cncf-kubernetes might be problematic. Click here for more details.
- airflow/providers/cncf/kubernetes/__init__.py +1 -1
- airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +1 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +1 -0
- airflow/providers/cncf/kubernetes/get_provider_info.py +6 -2
- airflow/providers/cncf/kubernetes/hooks/kubernetes.py +149 -0
- airflow/providers/cncf/kubernetes/k8s_model.py +1 -0
- airflow/providers/cncf/kubernetes/kube_client.py +1 -0
- airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +1 -1
- airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +4 -3
- airflow/providers/cncf/kubernetes/operators/job.py +239 -0
- airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +1 -0
- airflow/providers/cncf/kubernetes/operators/pod.py +35 -16
- airflow/providers/cncf/kubernetes/operators/resource.py +47 -13
- airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +27 -3
- airflow/providers/cncf/kubernetes/pod_generator.py +3 -1
- airflow/providers/cncf/kubernetes/pod_generator_deprecated.py +1 -0
- airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py +1 -0
- airflow/providers/cncf/kubernetes/python_kubernetes_script.py +1 -0
- airflow/providers/cncf/kubernetes/secret.py +1 -0
- airflow/providers/cncf/kubernetes/triggers/job.py +101 -0
- airflow/providers/cncf/kubernetes/triggers/kubernetes_pod.py +1 -0
- airflow/providers/cncf/kubernetes/triggers/pod.py +11 -3
- airflow/providers/cncf/kubernetes/utils/pod_manager.py +2 -1
- airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +1 -0
- {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/METADATA +8 -7
- {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/RECORD +28 -27
- {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -21,12 +21,14 @@ from __future__ import annotations
|
|
|
21
21
|
import datetime
|
|
22
22
|
import json
|
|
23
23
|
import logging
|
|
24
|
+
import math
|
|
24
25
|
import re
|
|
25
26
|
import shlex
|
|
26
27
|
import string
|
|
27
28
|
import warnings
|
|
28
29
|
from collections.abc import Container
|
|
29
30
|
from contextlib import AbstractContextManager
|
|
31
|
+
from enum import Enum
|
|
30
32
|
from functools import cached_property
|
|
31
33
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence
|
|
32
34
|
|
|
@@ -60,8 +62,8 @@ from airflow.providers.cncf.kubernetes.callbacks import ExecutionMode, Kubernete
|
|
|
60
62
|
from airflow.providers.cncf.kubernetes.hooks.kubernetes import KubernetesHook
|
|
61
63
|
from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import (
|
|
62
64
|
POD_NAME_MAX_LENGTH,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
add_unique_suffix,
|
|
66
|
+
create_unique_id,
|
|
65
67
|
)
|
|
66
68
|
from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator
|
|
67
69
|
from airflow.providers.cncf.kubernetes.triggers.pod import KubernetesPodTrigger
|
|
@@ -95,6 +97,13 @@ alphanum_lower = string.ascii_lowercase + string.digits
|
|
|
95
97
|
KUBE_CONFIG_ENV_VAR = "KUBECONFIG"
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
class PodEventType(Enum):
|
|
101
|
+
"""Type of Events emitted by kubernetes pod."""
|
|
102
|
+
|
|
103
|
+
WARNING = "Warning"
|
|
104
|
+
NORMAL = "Normal"
|
|
105
|
+
|
|
106
|
+
|
|
98
107
|
class PodReattachFailure(AirflowException):
|
|
99
108
|
"""When we expect to be able to find a pod but cannot."""
|
|
100
109
|
|
|
@@ -266,7 +275,7 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
266
275
|
labels: dict | None = None,
|
|
267
276
|
reattach_on_restart: bool = True,
|
|
268
277
|
startup_timeout_seconds: int = 120,
|
|
269
|
-
startup_check_interval_seconds: int =
|
|
278
|
+
startup_check_interval_seconds: int = 5,
|
|
270
279
|
get_logs: bool = True,
|
|
271
280
|
container_logs: Iterable[str] | str | Literal[True] = BASE_CONTAINER_NAME,
|
|
272
281
|
image_pull_policy: str | None = None,
|
|
@@ -548,8 +557,7 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
548
557
|
)
|
|
549
558
|
except PodLaunchFailedException:
|
|
550
559
|
if self.log_events_on_failure:
|
|
551
|
-
|
|
552
|
-
self.log.error("Pod Event: %s - %s", event.reason, event.message)
|
|
560
|
+
self._read_pod_events(pod, reraise=False)
|
|
553
561
|
raise
|
|
554
562
|
|
|
555
563
|
def extract_xcom(self, pod: k8s.V1Pod):
|
|
@@ -703,10 +711,13 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
703
711
|
pod=self.pod, event=event, client=self.client, mode=ExecutionMode.SYNC
|
|
704
712
|
)
|
|
705
713
|
|
|
714
|
+
follow = self.logging_interval is None
|
|
715
|
+
last_log_time = event.get("last_log_time")
|
|
716
|
+
|
|
706
717
|
if event["status"] in ("error", "failed", "timeout"):
|
|
707
718
|
# fetch some logs when pod is failed
|
|
708
719
|
if self.get_logs:
|
|
709
|
-
self.write_logs(self.pod)
|
|
720
|
+
self.write_logs(self.pod, follow=follow, since_time=last_log_time)
|
|
710
721
|
|
|
711
722
|
if self.do_xcom_push:
|
|
712
723
|
_ = self.extract_xcom(pod=self.pod)
|
|
@@ -716,13 +727,12 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
716
727
|
|
|
717
728
|
elif event["status"] == "running":
|
|
718
729
|
if self.get_logs:
|
|
719
|
-
last_log_time = event.get("last_log_time")
|
|
720
730
|
self.log.info("Resuming logs read from time %r", last_log_time)
|
|
721
731
|
|
|
722
732
|
pod_log_status = self.pod_manager.fetch_container_logs(
|
|
723
733
|
pod=self.pod,
|
|
724
734
|
container_name=self.BASE_CONTAINER_NAME,
|
|
725
|
-
follow=
|
|
735
|
+
follow=follow,
|
|
726
736
|
since_time=last_log_time,
|
|
727
737
|
)
|
|
728
738
|
|
|
@@ -735,7 +745,7 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
735
745
|
elif event["status"] == "success":
|
|
736
746
|
# fetch some logs when pod is executed successfully
|
|
737
747
|
if self.get_logs:
|
|
738
|
-
self.write_logs(self.pod)
|
|
748
|
+
self.write_logs(self.pod, follow=follow, since_time=last_log_time)
|
|
739
749
|
|
|
740
750
|
if self.do_xcom_push:
|
|
741
751
|
xcom_sidecar_output = self.extract_xcom(pod=self.pod)
|
|
@@ -764,14 +774,20 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
764
774
|
|
|
765
775
|
@deprecated(reason="use `trigger_reentry` instead.", category=AirflowProviderDeprecationWarning)
|
|
766
776
|
def execute_complete(self, context: Context, event: dict, **kwargs):
|
|
767
|
-
self.trigger_reentry(context=context, event=event)
|
|
777
|
+
return self.trigger_reentry(context=context, event=event)
|
|
768
778
|
|
|
769
|
-
def write_logs(self, pod: k8s.V1Pod):
|
|
779
|
+
def write_logs(self, pod: k8s.V1Pod, follow: bool = False, since_time: DateTime | None = None):
|
|
770
780
|
try:
|
|
781
|
+
since_seconds = (
|
|
782
|
+
math.ceil((datetime.datetime.now(tz=datetime.timezone.utc) - since_time).total_seconds())
|
|
783
|
+
if since_time
|
|
784
|
+
else None
|
|
785
|
+
)
|
|
771
786
|
logs = self.pod_manager.read_pod_logs(
|
|
772
787
|
pod=pod,
|
|
773
788
|
container_name=self.base_container_name,
|
|
774
|
-
follow=
|
|
789
|
+
follow=follow,
|
|
790
|
+
since_seconds=since_seconds,
|
|
775
791
|
)
|
|
776
792
|
for raw_line in logs:
|
|
777
793
|
line = raw_line.decode("utf-8", errors="backslashreplace").rstrip("\n")
|
|
@@ -855,7 +871,10 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
855
871
|
"""Will fetch and emit events from pod."""
|
|
856
872
|
with _optionally_suppress(reraise=reraise):
|
|
857
873
|
for event in self.pod_manager.read_pod_events(pod).items:
|
|
858
|
-
|
|
874
|
+
if event.type == PodEventType.NORMAL.value:
|
|
875
|
+
self.log.info("Pod Event: %s - %s", event.reason, event.message)
|
|
876
|
+
else:
|
|
877
|
+
self.log.error("Pod Event: %s - %s", event.reason, event.message)
|
|
859
878
|
|
|
860
879
|
def is_istio_enabled(self, pod: V1Pod) -> bool:
|
|
861
880
|
"""Check if istio is enabled for the namespace of the pod by inspecting the namespace labels."""
|
|
@@ -891,7 +910,7 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
891
910
|
self.log.info("Output of curl command to kill istio: %s", output_str)
|
|
892
911
|
resp.close()
|
|
893
912
|
if self.KILL_ISTIO_PROXY_SUCCESS_MSG not in output_str:
|
|
894
|
-
raise
|
|
913
|
+
raise AirflowException("Error while deleting istio-proxy sidecar: %s", output_str)
|
|
895
914
|
|
|
896
915
|
def process_pod_deletion(self, pod: k8s.V1Pod, *, reraise=True):
|
|
897
916
|
with _optionally_suppress(reraise=reraise):
|
|
@@ -1030,12 +1049,12 @@ class KubernetesPodOperator(BaseOperator):
|
|
|
1030
1049
|
pod = PodGenerator.reconcile_pods(pod_template, pod)
|
|
1031
1050
|
|
|
1032
1051
|
if not pod.metadata.name:
|
|
1033
|
-
pod.metadata.name =
|
|
1052
|
+
pod.metadata.name = create_unique_id(
|
|
1034
1053
|
task_id=self.task_id, unique=self.random_name_suffix, max_length=POD_NAME_MAX_LENGTH
|
|
1035
1054
|
)
|
|
1036
1055
|
elif self.random_name_suffix:
|
|
1037
1056
|
# user has supplied pod name, we're just adding suffix
|
|
1038
|
-
pod.metadata.name =
|
|
1057
|
+
pod.metadata.name = add_unique_suffix(name=pod.metadata.name)
|
|
1039
1058
|
|
|
1040
1059
|
if not pod.metadata.namespace:
|
|
1041
1060
|
hook_namespace = self.hook.get_namespace()
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import os
|
|
21
22
|
from functools import cached_property
|
|
22
|
-
from typing import TYPE_CHECKING
|
|
23
|
+
from typing import TYPE_CHECKING, Sequence
|
|
23
24
|
|
|
24
25
|
import yaml
|
|
25
26
|
from kubernetes.utils import create_from_yaml
|
|
26
27
|
|
|
28
|
+
from airflow.exceptions import AirflowException
|
|
27
29
|
from airflow.models import BaseOperator
|
|
28
30
|
from airflow.providers.cncf.kubernetes.hooks.kubernetes import KubernetesHook
|
|
29
31
|
from airflow.providers.cncf.kubernetes.utils.delete_from import delete_from_yaml
|
|
@@ -40,24 +42,29 @@ class KubernetesResourceBaseOperator(BaseOperator):
|
|
|
40
42
|
Abstract base class for all Kubernetes Resource operators.
|
|
41
43
|
|
|
42
44
|
:param yaml_conf: string. Contains the kubernetes resources to Create or Delete
|
|
45
|
+
:param yaml_conf_file: path to the kubernetes resources file (templated)
|
|
43
46
|
:param namespace: string. Contains the namespace to create all resources inside.
|
|
44
47
|
The namespace must preexist otherwise the resource creation will fail.
|
|
45
48
|
If the API object in the yaml file already contains a namespace definition then
|
|
46
49
|
this parameter has no effect.
|
|
47
50
|
:param kubernetes_conn_id: The :ref:`kubernetes connection id <howto/connection:kubernetes>`
|
|
48
51
|
for the Kubernetes cluster.
|
|
52
|
+
:param namespaced: specified that Kubernetes resource is or isn't in a namespace.
|
|
53
|
+
This parameter works only when custom_resource_definition parameter is True.
|
|
49
54
|
"""
|
|
50
55
|
|
|
51
|
-
template_fields = ("yaml_conf",)
|
|
56
|
+
template_fields: Sequence[str] = ("yaml_conf", "yaml_conf_file")
|
|
52
57
|
template_fields_renderers = {"yaml_conf": "yaml"}
|
|
53
58
|
|
|
54
59
|
def __init__(
|
|
55
60
|
self,
|
|
56
61
|
*,
|
|
57
|
-
yaml_conf: str,
|
|
62
|
+
yaml_conf: str | None = None,
|
|
63
|
+
yaml_conf_file: str | None = None,
|
|
58
64
|
namespace: str | None = None,
|
|
59
65
|
kubernetes_conn_id: str | None = KubernetesHook.default_conn_name,
|
|
60
66
|
custom_resource_definition: bool = False,
|
|
67
|
+
namespaced: bool = True,
|
|
61
68
|
config_file: str | None = None,
|
|
62
69
|
**kwargs,
|
|
63
70
|
) -> None:
|
|
@@ -65,9 +72,14 @@ class KubernetesResourceBaseOperator(BaseOperator):
|
|
|
65
72
|
self._namespace = namespace
|
|
66
73
|
self.kubernetes_conn_id = kubernetes_conn_id
|
|
67
74
|
self.yaml_conf = yaml_conf
|
|
75
|
+
self.yaml_conf_file = yaml_conf_file
|
|
68
76
|
self.custom_resource_definition = custom_resource_definition
|
|
77
|
+
self.namespaced = namespaced
|
|
69
78
|
self.config_file = config_file
|
|
70
79
|
|
|
80
|
+
if not any([self.yaml_conf, self.yaml_conf_file]):
|
|
81
|
+
raise AirflowException("One of `yaml_conf` or `yaml_conf_file` arguments must be provided")
|
|
82
|
+
|
|
71
83
|
@cached_property
|
|
72
84
|
def client(self) -> ApiClient:
|
|
73
85
|
return self.hook.api_client
|
|
@@ -109,18 +121,29 @@ class KubernetesCreateResourceOperator(KubernetesResourceBaseOperator):
|
|
|
109
121
|
|
|
110
122
|
def create_custom_from_yaml_object(self, body: dict):
|
|
111
123
|
group, version, namespace, plural = self.get_crd_fields(body)
|
|
112
|
-
self.
|
|
124
|
+
if self.namespaced:
|
|
125
|
+
self.custom_object_client.create_namespaced_custom_object(group, version, namespace, plural, body)
|
|
126
|
+
else:
|
|
127
|
+
self.custom_object_client.create_cluster_custom_object(group, version, plural, body)
|
|
113
128
|
|
|
114
|
-
def
|
|
115
|
-
resources = yaml.safe_load_all(self.yaml_conf)
|
|
129
|
+
def _create_objects(self, objects):
|
|
116
130
|
if not self.custom_resource_definition:
|
|
117
131
|
create_from_yaml(
|
|
118
132
|
k8s_client=self.client,
|
|
119
|
-
yaml_objects=
|
|
133
|
+
yaml_objects=objects,
|
|
120
134
|
namespace=self.get_namespace(),
|
|
121
135
|
)
|
|
122
136
|
else:
|
|
123
|
-
k8s_resource_iterator(self.create_custom_from_yaml_object,
|
|
137
|
+
k8s_resource_iterator(self.create_custom_from_yaml_object, objects)
|
|
138
|
+
|
|
139
|
+
def execute(self, context) -> None:
|
|
140
|
+
if self.yaml_conf:
|
|
141
|
+
self._create_objects(yaml.safe_load_all(self.yaml_conf))
|
|
142
|
+
elif self.yaml_conf_file and os.path.exists(self.yaml_conf_file):
|
|
143
|
+
with open(self.yaml_conf_file) as stream:
|
|
144
|
+
self._create_objects(yaml.safe_load_all(stream))
|
|
145
|
+
else:
|
|
146
|
+
raise AirflowException("File %s not found", self.yaml_conf_file)
|
|
124
147
|
|
|
125
148
|
|
|
126
149
|
class KubernetesDeleteResourceOperator(KubernetesResourceBaseOperator):
|
|
@@ -129,15 +152,26 @@ class KubernetesDeleteResourceOperator(KubernetesResourceBaseOperator):
|
|
|
129
152
|
def delete_custom_from_yaml_object(self, body: dict):
|
|
130
153
|
name = body["metadata"]["name"]
|
|
131
154
|
group, version, namespace, plural = self.get_crd_fields(body)
|
|
132
|
-
self.
|
|
155
|
+
if self.namespaced:
|
|
156
|
+
self.custom_object_client.delete_namespaced_custom_object(group, version, namespace, plural, name)
|
|
157
|
+
else:
|
|
158
|
+
self.custom_object_client.delete_cluster_custom_object(group, version, plural, name)
|
|
133
159
|
|
|
134
|
-
def
|
|
135
|
-
resources = yaml.safe_load_all(self.yaml_conf)
|
|
160
|
+
def _delete_objects(self, objects):
|
|
136
161
|
if not self.custom_resource_definition:
|
|
137
162
|
delete_from_yaml(
|
|
138
163
|
k8s_client=self.client,
|
|
139
|
-
yaml_objects=
|
|
164
|
+
yaml_objects=objects,
|
|
140
165
|
namespace=self.get_namespace(),
|
|
141
166
|
)
|
|
142
167
|
else:
|
|
143
|
-
k8s_resource_iterator(self.delete_custom_from_yaml_object,
|
|
168
|
+
k8s_resource_iterator(self.delete_custom_from_yaml_object, objects)
|
|
169
|
+
|
|
170
|
+
def execute(self, context) -> None:
|
|
171
|
+
if self.yaml_conf:
|
|
172
|
+
self._delete_objects(yaml.safe_load_all(self.yaml_conf))
|
|
173
|
+
elif self.yaml_conf_file and os.path.exists(self.yaml_conf_file):
|
|
174
|
+
with open(self.yaml_conf_file) as stream:
|
|
175
|
+
self._delete_objects(yaml.safe_load_all(stream))
|
|
176
|
+
else:
|
|
177
|
+
raise AirflowException("File %s not found", self.yaml_conf_file)
|
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import re
|
|
21
21
|
from functools import cached_property
|
|
22
|
+
from pathlib import Path
|
|
22
23
|
from typing import TYPE_CHECKING, Any
|
|
23
24
|
|
|
24
25
|
from kubernetes.client import CoreV1Api, CustomObjectsApi, models as k8s
|
|
@@ -26,6 +27,7 @@ from kubernetes.client import CoreV1Api, CustomObjectsApi, models as k8s
|
|
|
26
27
|
from airflow.exceptions import AirflowException
|
|
27
28
|
from airflow.providers.cncf.kubernetes import pod_generator
|
|
28
29
|
from airflow.providers.cncf.kubernetes.hooks.kubernetes import KubernetesHook, _load_body_to_dict
|
|
30
|
+
from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import add_unique_suffix
|
|
29
31
|
from airflow.providers.cncf.kubernetes.operators.custom_object_launcher import CustomObjectLauncher
|
|
30
32
|
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
|
|
31
33
|
from airflow.providers.cncf.kubernetes.pod_generator import MAX_LABEL_LEN, PodGenerator
|
|
@@ -71,6 +73,8 @@ class SparkKubernetesOperator(KubernetesPodOperator):
|
|
|
71
73
|
template_ext = ("yaml", "yml", "json")
|
|
72
74
|
ui_color = "#f4a460"
|
|
73
75
|
|
|
76
|
+
BASE_CONTAINER_NAME = "spark-kubernetes-driver"
|
|
77
|
+
|
|
74
78
|
def __init__(
|
|
75
79
|
self,
|
|
76
80
|
*,
|
|
@@ -108,6 +112,18 @@ class SparkKubernetesOperator(KubernetesPodOperator):
|
|
|
108
112
|
self.log_events_on_failure = log_events_on_failure
|
|
109
113
|
self.success_run_history_limit = success_run_history_limit
|
|
110
114
|
|
|
115
|
+
if self.base_container_name != self.BASE_CONTAINER_NAME:
|
|
116
|
+
self.log.warning(
|
|
117
|
+
"base_container_name is not supported and will be overridden to %s", self.BASE_CONTAINER_NAME
|
|
118
|
+
)
|
|
119
|
+
self.base_container_name = self.BASE_CONTAINER_NAME
|
|
120
|
+
|
|
121
|
+
if self.get_logs and self.container_logs != self.BASE_CONTAINER_NAME:
|
|
122
|
+
self.log.warning(
|
|
123
|
+
"container_logs is not supported and will be overridden to %s", self.BASE_CONTAINER_NAME
|
|
124
|
+
)
|
|
125
|
+
self.container_logs = [self.BASE_CONTAINER_NAME]
|
|
126
|
+
|
|
111
127
|
def _render_nested_template_fields(
|
|
112
128
|
self,
|
|
113
129
|
content: Any,
|
|
@@ -124,7 +140,16 @@ class SparkKubernetesOperator(KubernetesPodOperator):
|
|
|
124
140
|
|
|
125
141
|
def manage_template_specs(self):
|
|
126
142
|
if self.application_file:
|
|
127
|
-
|
|
143
|
+
try:
|
|
144
|
+
filepath = Path(self.application_file.rstrip()).resolve(strict=True)
|
|
145
|
+
except (FileNotFoundError, OSError, RuntimeError, ValueError):
|
|
146
|
+
application_file_body = self.application_file
|
|
147
|
+
else:
|
|
148
|
+
application_file_body = filepath.read_text()
|
|
149
|
+
template_body = _load_body_to_dict(application_file_body)
|
|
150
|
+
if not isinstance(template_body, dict):
|
|
151
|
+
msg = f"application_file body can't transformed into the dictionary:\n{application_file_body}"
|
|
152
|
+
raise TypeError(msg)
|
|
128
153
|
elif self.template_spec:
|
|
129
154
|
template_body = self.template_spec
|
|
130
155
|
else:
|
|
@@ -134,7 +159,7 @@ class SparkKubernetesOperator(KubernetesPodOperator):
|
|
|
134
159
|
return template_body
|
|
135
160
|
|
|
136
161
|
def create_job_name(self):
|
|
137
|
-
initial_name =
|
|
162
|
+
initial_name = add_unique_suffix(name=self.task_id, max_len=MAX_LABEL_LEN)
|
|
138
163
|
return re.sub(r"[^a-z0-9-]+", "-", initial_name.lower())
|
|
139
164
|
|
|
140
165
|
@staticmethod
|
|
@@ -263,7 +288,6 @@ class SparkKubernetesOperator(KubernetesPodOperator):
|
|
|
263
288
|
template_body=self.template_body,
|
|
264
289
|
)
|
|
265
290
|
self.pod = self.get_or_create_spark_crd(self.launcher, context)
|
|
266
|
-
self.BASE_CONTAINER_NAME = "spark-kubernetes-driver"
|
|
267
291
|
self.pod_request_obj = self.launcher.pod_spec
|
|
268
292
|
|
|
269
293
|
return super().execute(context=context)
|
|
@@ -22,6 +22,7 @@ API and outputs a kubernetes.client.models.V1Pod.
|
|
|
22
22
|
The advantage being that the full Kubernetes API
|
|
23
23
|
is supported and no serialization need be written.
|
|
24
24
|
"""
|
|
25
|
+
|
|
25
26
|
from __future__ import annotations
|
|
26
27
|
|
|
27
28
|
import copy
|
|
@@ -45,6 +46,7 @@ from airflow.exceptions import (
|
|
|
45
46
|
from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import (
|
|
46
47
|
POD_NAME_MAX_LENGTH,
|
|
47
48
|
add_pod_suffix,
|
|
49
|
+
add_unique_suffix,
|
|
48
50
|
rand_str,
|
|
49
51
|
)
|
|
50
52
|
from airflow.providers.cncf.kubernetes.pod_generator_deprecated import (
|
|
@@ -396,7 +398,7 @@ class PodGenerator:
|
|
|
396
398
|
UserWarning,
|
|
397
399
|
stacklevel=2,
|
|
398
400
|
)
|
|
399
|
-
pod_id =
|
|
401
|
+
pod_id = add_unique_suffix(name=pod_id, max_len=POD_NAME_MAX_LENGTH)
|
|
400
402
|
try:
|
|
401
403
|
image = pod_override_object.spec.containers[0].image # type: ignore
|
|
402
404
|
if not image:
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from functools import cached_property
|
|
20
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator
|
|
21
|
+
|
|
22
|
+
from airflow.providers.cncf.kubernetes.hooks.kubernetes import AsyncKubernetesHook
|
|
23
|
+
from airflow.triggers.base import BaseTrigger, TriggerEvent
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from kubernetes.client import V1Job
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KubernetesJobTrigger(BaseTrigger):
|
|
30
|
+
"""
|
|
31
|
+
KubernetesJobTrigger run on the trigger worker to check the state of Job.
|
|
32
|
+
|
|
33
|
+
:param job_name: The name of the job.
|
|
34
|
+
:param job_namespace: The namespace of the job.
|
|
35
|
+
:param kubernetes_conn_id: The :ref:`kubernetes connection id <howto/connection:kubernetes>`
|
|
36
|
+
for the Kubernetes cluster.
|
|
37
|
+
:param cluster_context: Context that points to kubernetes cluster.
|
|
38
|
+
:param config_file: Path to kubeconfig file.
|
|
39
|
+
:param poll_interval: Polling period in seconds to check for the status.
|
|
40
|
+
:param in_cluster: run kubernetes client with in_cluster configuration.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
job_name: str,
|
|
46
|
+
job_namespace: str,
|
|
47
|
+
kubernetes_conn_id: str | None = None,
|
|
48
|
+
poll_interval: float = 10.0,
|
|
49
|
+
cluster_context: str | None = None,
|
|
50
|
+
config_file: str | None = None,
|
|
51
|
+
in_cluster: bool | None = None,
|
|
52
|
+
):
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.job_name = job_name
|
|
55
|
+
self.job_namespace = job_namespace
|
|
56
|
+
self.kubernetes_conn_id = kubernetes_conn_id
|
|
57
|
+
self.poll_interval = poll_interval
|
|
58
|
+
self.cluster_context = cluster_context
|
|
59
|
+
self.config_file = config_file
|
|
60
|
+
self.in_cluster = in_cluster
|
|
61
|
+
|
|
62
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
63
|
+
"""Serialize KubernetesCreateJobTrigger arguments and classpath."""
|
|
64
|
+
return (
|
|
65
|
+
"airflow.providers.cncf.kubernetes.triggers.job.KubernetesJobTrigger",
|
|
66
|
+
{
|
|
67
|
+
"job_name": self.job_name,
|
|
68
|
+
"job_namespace": self.job_namespace,
|
|
69
|
+
"kubernetes_conn_id": self.kubernetes_conn_id,
|
|
70
|
+
"poll_interval": self.poll_interval,
|
|
71
|
+
"cluster_context": self.cluster_context,
|
|
72
|
+
"config_file": self.config_file,
|
|
73
|
+
"in_cluster": self.in_cluster,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def run(self) -> AsyncIterator[TriggerEvent]: # type: ignore[override]
|
|
78
|
+
"""Get current job status and yield a TriggerEvent."""
|
|
79
|
+
job: V1Job = await self.hook.wait_until_job_complete(name=self.job_name, namespace=self.job_namespace)
|
|
80
|
+
job_dict = job.to_dict()
|
|
81
|
+
error_message = self.hook.is_job_failed(job=job)
|
|
82
|
+
yield TriggerEvent(
|
|
83
|
+
{
|
|
84
|
+
"name": job.metadata.name,
|
|
85
|
+
"namespace": job.metadata.namespace,
|
|
86
|
+
"status": "error" if error_message else "success",
|
|
87
|
+
"message": f"Job failed with error: {error_message}"
|
|
88
|
+
if error_message
|
|
89
|
+
else "Job completed successfully",
|
|
90
|
+
"job": job_dict,
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@cached_property
|
|
95
|
+
def hook(self) -> AsyncKubernetesHook:
|
|
96
|
+
return AsyncKubernetesHook(
|
|
97
|
+
conn_id=self.kubernetes_conn_id,
|
|
98
|
+
in_cluster=self.in_cluster,
|
|
99
|
+
config_file=self.config_file,
|
|
100
|
+
cluster_context=self.cluster_context,
|
|
101
|
+
)
|
|
@@ -67,6 +67,7 @@ class KubernetesPodTrigger(BaseTrigger):
|
|
|
67
67
|
:param in_cluster: run kubernetes client with in_cluster configuration.
|
|
68
68
|
:param get_logs: get the stdout of the container as logs of the tasks.
|
|
69
69
|
:param startup_timeout: timeout in seconds to start up the pod.
|
|
70
|
+
:param startup_check_interval: interval in seconds to check if the pod has already started.
|
|
70
71
|
:param on_finish_action: What to do when the pod reaches its final state, or the execution is interrupted.
|
|
71
72
|
If "delete_pod", the pod will be deleted regardless its state; if "delete_succeeded_pod",
|
|
72
73
|
only succeeded pod will be deleted. You can set to "keep_pod" to keep the pod.
|
|
@@ -92,7 +93,7 @@ class KubernetesPodTrigger(BaseTrigger):
|
|
|
92
93
|
in_cluster: bool | None = None,
|
|
93
94
|
get_logs: bool = True,
|
|
94
95
|
startup_timeout: int = 120,
|
|
95
|
-
startup_check_interval: int =
|
|
96
|
+
startup_check_interval: int = 5,
|
|
96
97
|
on_finish_action: str = "delete_pod",
|
|
97
98
|
should_delete_pod: bool | None = None,
|
|
98
99
|
last_log_time: DateTime | None = None,
|
|
@@ -145,6 +146,7 @@ class KubernetesPodTrigger(BaseTrigger):
|
|
|
145
146
|
"in_cluster": self.in_cluster,
|
|
146
147
|
"get_logs": self.get_logs,
|
|
147
148
|
"startup_timeout": self.startup_timeout,
|
|
149
|
+
"startup_check_interval": self.startup_check_interval,
|
|
148
150
|
"trigger_start_time": self.trigger_start_time,
|
|
149
151
|
"should_delete_pod": self.should_delete_pod,
|
|
150
152
|
"on_finish_action": self.on_finish_action.value,
|
|
@@ -223,7 +225,7 @@ class KubernetesPodTrigger(BaseTrigger):
|
|
|
223
225
|
if not pod.status.phase == "Pending":
|
|
224
226
|
return self.define_container_state(pod)
|
|
225
227
|
self.log.info("Still waiting for pod to start. The pod state is %s", pod.status.phase)
|
|
226
|
-
await asyncio.sleep(self.
|
|
228
|
+
await asyncio.sleep(self.startup_check_interval)
|
|
227
229
|
delta = datetime.datetime.now(tz=datetime.timezone.utc) - self.trigger_start_time
|
|
228
230
|
raise PodLaunchTimeoutException("Pod did not leave 'Pending' phase within specified timeout")
|
|
229
231
|
|
|
@@ -243,7 +245,12 @@ class KubernetesPodTrigger(BaseTrigger):
|
|
|
243
245
|
container_state = self.define_container_state(pod)
|
|
244
246
|
if container_state == ContainerState.TERMINATED:
|
|
245
247
|
return TriggerEvent(
|
|
246
|
-
{
|
|
248
|
+
{
|
|
249
|
+
"status": "success",
|
|
250
|
+
"namespace": self.pod_namespace,
|
|
251
|
+
"name": self.pod_name,
|
|
252
|
+
"last_log_time": self.last_log_time,
|
|
253
|
+
}
|
|
247
254
|
)
|
|
248
255
|
elif container_state == ContainerState.FAILED:
|
|
249
256
|
return TriggerEvent(
|
|
@@ -252,6 +259,7 @@ class KubernetesPodTrigger(BaseTrigger):
|
|
|
252
259
|
"namespace": self.pod_namespace,
|
|
253
260
|
"name": self.pod_name,
|
|
254
261
|
"message": "Container state failed",
|
|
262
|
+
"last_log_time": self.last_log_time,
|
|
255
263
|
}
|
|
256
264
|
)
|
|
257
265
|
if time_get_more_logs and datetime.datetime.now(tz=datetime.timezone.utc) > time_get_more_logs:
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
# specific language governing permissions and limitations
|
|
16
16
|
# under the License.
|
|
17
17
|
"""Launches PODs."""
|
|
18
|
+
|
|
18
19
|
from __future__ import annotations
|
|
19
20
|
|
|
20
21
|
import enum
|
|
@@ -789,7 +790,7 @@ class PodManager(LoggingMixin):
|
|
|
789
790
|
_preload_content=False,
|
|
790
791
|
)
|
|
791
792
|
) as resp:
|
|
792
|
-
self._exec_pod_command(resp, "kill -
|
|
793
|
+
self._exec_pod_command(resp, "kill -2 1")
|
|
793
794
|
|
|
794
795
|
def _exec_pod_command(self, resp, command: str) -> str | None:
|
|
795
796
|
res = ""
|