apache-airflow-providers-cncf-kubernetes 3.1.0__py3-none-any.whl → 10.10.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.
- airflow/providers/cncf/kubernetes/__init__.py +18 -23
- airflow/providers/cncf/kubernetes/backcompat/__init__.py +17 -0
- airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +31 -49
- airflow/providers/cncf/kubernetes/callbacks.py +200 -0
- airflow/providers/cncf/kubernetes/cli/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/cli/kubernetes_command.py +195 -0
- airflow/providers/cncf/kubernetes/decorators/kubernetes.py +163 -0
- airflow/providers/cncf/kubernetes/decorators/kubernetes_cmd.py +118 -0
- airflow/providers/cncf/kubernetes/exceptions.py +37 -0
- airflow/providers/cncf/kubernetes/executors/__init__.py +17 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +831 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py +91 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +736 -0
- airflow/providers/cncf/kubernetes/executors/local_kubernetes_executor.py +306 -0
- airflow/providers/cncf/kubernetes/get_provider_info.py +249 -50
- airflow/providers/cncf/kubernetes/hooks/kubernetes.py +846 -112
- airflow/providers/cncf/kubernetes/k8s_model.py +62 -0
- airflow/providers/cncf/kubernetes/kube_client.py +156 -0
- airflow/providers/cncf/kubernetes/kube_config.py +125 -0
- airflow/providers/cncf/kubernetes/kubernetes_executor_templates/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/kubernetes_executor_templates/basic_template.yaml +79 -0
- airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +165 -0
- airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +368 -0
- airflow/providers/cncf/kubernetes/operators/job.py +646 -0
- airflow/providers/cncf/kubernetes/operators/kueue.py +132 -0
- airflow/providers/cncf/kubernetes/operators/pod.py +1417 -0
- airflow/providers/cncf/kubernetes/operators/resource.py +191 -0
- airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +336 -35
- airflow/providers/cncf/kubernetes/pod_generator.py +592 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_image_template.yaml +68 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_volume_template.yaml +74 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/git_sync_template.yaml +95 -0
- airflow/providers/cncf/kubernetes/python_kubernetes_script.jinja2 +51 -0
- airflow/providers/cncf/kubernetes/python_kubernetes_script.py +92 -0
- airflow/providers/cncf/kubernetes/resource_convert/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/resource_convert/configmap.py +52 -0
- airflow/providers/cncf/kubernetes/resource_convert/env_variable.py +39 -0
- airflow/providers/cncf/kubernetes/resource_convert/secret.py +40 -0
- airflow/providers/cncf/kubernetes/secret.py +128 -0
- airflow/providers/cncf/kubernetes/sensors/spark_kubernetes.py +30 -14
- airflow/providers/cncf/kubernetes/template_rendering.py +81 -0
- airflow/providers/cncf/kubernetes/triggers/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/triggers/job.py +176 -0
- airflow/providers/cncf/kubernetes/triggers/pod.py +344 -0
- airflow/providers/cncf/kubernetes/utils/__init__.py +3 -0
- airflow/providers/cncf/kubernetes/utils/container.py +118 -0
- airflow/providers/cncf/kubernetes/utils/delete_from.py +154 -0
- airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py +46 -0
- airflow/providers/cncf/kubernetes/utils/pod_manager.py +887 -152
- airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +25 -16
- airflow/providers/cncf/kubernetes/version_compat.py +38 -0
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/METADATA +125 -0
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/RECORD +62 -0
- {apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info → apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info}/WHEEL +1 -2
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/entry_points.txt +3 -0
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/licenses/NOTICE +5 -0
- airflow/providers/cncf/kubernetes/backcompat/pod.py +0 -119
- airflow/providers/cncf/kubernetes/backcompat/pod_runtime_info_env.py +0 -56
- airflow/providers/cncf/kubernetes/backcompat/volume.py +0 -62
- airflow/providers/cncf/kubernetes/backcompat/volume_mount.py +0 -58
- airflow/providers/cncf/kubernetes/example_dags/example_kubernetes.py +0 -163
- airflow/providers/cncf/kubernetes/example_dags/example_spark_kubernetes.py +0 -66
- airflow/providers/cncf/kubernetes/example_dags/example_spark_kubernetes_spark_pi.yaml +0 -57
- airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +0 -622
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/METADATA +0 -452
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/NOTICE +0 -6
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/RECORD +0 -29
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/entry_points.txt +0 -3
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/top_level.txt +0 -1
- /airflow/providers/cncf/kubernetes/{example_dags → decorators}/__init__.py +0 -0
- {apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info → apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,344 @@
|
|
|
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
|
+
import asyncio
|
|
20
|
+
import datetime
|
|
21
|
+
import traceback
|
|
22
|
+
from collections.abc import AsyncIterator
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from functools import cached_property
|
|
25
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
26
|
+
|
|
27
|
+
import tenacity
|
|
28
|
+
|
|
29
|
+
from airflow.providers.cncf.kubernetes.exceptions import KubernetesApiPermissionError
|
|
30
|
+
from airflow.providers.cncf.kubernetes.hooks.kubernetes import AsyncKubernetesHook
|
|
31
|
+
from airflow.providers.cncf.kubernetes.utils.pod_manager import (
|
|
32
|
+
AsyncPodManager,
|
|
33
|
+
OnFinishAction,
|
|
34
|
+
PodLaunchTimeoutException,
|
|
35
|
+
PodPhase,
|
|
36
|
+
)
|
|
37
|
+
from airflow.triggers.base import BaseTrigger, TriggerEvent
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from kubernetes_asyncio.client.models import V1Pod
|
|
41
|
+
from pendulum import DateTime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ContainerState(str, Enum):
|
|
45
|
+
"""
|
|
46
|
+
Possible container states.
|
|
47
|
+
|
|
48
|
+
See https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
WAITING = "waiting"
|
|
52
|
+
RUNNING = "running"
|
|
53
|
+
TERMINATED = "terminated"
|
|
54
|
+
FAILED = "failed"
|
|
55
|
+
UNDEFINED = "undefined"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KubernetesPodTrigger(BaseTrigger):
|
|
59
|
+
"""
|
|
60
|
+
KubernetesPodTrigger run on the trigger worker to check the state of Pod.
|
|
61
|
+
|
|
62
|
+
:param pod_name: The name of the pod.
|
|
63
|
+
:param pod_namespace: The namespace of the pod.
|
|
64
|
+
:param kubernetes_conn_id: The :ref:`kubernetes connection id <howto/connection:kubernetes>`
|
|
65
|
+
for the Kubernetes cluster.
|
|
66
|
+
:param cluster_context: Context that points to kubernetes cluster.
|
|
67
|
+
:param config_dict: Content of kubeconfig file in dict format.
|
|
68
|
+
:param poll_interval: Polling period in seconds to check for the status.
|
|
69
|
+
:param trigger_start_time: time in Datetime format when the trigger was started
|
|
70
|
+
:param in_cluster: run kubernetes client with in_cluster configuration.
|
|
71
|
+
:param get_logs: get the stdout of the container as logs of the tasks.
|
|
72
|
+
:param startup_timeout: timeout in seconds to start up the pod.
|
|
73
|
+
:param startup_check_interval: interval in seconds to check if the pod has already started.
|
|
74
|
+
:param schedule_timeout: timeout in seconds to schedule pod in cluster.
|
|
75
|
+
:param on_finish_action: What to do when the pod reaches its final state, or the execution is interrupted.
|
|
76
|
+
If "delete_pod", the pod will be deleted regardless its state; if "delete_succeeded_pod",
|
|
77
|
+
only succeeded pod will be deleted. You can set to "keep_pod" to keep the pod.
|
|
78
|
+
:param logging_interval: number of seconds to wait before kicking it back to
|
|
79
|
+
the operator to print latest logs. If ``None`` will wait until container done.
|
|
80
|
+
:param last_log_time: where to resume logs from
|
|
81
|
+
:param trigger_kwargs: additional keyword parameters to send in the event
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
pod_name: str,
|
|
87
|
+
pod_namespace: str,
|
|
88
|
+
trigger_start_time: datetime.datetime,
|
|
89
|
+
base_container_name: str,
|
|
90
|
+
kubernetes_conn_id: str | None = None,
|
|
91
|
+
poll_interval: float = 2,
|
|
92
|
+
cluster_context: str | None = None,
|
|
93
|
+
config_dict: dict | None = None,
|
|
94
|
+
in_cluster: bool | None = None,
|
|
95
|
+
get_logs: bool = True,
|
|
96
|
+
startup_timeout: int = 120,
|
|
97
|
+
startup_check_interval: float = 5,
|
|
98
|
+
schedule_timeout: int = 120,
|
|
99
|
+
on_finish_action: str = "delete_pod",
|
|
100
|
+
last_log_time: DateTime | None = None,
|
|
101
|
+
logging_interval: int | None = None,
|
|
102
|
+
trigger_kwargs: dict | None = None,
|
|
103
|
+
):
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.pod_name = pod_name
|
|
106
|
+
self.pod_namespace = pod_namespace
|
|
107
|
+
self.trigger_start_time = trigger_start_time
|
|
108
|
+
self.base_container_name = base_container_name
|
|
109
|
+
self.kubernetes_conn_id = kubernetes_conn_id
|
|
110
|
+
self.poll_interval = poll_interval
|
|
111
|
+
self.cluster_context = cluster_context
|
|
112
|
+
self.config_dict = config_dict
|
|
113
|
+
self.in_cluster = in_cluster
|
|
114
|
+
self.get_logs = get_logs
|
|
115
|
+
self.startup_timeout = startup_timeout
|
|
116
|
+
self.startup_check_interval = startup_check_interval
|
|
117
|
+
self.schedule_timeout = schedule_timeout
|
|
118
|
+
self.last_log_time = last_log_time
|
|
119
|
+
self.logging_interval = logging_interval
|
|
120
|
+
self.on_finish_action = OnFinishAction(on_finish_action)
|
|
121
|
+
self.trigger_kwargs = trigger_kwargs or {}
|
|
122
|
+
self._since_time = None
|
|
123
|
+
|
|
124
|
+
def serialize(self) -> tuple[str, dict[str, Any]]:
|
|
125
|
+
"""Serialize KubernetesCreatePodTrigger arguments and classpath."""
|
|
126
|
+
return (
|
|
127
|
+
"airflow.providers.cncf.kubernetes.triggers.pod.KubernetesPodTrigger",
|
|
128
|
+
{
|
|
129
|
+
"pod_name": self.pod_name,
|
|
130
|
+
"pod_namespace": self.pod_namespace,
|
|
131
|
+
"base_container_name": self.base_container_name,
|
|
132
|
+
"kubernetes_conn_id": self.kubernetes_conn_id,
|
|
133
|
+
"poll_interval": self.poll_interval,
|
|
134
|
+
"cluster_context": self.cluster_context,
|
|
135
|
+
"config_dict": self.config_dict,
|
|
136
|
+
"in_cluster": self.in_cluster,
|
|
137
|
+
"get_logs": self.get_logs,
|
|
138
|
+
"startup_timeout": self.startup_timeout,
|
|
139
|
+
"startup_check_interval": self.startup_check_interval,
|
|
140
|
+
"schedule_timeout": self.schedule_timeout,
|
|
141
|
+
"trigger_start_time": self.trigger_start_time,
|
|
142
|
+
"on_finish_action": self.on_finish_action.value,
|
|
143
|
+
"last_log_time": self.last_log_time,
|
|
144
|
+
"logging_interval": self.logging_interval,
|
|
145
|
+
"trigger_kwargs": self.trigger_kwargs,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def run(self) -> AsyncIterator[TriggerEvent]:
|
|
150
|
+
"""Get current pod status and yield a TriggerEvent."""
|
|
151
|
+
self.log.info(
|
|
152
|
+
"Checking pod %r in namespace %r with poll interval %r.",
|
|
153
|
+
self.pod_name,
|
|
154
|
+
self.pod_namespace,
|
|
155
|
+
self.poll_interval,
|
|
156
|
+
)
|
|
157
|
+
try:
|
|
158
|
+
state = await self._wait_for_pod_start()
|
|
159
|
+
if state == ContainerState.TERMINATED:
|
|
160
|
+
event = TriggerEvent(
|
|
161
|
+
{
|
|
162
|
+
"status": "success",
|
|
163
|
+
"namespace": self.pod_namespace,
|
|
164
|
+
"name": self.pod_name,
|
|
165
|
+
"message": "All containers inside pod have started successfully.",
|
|
166
|
+
**self.trigger_kwargs,
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
elif state == ContainerState.FAILED:
|
|
170
|
+
event = TriggerEvent(
|
|
171
|
+
{
|
|
172
|
+
"status": "failed",
|
|
173
|
+
"namespace": self.pod_namespace,
|
|
174
|
+
"name": self.pod_name,
|
|
175
|
+
"message": "pod failed",
|
|
176
|
+
**self.trigger_kwargs,
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
event = await self._wait_for_container_completion()
|
|
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
|
+
**self.trigger_kwargs,
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
except KubernetesApiPermissionError as e:
|
|
196
|
+
message = (
|
|
197
|
+
"Kubernetes API permission error: The triggerer may not have sufficient permissions to monitor or delete pods. "
|
|
198
|
+
"Please ensure the triggerer's service account is included in the 'pod-launcher-role' as defined in the latest Airflow Helm chart. "
|
|
199
|
+
f"Original error: {e}"
|
|
200
|
+
)
|
|
201
|
+
yield TriggerEvent(
|
|
202
|
+
{
|
|
203
|
+
"name": self.pod_name,
|
|
204
|
+
"namespace": self.pod_namespace,
|
|
205
|
+
"status": "error",
|
|
206
|
+
"message": message,
|
|
207
|
+
**self.trigger_kwargs,
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
return
|
|
211
|
+
except Exception as e:
|
|
212
|
+
self.log.exception(
|
|
213
|
+
"Unexpected error while waiting for pod %s in namespace %s",
|
|
214
|
+
self.pod_name,
|
|
215
|
+
self.pod_namespace,
|
|
216
|
+
)
|
|
217
|
+
yield TriggerEvent(
|
|
218
|
+
{
|
|
219
|
+
"name": self.pod_name,
|
|
220
|
+
"namespace": self.pod_namespace,
|
|
221
|
+
"status": "error",
|
|
222
|
+
"message": str(e),
|
|
223
|
+
"stack_trace": traceback.format_exc(),
|
|
224
|
+
**self.trigger_kwargs,
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
def _format_exception_description(self, exc: Exception) -> Any:
|
|
230
|
+
if isinstance(exc, PodLaunchTimeoutException):
|
|
231
|
+
return exc.args[0]
|
|
232
|
+
|
|
233
|
+
description = f"Trigger {self.__class__.__name__} failed with exception {exc.__class__.__name__}."
|
|
234
|
+
message = exc.args and exc.args[0] or ""
|
|
235
|
+
if message:
|
|
236
|
+
description += f"\ntrigger exception message: {message}"
|
|
237
|
+
curr_traceback = traceback.format_exc()
|
|
238
|
+
description += f"\ntrigger traceback:\n{curr_traceback}"
|
|
239
|
+
return description
|
|
240
|
+
|
|
241
|
+
async def _wait_for_pod_start(self) -> ContainerState:
|
|
242
|
+
"""Loops until pod phase leaves ``PENDING`` If timeout is reached, throws error."""
|
|
243
|
+
pod = await self._get_pod()
|
|
244
|
+
events_task = self.pod_manager.watch_pod_events(pod, self.startup_check_interval)
|
|
245
|
+
pod_start_task = self.pod_manager.await_pod_start(
|
|
246
|
+
pod=pod,
|
|
247
|
+
schedule_timeout=self.schedule_timeout,
|
|
248
|
+
startup_timeout=self.startup_timeout,
|
|
249
|
+
check_interval=self.startup_check_interval,
|
|
250
|
+
)
|
|
251
|
+
await asyncio.gather(pod_start_task, events_task)
|
|
252
|
+
return self.define_container_state(await self._get_pod())
|
|
253
|
+
|
|
254
|
+
async def _wait_for_container_completion(self) -> TriggerEvent:
|
|
255
|
+
"""
|
|
256
|
+
Wait for container completion.
|
|
257
|
+
|
|
258
|
+
Waits until container is no longer in running state. If trigger is configured with a logging period,
|
|
259
|
+
then will emit an event to resume the task for the purpose of fetching more logs.
|
|
260
|
+
"""
|
|
261
|
+
time_begin = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
262
|
+
time_get_more_logs = None
|
|
263
|
+
if self.logging_interval is not None:
|
|
264
|
+
time_get_more_logs = time_begin + datetime.timedelta(seconds=self.logging_interval)
|
|
265
|
+
while True:
|
|
266
|
+
pod = await self._get_pod()
|
|
267
|
+
container_state = self.define_container_state(pod)
|
|
268
|
+
if container_state == ContainerState.TERMINATED:
|
|
269
|
+
return TriggerEvent(
|
|
270
|
+
{
|
|
271
|
+
"status": "success",
|
|
272
|
+
"namespace": self.pod_namespace,
|
|
273
|
+
"name": self.pod_name,
|
|
274
|
+
"last_log_time": self.last_log_time,
|
|
275
|
+
**self.trigger_kwargs,
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
if container_state == ContainerState.FAILED:
|
|
279
|
+
return TriggerEvent(
|
|
280
|
+
{
|
|
281
|
+
"status": "failed",
|
|
282
|
+
"namespace": self.pod_namespace,
|
|
283
|
+
"name": self.pod_name,
|
|
284
|
+
"message": "Container state failed",
|
|
285
|
+
"last_log_time": self.last_log_time,
|
|
286
|
+
**self.trigger_kwargs,
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
self.log.debug("Container is not completed and still working.")
|
|
290
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
291
|
+
if time_get_more_logs and now >= time_get_more_logs:
|
|
292
|
+
if self.get_logs and self.logging_interval:
|
|
293
|
+
self.last_log_time = await self.pod_manager.fetch_container_logs_before_current_sec(
|
|
294
|
+
pod, container_name=self.base_container_name, since_time=self.last_log_time
|
|
295
|
+
)
|
|
296
|
+
time_get_more_logs = now + datetime.timedelta(seconds=self.logging_interval)
|
|
297
|
+
|
|
298
|
+
self.log.debug("Sleeping for %s seconds.", self.poll_interval)
|
|
299
|
+
await asyncio.sleep(self.poll_interval)
|
|
300
|
+
|
|
301
|
+
@tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
|
|
302
|
+
async def _get_pod(self) -> V1Pod:
|
|
303
|
+
"""Get the pod from Kubernetes with retries."""
|
|
304
|
+
pod = await self.hook.get_pod(name=self.pod_name, namespace=self.pod_namespace)
|
|
305
|
+
# Due to AsyncKubernetesHook overriding get_pod, we need to cast the return
|
|
306
|
+
# value to kubernetes_asyncio.V1Pod, because it's perceived as different type
|
|
307
|
+
return cast("V1Pod", pod)
|
|
308
|
+
|
|
309
|
+
@cached_property
|
|
310
|
+
def hook(self) -> AsyncKubernetesHook:
|
|
311
|
+
return AsyncKubernetesHook(
|
|
312
|
+
conn_id=self.kubernetes_conn_id,
|
|
313
|
+
in_cluster=self.in_cluster,
|
|
314
|
+
config_dict=self.config_dict,
|
|
315
|
+
cluster_context=self.cluster_context,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
@cached_property
|
|
319
|
+
def pod_manager(self) -> AsyncPodManager:
|
|
320
|
+
return AsyncPodManager(async_hook=self.hook)
|
|
321
|
+
|
|
322
|
+
def define_container_state(self, pod: V1Pod) -> ContainerState:
|
|
323
|
+
pod_containers = pod.status.container_statuses
|
|
324
|
+
|
|
325
|
+
if pod_containers is None:
|
|
326
|
+
return ContainerState.UNDEFINED
|
|
327
|
+
|
|
328
|
+
container = next(c for c in pod_containers if c.name == self.base_container_name)
|
|
329
|
+
|
|
330
|
+
for state in (ContainerState.RUNNING, ContainerState.WAITING, ContainerState.TERMINATED):
|
|
331
|
+
state_obj = getattr(container.state, state)
|
|
332
|
+
if state_obj is not None:
|
|
333
|
+
if state != ContainerState.TERMINATED:
|
|
334
|
+
return state
|
|
335
|
+
return ContainerState.TERMINATED if state_obj.exit_code == 0 else ContainerState.FAILED
|
|
336
|
+
return ContainerState.UNDEFINED
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def should_wait(pod_phase: PodPhase, container_state: ContainerState) -> bool:
|
|
340
|
+
return (
|
|
341
|
+
container_state == ContainerState.WAITING
|
|
342
|
+
or container_state == ContainerState.RUNNING
|
|
343
|
+
or (container_state == ContainerState.UNDEFINED and pod_phase == PodPhase.PENDING)
|
|
344
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
"""Helper functions for inspecting and interacting with containers in a Kubernetes Pod."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from contextlib import suppress
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from kubernetes.client.models.v1_container_status import V1ContainerStatus
|
|
26
|
+
from kubernetes.client.models.v1_pod import V1Pod
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_container_status(pod: V1Pod, container_name: str) -> V1ContainerStatus | None:
|
|
30
|
+
"""Retrieve container status."""
|
|
31
|
+
if pod and pod.status:
|
|
32
|
+
container_statuses = []
|
|
33
|
+
if pod.status.container_statuses:
|
|
34
|
+
container_statuses.extend(pod.status.container_statuses)
|
|
35
|
+
if pod.status.init_container_statuses:
|
|
36
|
+
container_statuses.extend(pod.status.init_container_statuses)
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
container_statuses = None
|
|
40
|
+
|
|
41
|
+
if container_statuses:
|
|
42
|
+
# In general the variable container_statuses can store multiple items matching different containers.
|
|
43
|
+
# The following generator expression yields all items that have name equal to the container_name.
|
|
44
|
+
# The function next() here calls the generator to get only the first value. If there's nothing found
|
|
45
|
+
# then None is returned.
|
|
46
|
+
return next((x for x in container_statuses if x.name == container_name), None)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def container_is_running(pod: V1Pod, container_name: str) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Examine V1Pod ``pod`` to determine whether ``container_name`` is running.
|
|
53
|
+
|
|
54
|
+
If that container is present and running, returns True. Returns False otherwise.
|
|
55
|
+
"""
|
|
56
|
+
container_status = get_container_status(pod, container_name)
|
|
57
|
+
if not container_status:
|
|
58
|
+
return False
|
|
59
|
+
return container_status.state.running is not None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def container_is_completed(pod: V1Pod, container_name: str) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Examine V1Pod ``pod`` to determine whether ``container_name`` is completed.
|
|
65
|
+
|
|
66
|
+
If that container is present and completed, returns True. Returns False otherwise.
|
|
67
|
+
"""
|
|
68
|
+
container_status = get_container_status(pod, container_name)
|
|
69
|
+
if not container_status:
|
|
70
|
+
return False
|
|
71
|
+
return container_status.state.terminated is not None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def container_is_succeeded(pod: V1Pod, container_name: str) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Examine V1Pod ``pod`` to determine whether ``container_name`` is completed and succeeded.
|
|
77
|
+
|
|
78
|
+
If that container is present and completed and succeeded, returns True. Returns False otherwise.
|
|
79
|
+
"""
|
|
80
|
+
container_status = get_container_status(pod, container_name)
|
|
81
|
+
if not container_status or container_status.state.terminated is None:
|
|
82
|
+
return False
|
|
83
|
+
return container_status.state.terminated.exit_code == 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def container_is_wait(pod: V1Pod, container_name: str) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Examine V1Pod ``pod`` to determine whether ``container_name`` is waiting.
|
|
89
|
+
|
|
90
|
+
If that container is present and waiting, returns True. Returns False otherwise.
|
|
91
|
+
"""
|
|
92
|
+
container_status = get_container_status(pod, container_name)
|
|
93
|
+
if not container_status:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
return container_status.state.waiting is not None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def container_is_terminated(pod: V1Pod, container_name: str) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Examine V1Pod ``pod`` to determine whether ``container_name`` is terminated.
|
|
102
|
+
|
|
103
|
+
If that container is present and terminated, returns True. Returns False otherwise.
|
|
104
|
+
"""
|
|
105
|
+
container_statuses = pod.status.container_statuses if pod and pod.status else None
|
|
106
|
+
if not container_statuses:
|
|
107
|
+
return False
|
|
108
|
+
container_status = next((x for x in container_statuses if x.name == container_name), None)
|
|
109
|
+
if not container_status:
|
|
110
|
+
return False
|
|
111
|
+
return container_status.state.terminated is not None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_container_termination_message(pod: V1Pod, container_name: str):
|
|
115
|
+
with suppress(AttributeError, TypeError):
|
|
116
|
+
container_statuses = pod.status.container_statuses
|
|
117
|
+
container_status = next((x for x in container_statuses if x.name == container_name), None)
|
|
118
|
+
return container_status.state.terminated.message if container_status else None
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
|
|
18
|
+
# from https://github.com/tomplus/kubernetes_asyncio/pull/239/files
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from kubernetes import client
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from kubernetes.client import ApiClient
|
|
29
|
+
|
|
30
|
+
DEFAULT_DELETION_BODY = client.V1DeleteOptions(
|
|
31
|
+
propagation_policy="Background",
|
|
32
|
+
grace_period_seconds=5,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def delete_from_dict(k8s_client, data, body, namespace, verbose=False, **kwargs):
|
|
37
|
+
api_exceptions = []
|
|
38
|
+
|
|
39
|
+
if "List" in data["kind"]:
|
|
40
|
+
kind = data["kind"].replace("List", "")
|
|
41
|
+
for yml_doc in data["items"]:
|
|
42
|
+
if kind != "":
|
|
43
|
+
yml_doc["apiVersion"] = data["apiVersion"]
|
|
44
|
+
yml_doc["kind"] = kind
|
|
45
|
+
try:
|
|
46
|
+
_delete_from_yaml_single_item(
|
|
47
|
+
k8s_client=k8s_client,
|
|
48
|
+
yml_document=yml_doc,
|
|
49
|
+
verbose=verbose,
|
|
50
|
+
namespace=namespace,
|
|
51
|
+
body=body,
|
|
52
|
+
**kwargs,
|
|
53
|
+
)
|
|
54
|
+
except client.rest.ApiException as api_exception:
|
|
55
|
+
api_exceptions.append(api_exception)
|
|
56
|
+
else:
|
|
57
|
+
try:
|
|
58
|
+
_delete_from_yaml_single_item(
|
|
59
|
+
k8s_client=k8s_client,
|
|
60
|
+
yml_document=data,
|
|
61
|
+
verbose=verbose,
|
|
62
|
+
namespace=namespace,
|
|
63
|
+
body=body,
|
|
64
|
+
**kwargs,
|
|
65
|
+
)
|
|
66
|
+
except client.rest.ApiException as api_exception:
|
|
67
|
+
api_exceptions.append(api_exception)
|
|
68
|
+
|
|
69
|
+
if api_exceptions:
|
|
70
|
+
raise FailToDeleteError(api_exceptions)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def delete_from_yaml(
|
|
74
|
+
*,
|
|
75
|
+
k8s_client: ApiClient,
|
|
76
|
+
yaml_objects=None,
|
|
77
|
+
verbose: bool = False,
|
|
78
|
+
namespace: str = "default",
|
|
79
|
+
body: dict | None = None,
|
|
80
|
+
**kwargs,
|
|
81
|
+
):
|
|
82
|
+
for yml_document in yaml_objects:
|
|
83
|
+
if yml_document is not None:
|
|
84
|
+
delete_from_dict(
|
|
85
|
+
k8s_client=k8s_client,
|
|
86
|
+
data=yml_document,
|
|
87
|
+
body=body,
|
|
88
|
+
namespace=namespace,
|
|
89
|
+
verbose=verbose,
|
|
90
|
+
**kwargs,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _delete_from_yaml_single_item(
|
|
95
|
+
*,
|
|
96
|
+
k8s_client: ApiClient,
|
|
97
|
+
yml_document: dict,
|
|
98
|
+
verbose: bool = False,
|
|
99
|
+
namespace: str = "default",
|
|
100
|
+
body: dict | None = None,
|
|
101
|
+
**kwargs,
|
|
102
|
+
):
|
|
103
|
+
if body is None:
|
|
104
|
+
body = DEFAULT_DELETION_BODY
|
|
105
|
+
|
|
106
|
+
# get group and version from apiVersion
|
|
107
|
+
group, _, version = yml_document["apiVersion"].partition("/")
|
|
108
|
+
if version == "":
|
|
109
|
+
version = group
|
|
110
|
+
group = "core"
|
|
111
|
+
# Take care for the case e.g. api_type is "apiextensions.k8s.io"
|
|
112
|
+
# Only replace the last instance
|
|
113
|
+
group = "".join(group.rsplit(".k8s.io", 1))
|
|
114
|
+
# convert group name from DNS subdomain format to
|
|
115
|
+
# python class name convention
|
|
116
|
+
group = "".join(word.capitalize() for word in group.split("."))
|
|
117
|
+
fcn_to_call = f"{group}{version.capitalize()}Api"
|
|
118
|
+
k8s_api = getattr(client, fcn_to_call)(k8s_client)
|
|
119
|
+
# Replace CamelCased action_type into snake_case
|
|
120
|
+
kind = yml_document["kind"]
|
|
121
|
+
kind = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", kind)
|
|
122
|
+
kind = re.sub("([a-z0-9])([A-Z])", r"\1_\2", kind).lower()
|
|
123
|
+
|
|
124
|
+
# Decide which namespace we are going to use for deleting the object
|
|
125
|
+
# IMPORTANT: the docs namespace takes precedence over the namespace in args
|
|
126
|
+
# create_from_yaml_single_item have same behaviour
|
|
127
|
+
if "namespace" in yml_document["metadata"]:
|
|
128
|
+
namespace = yml_document["metadata"]["namespace"]
|
|
129
|
+
name = yml_document["metadata"]["name"]
|
|
130
|
+
|
|
131
|
+
# Expect the user to delete namespaced objects more often
|
|
132
|
+
resp: client.V1Status
|
|
133
|
+
if hasattr(k8s_api, f"delete_namespaced_{kind}"):
|
|
134
|
+
resp = getattr(k8s_api, f"delete_namespaced_{kind}")(
|
|
135
|
+
name=name, namespace=namespace, body=body, **kwargs
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
resp = getattr(k8s_api, f"delete_{kind}")(name=name, body=body, **kwargs)
|
|
139
|
+
if verbose:
|
|
140
|
+
print(f"{kind} deleted. status='{resp.status}'")
|
|
141
|
+
return resp
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class FailToDeleteError(Exception):
|
|
145
|
+
"""For handling error if an error occurred when handling a yaml file during deletion of the resource."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, api_exceptions: list):
|
|
148
|
+
self.api_exceptions = api_exceptions
|
|
149
|
+
|
|
150
|
+
def __str__(self):
|
|
151
|
+
msg = ""
|
|
152
|
+
for api_exception in self.api_exceptions:
|
|
153
|
+
msg += f"Error from server ({api_exception.reason}):{api_exception.body}\n"
|
|
154
|
+
return msg
|
|
@@ -0,0 +1,46 @@
|
|
|
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 collections.abc import Callable, Iterator
|
|
20
|
+
|
|
21
|
+
from kubernetes.utils import FailToCreateError
|
|
22
|
+
|
|
23
|
+
from airflow.providers.cncf.kubernetes.utils.delete_from import FailToDeleteError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def k8s_resource_iterator(callback: Callable[[dict], None], resources: Iterator) -> None:
|
|
27
|
+
failures: list = []
|
|
28
|
+
for data in resources:
|
|
29
|
+
if data is not None:
|
|
30
|
+
if "List" in data["kind"]:
|
|
31
|
+
kind = data["kind"].replace("List", "")
|
|
32
|
+
for yml_doc in data["items"]:
|
|
33
|
+
if kind != "":
|
|
34
|
+
yml_doc["apiVersion"] = data["apiVersion"]
|
|
35
|
+
yml_doc["kind"] = kind
|
|
36
|
+
try:
|
|
37
|
+
callback(yml_doc)
|
|
38
|
+
except (FailToCreateError, FailToDeleteError) as failure:
|
|
39
|
+
failures.extend(failure.api_exceptions)
|
|
40
|
+
else:
|
|
41
|
+
try:
|
|
42
|
+
callback(data)
|
|
43
|
+
except (FailToCreateError, FailToDeleteError) as failure:
|
|
44
|
+
failures.extend(failure.api_exceptions)
|
|
45
|
+
if failures:
|
|
46
|
+
raise FailToCreateError(failures)
|