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.
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/PKG-INFO +2 -2
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/README.rst +1 -1
- {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
- {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
- {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
- {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
- {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
- {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
- apache_airflow_providers_cncf_kubernetes-8.0.0rc3/airflow/providers/cncf/kubernetes/operators/job.py +286 -0
- {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
- {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
- {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
- {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
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/pyproject.toml +1 -1
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/LICENSE +0 -0
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/__init__.py +0 -0
- {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
- {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
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/callbacks.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {apache_airflow_providers_cncf_kubernetes-8.0.0rc1 → apache_airflow_providers_cncf_kubernetes-8.0.0rc3}/airflow/providers/cncf/kubernetes/secret.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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.
|
|
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.
|
|
82
|
+
Release: ``8.0.0.rc3``
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
`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
|
apache_airflow_providers_cncf_kubernetes-8.0.0rc3/airflow/providers/cncf/kubernetes/operators/job.py
ADDED
|
@@ -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 =
|
|
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
|
-
|
|
691
|
+
self.pod = None
|
|
703
692
|
try:
|
|
704
|
-
|
|
705
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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:
|
|
@@ -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
|
|
161
|
+
if state == ContainerState.TERMINATED:
|
|
164
162
|
event = TriggerEvent(
|
|
165
|
-
{
|
|
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
|
-
"
|
|
176
|
-
"
|
|
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) ->
|
|
218
|
+
async def _wait_for_pod_start(self) -> ContainerState:
|
|
193
219
|
"""Loops until pod phase leaves ``PENDING`` If timeout is reached, throws error."""
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
{
|
|
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.
|
|
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 = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|