apache-airflow-providers-cncf-kubernetes 8.0.0rc1__tar.gz → 8.0.0rc3__tar.gz

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.
Files changed (56) hide show
  1. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/PKG-INFO +2 -2
  2. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/README.rst +1 -1
  3. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +1 -1
  4. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +2 -2
  5. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/get_provider_info.py +1 -0
  6. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/hooks/kubernetes.py +54 -2
  7. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +58 -0
  8. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +2 -2
  9. apache_airflow_providers_cncf_kubernetes-8.0.0rc3/airflow/providers/cncf/kubernetes/operators/job.py +286 -0
  10. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/operators/pod.py +62 -86
  11. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py +1 -1
  12. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/triggers/pod.py +60 -17
  13. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/utils/pod_manager.py +2 -2
  14. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/pyproject.toml +1 -1
  15. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/LICENSE +0 -0
  16. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/__init__.py +0 -0
  17. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/backcompat/__init__.py +0 -0
  18. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +0 -0
  19. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/callbacks.py +0 -0
  20. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/decorators/__init__.py +0 -0
  21. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/decorators/kubernetes.py +0 -0
  22. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/executors/__init__.py +0 -0
  23. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py +0 -0
  24. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/executors/local_kubernetes_executor.py +0 -0
  25. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/hooks/__init__.py +0 -0
  26. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/k8s_model.py +0 -0
  27. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/kube_client.py +0 -0
  28. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/kube_config.py +0 -0
  29. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/kubernetes_executor_templates/__init__.py +0 -0
  30. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/kubernetes_executor_templates/basic_template.yaml +0 -0
  31. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/operators/__init__.py +0 -0
  32. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +0 -0
  33. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/operators/resource.py +0 -0
  34. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +0 -0
  35. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_generator.py +0 -0
  36. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_generator_deprecated.py +0 -0
  37. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_template_file_examples/__init__.py +0 -0
  38. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_image_template.yaml +0 -0
  39. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_volume_template.yaml +0 -0
  40. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/pod_template_file_examples/git_sync_template.yaml +0 -0
  41. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/python_kubernetes_script.jinja2 +0 -0
  42. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/python_kubernetes_script.py +0 -0
  43. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/resource_convert/__init__.py +0 -0
  44. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/resource_convert/configmap.py +0 -0
  45. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/resource_convert/env_variable.py +0 -0
  46. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/resource_convert/secret.py +0 -0
  47. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/secret.py +0 -0
  48. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/sensors/__init__.py +0 -0
  49. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/sensors/spark_kubernetes.py +0 -0
  50. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/template_rendering.py +0 -0
  51. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/triggers/__init__.py +0 -0
  52. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/triggers/kubernetes_pod.py +0 -0
  53. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/utils/__init__.py +0 -0
  54. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/utils/delete_from.py +0 -0
  55. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py +0 -0
  56. {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apache-airflow-providers-cncf-kubernetes
3
- Version: 8.0.0rc1
3
+ Version: 8.0.0rc3
4
4
  Summary: Provider package apache-airflow-providers-cncf-kubernetes for Apache Airflow
5
5
  Keywords: airflow-provider,cncf.kubernetes,airflow,integration
6
6
  Author-email: Apache Software Foundation <dev@airflow.apache.org>
@@ -79,7 +79,7 @@ Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
79
79
 
80
80
  Package ``apache-airflow-providers-cncf-kubernetes``
81
81
 
82
- Release: ``8.0.0.rc1``
82
+ Release: ``8.0.0.rc3``
83
83
 
84
84
 
85
85
  `Kubernetes <https://kubernetes.io/>`__
@@ -42,7 +42,7 @@
42
42
 
43
43
  Package ``apache-airflow-providers-cncf-kubernetes``
44
44
 
45
- Release: ``8.0.0.rc1``
45
+ Release: ``8.0.0.rc3``
46
46
 
47
47
 
48
48
  `Kubernetes <https://kubernetes.io/>`__
@@ -442,7 +442,7 @@ class KubernetesExecutor(BaseExecutor):
442
442
  retries = self.task_publish_retries[key]
443
443
  # In case of exceeded quota errors, requeue the task as per the task_publish_max_retries
444
444
  if (
445
- e.status == 403
445
+ str(e.status) == "403"
446
446
  and "exceeded quota" in body["message"]
447
447
  and (self.task_publish_max_retries == -1 or retries < self.task_publish_max_retries)
448
448
  ):
@@ -136,7 +136,7 @@ class KubernetesJobWatcher(multiprocessing.Process, LoggingMixin):
136
136
  else:
137
137
  return watcher.stream(kube_client.list_namespaced_pod, self.namespace, **query_kwargs)
138
138
  except ApiException as e:
139
- if e.status == 410: # Resource version is too old
139
+ if str(e.status) == "410": # Resource version is too old
140
140
  if self.namespace == ALL_NAMESPACES:
141
141
  pods = kube_client.list_pod_for_all_namespaces(watch=False)
142
142
  else:
@@ -425,7 +425,7 @@ class AirflowKubernetesScheduler(LoggingMixin):
425
425
  )
426
426
  except ApiException as e:
427
427
  # If the pod is already deleted
428
- if e.status != 404:
428
+ if str(e.status) != "404":
429
429
  raise
430
430
 
431
431
  def patch_pod_executor_done(self, *, pod_name: str, namespace: str):
