apache-airflow-providers-cncf-kubernetes 10.0.1rc1__py3-none-any.whl → 10.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of apache-airflow-providers-cncf-kubernetes might be problematic. Click here for more details.

@@ -29,11 +29,11 @@ from airflow import __version__ as airflow_version
29
29
 
30
30
  __all__ = ["__version__"]
31
31
 
32
- __version__ = "10.0.1"
32
+ __version__ = "10.1.0"
33
33
 
34
34
  if packaging.version.parse(packaging.version.parse(airflow_version).base_version) < packaging.version.parse(
35
- "2.8.0"
35
+ "2.9.0"
36
36
  ):
37
37
  raise RuntimeError(
38
- f"The package `apache-airflow-providers-cncf-kubernetes:{__version__}` needs Apache Airflow 2.8.0+"
38
+ f"The package `apache-airflow-providers-cncf-kubernetes:{__version__}` needs Apache Airflow 2.9.0+"
39
39
  )
@@ -25,22 +25,18 @@ from datetime import datetime, timedelta
25
25
  from kubernetes import client
26
26
  from kubernetes.client.api_client import ApiClient
27
27
  from kubernetes.client.rest import ApiException
28
- from packaging.version import Version
29
28
 
30
- from airflow import __version__ as airflow_version
31
29
  from airflow.models import DagRun, TaskInstance
32
30
  from airflow.providers.cncf.kubernetes import pod_generator
33
31
  from airflow.providers.cncf.kubernetes.executors.kubernetes_executor import KubeConfig
34
32
  from airflow.providers.cncf.kubernetes.kube_client import get_kube_client
35
33
  from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import create_unique_id
36
34
  from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator
35
+ from airflow.providers.cncf.kubernetes.version_compat import AIRFLOW_V_3_0_PLUS
37
36
  from airflow.utils import cli as cli_utils, yaml
38
37
  from airflow.utils.cli import get_dag
39
38
  from airflow.utils.providers_configuration_loader import providers_configuration_loaded
40
39
 
41
- AIRFLOW_VERSION = Version(airflow_version)
42
- AIRFLOW_V_3_0_PLUS = Version(AIRFLOW_VERSION.base_version) >= Version("3.0.0")
43
-
44
40
 
45
41
  @cli_utils.action_cli
46
42
  @providers_configuration_loaded
@@ -239,11 +239,12 @@ class KubernetesExecutor(BaseExecutor):
239
239
  from airflow.models.taskinstance import TaskInstance
240
240
 
241
241
  hybrid_executor_enabled = hasattr(TaskInstance, "executor")
242
- default_executor = None
242
+ default_executor_alias = None
243
243
  if hybrid_executor_enabled:
244
244
  from airflow.executors.executor_loader import ExecutorLoader
245
245
 
246
- default_executor = str(ExecutorLoader.get_default_executor_name())
246
+ default_executor_name = ExecutorLoader.get_default_executor_name()
247
+ default_executor_alias = default_executor_name.alias
247
248
 
248
249
  with Stats.timer("kubernetes_executor.clear_not_launched_queued_tasks.duration"):
249
250
  self.log.debug("Clearing tasks that have not been launched")
@@ -253,7 +254,10 @@ class KubernetesExecutor(BaseExecutor):
253
254
  )
254
255
  if self.kubernetes_queue:
255
256
  query = query.where(TaskInstance.queue == self.kubernetes_queue)
