apache-airflow-providers-cncf-kubernetes 10.9.0rc1__py3-none-any.whl → 10.11.0rc2__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 +3 -3
- airflow/providers/cncf/kubernetes/exceptions.py +9 -3
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +24 -5
- airflow/providers/cncf/kubernetes/get_provider_info.py +6 -0
- airflow/providers/cncf/kubernetes/hooks/kubernetes.py +58 -21
- airflow/providers/cncf/kubernetes/kube_config.py +24 -1
- airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +63 -16
- airflow/providers/cncf/kubernetes/operators/job.py +9 -3
- airflow/providers/cncf/kubernetes/operators/pod.py +36 -45
- airflow/providers/cncf/kubernetes/operators/resource.py +2 -8
- airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +18 -3
- airflow/providers/cncf/kubernetes/secret.py +3 -0
- airflow/providers/cncf/kubernetes/triggers/pod.py +56 -24
- airflow/providers/cncf/kubernetes/utils/pod_manager.py +256 -111
- airflow/providers/cncf/kubernetes/version_compat.py +5 -1
- {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/METADATA +19 -17
- {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/RECORD +21 -20
- apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info/licenses/NOTICE +5 -0
- {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/WHEEL +0 -0
- {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/entry_points.txt +0 -0
- {airflow/providers/cncf/kubernetes → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info/licenses}/LICENSE +0 -0
|
@@ -30,7 +30,6 @@ from datetime import timedelta
|
|
|
30
30
|
from typing import TYPE_CHECKING, Literal, cast
|
|
31
31
|
|
|
32
32
|
import pendulum
|
|
33
|
-
import tenacity
|
|
34
33
|
from kubernetes import client, watch
|
|
35
34
|
from kubernetes.client.rest import ApiException
|
|
36
35
|
from kubernetes.stream import stream as kubernetes_stream
|
|
@@ -40,6 +39,11 @@ from urllib3.exceptions import HTTPError, TimeoutError
|
|
|
40
39
|
|
|
41
40
|
from airflow.exceptions import AirflowException
|
|
42
41
|
from airflow.providers.cncf.kubernetes.callbacks import ExecutionMode, KubernetesPodOperatorCallback
|
|
42
|
+
from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import (
|
|
43
|
+
KubernetesApiException,
|
|
44
|
+
PodLaunchFailedException,
|
|
45
|
+
generic_api_retry,
|
|
46
|
+
)
|
|
43
47
|
from airflow.providers.cncf.kubernetes.utils.container import (
|
|
44
48
|
container_is_completed,
|
|
45
49
|
container_is_running,
|
|
@@ -60,6 +64,8 @@ if TYPE_CHECKING:
|
|
|
60
64
|
from kubernetes.client.models.v1_pod_condition import V1PodCondition
|
|
61
65
|
from urllib3.response import HTTPResponse
|
|
62
66
|
|
|
67
|
+
from airflow.providers.cncf.kubernetes.hooks.kubernetes import AsyncKubernetesHook
|
|
68
|
+
|
|
63
69
|
|
|
64
70
|
EMPTY_XCOM_RESULT = "__airflow_xcom_result_empty__"
|
|
65
71
|
"""
|
|
@@ -69,17 +75,6 @@ Sentinel for no xcom result.
|
|
|
69
75
|
"""
|
|
70
76
|
|
|
71
77
|
|
|
72
|
-
class PodLaunchFailedException(AirflowException):
|
|
73
|
-
"""When pod launching fails in KubernetesPodOperator."""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def should_retry_start_pod(exception: BaseException) -> bool:
|
|
77
|
-
"""Check if an Exception indicates a transient error and warrants retrying."""
|
|
78
|
-
if isinstance(exception, ApiException):
|
|
79
|
-
return str(exception.status) == "409"
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
|
|
83
78
|
class PodPhase:
|
|
84
79
|
"""
|
|
85
80
|
Possible pod phases.
|
|
@@ -99,6 +94,109 @@ def check_exception_is_kubernetes_api_unauthorized(exc: BaseException):
|
|
|
99
94
|
return isinstance(exc, ApiException) and exc.status and str(exc.status) == "401"
|
|
100
95
|
|
|
101
96
|
|
|
97
|
+
async def watch_pod_events(
|
|
98
|
+
pod_manager: PodManager | AsyncPodManager,
|
|
99
|
+
pod: V1Pod,
|
|
100
|
+
check_interval: float = 1,
|
|
101
|
+
) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Read pod events and write them to the log.
|
|
104
|
+
|
|
105
|
+
This function supports both asynchronous and synchronous pod managers.
|
|
106
|
+
|
|
107
|
+
:param pod_manager: The pod manager instance (PodManager or AsyncPodManager).
|
|
108
|
+
:param pod: The pod object to monitor.
|
|
109
|
+
:param check_interval: Interval (in seconds) between checks.
|
|
110
|
+
"""
|
|
111
|
+
num_events = 0
|
|
112
|
+
is_async = isinstance(pod_manager, AsyncPodManager)
|
|
113
|
+
while not pod_manager.stop_watching_events:
|
|
114
|
+
if is_async:
|
|
115
|
+
events = await pod_manager.read_pod_events(pod)
|
|
116
|
+
else:
|
|
117
|
+
events = pod_manager.read_pod_events(pod)
|
|
118
|
+
for new_event in events.items[num_events:]:
|
|
119
|
+
involved_object: V1ObjectReference = new_event.involved_object
|
|
120
|
+
pod_manager.log.info(
|
|
121
|
+
"The Pod has an Event: %s from %s", new_event.message, involved_object.field_path
|
|
122
|
+
)
|
|
123
|
+
num_events = len(events.items)
|
|
124
|
+
await asyncio.sleep(check_interval)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def await_pod_start(
|
|
128
|
+
pod_manager: PodManager | AsyncPodManager,
|
|
129
|
+
pod: V1Pod,
|
|
130
|
+
schedule_timeout: int = 120,
|
|
131
|
+
startup_timeout: int = 120,
|
|
132
|
+
check_interval: float = 1,
|
|
133
|
+
):
|
|
134
|
+
"""
|
|
135
|
+
Monitor the startup phase of a Kubernetes pod, waiting for it to leave the ``Pending`` state.
|
|
136
|
+
|
|
137
|
+
This function is shared by both PodManager and AsyncPodManager to provide consistent pod startup tracking.
|
|
138
|
+
|
|
139
|
+
:param pod_manager: The pod manager instance (PodManager or AsyncPodManager).
|
|
140
|
+
:param pod: The pod object to monitor.
|
|
141
|
+
:param schedule_timeout: Maximum time (in seconds) to wait for the pod to be scheduled.
|
|
142
|
+
:param startup_timeout: Maximum time (in seconds) to wait for the pod to start running after being scheduled.
|
|
143
|
+
:param check_interval: Interval (in seconds) between status checks.
|
|
144
|
+
:param is_async: Set to True if called in an async context; otherwise, False.
|
|
145
|
+
"""
|
|
146
|
+
pod_manager.log.info("::group::Waiting until %ss to get the POD scheduled...", schedule_timeout)
|
|
147
|
+
pod_was_scheduled = False
|
|
148
|
+
start_check_time = time.time()
|
|
149
|
+
is_async = isinstance(pod_manager, AsyncPodManager)
|
|
150
|
+
while True:
|
|
151
|
+
if is_async:
|
|
152
|
+
remote_pod = await pod_manager.read_pod(pod)
|
|
153
|
+
else:
|
|
154
|
+
remote_pod = pod_manager.read_pod(pod)
|
|
155
|
+
pod_status = remote_pod.status
|
|
156
|
+
if pod_status.phase != PodPhase.PENDING:
|
|
157
|
+
pod_manager.stop_watching_events = True
|
|
158
|
+
pod_manager.log.info("::endgroup::")
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
# Check for timeout
|
|
162
|
+
pod_conditions: list[V1PodCondition] = pod_status.conditions
|
|
163
|
+
if pod_conditions and any(
|
|
164
|
+
(condition.type == "PodScheduled" and condition.status == "True") for condition in pod_conditions
|
|
165
|
+
):
|
|
166
|
+
if not pod_was_scheduled:
|
|
167
|
+
# POD was initially scheduled update timeout for getting POD launched
|
|
168
|
+
pod_was_scheduled = True
|
|
169
|
+
start_check_time = time.time()
|
|
170
|
+
pod_manager.log.info("Waiting %ss to get the POD running...", startup_timeout)
|
|
171
|
+
|
|
172
|
+
if time.time() - start_check_time >= startup_timeout:
|
|
173
|
+
pod_manager.log.info("::endgroup::")
|
|
174
|
+
raise PodLaunchTimeoutException(
|
|
175
|
+
f"Pod took too long to start. More than {startup_timeout}s. Check the pod events in kubernetes."
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
if time.time() - start_check_time >= schedule_timeout:
|
|
179
|
+
pod_manager.log.info("::endgroup::")
|
|
180
|
+
raise PodLaunchTimeoutException(
|
|
181
|
+
f"Pod took too long to be scheduled on the cluster, giving up. More than {schedule_timeout}s. Check the pod events in kubernetes."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Check for general problems to terminate early - ErrImagePull
|
|
185
|
+
if pod_status.container_statuses:
|
|
186
|
+
for container_status in pod_status.container_statuses:
|
|
187
|
+
container_state: V1ContainerState = container_status.state
|
|
188
|
+
container_waiting: V1ContainerStateWaiting | None = container_state.waiting
|
|
189
|
+
if container_waiting:
|
|
190
|
+
if container_waiting.reason in ["ErrImagePull", "InvalidImageName"]:
|
|
191
|
+
pod_manager.log.info("::endgroup::")
|
|
192
|
+
raise PodLaunchFailedException(
|
|
193
|
+
f"Pod docker image cannot be pulled, unable to start: {container_waiting.reason}"
|
|
194
|
+
f"\n{container_waiting.message}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
await asyncio.sleep(check_interval)
|
|
198
|
+
|
|
199
|
+
|
|
102
200
|
class PodLaunchTimeoutException(AirflowException):
|
|
103
201
|
"""When pod does not leave the ``Pending`` phase within specified timeout."""
|
|
104
202
|
|
|
@@ -239,6 +337,7 @@ class PodManager(LoggingMixin):
|
|
|
239
337
|
raise e
|
|
240
338
|
return resp
|
|
241
339
|
|
|
340
|
+
@generic_api_retry
|
|
242
341
|
def delete_pod(self, pod: V1Pod) -> None:
|
|
243
342
|
"""Delete POD."""
|
|
244
343
|
try:
|
|
@@ -250,28 +349,14 @@ class PodManager(LoggingMixin):
|
|
|
250
349
|
if str(e.status) != "404":
|
|
251
350
|
raise
|
|
252
351
|
|
|
253
|
-
@
|
|
254
|
-
stop=tenacity.stop_after_attempt(3),
|
|
255
|
-
wait=tenacity.wait_random_exponential(),
|
|
256
|
-
reraise=True,
|
|
257
|
-
retry=tenacity.retry_if_exception(should_retry_start_pod),
|
|
258
|
-
)
|
|
352
|
+
@generic_api_retry
|
|
259
353
|
def create_pod(self, pod: V1Pod) -> V1Pod:
|
|
260
354
|
"""Launch the pod asynchronously."""
|
|
261
355
|
return self.run_pod_async(pod)
|
|
262
356
|
|
|
263
357
|
async def watch_pod_events(self, pod: V1Pod, check_interval: int = 1) -> None:
|
|
264
358
|
"""Read pod events and writes into log."""
|
|
265
|
-
|
|
266
|
-
while not self.stop_watching_events:
|
|
267
|
-
events = self.read_pod_events(pod)
|
|
268
|
-
for new_event in events.items[num_events:]:
|
|
269
|
-
involved_object: V1ObjectReference = new_event.involved_object
|
|
270
|
-
self.log.info(
|
|
271
|
-
"The Pod has an Event: %s from %s", new_event.message, involved_object.field_path
|
|
272
|
-
)
|
|
273
|
-
num_events = len(events.items)
|
|
274
|
-
await asyncio.sleep(check_interval)
|
|
359
|
+
await watch_pod_events(pod_manager=self, pod=pod, check_interval=check_interval)
|
|
275
360
|
|
|
276
361
|
async def await_pod_start(
|
|
277
362
|
self, pod: V1Pod, schedule_timeout: int = 120, startup_timeout: int = 120, check_interval: int = 1
|
|
@@ -287,55 +372,13 @@ class PodManager(LoggingMixin):
|
|
|
287
372
|
:param check_interval: Interval (in seconds) between checks
|
|
288
373
|
:return:
|
|
289
374
|
"""
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
self.stop_watching_events = True
|
|
298
|
-
self.log.info("::endgroup::")
|
|
299
|
-
break
|
|
300
|
-
|
|
301
|
-
# Check for timeout
|
|
302
|
-
pod_conditions: list[V1PodCondition] = pod_status.conditions
|
|
303
|
-
if pod_conditions and any(
|
|
304
|
-
(condition.type == "PodScheduled" and condition.status == "True")
|
|
305
|
-
for condition in pod_conditions
|
|
306
|
-
):
|
|
307
|
-
if not pod_was_scheduled:
|
|
308
|
-
# POD was initially scheduled update timeout for getting POD launched
|
|
309
|
-
pod_was_scheduled = True
|
|
310
|
-
start_check_time = time.time()
|
|
311
|
-
self.log.info("Waiting %ss to get the POD running...", startup_timeout)
|
|
312
|
-
|
|
313
|
-
if time.time() - start_check_time >= startup_timeout:
|
|
314
|
-
self.log.info("::endgroup::")
|
|
315
|
-
raise PodLaunchFailedException(
|
|
316
|
-
f"Pod took too long to start. More than {startup_timeout}s. Check the pod events in kubernetes."
|
|
317
|
-
)
|
|
318
|
-
else:
|
|
319
|
-
if time.time() - start_check_time >= schedule_timeout:
|
|
320
|
-
self.log.info("::endgroup::")
|
|
321
|
-
raise PodLaunchFailedException(
|
|
322
|
-
f"Pod took too long to be scheduled on the cluster, giving up. More than {schedule_timeout}s. Check the pod events in kubernetes."
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
# Check for general problems to terminate early - ErrImagePull
|
|
326
|
-
if pod_status.container_statuses:
|
|
327
|
-
for container_status in pod_status.container_statuses:
|
|
328
|
-
container_state: V1ContainerState = container_status.state
|
|
329
|
-
container_waiting: V1ContainerStateWaiting | None = container_state.waiting
|
|
330
|
-
if container_waiting:
|
|
331
|
-
if container_waiting.reason in ["ErrImagePull", "InvalidImageName"]:
|
|
332
|
-
self.log.info("::endgroup::")
|
|
333
|
-
raise PodLaunchFailedException(
|
|
334
|
-
f"Pod docker image cannot be pulled, unable to start: {container_waiting.reason}"
|
|
335
|
-
f"\n{container_waiting.message}"
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
await asyncio.sleep(check_interval)
|
|
375
|
+
await await_pod_start(
|
|
376
|
+
pod_manager=self,
|
|
377
|
+
pod=pod,
|
|
378
|
+
schedule_timeout=schedule_timeout,
|
|
379
|
+
startup_timeout=startup_timeout,
|
|
380
|
+
check_interval=check_interval,
|
|
381
|
+
)
|
|
339
382
|
|
|
340
383
|
def _log_message(
|
|
341
384
|
self,
|
|
@@ -426,7 +469,7 @@ class PodManager(LoggingMixin):
|
|
|
426
469
|
try:
|
|
427
470
|
for raw_line in logs:
|
|
428
471
|
line = raw_line.decode("utf-8", errors="backslashreplace")
|
|
429
|
-
line_timestamp, message =
|
|
472
|
+
line_timestamp, message = parse_log_line(line)
|
|
430
473
|
if line_timestamp: # detect new log line
|
|
431
474
|
if message_to_log is None: # first line in the log
|
|
432
475
|
message_to_log = message
|
|
@@ -654,22 +697,6 @@ class PodManager(LoggingMixin):
|
|
|
654
697
|
time.sleep(2)
|
|
655
698
|
return remote_pod
|
|
656
699
|
|
|
657
|
-
def parse_log_line(self, line: str) -> tuple[DateTime | None, str]:
|
|
658
|
-
"""
|
|
659
|
-
Parse K8s log line and returns the final state.
|
|
660
|
-
|
|
661
|
-
:param line: k8s log line
|
|
662
|
-
:return: timestamp and log message
|
|
663
|
-
"""
|
|
664
|
-
timestamp, sep, message = line.strip().partition(" ")
|
|
665
|
-
if not sep:
|
|
666
|
-
return None, line
|
|
667
|
-
try:
|
|
668
|
-
last_log_time = cast("DateTime", pendulum.parse(timestamp))
|
|
669
|
-
except ParserError:
|
|
670
|
-
return None, line
|
|
671
|
-
return last_log_time, message
|
|
672
|
-
|
|
673
700
|
def container_is_running(self, pod: V1Pod, container_name: str) -> bool:
|
|
674
701
|
"""Read pod and checks if container is running."""
|
|
675
702
|
remote_pod = self.read_pod(pod)
|
|
@@ -680,7 +707,7 @@ class PodManager(LoggingMixin):
|
|
|
680
707
|
remote_pod = self.read_pod(pod)
|
|
681
708
|
return container_is_terminated(pod=remote_pod, container_name=container_name)
|
|
682
709
|
|
|
683
|
-
@
|
|
710
|
+
@generic_api_retry
|
|
684
711
|
def read_pod_logs(
|
|
685
712
|
self,
|
|
686
713
|
pod: V1Pod,
|
|
@@ -723,7 +750,6 @@ class PodManager(LoggingMixin):
|
|
|
723
750
|
post_termination_timeout=post_termination_timeout,
|
|
724
751
|
)
|
|
725
752
|
|
|
726
|
-
@tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
|
|
727
753
|
def get_init_container_names(self, pod: V1Pod) -> list[str]:
|
|
728
754
|
"""
|
|
729
755
|
Return container names from the POD except for the airflow-xcom-sidecar container.
|
|
@@ -732,7 +758,6 @@ class PodManager(LoggingMixin):
|
|
|
732
758
|
"""
|
|
733
759
|
return [container_spec.name for container_spec in pod.spec.init_containers]
|
|
734
760
|
|
|
735
|
-
@tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
|
|
736
761
|
def get_container_names(self, pod: V1Pod) -> list[str]:
|
|
737
762
|
"""
|
|
738
763
|
Return container names from the POD except for the airflow-xcom-sidecar container.
|
|
@@ -746,7 +771,7 @@ class PodManager(LoggingMixin):
|
|
|
746
771
|
if container_spec.name != PodDefaults.SIDECAR_CONTAINER_NAME
|
|
747
772
|
]
|
|
748
773
|
|
|
749
|
-
@
|
|
774
|
+
@generic_api_retry
|
|
750
775
|
def read_pod_events(self, pod: V1Pod) -> CoreV1EventList:
|
|
751
776
|
"""Read events from the POD."""
|
|
752
777
|
try:
|
|
@@ -754,15 +779,15 @@ class PodManager(LoggingMixin):
|
|
|
754
779
|
namespace=pod.metadata.namespace, field_selector=f"involvedObject.name={pod.metadata.name}"
|
|
755
780
|
)
|
|
756
781
|
except HTTPError as e:
|
|
757
|
-
raise
|
|
782
|
+
raise KubernetesApiException(f"There was an error reading the kubernetes API: {e}")
|
|
758
783
|
|
|
759
|
-
@
|
|
784
|
+
@generic_api_retry
|
|
760
785
|
def read_pod(self, pod: V1Pod) -> V1Pod:
|
|
761
786
|
"""Read POD information."""
|
|
762
787
|
try:
|
|
763
788
|
return self._client.read_namespaced_pod(pod.metadata.name, pod.metadata.namespace)
|
|
764
789
|
except HTTPError as e:
|
|
765
|
-
raise
|
|
790
|
+
raise KubernetesApiException(f"There was an error reading the kubernetes API: {e}")
|
|
766
791
|
|
|
767
792
|
def await_xcom_sidecar_container_start(
|
|
768
793
|
self, pod: V1Pod, timeout: int = 900, log_interval: int = 30
|
|
@@ -801,11 +826,7 @@ class PodManager(LoggingMixin):
|
|
|
801
826
|
finally:
|
|
802
827
|
self.extract_xcom_kill(pod)
|
|
803
828
|
|
|
804
|
-
@
|
|
805
|
-
stop=tenacity.stop_after_attempt(5),
|
|
806
|
-
wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
|
|
807
|
-
reraise=True,
|
|
808
|
-
)
|
|
829
|
+
@generic_api_retry
|
|
809
830
|
def extract_xcom_json(self, pod: V1Pod) -> str:
|
|
810
831
|
"""Retrieve XCom value and also check if xcom json is valid."""
|
|
811
832
|
command = (
|
|
@@ -846,11 +867,7 @@ class PodManager(LoggingMixin):
|
|
|
846
867
|
raise AirflowException(f"Failed to extract xcom from pod: {pod.metadata.name}")
|
|
847
868
|
return result
|
|
848
869
|
|
|
849
|
-
@
|
|
850
|
-
stop=tenacity.stop_after_attempt(5),
|
|
851
|
-
wait=tenacity.wait_exponential(multiplier=1, min=4, max=10),
|
|
852
|
-
reraise=True,
|
|
853
|
-
)
|
|
870
|
+
@generic_api_retry
|
|
854
871
|
def extract_xcom_kill(self, pod: V1Pod):
|
|
855
872
|
"""Kill xcom sidecar container."""
|
|
856
873
|
with closing(
|
|
@@ -915,3 +932,131 @@ class OnFinishAction(str, enum.Enum):
|
|
|
915
932
|
def is_log_group_marker(line: str) -> bool:
|
|
916
933
|
"""Check if the line is a log group marker like `::group::` or `::endgroup::`."""
|
|
917
934
|
return line.startswith("::group::") or line.startswith("::endgroup::")
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def parse_log_line(line: str) -> tuple[DateTime | None, str]:
|
|
938
|
+
"""
|
|
939
|
+
Parse K8s log line and returns the final state.
|
|
940
|
+
|
|
941
|
+
:param line: k8s log line
|
|
942
|
+
:return: timestamp and log message
|
|
943
|
+
"""
|
|
944
|
+
timestamp, sep, message = line.strip().partition(" ")
|
|
945
|
+
if not sep:
|
|
946
|
+
return None, line
|
|
947
|
+
try:
|
|
948
|
+
last_log_time = cast("DateTime", pendulum.parse(timestamp))
|
|
949
|
+
except ParserError:
|
|
950
|
+
return None, line
|
|
951
|
+
return last_log_time, message
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
class AsyncPodManager(LoggingMixin):
|
|
955
|
+
"""Create, monitor, and otherwise interact with Kubernetes pods for use with the KubernetesPodTriggerer."""
|
|
956
|
+
|
|
957
|
+
def __init__(
|
|
958
|
+
self,
|
|
959
|
+
async_hook: AsyncKubernetesHook,
|
|
960
|
+
callbacks: list[type[KubernetesPodOperatorCallback]] | None = None,
|
|
961
|
+
):
|
|
962
|
+
"""
|
|
963
|
+
Create the launcher.
|
|
964
|
+
|
|
965
|
+
:param kube_client: kubernetes client
|
|
966
|
+
:param callbacks:
|
|
967
|
+
"""
|
|
968
|
+
super().__init__()
|
|
969
|
+
self._hook = async_hook
|
|
970
|
+
self._watch = watch.Watch()
|
|
971
|
+
self._callbacks = callbacks or []
|
|
972
|
+
self.stop_watching_events = False
|
|
973
|
+
|
|
974
|
+
async def read_pod(self, pod: V1Pod) -> V1Pod:
|
|
975
|
+
"""Read POD information."""
|
|
976
|
+
return await self._hook.get_pod(
|
|
977
|
+
pod.metadata.name,
|
|
978
|
+
pod.metadata.namespace,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
async def read_pod_events(self, pod: V1Pod) -> CoreV1EventList:
|
|
982
|
+
"""Get pod's events."""
|
|
983
|
+
return await self._hook.get_pod_events(
|
|
984
|
+
pod.metadata.name,
|
|
985
|
+
pod.metadata.namespace,
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
async def watch_pod_events(self, pod: V1Pod, check_interval: float = 1) -> None:
|
|
989
|
+
"""Read pod events and writes into log."""
|
|
990
|
+
await watch_pod_events(pod_manager=self, pod=pod, check_interval=check_interval)
|
|
991
|
+
|
|
992
|
+
async def await_pod_start(
|
|
993
|
+
self, pod: V1Pod, schedule_timeout: int = 120, startup_timeout: int = 120, check_interval: float = 1
|
|
994
|
+
) -> None:
|
|
995
|
+
"""
|
|
996
|
+
Wait for the pod to reach phase other than ``Pending``.
|
|
997
|
+
|
|
998
|
+
:param pod:
|
|
999
|
+
:param schedule_timeout: Timeout (in seconds) for pod stay in schedule state
|
|
1000
|
+
(if pod is taking to long in schedule state, fails task)
|
|
1001
|
+
:param startup_timeout: Timeout (in seconds) for startup of the pod
|
|
1002
|
+
(if pod is pending for too long after being scheduled, fails task)
|
|
1003
|
+
:param check_interval: Interval (in seconds) between checks
|
|
1004
|
+
:return:
|
|
1005
|
+
"""
|
|
1006
|
+
await await_pod_start(
|
|
1007
|
+
pod_manager=self,
|
|
1008
|
+
pod=pod,
|
|
1009
|
+
schedule_timeout=schedule_timeout,
|
|
1010
|
+
startup_timeout=startup_timeout,
|
|
1011
|
+
check_interval=check_interval,
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
async def fetch_container_logs_before_current_sec(
|
|
1015
|
+
self, pod: V1Pod, container_name: str, since_time: DateTime | None = None
|
|
1016
|
+
) -> DateTime | None:
|
|
1017
|
+
"""
|
|
1018
|
+
Asynchronously read the log file of the specified pod.
|
|
1019
|
+
|
|
1020
|
+
This method streams logs from the base container, skipping log lines from the current second to prevent duplicate entries on subsequent reads. It is designed to handle long-running containers and gracefully suppresses transient interruptions.
|
|
1021
|
+
|
|
1022
|
+
:param pod: The pod specification to monitor.
|
|
1023
|
+
:param container_name: The name of the container within the pod.
|
|
1024
|
+
:param since_time: The timestamp from which to start reading logs.
|
|
1025
|
+
:return: The timestamp to use for the next log read, representing the start of the current second. Returns None if an exception occurred.
|
|
1026
|
+
"""
|
|
1027
|
+
now = pendulum.now()
|
|
1028
|
+
logs = await self._hook.read_logs(
|
|
1029
|
+
name=pod.metadata.name,
|
|
1030
|
+
namespace=pod.metadata.namespace,
|
|
1031
|
+
container_name=container_name,
|
|
1032
|
+
since_seconds=(math.ceil((now - since_time).total_seconds()) if since_time else None),
|
|
1033
|
+
)
|
|
1034
|
+
message_to_log = None
|
|
1035
|
+
try:
|
|
1036
|
+
now_seconds = now.replace(microsecond=0)
|
|
1037
|
+
for line in logs:
|
|
1038
|
+
line_timestamp, message = parse_log_line(line)
|
|
1039
|
+
# Skip log lines from the current second to prevent duplicate entries on the next read.
|
|
1040
|
+
# The API only allows specifying 'since_seconds', not an exact timestamp.
|
|
1041
|
+
if line_timestamp and line_timestamp.replace(microsecond=0) == now_seconds:
|
|
1042
|
+
break
|
|
1043
|
+
if line_timestamp: # detect new log line
|
|
1044
|
+
if message_to_log is None: # first line in the log
|
|
1045
|
+
message_to_log = message
|
|
1046
|
+
else: # previous log line is complete
|
|
1047
|
+
if message_to_log is not None:
|
|
1048
|
+
if is_log_group_marker(message_to_log):
|
|
1049
|
+
print(message_to_log)
|
|
1050
|
+
else:
|
|
1051
|
+
self.log.info("[%s] %s", container_name, message_to_log)
|
|
1052
|
+
message_to_log = message
|
|
1053
|
+
elif message_to_log: # continuation of the previous log line
|
|
1054
|
+
message_to_log = f"{message_to_log}\n{message}"
|
|
1055
|
+
finally:
|
|
1056
|
+
# log the last line and update the last_captured_timestamp
|
|
1057
|
+
if message_to_log is not None:
|
|
1058
|
+
if is_log_group_marker(message_to_log):
|
|
1059
|
+
print(message_to_log)
|
|
1060
|
+
else:
|
|
1061
|
+
self.log.info("[%s] %s", container_name, message_to_log)
|
|
1062
|
+
return now # Return the current time as the last log time to ensure logs from the current second are read in the next fetch.
|
|
@@ -35,4 +35,8 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
|
|
|
35
35
|
AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
|
|
36
36
|
AIRFLOW_V_3_1_PLUS = get_base_airflow_version_tuple() >= (3, 1, 0)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"AIRFLOW_V_3_0_PLUS",
|
|
41
|
+
"AIRFLOW_V_3_1_PLUS",
|
|
42
|
+
]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: apache-airflow-providers-cncf-kubernetes
|
|
3
|
-
Version: 10.
|
|
3
|
+
Version: 10.11.0rc2
|
|
4
4
|
Summary: Provider package apache-airflow-providers-cncf-kubernetes for Apache Airflow
|
|
5
5
|
Keywords: airflow-provider,cncf.kubernetes,airflow,integration
|
|
6
6
|
Author-email: Apache Software Foundation <dev@airflow.apache.org>
|
|
7
7
|
Maintainer-email: Apache Software Foundation <dev@airflow.apache.org>
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/x-rst
|
|
10
|
+
License-Expression: Apache-2.0
|
|
10
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
12
|
Classifier: Environment :: Console
|
|
12
13
|
Classifier: Environment :: Web Environment
|
|
@@ -14,22 +15,23 @@ Classifier: Intended Audience :: Developers
|
|
|
14
15
|
Classifier: Intended Audience :: System Administrators
|
|
15
16
|
Classifier: Framework :: Apache Airflow
|
|
16
17
|
Classifier: Framework :: Apache Airflow :: Provider
|
|
17
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
23
25
|
Requires-Dist: aiofiles>=23.2.0
|
|
24
|
-
Requires-Dist: apache-airflow>=2.
|
|
25
|
-
Requires-Dist: apache-airflow-providers-common-compat>=1.
|
|
26
|
+
Requires-Dist: apache-airflow>=2.11.0rc1
|
|
27
|
+
Requires-Dist: apache-airflow-providers-common-compat>=1.10.0rc1
|
|
26
28
|
Requires-Dist: asgiref>=3.5.2
|
|
27
|
-
Requires-Dist: cryptography>=41.0.0
|
|
28
|
-
Requires-Dist: kubernetes>=32.0.0,<
|
|
29
|
-
Requires-Dist: kubernetes_asyncio>=32.0.0,<
|
|
29
|
+
Requires-Dist: cryptography>=41.0.0,<46.0.0
|
|
30
|
+
Requires-Dist: kubernetes>=32.0.0,<35.0.0
|
|
31
|
+
Requires-Dist: kubernetes_asyncio>=32.0.0,<35.0.0
|
|
30
32
|
Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
|
|
31
|
-
Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.
|
|
32
|
-
Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.
|
|
33
|
+
Project-URL: Changelog, https://airflow.staged.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.11.0/changelog.html
|
|
34
|
+
Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.11.0
|
|
33
35
|
Project-URL: Mastodon, https://fosstodon.org/@airflow
|
|
34
36
|
Project-URL: Slack Chat, https://s.apache.org/airflow-slack
|
|
35
37
|
Project-URL: Source Code, https://github.com/apache/airflow
|
|
@@ -60,7 +62,7 @@ Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
|
|
|
60
62
|
|
|
61
63
|
Package ``apache-airflow-providers-cncf-kubernetes``
|
|
62
64
|
|
|
63
|
-
Release: ``10.
|
|
65
|
+
Release: ``10.11.0``
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
`Kubernetes <https://kubernetes.io/>`__
|
|
@@ -73,7 +75,7 @@ This is a provider package for ``cncf.kubernetes`` provider. All classes for thi
|
|
|
73
75
|
are in ``airflow.providers.cncf.kubernetes`` python package.
|
|
74
76
|
|
|
75
77
|
You can find package information and changelog for the provider
|
|
76
|
-
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.
|
|
78
|
+
in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.11.0/>`_.
|
|
77
79
|
|
|
78
80
|
Installation
|
|
79
81
|
------------
|
|
@@ -91,12 +93,12 @@ Requirements
|
|
|
91
93
|
PIP package Version required
|
|
92
94
|
========================================== ====================
|
|
93
95
|
``aiofiles`` ``>=23.2.0``
|
|
94
|
-
``apache-airflow`` ``>=2.
|
|
95
|
-
``apache-airflow-providers-common-compat`` ``>=1.
|
|
96
|
+
``apache-airflow`` ``>=2.11.0``
|
|
97
|
+
``apache-airflow-providers-common-compat`` ``>=1.10.0``
|
|
96
98
|
``asgiref`` ``>=3.5.2``
|
|
97
|
-
``cryptography`` ``>=41.0.0``
|
|
98
|
-
``kubernetes`` ``>=32.0.0,<
|
|
99
|
-
``kubernetes_asyncio`` ``>=32.0.0,<
|
|
99
|
+
``cryptography`` ``>=41.0.0,<46.0.0``
|
|
100
|
+
``kubernetes`` ``>=32.0.0,<35.0.0``
|
|
101
|
+
``kubernetes_asyncio`` ``>=32.0.0,<35.0.0``
|
|
100
102
|
========================================== ====================
|
|
101
103
|
|
|
102
104
|
Cross provider package dependencies
|
|
@@ -119,5 +121,5 @@ Dependent package
|
|
|
119
121
|
================================================================================================================== =================
|
|
120
122
|
|
|
121
123
|
The changelog for the provider package can be found in the
|
|
122
|
-
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.
|
|
124
|
+
`changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.11.0/changelog.html>`_.
|
|
123
125
|
|