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.
Files changed (72) hide show
  1. airflow/providers/cncf/kubernetes/__init__.py +18 -23
  2. airflow/providers/cncf/kubernetes/backcompat/__init__.py +17 -0
  3. airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +31 -49
  4. airflow/providers/cncf/kubernetes/callbacks.py +200 -0
  5. airflow/providers/cncf/kubernetes/cli/__init__.py +16 -0
  6. airflow/providers/cncf/kubernetes/cli/kubernetes_command.py +195 -0
  7. airflow/providers/cncf/kubernetes/decorators/kubernetes.py +163 -0
  8. airflow/providers/cncf/kubernetes/decorators/kubernetes_cmd.py +118 -0
  9. airflow/providers/cncf/kubernetes/exceptions.py +37 -0
  10. airflow/providers/cncf/kubernetes/executors/__init__.py +17 -0
  11. airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +831 -0
  12. airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py +91 -0
  13. airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +736 -0
  14. airflow/providers/cncf/kubernetes/executors/local_kubernetes_executor.py +306 -0
  15. airflow/providers/cncf/kubernetes/get_provider_info.py +249 -50
  16. airflow/providers/cncf/kubernetes/hooks/kubernetes.py +846 -112
  17. airflow/providers/cncf/kubernetes/k8s_model.py +62 -0
  18. airflow/providers/cncf/kubernetes/kube_client.py +156 -0
  19. airflow/providers/cncf/kubernetes/kube_config.py +125 -0
  20. airflow/providers/cncf/kubernetes/kubernetes_executor_templates/__init__.py +16 -0
  21. airflow/providers/cncf/kubernetes/kubernetes_executor_templates/basic_template.yaml +79 -0
  22. airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +165 -0
  23. airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +368 -0
  24. airflow/providers/cncf/kubernetes/operators/job.py +646 -0
  25. airflow/providers/cncf/kubernetes/operators/kueue.py +132 -0
  26. airflow/providers/cncf/kubernetes/operators/pod.py +1417 -0
  27. airflow/providers/cncf/kubernetes/operators/resource.py +191 -0
  28. airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +336 -35
  29. airflow/providers/cncf/kubernetes/pod_generator.py +592 -0
  30. airflow/providers/cncf/kubernetes/pod_template_file_examples/__init__.py +16 -0
  31. airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_image_template.yaml +68 -0
  32. airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_volume_template.yaml +74 -0
  33. airflow/providers/cncf/kubernetes/pod_template_file_examples/git_sync_template.yaml +95 -0
  34. airflow/providers/cncf/kubernetes/python_kubernetes_script.jinja2 +51 -0
  35. airflow/providers/cncf/kubernetes/python_kubernetes_script.py +92 -0
  36. airflow/providers/cncf/kubernetes/resource_convert/__init__.py +16 -0
  37. airflow/providers/cncf/kubernetes/resource_convert/configmap.py +52 -0
  38. airflow/providers/cncf/kubernetes/resource_convert/env_variable.py +39 -0
  39. airflow/providers/cncf/kubernetes/resource_convert/secret.py +40 -0
  40. airflow/providers/cncf/kubernetes/secret.py +128 -0
  41. airflow/providers/cncf/kubernetes/sensors/spark_kubernetes.py +30 -14
  42. airflow/providers/cncf/kubernetes/template_rendering.py +81 -0
  43. airflow/providers/cncf/kubernetes/triggers/__init__.py +16 -0
  44. airflow/providers/cncf/kubernetes/triggers/job.py +176 -0
  45. airflow/providers/cncf/kubernetes/triggers/pod.py +344 -0
  46. airflow/providers/cncf/kubernetes/utils/__init__.py +3 -0
  47. airflow/providers/cncf/kubernetes/utils/container.py +118 -0
  48. airflow/providers/cncf/kubernetes/utils/delete_from.py +154 -0
  49. airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py +46 -0
  50. airflow/providers/cncf/kubernetes/utils/pod_manager.py +887 -152
  51. airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +25 -16
  52. airflow/providers/cncf/kubernetes/version_compat.py +38 -0
  53. apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/METADATA +125 -0
  54. apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/RECORD +62 -0
  55. {apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info → apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info}/WHEEL +1 -2
  56. apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/entry_points.txt +3 -0
  57. apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/licenses/NOTICE +5 -0
  58. airflow/providers/cncf/kubernetes/backcompat/pod.py +0 -119
  59. airflow/providers/cncf/kubernetes/backcompat/pod_runtime_info_env.py +0 -56
  60. airflow/providers/cncf/kubernetes/backcompat/volume.py +0 -62
  61. airflow/providers/cncf/kubernetes/backcompat/volume_mount.py +0 -58
  62. airflow/providers/cncf/kubernetes/example_dags/example_kubernetes.py +0 -163
  63. airflow/providers/cncf/kubernetes/example_dags/example_spark_kubernetes.py +0 -66
  64. airflow/providers/cncf/kubernetes/example_dags/example_spark_kubernetes_spark_pi.yaml +0 -57
  65. airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +0 -622
  66. apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/METADATA +0 -452
  67. apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/NOTICE +0 -6
  68. apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/RECORD +0 -29
  69. apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/entry_points.txt +0 -3
  70. apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/top_level.txt +0 -1
  71. /airflow/providers/cncf/kubernetes/{example_dags → decorators}/__init__.py +0 -0
  72. {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
+ )
@@ -14,3 +14,6 @@
14
14
  # KIND, either express or implied. See the License for the
15
15
  # specific language governing permissions and limitations
16
16
  # under the License.
17
+ from __future__ import annotations
18
+
19
+ __all__ = ["xcom_sidecar", "pod_manager"]
@@ -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)