256
- elif hybrid_executor_enabled and KUBERNETES_EXECUTOR == default_executor:
257
+ # KUBERNETES_EXECUTOR is the string name/alias of the "core" executor represented by this
258
+ # module. The ExecutorName for "core" executors always contains an alias and cannot be modified
259
+ # to be different from the constant (in this case KUBERNETES_EXECUTOR).
260
+ elif hybrid_executor_enabled and default_executor_alias == KUBERNETES_EXECUTOR:
257
261
  query = query.where(
258
262
  or_(
259
263
  TaskInstance.executor == KUBERNETES_EXECUTOR,
@@ -28,8 +28,9 @@ def get_provider_info():
28
28
  "name": "Kubernetes",
29
29
  "description": "`Kubernetes <https://kubernetes.io/>`__\n",
30
30
  "state": "ready",
31
- "source-date-epoch": 1732429220,
31
+ "source-date-epoch": 1734537609,
32
32
  "versions": [
33
+ "10.1.0",
33
34
  "10.0.1",
34
35
  "10.0.0",
35
36
  "9.0.1",
@@ -101,7 +102,7 @@ def get_provider_info():
101
102
  ],
102
103
  "dependencies": [
103
104
  "aiofiles>=23.2.0",
104
- "apache-airflow>=2.8.0",
105
+ "apache-airflow>=2.9.0",
105
106
  "asgiref>=3.5.2",
106
107
  "cryptography>=41.0.0",
107
108
  "kubernetes>=29.0.0,<=31.0.0",
@@ -128,6 +129,7 @@ def get_provider_info():
128
129
  "integration-name": "Kubernetes",
129
130
  "python-modules": [
130
131
  "airflow.providers.cncf.kubernetes.operators.custom_object_launcher",
132
+ "airflow.providers.cncf.kubernetes.operators.kueue",
131
133
  "airflow.providers.cncf.kubernetes.operators.pod",
132
134
  "airflow.providers.cncf.kubernetes.operators.spark_kubernetes",
133
135
  "airflow.providers.cncf.kubernetes.operators.resource",
@@ -26,9 +26,11 @@ from time import sleep
26
26
  from typing import TYPE_CHECKING, Any
27
27
 
28
28
  import aiofiles
29
+ import requests
29
30
  import tenacity
30
31
  from asgiref.sync import sync_to_async
31
- from kubernetes import client, config, watch
32
+ from kubernetes import client, config, utils, watch
33
+ from kubernetes.client.models import V1Deployment
32
34
  from kubernetes.config import ConfigException
33
35
  from kubernetes_asyncio import client as async_client, config as async_config
34
36
  from urllib3.exceptions import HTTPError
@@ -47,7 +49,7 @@ from airflow.utils import yaml
47
49
 
48
50
  if TYPE_CHECKING:
49
51
  from kubernetes.client import V1JobList
50
- from kubernetes.client.models import V1Deployment, V1Job, V1Pod
52
+ from kubernetes.client.models import V1Job, V1Pod
51
53
 
52
54
  LOADING_KUBE_CONFIG_FILE_RESOURCE = "Loading Kubernetes configuration file kube_config from {}..."
53
55
 
@@ -489,12 +491,9 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
489
491
  :param name: Name of Deployment to retrieve
490
492
  :param namespace: Deployment namespace
491
493
  """
492
- try:
493
- return self.apps_v1_client.read_namespaced_deployment_status(
494
- name=name, namespace=namespace, pretty=True, **kwargs
495
- )
496
- except Exception as exc:
497
- raise exc
494
+ return self.apps_v1_client.read_namespaced_deployment_status(
495
+ name=name, namespace=namespace, pretty=True, **kwargs
496
+ )
498
497
 
499
498
  @tenacity.retry(
500
499
  stop=tenacity.stop_after_attempt(3),
@@ -644,6 +643,71 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
644
643
  body=body,
645
644
  )
646
645
 
646
+ def apply_from_yaml_file(
647
+ self,
648
+ api_client: Any = None,
649
+ yaml_file: str | None = None,
650
+ yaml_objects: list[dict] | None = None,
651
+ verbose: bool = False,
652
+ namespace: str = "default",
653
+ ):
654
+ """
655
+ Perform an action from a yaml file.
656
+
657
+ :param api_client: A Kubernetes client application.
658
+ :param yaml_file: Contains the path to yaml file.
659
+ :param yaml_objects: List of YAML objects; used instead of reading the yaml_file.
660
+ :param verbose: If True, print confirmation from create action. Default is False.
661
+ :param namespace: Contains the namespace to create all resources inside. The namespace must
662
+ preexist otherwise the resource creation will fail.
663
+ """
664
+ utils.create_from_yaml(
665
+ k8s_client=api_client or self.api_client,
666
+ yaml_objects=yaml_objects,
667
+ yaml_file=yaml_file,
668
+ verbose=verbose,
669
+ namespace=namespace or self.get_namespace(),
670
+ )
671
+
672
+ def check_kueue_deployment_running(
673
+ self, name: str, namespace: str, timeout: float = 300.0, polling_period_seconds: float = 2.0
674
+ ) -> None:
675
+ _timeout = timeout
676
+ while _timeout > 0:
677
+ try:
678
+ deployment = self.get_deployment_status(name=name, namespace=namespace)
679
+ except Exception as e:
680
+ self.log.exception("Exception occurred while checking for Deployment status.")
681
+ raise e
682
+
683
+ deployment_status = V1Deployment.to_dict(deployment)["status"]
684
+ replicas = deployment_status["replicas"]
685
+ ready_replicas = deployment_status["ready_replicas"]
686
+ unavailable_replicas = deployment_status["unavailable_replicas"]
687
+ if (
688
+ replicas is not None
689
+ and ready_replicas is not None
690
+ and unavailable_replicas is None
691
+ and replicas == ready_replicas
692
+ ):
693
+ return
694
+ else:
695
+ self.log.info("Waiting until Deployment will be ready...")
696
+ sleep(polling_period_seconds)
697
+
698
+ _timeout -= polling_period_seconds
699
+
700
+ raise AirflowException("Deployment timed out")
701
+
702
+ @staticmethod
703
+ def get_yaml_content_from_file(kueue_yaml_url) -> list[dict]:
704
+ """Download content of YAML file and separate it into several dictionaries."""
705
+ response = requests.get(kueue_yaml_url, allow_redirects=True)
706
+ if response.status_code != 200:
707
+ raise AirflowException("Was not able to read the yaml file from given URL")
708
+
709
+ return list(yaml.safe_load_all(response.text))
710
+
647
711
 
648
712
  def _get_bool(val) -> bool | None:
649
713
  """Convert val to bool if can be done with certainty; if we cannot infer intention we return None."""
@@ -0,0 +1,111 @@
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
+ """Manage a Kubernetes Kueue."""
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from collections.abc import Sequence
23
+ from functools import cached_property
24
+
25
+ from kubernetes.utils import FailToCreateError
26
+
27
+ from airflow.exceptions import AirflowException
28
+ from airflow.models import BaseOperator
29
+ from airflow.providers.cncf.kubernetes.hooks.kubernetes import KubernetesHook
30
+ from airflow.providers.cncf.kubernetes.operators.job import KubernetesJobOperator
31
+
32
+
33
+ class KubernetesInstallKueueOperator(BaseOperator):
34
+ """
35
+ Installs a Kubernetes Kueue.
36
+
37
+ .. seealso::
38
+ For more information on how to use this operator, take a look at the guide:
39
+ :ref:`howto/operator:KubernetesInstallKueueOperator`
40
+
41
+ :param kueue_version: The Kubernetes Kueue version to install.
42
+ :param kubernetes_conn_id: The :ref:`kubernetes connection id <howto/connection:kubernetes>`
43
+ for the Kubernetes cluster.
44
+ """
45
+
46
+ template_fields: Sequence[str] = (
47
+ "kueue_version",
48
+ "kubernetes_conn_id",
49
+ )
50
+
51
+ def __init__(self, kueue_version: str, kubernetes_conn_id: str = "kubernetes_default", *args, **kwargs):
52
+ super().__init__(*args, **kwargs)
53
+ self.kubernetes_conn_id = kubernetes_conn_id
54
+ self.kueue_version = kueue_version
55
+ self._kueue_yaml_url = (
56
+ f"https://github.com/kubernetes-sigs/kueue/releases/download/{self.kueue_version}/manifests.yaml"
57
+ )
58
+
59
+ @cached_property
60
+ def hook(self) -> KubernetesHook:
61
+ return KubernetesHook(conn_id=self.kubernetes_conn_id)
62
+
63
+ def execute(self, context):
64
+ yaml_objects = self.hook.get_yaml_content_from_file(kueue_yaml_url=self._kueue_yaml_url)
65
+ try:
66
+ self.hook.apply_from_yaml_file(yaml_objects=yaml_objects)
67
+ except FailToCreateError as ex:
68
+ error_bodies = [json.loads(e.body) for e in ex.api_exceptions]
69
+ if next((e for e in error_bodies if e.get("reason") == "AlreadyExists"), None):
70
+ self.log.info("Kueue is already enabled for the cluster")
71
+
72
+ if errors := [e for e in error_bodies if e.get("reason") != "AlreadyExists"]:
73
+ error_message = "\n".join(e.get("body") for e in errors)
74
+ raise AirflowException(error_message)
75
+ return
76
+
77
+ self.hook.check_kueue_deployment_running(name="kueue-controller-manager", namespace="kueue-system")
78
+ self.log.info("Kueue installed successfully!")
79
+
80
+
81
+ class KubernetesStartKueueJobOperator(KubernetesJobOperator):
82
+ """
83
+ Executes a Kubernetes Job in Kueue.
84
+
85
+ .. seealso::
86
+ For more information on how to use this operator, take a look at the guide:
87
+ :ref:`howto/operator:KubernetesStartKueueJobOperator`
88
+
89
+ :param queue_name: The name of the Queue in the cluster
90
+ """
91
+
92
+ template_fields = tuple({"queue_name"} | set(KubernetesJobOperator.template_fields))
93
+
94
+ def __init__(self, queue_name: str, *args, **kwargs) -> None:
95
+ super().__init__(*args, **kwargs)
96
+ self.queue_name = queue_name
97
+
98
+ if self.suspend is False:
99
+ raise AirflowException(
100
+ "The `suspend` parameter can't be False. If you want to use Kueue for running Job"
101
+ " in a Kubernetes cluster, set the `suspend` parameter to True.",
102
+ )
103
+ elif self.suspend is None:
104
+ self.log.info(
105
+ "You have not set parameter `suspend` in class %s. "
106
+ "For running a Job in Kueue the `suspend` parameter has been set to True.",
107
+ self.__class__.__name__,
108
+ )
109
+ self.suspend = True
110
+ self.labels.update({"kueue.x-k8s.io/queue-name": self.queue_name})
111
+ self.annotations.update({"kueue.x-k8s.io/queue-name": self.queue_name})
@@ -155,6 +155,9 @@ class KubernetesPodOperator(BaseOperator):
155
155
  :param startup_timeout_seconds: timeout in seconds to startup the pod.
156
156
  :param startup_check_interval_seconds: interval in seconds to check if the pod has already started
157
157
  :param get_logs: get the stdout of the base container as logs of the tasks.
158
+ :param init_container_logs: list of init containers whose logs will be published to stdout
159
+ Takes a sequence of containers, a single container name or True. If True,
160
+ all the containers logs are published.
158
161
  :param container_logs: list of containers whose logs will be published to stdout
159
162
  Takes a sequence of containers, a single container name or True. If True,
160
163
  all the containers logs are published. Works in conjunction with get_logs param.
@@ -278,6 +281,7 @@ class KubernetesPodOperator(BaseOperator):
278
281
  startup_check_interval_seconds: int = 5,
279
282
  get_logs: bool = True,
280
283
  base_container_name: str | None = None,
284
+ init_container_logs: Iterable[str] | str | Literal[True] | None = None,
281
285
  container_logs: Iterable[str] | str | Literal[True] | None = None,
282
286
  image_pull_policy: str | None = None,
283
287
  annotations: dict | None = None,
@@ -352,6 +356,7 @@ class KubernetesPodOperator(BaseOperator):
352
356
  # Fallback to the class variable BASE_CONTAINER_NAME here instead of via default argument value
353
357
  # in the init method signature, to be compatible with subclasses overloading the class variable value.
354
358
  self.base_container_name = base_container_name or self.BASE_CONTAINER_NAME
359
+ self.init_container_logs = init_container_logs
355
360
  self.container_logs = container_logs or self.base_container_name
356
361
  self.image_pull_policy = image_pull_policy
357
362
  self.node_selector = node_selector or {}
@@ -396,7 +401,9 @@ class KubernetesPodOperator(BaseOperator):
396
401
  self.remote_pod: k8s.V1Pod | None = None
397
402
  self.log_pod_spec_on_failure = log_pod_spec_on_failure
398
403
  self.on_finish_action = OnFinishAction(on_finish_action)
399
- self.is_delete_operator_pod = self.on_finish_action == OnFinishAction.DELETE_POD
404
+ # The `is_delete_operator_pod` parameter should have been removed in provider version 10.0.0.
405
+ # TODO: remove it from here and from the operator's parameters list when the next major version bumped
406
+ self._is_delete_operator_pod = self.on_finish_action == OnFinishAction.DELETE_POD
400
407
  self.termination_message_policy = termination_message_policy
401
408
  self.active_deadline_seconds = active_deadline_seconds
402
409
  self.logging_interval = logging_interval
@@ -598,6 +605,9 @@ class KubernetesPodOperator(BaseOperator):
598
605
  self.callbacks.on_pod_creation(
599
606
  pod=self.remote_pod, client=self.client, mode=ExecutionMode.SYNC
600
607
  )
608
+
609
+ self.await_init_containers_completion(pod=self.pod)
610
+
601
611
  self.await_pod_start(pod=self.pod)
602
612
  if self.callbacks:
603
613
  self.callbacks.on_pod_starting(
@@ -633,6 +643,22 @@ class KubernetesPodOperator(BaseOperator):
633
643
  if self.do_xcom_push:
634
644
  return result
635
645
 
646
+ @tenacity.retry(
647
+ wait=tenacity.wait_exponential(max=15),
648
+ retry=tenacity.retry_if_exception_type(PodCredentialsExpiredFailure),
649
+ reraise=True,
650
+ )
651
+ def await_init_containers_completion(self, pod: k8s.V1Pod):
652
+ try:
653
+ if self.init_container_logs:
654
+ self.pod_manager.fetch_requested_init_container_logs(
655
+ pod=pod,
656
+ init_containers=self.init_container_logs,
657
+ follow_logs=True,
658
+ )
659
+ except kubernetes.client.exceptions.ApiException as exc:
660
+ self._handle_api_exception(exc, pod)
661
+
636
662
  @tenacity.retry(
637
663
  wait=tenacity.wait_exponential(max=15),
638
664
  retry=tenacity.retry_if_exception_type(PodCredentialsExpiredFailure),
@@ -651,16 +677,21 @@ class KubernetesPodOperator(BaseOperator):
651
677
  ):
652
678
  self.pod_manager.await_container_completion(pod=pod, container_name=self.base_container_name)
653
679
  except kubernetes.client.exceptions.ApiException as exc:
654
- if exc.status and str(exc.status) == "401":
655
- self.log.warning(
656
- "Failed to check container status due to permission error. Refreshing credentials and retrying."
657
- )
658
- self._refresh_cached_properties()
659
- self.pod_manager.read_pod(
660
- pod=pod
661
- ) # attempt using refreshed credentials, raises if still invalid
662
- raise PodCredentialsExpiredFailure("Kubernetes credentials expired, retrying after refresh.")
663
- raise exc
680
+ self._handle_api_exception(exc, pod)
681
+
682
+ def _handle_api_exception(
683
+ self,
684
+ exc: kubernetes.client.exceptions.ApiException,
685
+ pod: k8s.V1Pod,
686
+ ):
687
+ if exc.status and str(exc.status) == "401":
688
+ self.log.warning(
689
+ "Failed to check container status due to permission error. Refreshing credentials and retrying."
690
+ )
691
+ self._refresh_cached_properties()
692
+ self.pod_manager.read_pod(pod=pod) # attempt using refreshed credentials, raises if still invalid
693
+ raise PodCredentialsExpiredFailure("Kubernetes credentials expired, retrying after refresh.")
694
+ raise exc
664
695
 
665
696
  def _refresh_cached_properties(self):
666
697
  del self.hook
@@ -19,6 +19,7 @@
19
19
  from __future__ import annotations
20
20
 
21
21
  import enum
22
+ import itertools
22
23
  import json
23
24
  import math
24
25
  import time
@@ -117,7 +118,13 @@ class PodOperatorHookProtocol(Protocol):
117
118
 
118
119
  def get_container_status(pod: V1Pod, container_name: str) -> V1ContainerStatus | None:
119
120
  """Retrieve container status."""
120
- container_statuses = pod.status.container_statuses if pod and pod.status else None
121
+ if pod and pod.status:
122
+ container_statuses = itertools.chain(
123
+ pod.status.container_statuses, pod.status.init_container_statuses
124
+ )
125
+ else:
126
+ container_statuses = None
127
+
121
128
  if container_statuses:
122
129
  # In general the variable container_statuses can store multiple items matching different containers.
123
130
  # The following generator expression yields all items that have name equal to the container_name.
@@ -166,6 +173,19 @@ def container_is_succeeded(pod: V1Pod, container_name: str) -> bool:
166
173
  return container_status.state.terminated.exit_code == 0
167
174
 
168
175
 
176
+ def container_is_wait(pod: V1Pod, container_name: str) -> bool:
177
+ """
178
+ Examine V1Pod ``pod`` to determine whether ``container_name`` is waiting.
179
+
180
+ If that container is present and waiting, returns True. Returns False otherwise.
181
+ """
182
+ container_status = get_container_status(pod, container_name)
183
+ if not container_status:
184
+ return False
185
+
186
+ return container_status.state.waiting is not None
187
+
188
+
169
189
  def container_is_terminated(pod: V1Pod, container_name: str) -> bool:
170
190
  """
171
191
  Examine V1Pod ``pod`` to determine whether ``container_name`` is terminated.
@@ -451,7 +471,10 @@ class PodManager(LoggingMixin):
451
471
  line=line, client=self._client, mode=ExecutionMode.SYNC
452
472
  )
453
473
  if message_to_log is not None:
454
- self.log.info("[%s] %s", container_name, message_to_log)
474
+ if is_log_group_marker(message_to_log):
475
+ print(message_to_log)
476
+ else:
477
+ self.log.info("[%s] %s", container_name, message_to_log)
455
478
  last_captured_timestamp = message_timestamp
456
479
  message_to_log = message
457
480
  message_timestamp = line_timestamp
@@ -467,7 +490,10 @@ class PodManager(LoggingMixin):
467
490
  line=line, client=self._client, mode=ExecutionMode.SYNC
468
491
  )
469
492
  if message_to_log is not None:
470
- self.log.info("[%s] %s", container_name, message_to_log)
493
+ if is_log_group_marker(message_to_log):
494
+ print(message_to_log)
495
+ else:
496
+ self.log.info("[%s] %s", container_name, message_to_log)
471
497
  last_captured_timestamp = message_timestamp
472
498
  except TimeoutError as e:
473
499
  # in case of timeout, increment return time by 2 seconds to avoid
@@ -503,7 +529,7 @@ class PodManager(LoggingMixin):
503
529
  time.sleep(1)
504
530
 
505
531
  def _reconcile_requested_log_containers(
506
- self, requested: Iterable[str] | str | bool, actual: list[str], pod_name
532
+ self, requested: Iterable[str] | str | bool | None, actual: list[str], pod_name
507
533
  ) -> list[str]:
508
534
  """Return actual containers based on requested."""
509
535
  containers_to_log = []
@@ -546,6 +572,31 @@ class PodManager(LoggingMixin):
546
572
  self.log.error("Could not retrieve containers for the pod: %s", pod_name)
547
573
  return containers_to_log
548
574
 
575
+ def fetch_requested_init_container_logs(
576
+ self, pod: V1Pod, init_containers: Iterable[str] | str | Literal[True] | None, follow_logs=False
577
+ ) -> list[PodLoggingStatus]:
578
+ """
579
+ Follow the logs of containers in the specified pod and publish it to airflow logging.
580
+
581
+ Returns when all the containers exit.
582
+
583
+ :meta private:
584
+ """
585
+ pod_logging_statuses = []
586
+ all_containers = self.get_init_container_names(pod)
587
+ containers_to_log = self._reconcile_requested_log_containers(
588
+ requested=init_containers,
589
+ actual=all_containers,
590
+ pod_name=pod.metadata.name,
591
+ )
592
+ # sort by spec.initContainers because containers runs sequentially
593
+ containers_to_log = sorted(containers_to_log, key=lambda cn: all_containers.index(cn))
594
+ for c in containers_to_log:
595
+ self._await_init_container_start(pod=pod, container_name=c)
596
+ status = self.fetch_container_logs(pod=pod, container_name=c, follow=follow_logs)
597
+ pod_logging_statuses.append(status)
598
+ return pod_logging_statuses
599
+
549
600
  def fetch_requested_container_logs(
550
601
  self, pod: V1Pod, containers: Iterable[str] | str | Literal[True], follow_logs=False
551
602
  ) -> list[PodLoggingStatus]:
@@ -673,9 +724,22 @@ class PodManager(LoggingMixin):
673
724
  post_termination_timeout=post_termination_timeout,
674
725
  )
675
726
 
727
+ @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
728
+ def get_init_container_names(self, pod: V1Pod) -> list[str]:
729
+ """
730
+ Return container names from the POD except for the airflow-xcom-sidecar container.
731
+
732
+ :meta private:
733
+ """
734
+ return [container_spec.name for container_spec in pod.spec.init_containers]
735
+
676
736
  @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
677
737
  def get_container_names(self, pod: V1Pod) -> list[str]:
678
- """Return container names from the POD except for the airflow-xcom-sidecar container."""
738
+ """
739
+ Return container names from the POD except for the airflow-xcom-sidecar container.
740
+
741
+ :meta private:
742
+ """
679
743
  pod_info = self.read_pod(pod)
680
744
  return [
681
745
  container_spec.name
@@ -791,7 +855,7 @@ class PodManager(LoggingMixin):
791
855
  _preload_content=False,
792
856
  )
793
857
  ) as resp:
794
- self._exec_pod_command(resp, "kill -2 1")
858
+ self._exec_pod_command(resp, "kill -2 $(pgrep -u $(whoami) -f trap)")
795
859
 
796
860
  def _exec_pod_command(self, resp, command: str) -> str | None:
797
861
  res = ""
@@ -813,6 +877,20 @@ class PodManager(LoggingMixin):
813
877
  return res
814
878
  return None
815
879
 
880
+ def _await_init_container_start(self, pod: V1Pod, container_name: str):
881
+ while True:
882
+ remote_pod = self.read_pod(pod)
883
+
884
+ if (
885
+ remote_pod.status is not None
886
+ and remote_pod.status.phase != PodPhase.PENDING
887
+ and get_container_status(remote_pod, container_name) is not None
888
+ and not container_is_wait(remote_pod, container_name)
889
+ ):
890
+ return
891
+
892
+ time.sleep(1)
893
+
816
894
 
817
895
  class OnFinishAction(str, enum.Enum):
818
896
  """Action to take when the pod finishes."""
@@ -820,3 +898,8 @@ class OnFinishAction(str, enum.Enum):
820
898
  KEEP_POD = "keep_pod"
821
899
  DELETE_POD = "delete_pod"
822
900
  DELETE_SUCCEEDED_POD = "delete_succeeded_pod"
