apache-airflow-providers-cncf-kubernetes 10.1.0rc2__py3-none-any.whl → 10.3.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of apache-airflow-providers-cncf-kubernetes might be problematic. Click here for more details.
- airflow/providers/cncf/kubernetes/LICENSE +0 -52
- airflow/providers/cncf/kubernetes/__init__.py +1 -1
- airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +2 -3
- airflow/providers/cncf/kubernetes/callbacks.py +90 -8
- airflow/providers/cncf/kubernetes/cli/kubernetes_command.py +3 -4
- airflow/providers/cncf/kubernetes/decorators/kubernetes.py +10 -5
- airflow/providers/cncf/kubernetes/exceptions.py +29 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +36 -113
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +27 -15
- airflow/providers/cncf/kubernetes/get_provider_info.py +14 -21
- airflow/providers/cncf/kubernetes/hooks/kubernetes.py +20 -10
- airflow/providers/cncf/kubernetes/kube_config.py +0 -4
- airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +1 -1
- airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +3 -3
- airflow/providers/cncf/kubernetes/operators/job.py +4 -4
- airflow/providers/cncf/kubernetes/operators/kueue.py +2 -2
- airflow/providers/cncf/kubernetes/operators/pod.py +102 -44
- airflow/providers/cncf/kubernetes/operators/resource.py +1 -1
- airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +23 -19
- airflow/providers/cncf/kubernetes/pod_generator.py +51 -21
- airflow/providers/cncf/kubernetes/resource_convert/env_variable.py +1 -2
- airflow/providers/cncf/kubernetes/secret.py +1 -2
- airflow/providers/cncf/kubernetes/sensors/spark_kubernetes.py +1 -2
- airflow/providers/cncf/kubernetes/template_rendering.py +10 -2
- airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py +1 -2
- airflow/providers/cncf/kubernetes/utils/pod_manager.py +12 -11
- {apache_airflow_providers_cncf_kubernetes-10.1.0rc2.dist-info → apache_airflow_providers_cncf_kubernetes-10.3.0rc1.dist-info}/METADATA +9 -26
- {apache_airflow_providers_cncf_kubernetes-10.1.0rc2.dist-info → apache_airflow_providers_cncf_kubernetes-10.3.0rc1.dist-info}/RECORD +30 -30
- airflow/providers/cncf/kubernetes/pod_generator_deprecated.py +0 -309
- {apache_airflow_providers_cncf_kubernetes-10.1.0rc2.dist-info → apache_airflow_providers_cncf_kubernetes-10.3.0rc1.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_cncf_kubernetes-10.1.0rc2.dist-info → apache_airflow_providers_cncf_kubernetes-10.3.0rc1.dist-info}/entry_points.txt +0 -0
|
@@ -199,55 +199,3 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
199
199
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
200
|
See the License for the specific language governing permissions and
|
|
201
201
|
limitations under the License.
|
|
202
|
-
|
|
203
|
-
============================================================================
|
|
204
|
-
APACHE AIRFLOW SUBCOMPONENTS:
|
|
205
|
-
|
|
206
|
-
The Apache Airflow project contains subcomponents with separate copyright
|
|
207
|
-
notices and license terms. Your use of the source code for the these
|
|
208
|
-
subcomponents is subject to the terms and conditions of the following
|
|
209
|
-
licenses.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
========================================================================
|
|
213
|
-
Third party Apache 2.0 licenses
|
|
214
|
-
========================================================================
|
|
215
|
-
|
|
216
|
-
The following components are provided under the Apache 2.0 License.
|
|
217
|
-
See project link for details. The text of each license is also included
|
|
218
|
-
at 3rd-party-licenses/LICENSE-[project].txt.
|
|
219
|
-
|
|
220
|
-
(ALv2 License) hue v4.3.0 (https://github.com/cloudera/hue/)
|
|
221
|
-
(ALv2 License) jqclock v2.3.0 (https://github.com/JohnRDOrazio/jQuery-Clock-Plugin)
|
|
222
|
-
(ALv2 License) bootstrap3-typeahead v4.0.2 (https://github.com/bassjobsen/Bootstrap-3-Typeahead)
|
|
223
|
-
(ALv2 License) connexion v2.7.0 (https://github.com/zalando/connexion)
|
|
224
|
-
|
|
225
|
-
========================================================================
|
|
226
|
-
MIT licenses
|
|
227
|
-
========================================================================
|
|
228
|
-
|
|
229
|
-
The following components are provided under the MIT License. See project link for details.
|
|
230
|
-
The text of each license is also included at 3rd-party-licenses/LICENSE-[project].txt.
|
|
231
|
-
|
|
232
|
-
(MIT License) jquery v3.5.1 (https://jquery.org/license/)
|
|
233
|
-
(MIT License) dagre-d3 v0.6.4 (https://github.com/cpettitt/dagre-d3)
|
|
234
|
-
(MIT License) bootstrap v3.4.1 (https://github.com/twbs/bootstrap/)
|
|
235
|
-
(MIT License) d3-tip v0.9.1 (https://github.com/Caged/d3-tip)
|
|
236
|
-
(MIT License) dataTables v1.10.25 (https://datatables.net)
|
|
237
|
-
(MIT License) normalize.css v3.0.2 (http://necolas.github.io/normalize.css/)
|
|
238
|
-
(MIT License) ElasticMock v1.3.2 (https://github.com/vrcmarcos/elasticmock)
|
|
239
|
-
(MIT License) MomentJS v2.24.0 (http://momentjs.com/)
|
|
240
|
-
(MIT License) eonasdan-bootstrap-datetimepicker v4.17.49 (https://github.com/eonasdan/bootstrap-datetimepicker/)
|
|
241
|
-
|
|
242
|
-
========================================================================
|
|
243
|
-
BSD 3-Clause licenses
|
|
244
|
-
========================================================================
|
|
245
|
-
The following components are provided under the BSD 3-Clause license. See project links for details.
|
|
246
|
-
The text of each license is also included at 3rd-party-licenses/LICENSE-[project].txt.
|
|
247
|
-
|
|
248
|
-
(BSD 3 License) d3 v5.16.0 (https://d3js.org)
|
|
249
|
-
(BSD 3 License) d3-shape v2.1.0 (https://github.com/d3/d3-shape)
|
|
250
|
-
(BSD 3 License) cgroupspy 0.2.1 (https://github.com/cloudsigma/cgroupspy)
|
|
251
|
-
|
|
252
|
-
========================================================================
|
|
253
|
-
See 3rd-party-licenses/LICENSES-ui.txt for packages used in `/airflow/www`
|
|
@@ -29,7 +29,7 @@ from airflow import __version__ as airflow_version
|
|
|
29
29
|
|
|
30
30
|
__all__ = ["__version__"]
|
|
31
31
|
|
|
32
|
-
__version__ = "10.
|
|
32
|
+
__version__ = "10.3.0"
|
|
33
33
|
|
|
34
34
|
if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
|
|
35
35
|
"2.9.0"
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
-
from kubernetes.client import ApiClient, models as k8s
|
|
22
|
-
|
|
23
21
|
from airflow.exceptions import AirflowException
|
|
22
|
+
from kubernetes.client import ApiClient, models as k8s
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
def _convert_kube_model_object(obj, new_class):
|
|
@@ -74,7 +73,7 @@ def convert_env_vars(env_vars: list[k8s.V1EnvVar] | dict[str, str]) -> list[k8s.
|
|
|
74
73
|
"""
|
|
75
74
|
Coerce env var collection for kubernetes.
|
|
76
75
|
|
|
77
|
-
If the collection is a str-str dict, convert it into a list of ``V1EnvVar``
|
|
76
|
+
If the collection is a str-str dict, convert it into a list of ``V1EnvVar`` variables.
|
|
78
77
|
"""
|
|
79
78
|
if isinstance(env_vars, dict):
|
|
80
79
|
return [k8s.V1EnvVar(name=k, value=v) for k, v in env_vars.items()]
|
|
@@ -17,11 +17,16 @@
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
from enum import Enum
|
|
20
|
-
from typing import Union
|
|
20
|
+
from typing import TYPE_CHECKING, Union
|
|
21
21
|
|
|
22
|
-
import kubernetes.client as k8s
|
|
23
22
|
import kubernetes_asyncio.client as async_k8s
|
|
24
23
|
|
|
24
|
+
import kubernetes.client as k8s
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
|
|
28
|
+
from airflow.utils.context import Context
|
|
29
|
+
|
|
25
30
|
client_type = Union[k8s.CoreV1Api, async_k8s.CoreV1Api]
|
|
26
31
|
|
|
27
32
|
|
|
@@ -41,7 +46,7 @@ class KubernetesPodOperatorCallback:
|
|
|
41
46
|
"""
|
|
42
47
|
|
|
43
48
|
@staticmethod
|
|
44
|
-
def on_sync_client_creation(*, client: k8s.CoreV1Api, **kwargs) -> None:
|
|
49
|
+
def on_sync_client_creation(*, client: k8s.CoreV1Api, operator: KubernetesPodOperator, **kwargs) -> None:
|
|
45
50
|
"""
|
|
46
51
|
Invoke this callback after creating the sync client.
|
|
47
52
|
|
|
@@ -50,7 +55,34 @@ class KubernetesPodOperatorCallback:
|
|
|
50
55
|
pass
|
|
51
56
|
|
|
52
57
|
@staticmethod
|
|
53
|
-
def
|
|
58
|
+
def on_pod_manifest_created(
|
|
59
|
+
*,
|
|
60
|
+
pod_request: k8s.V1Pod,
|
|
61
|
+
client: client_type,
|
|
62
|
+
mode: str,
|
|
63
|
+
operator: KubernetesPodOperator,
|
|
64
|
+
context: Context,
|
|
65
|
+
**kwargs,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Invoke this callback after KPO creates the V1Pod manifest but before the pod is created.
|
|
69
|
+
|
|
70
|
+
:param pod_request: the kubernetes pod manifest
|
|
71
|
+
:param client: the Kubernetes client that can be used in the callback.
|
|
72
|
+
:param mode: the current execution mode, it's one of (`sync`, `async`).
|
|
73
|
+
"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def on_pod_creation(
|
|
78
|
+
*,
|
|
79
|
+
pod: k8s.V1Pod,
|
|
80
|
+
client: client_type,
|
|
81
|
+
mode: str,
|
|
82
|
+
operator: KubernetesPodOperator,
|
|
83
|
+
context: Context,
|
|
84
|
+
**kwargs,
|
|
85
|
+
) -> None:
|
|
54
86
|
"""
|
|
55
87
|
Invoke this callback after creating the pod.
|
|
56
88
|
|
|
@@ -61,7 +93,15 @@ class KubernetesPodOperatorCallback:
|
|
|
61
93
|
pass
|
|
62
94
|
|
|
63
95
|
@staticmethod
|
|
64
|
-
def on_pod_starting(
|
|
96
|
+
def on_pod_starting(
|
|
97
|
+
*,
|
|
98
|
+
pod: k8s.V1Pod,
|
|
99
|
+
client: client_type,
|
|
100
|
+
mode: str,
|
|
101
|
+
operator: KubernetesPodOperator,
|
|
102
|
+
context: Context,
|
|
103
|
+
**kwargs,
|
|
104
|
+
) -> None:
|
|
65
105
|
"""
|
|
66
106
|
Invoke this callback when the pod starts.
|
|
67
107
|
|
|
@@ -72,7 +112,15 @@ class KubernetesPodOperatorCallback:
|
|
|
72
112
|
pass
|
|
73
113
|
|
|
74
114
|
@staticmethod
|
|
75
|
-
def on_pod_completion(
|
|
115
|
+
def on_pod_completion(
|
|
116
|
+
*,
|
|
117
|
+
pod: k8s.V1Pod,
|
|
118
|
+
client: client_type,
|
|
119
|
+
mode: str,
|
|
120
|
+
operator: KubernetesPodOperator,
|
|
121
|
+
context: Context,
|
|
122
|
+
**kwargs,
|
|
123
|
+
) -> None:
|
|
76
124
|
"""
|
|
77
125
|
Invoke this callback when the pod completes.
|
|
78
126
|
|
|
@@ -83,7 +131,34 @@ class KubernetesPodOperatorCallback:
|
|
|
83
131
|
pass
|
|
84
132
|
|
|
85
133
|
@staticmethod
|
|
86
|
-
def
|
|
134
|
+
def on_pod_teardown(
|
|
135
|
+
*,
|
|
136
|
+
pod: k8s.V1Pod,
|
|
137
|
+
client: client_type,
|
|
138
|
+
mode: str,
|
|
139
|
+
operator: KubernetesPodOperator,
|
|
140
|
+
context: Context,
|
|
141
|
+
**kwargs,
|
|
142
|
+
) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Invoke this callback after all pod completion callbacks but before the pod is deleted.
|
|
145
|
+
|
|
146
|
+
:param pod: the completed pod.
|
|
147
|
+
:param client: the Kubernetes client that can be used in the callback.
|
|
148
|
+
:param mode: the current execution mode, it's one of (`sync`, `async`).
|
|
149
|
+
"""
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def on_pod_cleanup(
|
|
154
|
+
*,
|
|
155
|
+
pod: k8s.V1Pod,
|
|
156
|
+
client: client_type,
|
|
157
|
+
mode: str,
|
|
158
|
+
operator: KubernetesPodOperator,
|
|
159
|
+
context: Context,
|
|
160
|
+
**kwargs,
|
|
161
|
+
):
|
|
87
162
|
"""
|
|
88
163
|
Invoke this callback after cleaning/deleting the pod.
|
|
89
164
|
|
|
@@ -95,7 +170,14 @@ class KubernetesPodOperatorCallback:
|
|
|
95
170
|
|
|
96
171
|
@staticmethod
|
|
97
172
|
def on_operator_resuming(
|
|
98
|
-
*,
|
|
173
|
+
*,
|
|
174
|
+
pod: k8s.V1Pod,
|
|
175
|
+
event: dict,
|
|
176
|
+
client: client_type,
|
|
177
|
+
mode: str,
|
|
178
|
+
operator: KubernetesPodOperator,
|
|
179
|
+
context: Context,
|
|
180
|
+
**kwargs,
|
|
99
181
|
) -> None:
|
|
100
182
|
"""
|
|
101
183
|
Invoke this callback when resuming the `KubernetesPodOperator` from deferred state.
|
|
@@ -22,10 +22,6 @@ import os
|
|
|
22
22
|
import sys
|
|
23
23
|
from datetime import datetime, timedelta
|
|
24
24
|
|
|
25
|
-
from kubernetes import client
|
|
26
|
-
from kubernetes.client.api_client import ApiClient
|
|
27
|
-
from kubernetes.client.rest import ApiException
|
|
28
|
-
|
|
29
25
|
from airflow.models import DagRun, TaskInstance
|
|
30
26
|
from airflow.providers.cncf.kubernetes import pod_generator
|
|
31
27
|
from airflow.providers.cncf.kubernetes.executors.kubernetes_executor import KubeConfig
|
|
@@ -36,6 +32,9 @@ from airflow.providers.cncf.kubernetes.version_compat import AIRFLOW_V_3_0_PLUS
|
|
|
36
32
|
from airflow.utils import cli as cli_utils, yaml
|
|
37
33
|
from airflow.utils.cli import get_dag
|
|
38
34
|
from airflow.utils.providers_configuration_loader import providers_configuration_loaded
|
|
35
|
+
from kubernetes import client
|
|
36
|
+
from kubernetes.client.api_client import ApiClient
|
|
37
|
+
from kubernetes.client.rest import ApiException
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
@cli_utils.action_cli
|
|
@@ -19,20 +19,19 @@ from __future__ import annotations
|
|
|
19
19
|
import base64
|
|
20
20
|
import os
|
|
21
21
|
import pickle
|
|
22
|
-
import uuid
|
|
23
22
|
from collections.abc import Sequence
|
|
24
23
|
from shlex import quote
|
|
25
24
|
from tempfile import TemporaryDirectory
|
|
26
25
|
from typing import TYPE_CHECKING, Callable
|
|
27
26
|
|
|
28
27
|
import dill
|
|
29
|
-
from kubernetes.client import models as k8s
|
|
30
28
|
|
|
31
29
|
from airflow.decorators.base import DecoratedOperator, TaskDecorator, task_decorator_factory
|
|
32
30
|
from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator
|
|
33
31
|
from airflow.providers.cncf.kubernetes.python_kubernetes_script import (
|
|
34
32
|
write_python_script,
|
|
35
33
|
)
|
|
34
|
+
from kubernetes.client import models as k8s
|
|
36
35
|
|
|
37
36
|
if TYPE_CHECKING:
|
|
38
37
|
from airflow.utils.context import Context
|
|
@@ -68,9 +67,15 @@ class _KubernetesDecoratedOperator(DecoratedOperator, KubernetesPodOperator):
|
|
|
68
67
|
|
|
69
68
|
def __init__(self, namespace: str | None = None, use_dill: bool = False, **kwargs) -> None:
|
|
70
69
|
self.use_dill = use_dill
|
|
70
|
+
|
|
71
|
+
# If the name was not provided, we generate operator name from the python_callable
|
|
72
|
+
# we also instruct operator to add a random suffix to avoid collisions by default
|
|
73
|
+
op_name = kwargs.pop("name", f"k8s-airflow-pod-{kwargs['python_callable'].__name__}")
|
|
74
|
+
random_name_suffix = kwargs.pop("random_name_suffix", True)
|
|
71
75
|
super().__init__(
|
|
72
76
|
namespace=namespace,
|
|
73
|
-
name=
|
|
77
|
+
name=op_name,
|
|
78
|
+
random_name_suffix=random_name_suffix,
|
|
74
79
|
cmds=["placeholder-command"],
|
|
75
80
|
**kwargs,
|
|
76
81
|
)
|
|
@@ -119,7 +124,7 @@ class _KubernetesDecoratedOperator(DecoratedOperator, KubernetesPodOperator):
|
|
|
119
124
|
}
|
|
120
125
|
write_python_script(jinja_context=jinja_context, filename=script_filename)
|
|
121
126
|
|
|
122
|
-
self.env_vars = [
|
|
127
|
+
self.env_vars: list[k8s.V1EnvVar] = [
|
|
123
128
|
*self.env_vars,
|
|
124
129
|
k8s.V1EnvVar(name=_PYTHON_SCRIPT_ENV, value=_read_file_contents(script_filename)),
|
|
125
130
|
k8s.V1EnvVar(name=_PYTHON_INPUT_ENV, value=_read_file_contents(input_filename)),
|
|
@@ -138,7 +143,7 @@ def kubernetes_task(
|
|
|
138
143
|
Kubernetes operator decorator.
|
|
139
144
|
|
|
140
145
|
This wraps a function to be executed in K8s using KubernetesPodOperator.
|
|
141
|
-
Also accepts any argument that
|
|
146
|
+
Also accepts any argument that KubernetesPodOperator will via ``kwargs``. Can be
|
|
142
147
|
reused in a single DAG.
|
|
143
148
|
|
|
144
149
|
:param python_callable: Function to decorate
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from airflow.exceptions import (
|
|
20
|
+
AirflowException,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PodMutationHookException(AirflowException):
|
|
25
|
+
"""Raised when exception happens during Pod Mutation Hook execution."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PodReconciliationError(AirflowException):
|
|
29
|
+
"""Raised when an error is encountered while trying to merge pod configs."""
|
|
@@ -37,8 +37,11 @@ from queue import Empty, Queue
|
|
|
37
37
|
from typing import TYPE_CHECKING, Any
|
|
38
38
|
|
|
39
39
|
from deprecated import deprecated
|
|
40
|
+
from sqlalchemy import select
|
|
41
|
+
|
|
42
|
+
from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator
|
|
43
|
+
from airflow.providers.cncf.kubernetes.version_compat import AIRFLOW_V_3_0_PLUS
|
|
40
44
|
from kubernetes.dynamic import DynamicClient
|
|
41
|
-
from sqlalchemy import or_, select, update
|
|
42
45
|
|
|
43
46
|
try:
|
|
44
47
|
from airflow.cli.cli_config import ARG_LOGICAL_DATE
|
|
@@ -60,16 +63,14 @@ from airflow.cli.cli_config import (
|
|
|
60
63
|
from airflow.configuration import conf
|
|
61
64
|
from airflow.exceptions import AirflowProviderDeprecationWarning
|
|
62
65
|
from airflow.executors.base_executor import BaseExecutor
|
|
63
|
-
from airflow.
|
|
66
|
+
from airflow.providers.cncf.kubernetes.exceptions import PodMutationHookException, PodReconciliationError
|
|
64
67
|
from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import (
|
|
65
68
|
ADOPTED,
|
|
66
69
|
POD_EXECUTOR_DONE_KEY,
|
|
67
70
|
)
|
|
68
71
|
from airflow.providers.cncf.kubernetes.kube_config import KubeConfig
|
|
69
72
|
from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import annotations_to_key
|
|
70
|
-
from airflow.providers.cncf.kubernetes.pod_generator import PodMutationHookException, PodReconciliationError
|
|
71
73
|
from airflow.stats import Stats
|
|
72
|
-
from airflow.utils.event_scheduler import EventScheduler
|
|
73
74
|
from airflow.utils.log.logging_mixin import remove_escape_codes
|
|
74
75
|
from airflow.utils.session import NEW_SESSION, provide_session
|
|
75
76
|
from airflow.utils.state import TaskInstanceState
|
|
@@ -77,10 +78,9 @@ from airflow.utils.state import TaskInstanceState
|
|
|
77
78
|
if TYPE_CHECKING:
|
|
78
79
|
import argparse
|
|
79
80
|
|
|
80
|
-
from kubernetes import client
|
|
81
|
-
from kubernetes.client import models as k8s
|
|
82
81
|
from sqlalchemy.orm import Session
|
|
83
82
|
|
|
83
|
+
from airflow.executors import workloads
|
|
84
84
|
from airflow.executors.base_executor import CommandType
|
|
85
85
|
from airflow.models.taskinstance import TaskInstance
|
|
86
86
|
from airflow.models.taskinstancekey import TaskInstanceKey
|
|
@@ -91,6 +91,8 @@ if TYPE_CHECKING:
|
|
|
91
91
|
from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_utils import (
|
|
92
92
|
AirflowKubernetesScheduler,
|
|
93
93
|
)
|
|
94
|
+
from kubernetes import client
|
|
95
|
+
from kubernetes.client import models as k8s
|
|
94
96
|
|
|
95
97
|
# CLI Args
|
|
96
98
|
ARG_NAMESPACE = Arg(
|
|
@@ -137,6 +139,11 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
137
139
|
RUNNING_POD_LOG_LINES = 100
|
|
138
140
|
supports_ad_hoc_ti_run: bool = True
|
|
139
141
|
|
|
142
|
+
if TYPE_CHECKING and AIRFLOW_V_3_0_PLUS:
|
|
143
|
+
# In the v3 path, we store workloads, not commands as strings.
|
|
144
|
+
# TODO: TaskSDK: move this type change into BaseExecutor
|
|
145
|
+
queued_tasks: dict[TaskInstanceKey, workloads.All] # type: ignore[assignment]
|
|
146
|
+
|
|
140
147
|
def __init__(self):
|
|
141
148
|
self.kube_config = KubeConfig()
|
|
142
149
|
self._manager = multiprocessing.Manager()
|
|
@@ -145,7 +152,6 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
145
152
|
self.kube_scheduler: AirflowKubernetesScheduler | None = None
|
|
146
153
|
self.kube_client: client.CoreV1Api | None = None
|
|
147
154
|
self.scheduler_job_id: str | None = None
|
|
148
|
-
self.event_scheduler: EventScheduler | None = None
|
|
149
155
|
self.last_handled: dict[TaskInstanceKey, float] = {}
|
|
150
156
|
self.kubernetes_queue: str | None = None
|
|
151
157
|
self.task_publish_retries: Counter[TaskInstanceKey] = Counter()
|
|
@@ -218,96 +224,6 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
218
224
|
pod_combined_search_str_to_pod_map[search_str] = pod
|
|
219
225
|
return pod_combined_search_str_to_pod_map
|
|
220
226
|
|
|
221
|
-
@provide_session
|
|
222
|
-
def clear_not_launched_queued_tasks(self, session: Session = NEW_SESSION) -> None:
|
|
223
|
-
"""
|
|
224
|
-
Clear tasks that were not yet launched, but were previously queued.
|
|
225
|
-
|
|
226
|
-
Tasks can end up in a "Queued" state when a rescheduled/deferred operator
|
|
227
|
-
comes back up for execution (with the same try_number) before the
|
|
228
|
-
pod of its previous incarnation has been fully removed (we think).
|
|
229
|
-
|
|
230
|
-
It's also possible when an executor abruptly shuts down (leaving a non-empty
|
|
231
|
-
task_queue on that executor), but that scenario is handled via normal adoption.
|
|
232
|
-
|
|
233
|
-
This method checks each of our queued tasks to see if the corresponding pod
|
|
234
|
-
is around, and if not, and there's no matching entry in our own
|
|
235
|
-
task_queue, marks it for re-execution.
|
|
236
|
-
"""
|
|
237
|
-
if TYPE_CHECKING:
|
|
238
|
-
assert self.kube_client
|
|
239
|
-
from airflow.models.taskinstance import TaskInstance
|
|
240
|
-
|
|
241
|
-
hybrid_executor_enabled = hasattr(TaskInstance, "executor")
|
|
242
|
-
default_executor_alias = None
|
|
243
|
-
if hybrid_executor_enabled:
|
|
244
|
-
from airflow.executors.executor_loader import ExecutorLoader
|
|
245
|
-
|
|
246
|
-
default_executor_name = ExecutorLoader.get_default_executor_name()
|
|
247
|
-
default_executor_alias = default_executor_name.alias
|
|
248
|
-
|
|
249
|
-
with Stats.timer("kubernetes_executor.clear_not_launched_queued_tasks.duration"):
|
|
250
|
-
self.log.debug("Clearing tasks that have not been launched")
|
|
251
|
-
query = select(TaskInstance).where(
|
|
252
|
-
TaskInstance.state == TaskInstanceState.QUEUED,
|
|
253
|
-
TaskInstance.queued_by_job_id == self.job_id,
|
|
254
|
-
)
|
|
255
|
-
if self.kubernetes_queue:
|
|
256
|
-
query = query.where(TaskInstance.queue == self.kubernetes_queue)
|
|
257
|
-
# KUBERNETES_EXECUTOR is the string name/alias of the "core" executor represented by this
|
|
258
|
-
# module. The ExecutorName for "core" executors always contains an alias and cannot be modified
|
|
259
|
-
# to be different from the constant (in this case KUBERNETES_EXECUTOR).
|
|
260
|
-
elif hybrid_executor_enabled and default_executor_alias == KUBERNETES_EXECUTOR:
|
|
261
|
-
query = query.where(
|
|
262
|
-
or_(
|
|
263
|
-
TaskInstance.executor == KUBERNETES_EXECUTOR,
|
|
264
|
-
TaskInstance.executor.is_(None),
|
|
265
|
-
),
|
|
266
|
-
)
|
|
267
|
-
elif hybrid_executor_enabled:
|
|
268
|
-
query = query.where(TaskInstance.executor == KUBERNETES_EXECUTOR)
|
|
269
|
-
queued_tis: list[TaskInstance] = session.scalars(query).all()
|
|
270
|
-
self.log.info("Found %s queued task instances", len(queued_tis))
|
|
271
|
-
|
|
272
|
-
# Go through the "last seen" dictionary and clean out old entries
|
|
273
|
-
allowed_age = self.kube_config.worker_pods_queued_check_interval * 3
|
|
274
|
-
for key, timestamp in list(self.last_handled.items()):
|
|
275
|
-
if time.time() - timestamp > allowed_age:
|
|
276
|
-
del self.last_handled[key]
|
|
277
|
-
|
|
278
|
-
if not queued_tis:
|
|
279
|
-
return
|
|
280
|
-
|
|
281
|
-
pod_combined_search_str_to_pod_map = self.get_pod_combined_search_str_to_pod_map()
|
|
282
|
-
|
|
283
|
-
for ti in queued_tis:
|
|
284
|
-
self.log.debug("Checking task instance %s", ti)
|
|
285
|
-
|
|
286
|
-
# Check to see if we've handled it ourselves recently
|
|
287
|
-
if ti.key in self.last_handled:
|
|
288
|
-
continue
|
|
289
|
-
|
|
290
|
-
# Build the pod selector
|
|
291
|
-
base_selector = f"dag_id={ti.dag_id},task_id={ti.task_id}"
|
|
292
|
-
if ti.map_index >= 0:
|
|
293
|
-
# Old tasks _couldn't_ be mapped, so we don't have to worry about compat
|
|
294
|
-
base_selector += f",map_index={ti.map_index}"
|
|
295
|
-
|
|
296
|
-
search_str = f"{base_selector},run_id={ti.run_id}"
|
|
297
|
-
if search_str in pod_combined_search_str_to_pod_map:
|
|
298
|
-
continue
|
|
299
|
-
self.log.info("TaskInstance: %s found in queued state but was not launched, rescheduling", ti)
|
|
300
|
-
session.execute(
|
|
301
|
-
update(TaskInstance)
|
|
302
|
-
.where(
|
|
303
|
-
TaskInstance.dag_id == ti.dag_id,
|
|
304
|
-
TaskInstance.task_id == ti.task_id,
|
|
305
|
-
TaskInstance.run_id == ti.run_id,
|
|
306
|
-
TaskInstance.map_index == ti.map_index,
|
|
307
|
-
)
|
|
308
|
-
.values(state=TaskInstanceState.SCHEDULED)
|
|
309
|
-
)
|
|
310
|
-
|
|
311
227
|
def start(self) -> None:
|
|
312
228
|
"""Start the executor."""
|
|
313
229
|
self.log.info("Start Kubernetes executor")
|
|
@@ -325,15 +241,6 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
325
241
|
kube_client=self.kube_client,
|
|
326
242
|
scheduler_job_id=self.scheduler_job_id,
|
|
327
243
|
)
|
|
328
|
-
self.event_scheduler = EventScheduler()
|
|
329
|
-
|
|
330
|
-
self.event_scheduler.call_regular_interval(
|
|
331
|
-
self.kube_config.worker_pods_queued_check_interval,
|
|
332
|
-
self.clear_not_launched_queued_tasks,
|
|
333
|
-
)
|
|
334
|
-
# We also call this at startup as that's the most likely time to see
|
|
335
|
-
# stuck queued tasks
|
|
336
|
-
self.clear_not_launched_queued_tasks()
|
|
337
244
|
|
|
338
245
|
def execute_async(
|
|
339
246
|
self,
|
|
@@ -351,8 +258,6 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
351
258
|
else:
|
|
352
259
|
self.log.info("Add task %s with command %s", key, command)
|
|
353
260
|
|
|
354
|
-
from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator
|
|
355
|
-
|
|
356
261
|
try:
|
|
357
262
|
kube_executor_config = PodGenerator.from_obj(executor_config)
|
|
358
263
|
except Exception:
|
|
@@ -370,6 +275,29 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
370
275
|
# try and remove it from the QUEUED state while we process it
|
|
371
276
|
self.last_handled[key] = time.time()
|
|
372
277
|
|
|
278
|
+
def queue_workload(self, workload: workloads.All, session: Session | None) -> None:
|
|
279
|
+
from airflow.executors import workloads
|
|
280
|
+
|
|
281
|
+
if not isinstance(workload, workloads.ExecuteTask):
|
|
282
|
+
raise RuntimeError(f"{type(self)} cannot handle workloads of type {type(workload)}")
|
|
283
|
+
ti = workload.ti
|
|
284
|
+
self.queued_tasks[ti.key] = workload
|
|
285
|
+
|
|
286
|
+
def _process_workloads(self, workloads: list[workloads.All]) -> None:
|
|
287
|
+
# Airflow V3 version
|
|
288
|
+
for w in workloads:
|
|
289
|
+
# TODO: AIP-72 handle populating tokens once https://github.com/apache/airflow/issues/45107 is handled.
|
|
290
|
+
command = [w]
|
|
291
|
+
key = w.ti.key # type: ignore[union-attr]
|
|
292
|
+
queue = w.ti.queue # type: ignore[union-attr]
|
|
293
|
+
|
|
294
|
+
# TODO: will be handled by https://github.com/apache/airflow/issues/46892
|
|
295
|
+
executor_config = {} # type: ignore[var-annotated]
|
|
296
|
+
|
|
297
|
+
del self.queued_tasks[key]
|
|
298
|
+
self.execute_async(key=key, command=command, queue=queue, executor_config=executor_config) # type: ignore[arg-type]
|
|
299
|
+
self.running.add(key)
|
|
300
|
+
|
|
373
301
|
def sync(self) -> None:
|
|
374
302
|
"""Synchronize task state."""
|
|
375
303
|
if TYPE_CHECKING:
|
|
@@ -378,7 +306,6 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
378
306
|
assert self.kube_config
|
|
379
307
|
assert self.result_queue
|
|
380
308
|
assert self.task_queue
|
|
381
|
-
assert self.event_scheduler
|
|
382
309
|
|
|
383
310
|
if self.running:
|
|
384
311
|
self.log.debug("self.running: %s", self.running)
|
|
@@ -466,10 +393,6 @@ class KubernetesExecutor(BaseExecutor):
|
|
|
466
393
|
finally:
|
|
467
394
|
self.task_queue.task_done()
|
|
468
395
|
|
|
469
|
-
# Run any pending timed events
|
|
470
|
-
next_event = self.event_scheduler.run(blocking=False)
|
|
471
|
-
self.log.debug("Next timed event is in %f", next_event)
|
|
472
|
-
|
|
473
396
|
@provide_session
|
|
474
397
|
def _change_state(
|
|
475
398
|
self,
|
|
@@ -23,8 +23,6 @@ import time
|
|
|
23
23
|
from queue import Empty, Queue
|
|
24
24
|
from typing import TYPE_CHECKING, Any
|
|
25
25
|
|
|
26
|
-
from kubernetes import client, watch
|
|
27
|
-
from kubernetes.client.rest import ApiException
|
|
28
26
|
from urllib3.exceptions import ReadTimeoutError
|
|
29
27
|
|
|
30
28
|
from airflow.exceptions import AirflowException
|
|
@@ -45,15 +43,16 @@ from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator
|
|
|
45
43
|
from airflow.utils.log.logging_mixin import LoggingMixin
|
|
46
44
|
from airflow.utils.singleton import Singleton
|
|
47
45
|
from airflow.utils.state import TaskInstanceState
|
|
46
|
+
from kubernetes import client, watch
|
|
47
|
+
from kubernetes.client.rest import ApiException
|
|
48
48
|
|
|
49
49
|
if TYPE_CHECKING:
|
|
50
|
-
from kubernetes.client import Configuration, models as k8s
|
|
51
|
-
|
|
52
50
|
from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import (
|
|
53
51
|
KubernetesJobType,
|
|
54
52
|
KubernetesResultsType,
|
|
55
53
|
KubernetesWatchType,
|
|
56
54
|
)
|
|
55
|
+
from kubernetes.client import Configuration, models as k8s
|
|
57
56
|
|
|
58
57
|
|
|
59
58
|
class ResourceVersion(metaclass=Singleton):
|
|
@@ -147,10 +146,10 @@ class KubernetesJobWatcher(multiprocessing.Process, LoggingMixin):
|
|
|
147
146
|
# For info about k8s timeout settings see
|
|
148
147
|
# https://github.com/kubernetes-client/python/blob/v29.0.0/examples/watch/timeout-settings.md
|
|
149
148
|
# and https://github.com/kubernetes-client/python/blob/v29.0.0/kubernetes/client/api_client.py#L336-L339
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
if "_request_timeout" not in kwargs:
|
|
150
|
+
kwargs["_request_timeout"] = 30
|
|
151
|
+
if "timeout_seconds" not in kwargs:
|
|
152
|
+
kwargs["timeout_seconds"] = 3600
|
|
154
153
|
|
|
155
154
|
logical_date_key = get_logical_date_key()
|
|
156
155
|
for event in self._pod_events(kube_client=kube_client, query_kwargs=kwargs):
|
|
@@ -231,12 +230,8 @@ class KubernetesJobWatcher(multiprocessing.Process, LoggingMixin):
|
|
|
231
230
|
)
|
|
232
231
|
elif status == "Pending":
|
|
233
232
|
# deletion_timestamp is set by kube server when a graceful deletion is requested.
|
|
234
|
-
# since kube server have received request to delete pod set TI state failed
|
|
235
233
|
if event["type"] == "DELETED" and pod.metadata.deletion_timestamp:
|
|
236
234
|
self.log.info("Event: Failed to start pod %s, annotations: %s", pod_name, annotations_string)
|
|
237
|
-
self.watcher_queue.put(
|
|
238
|
-
(pod_name, namespace, TaskInstanceState.FAILED, annotations, resource_version)
|
|
239
|
-
)
|
|
240
235
|
elif (
|
|
241
236
|
self.kube_config.worker_pod_pending_fatal_container_state_reasons
|
|
242
237
|
and "status" in event["raw_object"]
|
|
@@ -393,8 +388,24 @@ class AirflowKubernetesScheduler(LoggingMixin):
|
|
|
393
388
|
key, command, kube_executor_config, pod_template_file = next_job
|
|
394
389
|
|
|
395
390
|
dag_id, task_id, run_id, try_number, map_index = key
|
|
396
|
-
|
|
397
|
-
if command
|
|
391
|
+
ser_input = ""
|
|
392
|
+
if len(command) == 1:
|
|
393
|
+
from airflow.executors.workloads import ExecuteTask
|
|
394
|
+
|
|
395
|
+
if isinstance(command[0], ExecuteTask):
|
|
396
|
+
workload = command[0]
|
|
397
|
+
ser_input = workload.model_dump_json()
|
|
398
|
+
command = [
|
|
399
|
+
"python",
|
|
400
|
+
"-m",
|
|
401
|
+
"airflow.sdk.execution_time.execute_workload",
|
|
402
|
+
"/tmp/execute/input.json",
|
|
403
|
+
]
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError(
|
|
406
|
+
f"KubernetesExecutor doesn't know how to handle workload of type: {type(command[0])}"
|
|
407
|
+
)
|
|
408
|
+
elif command[0:3] != ["airflow", "tasks", "run"]:
|
|
398
409
|
raise ValueError('The command must start with ["airflow", "tasks", "run"].')
|
|
399
410
|
|
|
400
411
|
base_worker_pod = get_base_pod_from_template(pod_template_file, self.kube_config)
|
|
@@ -415,7 +426,8 @@ class AirflowKubernetesScheduler(LoggingMixin):
|
|
|
415
426
|
map_index=map_index,
|
|
416
427
|
date=None,
|
|
417
428
|
run_id=run_id,
|
|
418
|
-
args=command,
|
|
429
|
+
args=list(command),
|
|
430
|
+
content_json_for_volume=ser_input,
|
|
419
431
|
pod_override_object=kube_executor_config,
|
|
420
432
|
base_worker_pod=base_worker_pod,
|
|
421
433
|
with_mutation_hook=True,
|