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.

Files changed (28) hide show
  1. airflow/providers/cncf/kubernetes/__init__.py +1 -1
  2. airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +1 -0
  3. airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +1 -0
  4. airflow/providers/cncf/kubernetes/get_provider_info.py +6 -2
  5. airflow/providers/cncf/kubernetes/hooks/kubernetes.py +149 -0
  6. airflow/providers/cncf/kubernetes/k8s_model.py +1 -0
  7. airflow/providers/cncf/kubernetes/kube_client.py +1 -0
  8. airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +1 -1
  9. airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +4 -3
  10. airflow/providers/cncf/kubernetes/operators/job.py +239 -0
  11. airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +1 -0
  12. airflow/providers/cncf/kubernetes/operators/pod.py +35 -16
  13. airflow/providers/cncf/kubernetes/operators/resource.py +47 -13
  14. airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +27 -3
  15. airflow/providers/cncf/kubernetes/pod_generator.py +3 -1
  16. airflow/providers/cncf/kubernetes/pod_generator_deprecated.py +1 -0
  17. airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py +1 -0
  18. airflow/providers/cncf/kubernetes/python_kubernetes_script.py +1 -0
  19. airflow/providers/cncf/kubernetes/secret.py +1 -0
  20. airflow/providers/cncf/kubernetes/triggers/job.py +101 -0
  21. airflow/providers/cncf/kubernetes/triggers/kubernetes_pod.py +1 -0
  22. airflow/providers/cncf/kubernetes/triggers/pod.py +11 -3
  23. airflow/providers/cncf/kubernetes/utils/pod_manager.py +2 -1
  24. airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +1 -0
  25. {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/METADATA +8 -7
  26. {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/RECORD +28 -27
  27. {apache_airflow_providers_cncf_kubernetes-8.0.1.dist-info → apache_airflow_providers_cncf_kubernetes-8.1.0.dist-info}/WHEEL +0 -0
  28. {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
- add_pod_suffix,
64
- create_pod_id,
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 = 1,
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
- for event in self.pod_manager.read_pod_events(pod).items:
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=self.logging_interval is None,
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=False,
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
- self.log.error("Pod Event: %s - %s", event.reason, event.message)
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 Exception("Error while deleting istio-proxy sidecar: %s", output_str)
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 = create_pod_id(
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 = add_pod_suffix(pod_name=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.custom_object_client.create_namespaced_custom_object(group, version, namespace, plural, body)
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 execute(self, context) -> None:
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=resources,
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, resources)
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.custom_object_client.delete_namespaced_custom_object(group, version, namespace, plural, name)
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 execute(self, context) -> None:
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=resources,
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, resources)
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
- template_body = _load_body_to_dict(open(self.application_file))
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 = PodGenerator.make_unique_pod_id(self.task_id)[:MAX_LABEL_LEN]
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 = add_pod_suffix(pod_name=pod_id, max_len=POD_NAME_MAX_LENGTH)
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:
@@ -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
@@ -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 json
@@ -16,6 +16,7 @@
16
16
  # specific language governing permissions and limitations
17
17
  # under the License.
18
18
  """Utilities for using the kubernetes decorator."""
19
+
19
20
  from __future__ import annotations
20
21
 
21
22
  import os
@@ -15,6 +15,7 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
  """Classes for interacting with Kubernetes API."""
18
+
18
19
  from __future__ import annotations
19
20
 
20
21
  import copy
@@ -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
+ )
@@ -16,6 +16,7 @@
16
16
  # specific language governing permissions and limitations
17
17
  # under the License.
18
18
  """This module is deprecated. Please use :mod:`airflow.providers.cncf.kubernetes.triggers.pod` instead."""
19
+
19
20
  from __future__ import annotations
20
21
 
21
22
  import warnings
@@ -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 = 1,
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.poll_interval)
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
- {"status": "success", "namespace": self.pod_namespace, "name": self.pod_name}
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 -s SIGINT 1")
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 = ""
@@ -15,6 +15,7 @@
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
17
  """Attach a sidecar container that blocks the pod from completing until Airflow pulls result data."""
18
+
18
19
  from __future__ import annotations
19
20
 
20
21
  import copy