901
+
902
+
903
+ def is_log_group_marker(line: str) -> bool:
904
+ """Check if the line is a log group marker like `::group::` or `::endgroup::`."""
905
+ return line.startswith("::group::") or line.startswith("::endgroup::")
@@ -0,0 +1,36 @@
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
+ # NOTE! THIS FILE IS COPIED MANUALLY IN OTHER PROVIDERS DELIBERATELY TO AVOID ADDING UNNECESSARY
19
+ # DEPENDENCIES BETWEEN PROVIDERS. IF YOU WANT TO ADD CONDITIONAL CODE IN YOUR PROVIDER THAT DEPENDS
20
+ # ON AIRFLOW VERSION, PLEASE COPY THIS FILE TO THE ROOT PACKAGE OF YOUR PROVIDER AND IMPORT
21
+ # THOSE CONSTANTS FROM IT RATHER THAN IMPORTING THEM FROM ANOTHER PROVIDER OR TEST CODE
22
+ #
23
+ from __future__ import annotations
24
+
25
+
26
+ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
27
+ from packaging.version import Version
28
+
29
+ from airflow import __version__
30
+
31
+ airflow_version = Version(__version__)
32
+ return airflow_version.major, airflow_version.minor, airflow_version.micro
33
+
34
+
35
+ AIRFLOW_V_2_10_PLUS = get_base_airflow_version_tuple() >= (2, 10, 0)
36
+ AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: apache-airflow-providers-cncf-kubernetes
3
- Version: 10.0.1rc1
3
+ Version: 10.1.0
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>
@@ -21,18 +21,18 @@ Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Topic :: System :: Monitoring
23
23
  Requires-Dist: aiofiles>=23.2.0