@@ -115,6 +115,7 @@ def get_provider_info():
115
115
  "airflow.providers.cncf.kubernetes.operators.pod",
116
116
  "airflow.providers.cncf.kubernetes.operators.spark_kubernetes",
117
117
  "airflow.providers.cncf.kubernetes.operators.resource",
118
+ "airflow.providers.cncf.kubernetes.operators.job",
118
119
  ],
119
120
  }
120
121
  ],
@@ -37,7 +37,7 @@ from airflow.providers.cncf.kubernetes.utils.pod_manager import PodOperatorHookP
37
37
  from airflow.utils import yaml
38
38
 
39
39
  if TYPE_CHECKING:
40
- from kubernetes.client.models import V1Pod
40
+ from kubernetes.client.models import V1Deployment, V1Job, V1Pod
41
41
 
42
42
  LOADING_KUBE_CONFIG_FILE_RESOURCE = "Loading Kubernetes configuration file kube_config from {}..."
43
43
 
@@ -282,10 +282,18 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
282
282
  def core_v1_client(self) -> client.CoreV1Api:
283
283
  return client.CoreV1Api(api_client=self.api_client)
284
284
 
285
+ @cached_property
286
+ def apps_v1_client(self) -> client.AppsV1Api:
287
+ return client.AppsV1Api(api_client=self.api_client)
288
+
285
289
  @cached_property
286
290
  def custom_object_client(self) -> client.CustomObjectsApi:
287
291
  return client.CustomObjectsApi(api_client=self.api_client)
288
292
 
293
+ @cached_property
294
+ def batch_v1_client(self) -> client.BatchV1Api:
295
+ return client.BatchV1Api(api_client=self.api_client)
296
+
289
297
  def create_custom_object(
290
298
  self, group: str, version: str, plural: str, body: str | dict, namespace: str | None = None
291
299
  ):
@@ -450,6 +458,50 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
450
458
  **kwargs,
451
459
  )
452
460
 
461
+ def get_deployment_status(
462
+ self,
463
+ name: str,
464
+ namespace: str = "default",
465
+ **kwargs,
466
+ ) -> V1Deployment:
467
+ """Get status of existing Deployment.
468
+
469
+ :param name: Name of Deployment to retrieve
470
+ :param namespace: Deployment namespace
471
+ """
472
+ try:
473
+ return self.apps_v1_client.read_namespaced_deployment_status(
474
+ name=name, namespace=namespace, pretty=True, **kwargs
475
+ )
476
+ except Exception as exc:
477
+ raise exc
478
+
479
+ def create_job(
480
+ self,
481
+ job: V1Job,
482
+ **kwargs,
483
+ ) -> V1Job:
484
+ """
485
+ Run Job.
486
+
487
+ :param job: A kubernetes Job object
488
+ """
489
+ sanitized_job = self.batch_v1_client.api_client.sanitize_for_serialization(job)
490
+ json_job = json.dumps(sanitized_job, indent=2)
491
+
492
+ self.log.debug("Job Creation Request: \n%s", json_job)
493
+ try:
494
+ resp = self.batch_v1_client.create_namespaced_job(
495
+ body=sanitized_job, namespace=job.metadata.namespace, **kwargs
496
+ )
497
+ self.log.debug("Job Creation Response: %s", resp)
498
+ except Exception as e:
499
+ self.log.exception(
500
+ "Exception when attempting to create Namespaced Job: %s", str(json_job).replace("\n", " ")
501
+ )
502
+ raise e
503
+ return resp
504
+
453
505
 
454
506
  def _get_bool(val) -> bool | None:
455
507
  """Convert val to bool if can be done with certainty; if we cannot infer intention we return None."""
@@ -584,7 +636,7 @@ class AsyncKubernetesHook(KubernetesHook):
584
636
  )
585
637
  except async_client.ApiException as e:
586
638
  # If the pod is already deleted
587
- if e.status != 404:
639
+ if str(e.status) != "404":
588
640
  raise
589
641
 
590
642
  async def read_logs(self, name: str, namespace: str):
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
  import logging
20
20
  import secrets
21
21
  import string
22
+ import warnings
22
23
  from typing import TYPE_CHECKING
23
24
 
24
25
  import pendulum
@@ -26,6 +27,7 @@ from slugify import slugify
26
27
 
27
28
  from airflow.compat.functools import cache
28
29
  from airflow.configuration import conf
30
+ from airflow.exceptions import AirflowProviderDeprecationWarning
29
31
 
30
32
  if TYPE_CHECKING:
31
33
  from airflow.models.taskinstancekey import TaskInstanceKey
@@ -45,6 +47,18 @@ def rand_str(num):
45
47
  return "".join(secrets.choice(alphanum_lower) for _ in range(num))
46
48
 
47
49
 
50
+ def add_unique_suffix(*, name: str, rand_len: int = 8, max_len: int = POD_NAME_MAX_LENGTH) -> str:
51
+ """Add random string to pod or job name while staying under max length.
52
+
53
+ :param name: name of the pod or job
54
+ :param rand_len: length of the random string to append
55
+ :param max_len: maximum length of the pod name
56
+ :meta private:
57
+ """
58
+ suffix = "-" + rand_str(rand_len)
59
+ return name[: max_len - len(suffix)].strip("-.") + suffix
60
+
61
+
48
62
  def add_pod_suffix(*, pod_name: str, rand_len: int = 8, max_len: int = POD_NAME_MAX_LENGTH) -> str:
49
63
  """Add random string to pod name while staying under max length.
50
64
 
@@ -53,10 +67,48 @@ def add_pod_suffix(*, pod_name: str, rand_len: int = 8, max_len: int = POD_NAME_
53
67
  :param max_len: maximum length of the pod name
54
68
  :meta private:
55
69
  """
70
+ warnings.warn(
71
+ "This function is deprecated. Please use `add_unique_suffix`.",
72
+ AirflowProviderDeprecationWarning,
73
+ stacklevel=2,
74
+ )
75
+
56
76
  suffix = "-" + rand_str(rand_len)
57
77
  return pod_name[: max_len - len(suffix)].strip("-.") + suffix
58
78
 
59
79
 
80
+ def create_unique_id(
81
+ dag_id: str | None = None,
82
+ task_id: str | None = None,
83
+ *,
84
+ max_length: int = POD_NAME_MAX_LENGTH,
85
+ unique: bool = True,
86
+ ) -> str:
87
+ """
88
+ Generate unique pod or job ID given a dag_id and / or task_id.
89
+
90
+ :param dag_id: DAG ID
91
+ :param task_id: Task ID
92
+ :param max_length: max number of characters
93
+ :param unique: whether a random string suffix should be added
94
+ :return: A valid identifier for a kubernetes pod name
95
+ """
96
+ if not (dag_id or task_id):
97
+ raise ValueError("Must supply either dag_id or task_id.")
98
+ name = ""
99
+ if dag_id:
100
+ name += dag_id
101
+ if task_id:
102
+ if name:
103
+ name += "-"
104
+ name += task_id
105
+ base_name = slugify(name, lowercase=True)[:max_length].strip(".-")
106
+ if unique:
107
+ return add_pod_suffix(pod_name=base_name, rand_len=8, max_len=max_length)
108
+ else:
109
+ return base_name
110
+
111
+
60
112
  def create_pod_id(
61
113
  dag_id: str | None = None,
62
114
  task_id: str | None = None,
@@ -73,6 +125,12 @@ def create_pod_id(
73
125
  :param unique: whether a random string suffix should be added
74
126
  :return: A valid identifier for a kubernetes pod name
75
127
  """
128
+ warnings.warn(
129
+ "This function is deprecated. Please use `create_unique_id`.",
130
+ AirflowProviderDeprecationWarning,
131
+ stacklevel=2,
132
+ )
133
+
76
134
  if not (dag_id or task_id):
77
135
  raise ValueError("Must supply either dag_id or task_id.")
78
136
  name = ""
@@ -43,7 +43,7 @@ from airflow.utils.log.logging_mixin import LoggingMixin
43
43
  def should_retry_start_spark_job(exception: BaseException) -> bool:
44
44
  """Check if an Exception indicates a transient error and warrants retrying."""
45
45
  if isinstance(exception, ApiException):
46
- return exception.status == 409
46
+ return str(exception.status) == "409"
47
47
  return False
48
48
 
49
49
 
@@ -363,5 +363,5 @@ class CustomObjectLauncher(LoggingMixin):
363
363
  )
364
364
  except ApiException as e:
365
365
  # If the pod is already deleted
366
- if e.status != 404:
366
+ if str(e.status) != "404":
367
367
  raise
@@ -0,0 +1,286 @@
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
+ """Executes a Kubernetes Job."""
18
+ from __future__ import annotations
19
+
20
+ import copy
21
+ import logging
22
+ import os
23
+ from functools import cached_property
24
+ from typing import TYPE_CHECKING, Sequence
25
+
26
+ from kubernetes.client import BatchV1Api, models as k8s
27
+ from kubernetes.client.api_client import ApiClient
28
+
29
+ from airflow.providers.cncf.kubernetes.hooks.kubernetes import KubernetesHook
30
+ from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import (
31
+ add_unique_suffix,
32
+ create_unique_id,
33
+ )
34
+ from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
35
+ from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator, merge_objects
36
+ from airflow.utils import yaml
37
+
38
+ if TYPE_CHECKING:
39
+ from airflow.utils.context import Context
40
+
41
+ log = logging.getLogger(__name__)
42
+
43
+
44
+ class KubernetesJobOperator(KubernetesPodOperator):
45
+ """
46
+ Executes a Kubernetes Job.
47
+
48
+ .. seealso::
49
+ For more information on how to use this operator, take a look at the guide:
50
+ :ref:`howto/operator:KubernetesJobOperator`
51
+
52
+ .. note::
53
+ If you use `Google Kubernetes Engine <https://cloud.google.com/kubernetes-engine/>`__
54
+ and Airflow is not running in the same cluster, consider using
55
+ :class:`~airflow.providers.google.cloud.operators.kubernetes_engine.GKEStartJobOperator`, which
56
+ simplifies the authorization process.
57
+
58
+ :param job_template_file: path to job template file (templated)
59
+ :param full_job_spec: The complete JodSpec
60
+ :param backoff_limit: Specifies the number of retries before marking this job failed. Defaults to 6
61
+ :param completion_mode: CompletionMode specifies how Pod completions are tracked. It can be `NonIndexed` (default) or `Indexed`.
62
+ :param completions: Specifies the desired number of successfully finished pods the job should be run with.
63
+ :param manual_selector: manualSelector controls generation of pod labels and pod selectors.
64
+ :param parallelism: Specifies the maximum desired number of pods the job should run at any given time.
65
+ :param selector: The selector of this V1JobSpec.
66
+ :param suspend: Suspend specifies whether the Job controller should create Pods or not.
67
+ :param ttl_seconds_after_finished: ttlSecondsAfterFinished limits the lifetime of a Job that has finished execution (either Complete or Failed).
68
+ """
69
+
70
+ template_fields: Sequence[str] = tuple({"job_template_file"} | set(KubernetesPodOperator.template_fields))
71
+
72
+ def __init__(
73
+ self,
74
+ *,
75
+ job_template_file: str | None = None,
76
+ full_job_spec: k8s.V1Job | None = None,
77
+ backoff_limit: int | None = None,
78
+ completion_mode: str | None = None,
79
+ completions: int | None = None,
80
+ manual_selector: bool | None = None,
81
+ parallelism: int | None = None,
82
+ selector: k8s.V1LabelSelector | None = None,
83
+ suspend: bool | None = None,
84
+ ttl_seconds_after_finished: int | None = None,
85
+ **kwargs,
86
+ ) -> None:
87
+ super().__init__(**kwargs)
88
+ self.job_template_file = job_template_file
89
+ self.full_job_spec = full_job_spec
90
+ self.job_request_obj: k8s.V1Job | None = None
91
+ self.job: k8s.V1Job | None = None
92
+ self.backoff_limit = backoff_limit
93
+ self.completion_mode = completion_mode
94
+ self.completions = completions
95
+ self.manual_selector = manual_selector
96
+ self.parallelism = parallelism
97
+ self.selector = selector
98
+ self.suspend = suspend
99
+ self.ttl_seconds_after_finished = ttl_seconds_after_finished
100
+
101
+ @cached_property
102
+ def _incluster_namespace(self):
103
+ from pathlib import Path
104
+
105
+ path = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
106
+ return path.exists() and path.read_text() or None
107
+
108
+ @cached_property
109
+ def hook(self) -> KubernetesHook:
110
+ hook = KubernetesHook(
111
+ conn_id=self.kubernetes_conn_id,
112
+ in_cluster=self.in_cluster,
113
+ config_file=self.config_file,
114
+ cluster_context=self.cluster_context,
115
+ )
116
+ return hook
117
+
118
+ @cached_property
119
+ def client(self) -> BatchV1Api:
120
+ return self.hook.batch_v1_client
121
+
122
+ def create_job(self, job_request_obj: k8s.V1Job) -> k8s.V1Job:
123
+ self.log.debug("Starting job:\n%s", yaml.safe_dump(job_request_obj.to_dict()))
124
+ self.hook.create_job(job=job_request_obj)
125
+
126
+ return job_request_obj
127
+
128
+ def execute(self, context: Context):
129
+ self.job_request_obj = self.build_job_request_obj(context)
130
+ self.job = self.create_job( # must set `self.job` for `on_kill`
131
+ job_request_obj=self.job_request_obj
132
+ )
133
+
134
+ ti = context["ti"]
135
+ ti.xcom_push(key="job_name", value=self.job.metadata.name)
136
+ ti.xcom_push(key="job_namespace", value=self.job.metadata.namespace)
137
+
138
+ @staticmethod
139
+ def deserialize_job_template_file(path: str) -> k8s.V1Job:
140
+ """
141
+ Generate a Job from a file.
142
+
143
+ Unfortunately we need access to the private method
144
+ ``_ApiClient__deserialize_model`` from the kubernetes client.
145
+ This issue is tracked here: https://github.com/kubernetes-client/python/issues/977.
146
+
147
+ :param path: Path to the file
148
+ :return: a kubernetes.client.models.V1Job
149
+ """
150
+ if os.path.exists(path):
151
+ with open(path) as stream:
152
+ job = yaml.safe_load(stream)
153
+ else:
154
+ job = None
155
+ log.warning("Template file %s does not exist", path)
156
+
157
+ api_client = ApiClient()
158
+ return api_client._ApiClient__deserialize_model(job, k8s.V1Job)
159
+
160
+ def on_kill(self) -> None:
161
+ if self.job:
162
+ job = self.job
163
+ kwargs = {
164
+ "name": job.metadata.name,
165
+ "namespace": job.metadata.namespace,
166
+ }
167
+ if self.termination_grace_period is not None:
168
+ kwargs.update(grace_period_seconds=self.termination_grace_period)
169
+ self.client.delete_namespaced_job(**kwargs)
170
+
171
+ def build_job_request_obj(self, context: Context | None = None) -> k8s.V1Job:
172
+ """
173
+ Return V1Job object based on job template file, full job spec, and other operator parameters.
174
+
175
+ The V1Job attributes are derived (in order of precedence) from operator params, full job spec, job
176
+ template file.
177
+ """
178
+ self.log.debug("Creating job for KubernetesJobOperator task %s", self.task_id)
179
+ if self.job_template_file:
180
+ self.log.debug("Job template file found, will parse for base job")
181
+ job_template = self.deserialize_job_template_file(self.job_template_file)
182
+ if self.full_job_spec:
183
+ job_template = self.reconcile_jobs(job_template, self.full_job_spec)
184
+ elif self.full_job_spec:
185
+ job_template = self.full_job_spec
186
+ else:
187
+ job_template = k8s.V1Job(metadata=k8s.V1ObjectMeta())
188
+
189
+ pod_template = super().build_pod_request_obj(context)
190
+ pod_template_spec = k8s.V1PodTemplateSpec(
191
+ metadata=pod_template.metadata,
192
+ spec=pod_template.spec,
193
+ )
194
+
195
+ job = k8s.V1Job(
196
+ api_version="batch/v1",
197
+ kind="Job",
198
+ metadata=k8s.V1ObjectMeta(
199
+ namespace=self.namespace,
200
+ labels=self.labels,
201
+ name=self.name,
202
+ annotations=self.annotations,
203
+ ),
204
+ spec=k8s.V1JobSpec(
205
+ active_deadline_seconds=self.active_deadline_seconds,
206
+ backoff_limit=self.backoff_limit,
207
+ completion_mode=self.completion_mode,
208
+ completions=self.completions,
209
+ manual_selector=self.manual_selector,
210
+ parallelism=self.parallelism,
211
+ selector=self.selector,
212
+ suspend=self.suspend,
213
+ template=pod_template_spec,
214
+ ttl_seconds_after_finished=self.ttl_seconds_after_finished,
215
+ ),
216
+ )
217
+
218
+ job = self.reconcile_jobs(job_template, job)
219
+
220
+ if not job.metadata.name:
221
+ job.metadata.name = create_unique_id(
222
+ task_id=self.task_id, unique=self.random_name_suffix, max_length=80
223
+ )
224
+ elif self.random_name_suffix:
225
+ # user has supplied job name, we're just adding suffix
226
+ job.metadata.name = add_unique_suffix(name=job.metadata.name)
227
+
228
+ job.metadata.name = f"job-{job.metadata.name}"
229
+
230
+ if not job.metadata.namespace:
231
+ hook_namespace = self.hook.get_namespace()
232
+ job_namespace = self.namespace or hook_namespace or self._incluster_namespace or "default"
233
+ job.metadata.namespace = job_namespace
234
+
235
+ self.log.info("Building job %s ", job.metadata.name)
236
+
237
+ return job
238
+
239
+ @staticmethod
240
+ def reconcile_jobs(base_job: k8s.V1Job, client_job: k8s.V1Job | None) -> k8s.V1Job:
241
+ """
242
+ Merge Kubernetes Job objects.
243
+
244
+ :param base_job: has the base attributes which are overwritten if they exist
245
+ in the client job and remain if they do not exist in the client_job
246
+ :param client_job: the job that the client wants to create.
247
+ :return: the merged jobs
248
+
249
+ This can't be done recursively as certain fields are overwritten and some are concatenated.
250
+ """
251
+ if client_job is None:
252
+ return base_job
253
+
254
+ client_job_cp = copy.deepcopy(client_job)
255
+ client_job_cp.spec = KubernetesJobOperator.reconcile_job_specs(base_job.spec, client_job_cp.spec)
256
+ client_job_cp.metadata = PodGenerator.reconcile_metadata(base_job.metadata, client_job_cp.metadata)
257
+ client_job_cp = merge_objects(base_job, client_job_cp)
258
+
259
+ return client_job_cp
260
+
261
+ @staticmethod
262
+ def reconcile_job_specs(
263
+ base_spec: k8s.V1JobSpec | None, client_spec: k8s.V1JobSpec | None
264
+ ) -> k8s.V1JobSpec | None:
265
+ """
266
+ Merge Kubernetes JobSpec objects.
267
+
268
+ :param base_spec: has the base attributes which are overwritten if they exist
269
+ in the client_spec and remain if they do not exist in the client_spec
270
+ :param client_spec: the spec that the client wants to create.
271
+ :return: the merged specs
272
+ """
273
+ if base_spec and not client_spec:
274
+ return base_spec
275
+ if not base_spec and client_spec:
276
+ return client_spec
277
+ elif client_spec and base_spec:
278
+ client_spec.template.spec = PodGenerator.reconcile_specs(
279
+ base_spec.template.spec, client_spec.template.spec
280
+ )
281
+ client_spec.template.metadata = PodGenerator.reconcile_metadata(
282
+ base_spec.template.metadata, client_spec.template.metadata
283
+ )
284
+ return merge_objects(base_spec, client_spec)
285
+
286
+ return None
@@ -18,6 +18,7 @@
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ import datetime
21
22
  import json
22
23
  import logging
23
24
  import re
@@ -30,6 +31,7 @@ from functools import cached_property
30
31
  from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence
31
32
 
32
33
  import kubernetes
34
+ from deprecated import deprecated
33
35
  from kubernetes.client import CoreV1Api, V1Pod, models as k8s
34
36
  from kubernetes.stream import stream
35
37
  from urllib3.exceptions import HTTPError
@@ -68,7 +70,6 @@ from airflow.providers.cncf.kubernetes.utils.pod_manager import (
68
70
  EMPTY_XCOM_RESULT,
69
71
  OnFinishAction,
70
72
  PodLaunchFailedException,
71
- PodLaunchTimeoutException,
72
73
  PodManager,
73
74
  PodNotFoundException,
74
75
  PodOperatorHookProtocol,
@@ -79,7 +80,6 @@ from airflow.providers.cncf.kubernetes.utils.pod_manager import (
79
80
  from airflow.settings import pod_mutation_hook
80
81
  from airflow.utils import yaml
81
82
  from airflow.utils.helpers import prune_dict, validate_key
82
- from airflow.utils.timezone import utcnow
83
83
  from airflow.version import version as airflow_version
84
84
 
85
85
  if TYPE_CHECKING:
@@ -656,7 +656,7 @@ class KubernetesPodOperator(BaseOperator):
656
656
 
657
657
  def invoke_defer_method(self, last_log_time: DateTime | None = None):
658
658
  """Redefine triggers which are being used in child classes."""
659
- trigger_start_time = utcnow()
659
+ trigger_start_time = datetime.datetime.now(tz=datetime.timezone.utc)
660
660
  self.defer(
661
661
  trigger=KubernetesPodTrigger(
662
662
  pod_name=self.pod.metadata.name, # type: ignore[union-attr]
@@ -678,117 +678,93 @@ class KubernetesPodOperator(BaseOperator):
678
678
  method_name="trigger_reentry",
679
679
  )
680
680
 
681
- @staticmethod
682
- def raise_for_trigger_status(event: dict[str, Any]) -> None:
683
- """Raise exception if pod is not in expected state."""
684
- if event["status"] == "error":
685
- error_type = event["error_type"]
686
- description = event["description"]
687
- if error_type == "PodLaunchTimeoutException":
688
- raise PodLaunchTimeoutException(description)
689
- else:
690
- raise AirflowException(description)
691
-
692
681
  def trigger_reentry(self, context: Context, event: dict[str, Any]) -> Any:
693
682
  """
694
683
  Point of re-entry from trigger.
695
684
 
696
- If ``logging_interval`` is None, then at this point the pod should be done and we'll just fetch
685
+ If ``logging_interval`` is None, then at this point, the pod should be done, and we'll just fetch
697
686
  the logs and exit.
698
687
 
699
- If ``logging_interval`` is not None, it could be that the pod is still running and we'll just
688
+ If ``logging_interval`` is not None, it could be that the pod is still running, and we'll just
700
689
  grab the latest logs and defer back to the trigger again.
701
690
  """
702
- remote_pod = None
691
+ self.pod = None
703
692
  try:
704
- self.pod_request_obj = self.build_pod_request_obj(context)
705
- self.pod = self.find_pod(
706
- namespace=self.namespace or self.pod_request_obj.metadata.namespace,
707
- context=context,
708
- )
693
+ pod_name = event["name"]
694
+ pod_namespace = event["namespace"]
709
695
 
710
- # we try to find pod before possibly raising so that on_kill will have `pod` attr
711
- self.raise_for_trigger_status(event)
696
+ self.pod = self.hook.get_pod(pod_name, pod_namespace)
712
697
 
713
698
  if not self.pod:
714
699
  raise PodNotFoundException("Could not find pod after resuming from deferral")
715
700
 
716
- if self.get_logs:
717
- last_log_time = event and event.get("last_log_time")
718
- if last_log_time:
719
- self.log.info("Resuming logs read from time %r", last_log_time)
720
- pod_log_status = self.pod_manager.fetch_container_logs(
721
- pod=self.pod,
722
- container_name=self.BASE_CONTAINER_NAME,
723
- follow=self.logging_interval is None,
724
- since_time=last_log_time,
725
- )
726
- if pod_log_status.running:
727
- self.log.info("Container still running; deferring again.")
728
- self.invoke_defer_method(pod_log_status.last_log_time)
729
-
730
- if self.do_xcom_push:
731
- result = self.extract_xcom(pod=self.pod)
732
- remote_pod = self.pod_manager.await_pod_completion(self.pod)
733
- except TaskDeferred:
734
- raise
735
- except Exception:
736
- self.cleanup(
737
- pod=self.pod or self.pod_request_obj,
738
- remote_pod=remote_pod,
739
- )
740
- raise
741
- self.cleanup(
742
- pod=self.pod or self.pod_request_obj,
743
- remote_pod=remote_pod,
744
- )
745
- if self.do_xcom_push:
746
- return result
747
-
748
- def execute_complete(self, context: Context, event: dict, **kwargs):
749
- self.log.debug("Triggered with event: %s", event)
750
- pod = None
751
- try:
752
- pod = self.hook.get_pod(
753
- event["name"],
754
- event["namespace"],
755
- )
756
- if self.callbacks:
701
+ if self.callbacks and event["status"] != "running":
757
702
  self.callbacks.on_operator_resuming(
758
- pod=pod, event=event, client=self.client, mode=ExecutionMode.SYNC
703
+ pod=self.pod, event=event, client=self.client, mode=ExecutionMode.SYNC
759
704
  )
705
+
760
706
  if event["status"] in ("error", "failed", "timeout"):
761
707
  # fetch some logs when pod is failed
762
708
  if self.get_logs:
763
- self.write_logs(pod)
764
- if "stack_trace" in event:
765
- message = f"{event['message']}\n{event['stack_trace']}"
766
- else:
767
- message = event["message"]
709
+ self.write_logs(self.pod)
710
+
768
711
  if self.do_xcom_push:
769
- # In the event of base container failure, we need to kill the xcom sidecar.
770
- # We disregard xcom output and do that here
771
- _ = self.extract_xcom(pod=pod)
712
+ _ = self.extract_xcom(pod=self.pod)
713
+
714
+ message = event.get("stack_trace", event["message"])
772
715
  raise AirflowException(message)
716
+
717
+ elif event["status"] == "running":
718
+ if self.get_logs:
719
+ last_log_time = event.get("last_log_time")
720
+ self.log.info("Resuming logs read from time %r", last_log_time)
721
+
722
+ pod_log_status = self.pod_manager.fetch_container_logs(
723
+ pod=self.pod,
724
+ container_name=self.BASE_CONTAINER_NAME,
725
+ follow=self.logging_interval is None,
726
+ since_time=last_log_time,
727
+ )
728
+
729
+ if pod_log_status.running:
730
+ self.log.info("Container still running; deferring again.")
731
+ self.invoke_defer_method(pod_log_status.last_log_time)
732
+ else:
733
+ self.invoke_defer_method()
734
+
773
735
  elif event["status"] == "success":
774
736
  # fetch some logs when pod is executed successfully
775
737
  if self.get_logs:
776
- self.write_logs(pod)
738
+ self.write_logs(self.pod)
777
739
 
778
740
  if self.do_xcom_push:
779
- xcom_sidecar_output = self.extract_xcom(pod=pod)
741
+ xcom_sidecar_output = self.extract_xcom(pod=self.pod)
780
742
  return xcom_sidecar_output
743
+ return
744
+ except TaskDeferred:
745
+ raise
781
746
  finally:
782
- istio_enabled = self.is_istio_enabled(pod)
783
- # Skip await_pod_completion when the event is 'timeout' due to the pod can hang
784
- # on the ErrImagePull or ContainerCreating step and it will never complete
785
- if event["status"] != "timeout":
786
- pod = self.pod_manager.await_pod_completion(pod, istio_enabled, self.base_container_name)
787
- if pod is not None:
788
- self.post_complete_action(
789
- pod=pod,
790
- remote_pod=pod,
791
- )
747
+ self._clean(event)
748
+
749
+ def _clean(self, event: dict[str, Any]):
750
+ if event["status"] == "running":
751
+ return
752
+ istio_enabled = self.is_istio_enabled(self.pod)
753
+ # Skip await_pod_completion when the event is 'timeout' due to the pod can hang
754
+ # on the ErrImagePull or ContainerCreating step and it will never complete
755
+ if event["status"] != "timeout":
756
+ self.pod = self.pod_manager.await_pod_completion(
757
+ self.pod, istio_enabled, self.base_container_name
758
+ )
759
+ if self.pod is not None:
760
+ self.post_complete_action(
761
+ pod=self.pod,
762
+ remote_pod=self.pod,
763
+ )
764
+
765
+ @deprecated(reason="use `trigger_reentry` instead.", category=AirflowProviderDeprecationWarning)
766
+ def execute_complete(self, context: Context, event: dict, **kwargs):
767
+ self.trigger_reentry(context=context, event=event)
792
768
 
793
769
  def write_logs(self, pod: k8s.V1Pod):
794
770
  try:
@@ -119,7 +119,7 @@ class PodLauncher(LoggingMixin):
119
119
  )
120
120
  except ApiException as e:
121
121
  # If the pod is already deleted
122
- if e.status != 404:
122
+ if str(e.status) != "404":
123
123
  raise
124
124
 
125
125
  def start_pod(self, pod: V1Pod, startup_timeout: int = 120):
@@ -30,10 +30,8 @@ from airflow.providers.cncf.kubernetes.utils.pod_manager import (
30
30
  OnFinishAction,
31
31
  PodLaunchTimeoutException,
32
32
  PodPhase,
33
- container_is_running,
34
33
  )
35
34
  from airflow.triggers.base import BaseTrigger, TriggerEvent
36
- from airflow.utils import timezone
37
35
 
38
36
  if TYPE_CHECKING:
39
37
  from kubernetes_asyncio.client.models import V1Pod
@@ -160,22 +158,50 @@ class KubernetesPodTrigger(BaseTrigger):
160
158
  self.log.info("Checking pod %r in namespace %r.", self.pod_name, self.pod_namespace)
161
159
  try:
162
160
  state = await self._wait_for_pod_start()
163
- if state in PodPhase.terminal_states:
161
+ if state == ContainerState.TERMINATED:
164
162
  event = TriggerEvent(
165
- {"status": "done", "namespace": self.pod_namespace, "pod_name": self.pod_name}
163
+ {
164
+ "status": "success",
165
+ "namespace": self.pod_namespace,
166
+ "name": self.pod_name,
167
+ "message": "All containers inside pod have started successfully.",
168
+ }
169
+ )
170
+ elif state == ContainerState.FAILED:
171
+ event = TriggerEvent(
172
+ {
173
+ "status": "failed",
174
+ "namespace": self.pod_namespace,
175
+ "name": self.pod_name,
176
+ "message": "pod failed",
177
+ }
166
178
  )
167
179
  else:
168
180
  event = await self._wait_for_container_completion()
169
181
  yield event
182
+ return
183
+ except PodLaunchTimeoutException as e:
184
+ message = self._format_exception_description(e)
185
+ yield TriggerEvent(
186
+ {
187
+ "name": self.pod_name,
188
+ "namespace": self.pod_namespace,
189
+ "status": "timeout",
190
+ "message": message,
191
+ }
192
+ )
193
+ return
170
194
  except Exception as e:
171
- description = self._format_exception_description(e)
172
195
  yield TriggerEvent(
173
196
  {
197
+ "name": self.pod_name,
198
+ "namespace": self.pod_namespace,
174
199
  "status": "error",
175
- "error_type": e.__class__.__name__,
176
- "description": description,
200
+ "message": str(e),
201
+ "stack_trace": traceback.format_exc(),
177
202
  }
178
203
  )
204
+ return
179
205
 
180
206
  def _format_exception_description(self, exc: Exception) -> Any:
181
207
  if isinstance(exc, PodLaunchTimeoutException):
@@ -189,16 +215,16 @@ class KubernetesPodTrigger(BaseTrigger):
189
215
  description += f"\ntrigger traceback:\n{curr_traceback}"
190
216
  return description
191
217
 
192
- async def _wait_for_pod_start(self) -> Any:
218
+ async def _wait_for_pod_start(self) -> ContainerState:
193
219
  """Loops until pod phase leaves ``PENDING`` If timeout is reached, throws error."""
194
- start_time = timezone.utcnow()
195
- timeout_end = start_time + datetime.timedelta(seconds=self.startup_timeout)
196
- while timeout_end > timezone.utcnow():
220
+ delta = datetime.datetime.now(tz=datetime.timezone.utc) - self.trigger_start_time
221
+ while self.startup_timeout >= delta.total_seconds():
197
222
  pod = await self.hook.get_pod(self.pod_name, self.pod_namespace)
198
223
  if not pod.status.phase == "Pending":
199
- return pod.status.phase
224
+ return self.define_container_state(pod)
200
225
  self.log.info("Still waiting for pod to start. The pod state is %s", pod.status.phase)
201
226
  await asyncio.sleep(self.poll_interval)
227
+ delta = datetime.datetime.now(tz=datetime.timezone.utc) - self.trigger_start_time
202
228
  raise PodLaunchTimeoutException("Pod did not leave 'Pending' phase within specified timeout")
203
229
 
204
230
  async def _wait_for_container_completion(self) -> TriggerEvent:
@@ -208,18 +234,35 @@ class KubernetesPodTrigger(BaseTrigger):
208
234
  Waits until container is no longer in running state. If trigger is configured with a logging period,
209
235
  then will emit an event to resume the task for the purpose of fetching more logs.
210
236
  """
211
- time_begin = timezone.utcnow()
237
+ time_begin = datetime.datetime.now(tz=datetime.timezone.utc)
212
238
  time_get_more_logs = None
213
239
  if self.logging_interval is not None:
214
240
  time_get_more_logs = time_begin + datetime.timedelta(seconds=self.logging_interval)
215
241
  while True:
216
242
  pod = await self.hook.get_pod(self.pod_name, self.pod_namespace)
217
- if not container_is_running(pod=pod, container_name=self.base_container_name):
243
+ container_state = self.define_container_state(pod)
244
+ if container_state == ContainerState.TERMINATED:
245
+ return TriggerEvent(
246
+ {"status": "success", "namespace": self.pod_namespace, "name": self.pod_name}
247
+ )
248
+ elif container_state == ContainerState.FAILED:
249
+ return TriggerEvent(
250
+ {
251
+ "status": "failed",
252
+ "namespace": self.pod_namespace,
253
+ "name": self.pod_name,
254
+ "message": "Container state failed",
255
+ }
256
+ )
257
+ if time_get_more_logs and datetime.datetime.now(tz=datetime.timezone.utc) > time_get_more_logs:
218
258
  return TriggerEvent(
219
- {"status": "done", "namespace": self.pod_namespace, "pod_name": self.pod_name}
259
+ {
260
+ "status": "running",
261
+ "last_log_time": self.last_log_time,
262
+ "namespace": self.pod_namespace,
263
+ "name": self.pod_name,
264
+ }
220
265
  )
221
- if time_get_more_logs and timezone.utcnow() > time_get_more_logs:
222
- return TriggerEvent({"status": "running", "last_log_time": self.last_log_time})
223
266
  await asyncio.sleep(self.poll_interval)
224
267
 
225
268
  def _get_async_hook(self) -> AsyncKubernetesHook:
@@ -67,7 +67,7 @@ class PodLaunchFailedException(AirflowException):
67
67
  def should_retry_start_pod(exception: BaseException) -> bool:
68
68
  """Check if an Exception indicates a transient error and warrants retrying."""
69
69
  if isinstance(exception, ApiException):
70
- return exception.status == 409
70
+ return str(exception.status) == "409"
71
71
  return False
72
72
 
73
73
 
@@ -340,7 +340,7 @@ class PodManager(LoggingMixin):
340
340
  )
341
341
  except ApiException as e:
342
342
  # If the pod is already deleted
343
- if e.status != 404:
343
+ if str(e.status) != "404":
344
344
  raise
345
345
 
346
346
  @tenacity.retry(
@@ -28,7 +28,7 @@ build-backend = "flit_core.buildapi"
28
28
 
29
29
  [project]
30
30
  name = "apache-airflow-providers-cncf-kubernetes"
31
- version = "8.0.0.rc1"
31
+ version = "8.0.0.rc3"
32
32
  description = "Provider package apache-airflow-providers-cncf-kubernetes for Apache Airflow"
33
33
  readme = "README.rst"
34
34
  authors = [