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.
Files changed (21) hide show
  1. airflow/providers/cncf/kubernetes/__init__.py +3 -3
  2. airflow/providers/cncf/kubernetes/exceptions.py +9 -3
  3. airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +24 -5
  4. airflow/providers/cncf/kubernetes/get_provider_info.py +6 -0
  5. airflow/providers/cncf/kubernetes/hooks/kubernetes.py +58 -21
  6. airflow/providers/cncf/kubernetes/kube_config.py +24 -1
  7. airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +63 -16
  8. airflow/providers/cncf/kubernetes/operators/job.py +9 -3
  9. airflow/providers/cncf/kubernetes/operators/pod.py +36 -45
  10. airflow/providers/cncf/kubernetes/operators/resource.py +2 -8
  11. airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +18 -3
  12. airflow/providers/cncf/kubernetes/secret.py +3 -0
  13. airflow/providers/cncf/kubernetes/triggers/pod.py +56 -24
  14. airflow/providers/cncf/kubernetes/utils/pod_manager.py +256 -111
  15. airflow/providers/cncf/kubernetes/version_compat.py +5 -1
  16. {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/METADATA +19 -17
  17. {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/RECORD +21 -20
  18. apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info/licenses/NOTICE +5 -0
  19. {apache_airflow_providers_cncf_kubernetes-10.9.0rc1.dist-info → apache_airflow_providers_cncf_kubernetes-10.11.0rc2.dist-info}/WHEEL +0 -0
  20. {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
  21. {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
- @tenacity.retry(
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
- num_events = 0
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
- self.log.info("::group::Waiting until %ss to get the POD scheduled...", schedule_timeout)
291
- pod_was_scheduled = False
292
- start_check_time = time.time()
293
- while True:
294
- remote_pod = self.read_pod(pod)
295
- pod_status = remote_pod.status
296
- if pod_status.phase != PodPhase.PENDING:
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 = self.parse_log_line(line)
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
- @tenacity.retry(stop=tenacity.stop_after_attempt(6), wait=tenacity.wait_exponential(max=15), reraise=True)
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
- @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
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 AirflowException(f"There was an error reading the kubernetes API: {e}")
782
+ raise KubernetesApiException(f"There was an error reading the kubernetes API: {e}")
758
783
 
759
- @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
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 AirflowException(f"There was an error reading the kubernetes API: {e}")
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
- @tenacity.retry(
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
- @tenacity.retry(
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
- __all__ = ["AIRFLOW_V_3_0_PLUS", "AIRFLOW_V_3_1_PLUS"]
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.9.0rc1
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.10.0rc1
25
- Requires-Dist: apache-airflow-providers-common-compat>=1.8.0rc1
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,<34.0.0
29
- Requires-Dist: kubernetes_asyncio>=32.0.0,<34.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.9.0/changelog.html
32
- Project-URL: Documentation, https://airflow.staged.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.9.0
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.9.0``
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.9.0/>`_.
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.10.0``
95
- ``apache-airflow-providers-common-compat`` ``>=1.8.0``
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,<34.0.0``
99
- ``kubernetes_asyncio`` ``>=32.0.0,<34.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.9.0/changelog.html>`_.
124
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.11.0/changelog.html>`_.
123
125