24
- Requires-Dist: apache-airflow>=2.8.0rc0
24
+ Requires-Dist: apache-airflow>=2.9.0
25
25
  Requires-Dist: asgiref>=3.5.2
26
26
  Requires-Dist: cryptography>=41.0.0
27
27
  Requires-Dist: google-re2>=1.0
28
28
  Requires-Dist: kubernetes>=29.0.0,<=31.0.0
29
29
  Requires-Dist: kubernetes_asyncio>=29.0.0,<=31.0.0
30
30
  Project-URL: Bug Tracker, https://github.com/apache/airflow/issues
31
- Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.0.1/changelog.html
32
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.0.1
31
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.1.0/changelog.html
32
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.1.0
33
33
  Project-URL: Slack Chat, https://s.apache.org/airflow-slack
34
34
  Project-URL: Source Code, https://github.com/apache/airflow
35
- Project-URL: Twitter, https://twitter.com/ApacheAirflow
35
+ Project-URL: Twitter, https://x.com/ApacheAirflow
36
36
  Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
37
37
 
38
38
 
@@ -79,7 +79,7 @@ Project-URL: YouTube, https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/
79
79
 
80
80
  Package ``apache-airflow-providers-cncf-kubernetes``
81
81
 
82
- Release: ``10.0.1.rc1``
82
+ Release: ``10.1.0``
83
83
 
84
84
 
85
85
  `Kubernetes <https://kubernetes.io/>`__
@@ -92,7 +92,7 @@ This is a provider package for ``cncf.kubernetes`` provider. All classes for thi
92
92
  are in ``airflow.providers.cncf.kubernetes`` python package.
93
93
 
94
94
  You can find package information and changelog for the provider
95
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.0.1/>`_.
95
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.1.0/>`_.
96
96
 
97
97
  Installation
98
98
  ------------
@@ -110,7 +110,7 @@ Requirements
110
110
  PIP package Version required
111
111
  ====================== =====================
112
112
  ``aiofiles`` ``>=23.2.0``
113
- ``apache-airflow`` ``>=2.8.0``
113
+ ``apache-airflow`` ``>=2.9.0``
114
114
  ``asgiref`` ``>=3.5.2``
115
115
  ``cryptography`` ``>=41.0.0``
116
116
  ``kubernetes`` ``>=29.0.0,<=31.0.0``
@@ -119,4 +119,4 @@ PIP package Version required
119
119
  ====================== =====================
120
120
 
121
121
  The changelog for the provider package can be found in the
122
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.0.1/changelog.html>`_.
122
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/10.1.0/changelog.html>`_.
@@ -1,7 +1,7 @@
1
1
  airflow/providers/cncf/kubernetes/LICENSE,sha256=FFb4jd2AXnOOf7XLP04pQW6jbdhG49TxlGY6fFpCV1Y,13609
2
- airflow/providers/cncf/kubernetes/__init__.py,sha256=WSfPJeVCftXHYzTgnGbSLoI5aWLsAVG5tXsBoOY9wSg,1503
2
+ airflow/providers/cncf/kubernetes/__init__.py,sha256=JgM645XlySCq1ye8U2Ey6y4rVnjnol6jpa3JvSyh5ag,1503
3
3
  airflow/providers/cncf/kubernetes/callbacks.py,sha256=SK_gKvGWuU-nxHfsqsYMlNQ8HZbHfpvyItOqieel2lc,4162
4
- airflow/providers/cncf/kubernetes/get_provider_info.py,sha256=EtOtYPHX8N8LfiWkdJpTO2P4QdMSZDO2luESnyiWMUk,18030
4
+ airflow/providers/cncf/kubernetes/get_provider_info.py,sha256=W7WIaR7A-NHGZzM3IKgYaNS_iI4KNFMvj1_bIPGe4gY,18125
5
5
  airflow/providers/cncf/kubernetes/k8s_model.py,sha256=xmdFhX29DjegoZ-cq8-KDL9soVYXf4OpU6fAGr3cPTU,2101
6
6
  airflow/providers/cncf/kubernetes/kube_client.py,sha256=yflZxLousXA9d7t67KrEy55qzb1cUhEyy6yCPkEem28,5329
7
7
  airflow/providers/cncf/kubernetes/kube_config.py,sha256=FAmhZZ_Z2JtoVzL6wENSjcwrlwAenHttTX_Ild9aEms,5225
@@ -12,25 +12,27 @@ airflow/providers/cncf/kubernetes/python_kubernetes_script.jinja2,sha256=I0EHRGw
12
12
  airflow/providers/cncf/kubernetes/python_kubernetes_script.py,sha256=KnTlZSWCZhwvj89fSc2kgIRTaI4iLNKPquHc2wXnluo,3460
13
13
  airflow/providers/cncf/kubernetes/secret.py,sha256=wj-T9gouqau_X14slAstGmnSxqXJQzdLwUdURzHna0I,5209
14
14
  airflow/providers/cncf/kubernetes/template_rendering.py,sha256=AZesc6MDfpFHoN1PvQpVwM-PzwbKYC1IsdChYvTubZ8,2965
15
+ airflow/providers/cncf/kubernetes/version_compat.py,sha256=aHg90_DtgoSnQvILFICexMyNlHlALBdaeWqkX3dFDug,1605
15
16
  airflow/providers/cncf/kubernetes/backcompat/__init__.py,sha256=KXF76f3v1jIFUBNz8kwxVMvm7i4mNo35LbIG9IijBNc,1299
16
17
  airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py,sha256=2jIehZsixt4ZGwTkj7kC8Uq3w8XwFAEzZV4g8SyIUZI,4340
17
18
  airflow/providers/cncf/kubernetes/cli/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
18
- airflow/providers/cncf/kubernetes/cli/kubernetes_command.py,sha256=wkI2gW2VfS_Ru1zX-Tr5ymLRP20ghAjdkD8RBtbUvt0,7384
19
+ airflow/providers/cncf/kubernetes/cli/kubernetes_command.py,sha256=0n1Tj7w6jWfeRtf_VTn1dA5XN1MlOziXFDBA51nHquM,7252
19
20
  airflow/providers/cncf/kubernetes/decorators/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
20
21
  airflow/providers/cncf/kubernetes/decorators/kubernetes.py,sha256=-YgRnFGyS-tdoAqwKP_Jj5mh_YJlKpk5TvwYnphvp4o,5814
21
22
  airflow/providers/cncf/kubernetes/executors/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
22
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py,sha256=2WPy9JCKVv9KinR0PZc1Sa8EXhjQMzArIxCms-H_EXE,35065
23
+ airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py,sha256=ZUOmeohZkIxF1K77EoKQx275YC5Yf8wu63ijzNGsMsU,35436
23
24
  airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py,sha256=L8_8HOHd_4O8WW6xT2tp49-yOj0EMKCYK5YqMOOx_bI,1973
24
25
  airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py,sha256=lNdHp7Q5aTOxHBew-_H3cl9F4NLgmElNidhWXO3UKGw,24302
25
26
  airflow/providers/cncf/kubernetes/executors/local_kubernetes_executor.py,sha256=kx6pHAfSnKiDgT6iDOS4fJRq49DodFnyxUyd-iJsuI8,11512
26
27
  airflow/providers/cncf/kubernetes/hooks/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
27
- airflow/providers/cncf/kubernetes/hooks/kubernetes.py,sha256=SIdFhIA1Et4asGMNbX96ZaRLPlAxDhM3l5liD_eJuH4,33659
28
+ airflow/providers/cncf/kubernetes/hooks/kubernetes.py,sha256=eH9yTOf9vNFuBCspFtVQjfLDX2v-UwaU5xlyq8GlHR0,36306
28
29
  airflow/providers/cncf/kubernetes/kubernetes_executor_templates/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
29
30
  airflow/providers/cncf/kubernetes/kubernetes_executor_templates/basic_template.yaml,sha256=yzJmXN4ZyB4aDwI_GIugpL9-f1YMVy__X-LQSbeU95A,2567
30
31
  airflow/providers/cncf/kubernetes/operators/__init__.py,sha256=mlJxuZLkd5x-iq2SBwD3mvRQpt3YR7wjz_nceyF1IaI,787
31
32
  airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py,sha256=ZEXw_PqGItO47AEgGKqAqwFHFo-gb9-7jgEMRJgOfNU,15311
32
33
  airflow/providers/cncf/kubernetes/operators/job.py,sha256=mWstFYBzET1Pm7RYJyBOSZgjG_tb1s__DmWjb2An9jY,23738
33
- airflow/providers/cncf/kubernetes/operators/pod.py,sha256=X_6OLr0BkaffKSTUGhGpC7yvuxvFIpaXZLGW7A2-I7k,53495
34
+ airflow/providers/cncf/kubernetes/operators/kueue.py,sha256=KE-y9_cnG3VgjSY4vxfvA2BEd5GYHn5y7xzFVa3UXa4,4538
35
+ airflow/providers/cncf/kubernetes/operators/pod.py,sha256=UxvNUYUGY8RtZ4cNKG-CZHxIbpqnzFpbJwY6HAdjI_Y,54871
34
36
  airflow/providers/cncf/kubernetes/operators/resource.py,sha256=4nS-eiVEGlotp-gCkHlwRuenj3pnKhZ4khh9s2cjZms,7597
35
37
  airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py,sha256=QQbJ9MwIHkTCnHwu2BLkJdTeYjhmla-7zY0zGwXhTpk,13822
36
38
  airflow/providers/cncf/kubernetes/pod_template_file_examples/__init__.py,sha256=9hdXHABrVpkbpjZgUft39kOFL2xSGeG4GEua0Hmelus,785
@@ -49,9 +51,9 @@ airflow/providers/cncf/kubernetes/triggers/pod.py,sha256=7vLj9SvOOwh9p5XSZIr7mj4
49
51
  airflow/providers/cncf/kubernetes/utils/__init__.py,sha256=ClZN0VPjWySdVwS_ktH7rrgL9VLAcs3OSJSB9s3zaYw,863
50
52
  airflow/providers/cncf/kubernetes/utils/delete_from.py,sha256=poObZSoEJwQyaYWilEURs8f4CDY2sn_pfwS31Lf579A,5195
51
53
  airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py,sha256=DLypjkD_3YDixRTcsxEjgvHZNbbG9qamlz05eBqaWzU,1955
52
- airflow/providers/cncf/kubernetes/utils/pod_manager.py,sha256=LmXsFO7CYP8YTZla4poN30UtayLyGdVkqicSPZh6hIA,33286
54
+ airflow/providers/cncf/kubernetes/utils/pod_manager.py,sha256=-aW_4PucvfUk_VjH56ZjbxEn6BFQS1j_aDnlrKEprkc,36451
53
55
  airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py,sha256=k6bdmVJ21OrAwGmWwledRrAmaty9ZrmbuM-IbaI4mqo,2519
54
- apache_airflow_providers_cncf_kubernetes-10.0.1rc1.dist-info/entry_points.txt,sha256=ByD3QJJyP9CfmTYtpNI1953akD38RUDgpGXLaq9vpOw,111
55
- apache_airflow_providers_cncf_kubernetes-10.0.1rc1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
56
- apache_airflow_providers_cncf_kubernetes-10.0.1rc1.dist-info/METADATA,sha256=Yk223W1_xbDZgue95jFL53eGMjlexGU_PFnFkJi6-eU,5222
57
- apache_airflow_providers_cncf_kubernetes-10.0.1rc1.dist-info/RECORD,,
56
+ apache_airflow_providers_cncf_kubernetes-10.1.0.dist-info/entry_points.txt,sha256=ByD3QJJyP9CfmTYtpNI1953akD38RUDgpGXLaq9vpOw,111
57
+ apache_airflow_providers_cncf_kubernetes-10.1.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
58
+ apache_airflow_providers_cncf_kubernetes-10.1.0.dist-info/METADATA,sha256=exAz14WT2PAT9A-wXbAqI0rr8v4Si49-gxXcacO-OAU,5206
59
+ apache_airflow_providers_cncf_kubernetes-10.1.0.dist-info/RECORD,,