apache-airflow-providers-cncf-kubernetes 7.11.0rc1__tar.gz → 7.12.0__tar.gz

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.

Files changed (49) hide show
  1. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/PKG-INFO +7 -7
  2. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/README.rst +3 -3
  3. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/__init__.py +1 -1
  4. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +36 -11
  5. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py +4 -3
  6. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +8 -1
  7. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/get_provider_info.py +2 -1
  8. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/hooks/kubernetes.py +4 -4
  9. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +4 -6
  10. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/operators/pod.py +19 -4
  11. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_generator.py +8 -4
  12. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/triggers/pod.py +17 -13
  13. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/utils/pod_manager.py +35 -13
  14. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/pyproject.toml +4 -4
  15. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/LICENSE +0 -0
  16. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/backcompat/__init__.py +0 -0
  17. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +0 -0
  18. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/decorators/__init__.py +0 -0
  19. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/decorators/kubernetes.py +0 -0
  20. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/executors/__init__.py +0 -0
  21. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/executors/local_kubernetes_executor.py +0 -0
  22. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/hooks/__init__.py +0 -0
  23. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/k8s_model.py +0 -0
  24. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/kube_client.py +0 -0
  25. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/kube_config.py +0 -0
  26. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/kubernetes_executor_templates/__init__.py +0 -0
  27. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/kubernetes_executor_templates/basic_template.yaml +0 -0
  28. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/operators/__init__.py +0 -0
  29. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +0 -0
  30. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/operators/resource.py +0 -0
  31. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +0 -0
  32. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_generator_deprecated.py +0 -0
  33. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_launcher_deprecated.py +0 -0
  34. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_template_file_examples/__init__.py +0 -0
  35. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_image_template.yaml +0 -0
  36. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_volume_template.yaml +0 -0
  37. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/pod_template_file_examples/git_sync_template.yaml +0 -0
  38. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/python_kubernetes_script.jinja2 +0 -0
  39. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/python_kubernetes_script.py +0 -0
  40. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/secret.py +0 -0
  41. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/sensors/__init__.py +0 -0
  42. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/sensors/spark_kubernetes.py +0 -0
  43. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/template_rendering.py +0 -0
  44. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/triggers/__init__.py +0 -0
  45. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/triggers/kubernetes_pod.py +0 -0
  46. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/utils/__init__.py +0 -0
  47. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/utils/delete_from.py +0 -0
  48. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py +0 -0
  49. {apache_airflow_providers_cncf_kubernetes-7.11.0rc1 → apache_airflow_providers_cncf_kubernetes-7.12.0}/airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apache-airflow-providers-cncf-kubernetes
3
- Version: 7.11.0rc1
3
+ Version: 7.12.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,15 +21,15 @@ Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Topic :: System :: Monitoring
23
23
  Requires-Dist: aiofiles>=23.2.0
24
- Requires-Dist: apache-airflow>=2.6.0.dev0
24
+ Requires-Dist: apache-airflow>=2.6.0
25
25
  Requires-Dist: asgiref>=3.5.2
26
26
  Requires-Dist: cryptography>=2.0.0
27
27
  Requires-Dist: google-re2>=1.0
28
28
  Requires-Dist: kubernetes>=21.7.0,<24
29
29
  Requires-Dist: kubernetes_asyncio>=18.20.1,<25
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/7.11.0/changelog.html
32
- Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.11.0
31
+ Project-URL: Changelog, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0/changelog.html
32
+ Project-URL: Documentation, https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.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
35
  Project-URL: Twitter, https://twitter.com/ApacheAirflow
@@ -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: ``7.11.0.rc1``
82
+ Release: ``7.12.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/7.11.0/>`_.
95
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0/>`_.
96
96
 
97
97
  Installation
98
98
  ------------
@@ -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/7.11.0/changelog.html>`_.
122
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0/changelog.html>`_.
@@ -42,7 +42,7 @@
42
42
 
43
43
  Package ``apache-airflow-providers-cncf-kubernetes``
44
44
 
45
- Release: ``7.11.0.rc1``
45
+ Release: ``7.12.0``
46
46
 
47
47
 
48
48
  `Kubernetes <https://kubernetes.io/>`__
@@ -55,7 +55,7 @@ This is a provider package for ``cncf.kubernetes`` provider. All classes for thi
55
55
  are in ``airflow.providers.cncf.kubernetes`` python package.
56
56
 
57
57
  You can find package information and changelog for the provider
58
- in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.11.0/>`_.
58
+ in the `documentation <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0/>`_.
59
59
 
60
60
  Installation
61
61
  ------------
@@ -82,4 +82,4 @@ PIP package Version required
82
82
  ====================== ==================
83
83
 
84
84
  The changelog for the provider package can be found in the
85
- `changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.11.0/changelog.html>`_.
85
+ `changelog <https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0/changelog.html>`_.
@@ -27,7 +27,7 @@ import packaging.version
27
27
 
28
28
  __all__ = ["__version__"]
29
29
 
30
- __version__ = "7.11.0"
30
+ __version__ = "7.12.0"
31
31
 
32
32
  try:
33
33
  from airflow import __version__ as airflow_version
@@ -34,6 +34,7 @@ from datetime import datetime
34
34
  from queue import Empty, Queue
35
35
  from typing import TYPE_CHECKING, Any, Sequence
36
36
 
37
+ from kubernetes.dynamic import DynamicClient
37
38
  from sqlalchemy import select, update
38
39
 
39
40
  from airflow.providers.cncf.kubernetes.pod_generator import PodMutationHookException, PodReconciliationError
@@ -74,7 +75,10 @@ except ImportError:
74
75
  raise
75
76
  from airflow.configuration import conf
76
77
  from airflow.executors.base_executor import BaseExecutor
77
- from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import POD_EXECUTOR_DONE_KEY
78
+ from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import (
79
+ ADOPTED,
80
+ POD_EXECUTOR_DONE_KEY,
81
+ )
78
82
  from airflow.providers.cncf.kubernetes.kube_config import KubeConfig
79
83
  from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import annotations_to_key
80
84
  from airflow.utils.event_scheduler import EventScheduler
@@ -160,19 +164,31 @@ class KubernetesExecutor(BaseExecutor):
160
164
  super().__init__(parallelism=self.kube_config.parallelism)
161
165
 
162
166
  def _list_pods(self, query_kwargs):
167
+ query_kwargs["header_params"] = {
168
+ "Accept": "application/json;as=PartialObjectMetadataList;v=v1;g=meta.k8s.io"
169
+ }
170
+ dynamic_client = DynamicClient(self.kube_client.api_client)
171
+ pod_resource = dynamic_client.resources.get(api_version="v1", kind="Pod")
163
172
  if self.kube_config.multi_namespace_mode:
164
173
  if self.kube_config.multi_namespace_mode_namespace_list:
165
- pods = []
166
- for namespace in self.kube_config.multi_namespace_mode_namespace_list:
167
- pods.extend(
168
- self.kube_client.list_namespaced_pod(namespace=namespace, **query_kwargs).items
169
- )
174
+ namespaces = self.kube_config.multi_namespace_mode_namespace_list
170
175
  else:
171
- pods = self.kube_client.list_pod_for_all_namespaces(**query_kwargs).items
176
+ namespaces = [None]
172
177
  else:
173
- pods = self.kube_client.list_namespaced_pod(
174
- namespace=self.kube_config.kube_namespace, **query_kwargs
175
- ).items
178
+ namespaces = [self.kube_config.kube_namespace]
179
+
180
+ pods = []
181
+ for namespace in namespaces:
182
+ # Dynamic Client list pods is throwing TypeError when there are no matching pods to return
183
+ # This bug was fixed in MR https://github.com/kubernetes-client/python/pull/2155
184
+ # TODO: Remove the try-except clause once we upgrade the K8 Python client version which
185
+ # includes the above MR
186
+ try:
187
+ pods.extend(
188
+ dynamic_client.get(resource=pod_resource, namespace=namespace, **query_kwargs).items
189
+ )
190
+ except TypeError:
191
+ continue
176
192
 
177
193
  return pods
178
194
 
@@ -450,7 +466,7 @@ class KubernetesExecutor(BaseExecutor):
450
466
  def _change_state(
451
467
  self,
452
468
  key: TaskInstanceKey,
453
- state: TaskInstanceState | None,
469
+ state: TaskInstanceState | str | None,
454
470
  pod_name: str,
455
471
  namespace: str,
456
472
  session: Session = NEW_SESSION,
@@ -458,6 +474,15 @@ class KubernetesExecutor(BaseExecutor):
458
474
  if TYPE_CHECKING:
459
475
  assert self.kube_scheduler
460
476
 
477
+ if state == ADOPTED:
478
+ # When the task pod is adopted by another executor,
479
+ # then remove the task from the current executor running queue.
480
+ try:
481
+ self.running.remove(key)
482
+ except KeyError:
483
+ self.log.debug("TI key not in running: %s", key)
484
+ return
485
+
461
486
  if state == TaskInstanceState.RUNNING:
462
487
  self.event_buffer[key] = state, None
463
488
  return
@@ -16,8 +16,9 @@
16
16
  # under the License.
17
17
  from __future__ import annotations
18
18
 
19
- from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
19
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
20
20
 
21
+ ADOPTED = "adopted"
21
22
  if TYPE_CHECKING:
22
23
  from airflow.executors.base_executor import CommandType
23
24
  from airflow.models.taskinstance import TaskInstanceKey
@@ -27,10 +28,10 @@ if TYPE_CHECKING:
27
28
  KubernetesJobType = Tuple[TaskInstanceKey, CommandType, Any, Optional[str]]
28
29
 
29
30
  # key, pod state, pod_name, namespace, resource_version
30
- KubernetesResultsType = Tuple[TaskInstanceKey, Optional[TaskInstanceState], str, str, str]
31
+ KubernetesResultsType = Tuple[TaskInstanceKey, Optional[Union[TaskInstanceState, str]], str, str, str]
31
32
 
32
33
  # pod_name, namespace, pod state, annotations, resource_version
33
- KubernetesWatchType = Tuple[str, str, Optional[TaskInstanceState], Dict[str, str], str]
34
+ KubernetesWatchType = Tuple[str, str, Optional[Union[TaskInstanceState, str]], Dict[str, str], str]
34
35
 
35
36
  ALL_NAMESPACES = "ALL_NAMESPACES"
36
37
  POD_EXECUTOR_DONE_KEY = "airflow_executor_done"
@@ -40,6 +40,7 @@ from airflow.utils.state import TaskInstanceState
40
40
 
41
41
  try:
42
42
  from airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types import (
43
+ ADOPTED,
43
44
  ALL_NAMESPACES,
44
45
  POD_EXECUTOR_DONE_KEY,
45
46
  )
@@ -220,7 +221,13 @@ class KubernetesJobWatcher(multiprocessing.Process, LoggingMixin):
220
221
  pod = event["object"]
221
222
  annotations_string = annotations_for_logging_task_metadata(annotations)
222
223
  """Process status response."""
223
- if status == "Pending":
224
+ if event["type"] == "DELETED" and not pod.metadata.deletion_timestamp:
225
+ # This will happen only when the task pods are adopted by another executor.
226
+ # So, there is no change in the pod state.
227
+ # However, need to free the executor slot from the current executor.
228
+ self.log.info("Event: pod %s adopted, annotations: %s", pod_name, annotations_string)
229
+ self.watcher_queue.put((pod_name, namespace, ADOPTED, annotations, resource_version))
230
+ elif status == "Pending":
224
231
  # deletion_timestamp is set by kube server when a graceful deletion is requested.
225
232
  # since kube server have received request to delete pod set TI state failed
226
233
  if event["type"] == "DELETED" and pod.metadata.deletion_timestamp:
@@ -28,8 +28,9 @@ def get_provider_info():
28
28
  "name": "Kubernetes",
29
29
  "description": "`Kubernetes <https://kubernetes.io/>`__\n",
30
30
  "suspended": False,
31
- "source-date-epoch": 1701983367,
31
+ "source-date-epoch": 1703288121,
32
32
  "versions": [
33
+ "7.12.0",
33
34
  "7.11.0",
34
35
  "7.10.0",
35
36
  "7.9.0",
@@ -86,8 +86,8 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
86
86
 
87
87
  DEFAULT_NAMESPACE = "default"
88
88
 
89
- @staticmethod
90
- def get_connection_form_widgets() -> dict[str, Any]:
89
+ @classmethod
90
+ def get_connection_form_widgets(cls) -> dict[str, Any]:
91
91
  """Return connection widgets to add to connection form."""
92
92
  from flask_appbuilder.fieldwidgets import BS3TextFieldWidget
93
93
  from flask_babel import lazy_gettext
@@ -111,8 +111,8 @@ class KubernetesHook(BaseHook, PodOperatorHookProtocol):
111
111
  ),
112
112
  }
113
113
 
114
- @staticmethod
115
- def get_ui_field_behaviour() -> dict[str, Any]:
114
+ @classmethod
115
+ def get_ui_field_behaviour(cls) -> dict[str, Any]:
116
116
  """Return custom field behaviour."""
117
117
  return {
118
118
  "hidden_fields": ["host", "schema", "login", "password", "port", "extra"],
@@ -34,6 +34,8 @@ log = logging.getLogger(__name__)
34
34
 
35
35
  alphanum_lower = string.ascii_lowercase + string.digits
36
36
 
37
+ POD_NAME_MAX_LENGTH = 63 # Matches Linux kernel's HOST_NAME_MAX default value minus 1.
38
+
37
39
 
38
40
  def rand_str(num):
39
41
  """Generate random lowercase alphanumeric string of length num.
@@ -43,7 +45,7 @@ def rand_str(num):
43
45
  return "".join(secrets.choice(alphanum_lower) for _ in range(num))
44
46
 
45
47
 
46
- def add_pod_suffix(*, pod_name: str, rand_len: int = 8, max_len: int = 80) -> str:
48
+ def add_pod_suffix(*, pod_name: str, rand_len: int = 8, max_len: int = POD_NAME_MAX_LENGTH) -> str:
47
49
  """Add random string to pod name while staying under max length.
48
50
 
49
51
  :param pod_name: name of the pod
@@ -59,16 +61,12 @@ def create_pod_id(
59
61
  dag_id: str | None = None,
60
62
  task_id: str | None = None,
61
63
  *,
62
- max_length: int = 80,
64
+ max_length: int = POD_NAME_MAX_LENGTH,
63
65
  unique: bool = True,
64
66
  ) -> str:
65
67
  """
66
68
  Generate unique pod ID given a dag_id and / or task_id.
67
69
 
68
- The default of 80 for max length is somewhat arbitrary, mainly a balance between
69
- content and not overwhelming terminal windows of reasonable width. The true
70
- upper limit is 253, and this is enforced in construct_pod.
71
-
72
70
  :param dag_id: DAG ID
73
71
  :param task_id: Task ID
74
72
  :param max_length: max number of characters
@@ -51,6 +51,7 @@ from airflow.providers.cncf.kubernetes.backcompat.backwards_compat_converters im
51
51
  convert_volume_mount,
52
52
  )
53
53
  from airflow.providers.cncf.kubernetes.hooks.kubernetes import KubernetesHook
54
+ from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import POD_NAME_MAX_LENGTH
54
55
  from airflow.providers.cncf.kubernetes.pod_generator import PodGenerator
55
56
  from airflow.providers.cncf.kubernetes.triggers.pod import KubernetesPodTrigger
56
57
  from airflow.providers.cncf.kubernetes.utils import xcom_sidecar # type: ignore[attr-defined]
@@ -92,7 +93,7 @@ def _rand_str(num):
92
93
  return "".join(secrets.choice(alphanum_lower) for _ in range(num))
93
94
 
94
95
 
95
- def _add_pod_suffix(*, pod_name, rand_len=8, max_len=253):
96
+ def _add_pod_suffix(*, pod_name, rand_len=8, max_len=POD_NAME_MAX_LENGTH):
96
97
  """Add random string to pod name while staying under max len.
97
98
 
98
99
  TODO: when min airflow version >= 2.5, delete this function and import from kubernetes_helper_functions.
@@ -107,7 +108,7 @@ def _create_pod_id(
107
108
  dag_id: str | None = None,
108
109
  task_id: str | None = None,
109
110
  *,
110
- max_length: int = 80,
111
+ max_length: int = POD_NAME_MAX_LENGTH,
111
112
  unique: bool = True,
112
113
  ) -> str:
113
114
  """
@@ -218,6 +219,7 @@ class KubernetesPodOperator(BaseOperator):
218
219
  /airflow/xcom/return.json in the container will also be pushed to an
219
220
  XCom when the container completes.
220
221
  :param pod_template_file: path to pod template file (templated)
222
+ :param pod_template_dict: pod template dictionary (templated)
221
223
  :param priority_class_name: priority class name for the launched Pod
222
224
  :param pod_runtime_info_envs: (Optional) A list of environment variables,
223
225
  to be set in the container.
@@ -267,6 +269,7 @@ class KubernetesPodOperator(BaseOperator):
267
269
  "labels",
268
270
  "config_file",
269
271
  "pod_template_file",
272
+ "pod_template_dict",
270
273
  "namespace",
271
274
  "container_resources",
272
275
  "volumes",
@@ -322,6 +325,7 @@ class KubernetesPodOperator(BaseOperator):
322
325
  log_events_on_failure: bool = False,
323
326
  do_xcom_push: bool = False,
324
327
  pod_template_file: str | None = None,
328
+ pod_template_dict: dict | None = None,
325
329
  priority_class_name: str | None = None,
326
330
  pod_runtime_info_envs: list[k8s.V1EnvVar] | None = None,
327
331
  termination_grace_period: int | None = None,
@@ -404,6 +408,7 @@ class KubernetesPodOperator(BaseOperator):
404
408
  self.log_events_on_failure = log_events_on_failure
405
409
  self.priority_class_name = priority_class_name
406
410
  self.pod_template_file = pod_template_file
411
+ self.pod_template_dict = pod_template_dict
407
412
  self.name = self._set_name(name)
408
413
  self.random_name_suffix = random_name_suffix
409
414
  self.termination_grace_period = termination_grace_period
@@ -672,6 +677,7 @@ class KubernetesPodOperator(BaseOperator):
672
677
  )
673
678
 
674
679
  def execute_complete(self, context: Context, event: dict, **kwargs):
680
+ self.log.debug("Triggered with event: %s", event)
675
681
  pod = None
676
682
  try:
677
683
  pod = self.hook.get_pod(
@@ -682,7 +688,11 @@ class KubernetesPodOperator(BaseOperator):
682
688
  # fetch some logs when pod is failed
683
689
  if self.get_logs:
684
690
  self.write_logs(pod)
685
- raise AirflowException(event["message"])
691
+ if "stack_trace" in event:
692
+ message = f"{event['message']}\n{event['stack_trace']}"
693
+ else:
694
+ message = event["message"]
695
+ raise AirflowException(message)
686
696
  elif event["status"] == "success":
687
697
  # fetch some logs when pod is executed successfully
688
698
  if self.get_logs:
@@ -892,6 +902,11 @@ class KubernetesPodOperator(BaseOperator):
892
902
  pod_template = pod_generator.PodGenerator.deserialize_model_file(self.pod_template_file)
893
903
  if self.full_pod_spec:
894
904
  pod_template = PodGenerator.reconcile_pods(pod_template, self.full_pod_spec)
905
+ elif self.pod_template_dict:
906
+ self.log.debug("Pod template dict found, will parse for base pod")
907
+ pod_template = pod_generator.PodGenerator.deserialize_model_dict(self.pod_template_dict)
908
+ if self.full_pod_spec:
909
+ pod_template = PodGenerator.reconcile_pods(pod_template, self.full_pod_spec)
895
910
  elif self.full_pod_spec:
896
911
  pod_template = self.full_pod_spec
897
912
  else:
@@ -948,7 +963,7 @@ class KubernetesPodOperator(BaseOperator):
948
963
 
949
964
  if not pod.metadata.name:
950
965
  pod.metadata.name = _create_pod_id(
951
- task_id=self.task_id, unique=self.random_name_suffix, max_length=80
966
+ task_id=self.task_id, unique=self.random_name_suffix, max_length=POD_NAME_MAX_LENGTH
952
967
  )
953
968
  elif self.random_name_suffix:
954
969
  # user has supplied pod name, we're just adding suffix
@@ -41,7 +41,11 @@ from airflow.exceptions import (
41
41
  AirflowException,
42
42
  RemovedInAirflow3Warning,
43
43
  )
44
- from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import add_pod_suffix, rand_str
44
+ from airflow.providers.cncf.kubernetes.kubernetes_helper_functions import (
45
+ POD_NAME_MAX_LENGTH,
46
+ add_pod_suffix,
47
+ rand_str,
48
+ )
45
49
  from airflow.providers.cncf.kubernetes.pod_generator_deprecated import (
46
50
  PodDefaults,
47
51
  PodGenerator as PodGeneratorDeprecated,
@@ -380,11 +384,11 @@ class PodGenerator:
380
384
  - executor_config
381
385
  - dynamic arguments
382
386
  """
383
- if len(pod_id) > 253:
387
+ if len(pod_id) > POD_NAME_MAX_LENGTH:
384
388
  warnings.warn(
385
- "pod_id supplied is longer than 253 characters; truncating and adding unique suffix."
389
+ f"pod_id supplied is longer than {POD_NAME_MAX_LENGTH} characters; truncating and adding unique suffix."
386
390
  )
387
- pod_id = add_pod_suffix(pod_name=pod_id, max_len=253)
391
+ pod_id = add_pod_suffix(pod_name=pod_id, max_len=POD_NAME_MAX_LENGTH)
388
392
  try:
389
393
  image = pod_override_object.spec.containers[0].image # type: ignore
390
394
  if not image:
@@ -18,9 +18,11 @@ from __future__ import annotations
18
18
 
19
19
  import asyncio
20
20
  import datetime
21
+ import traceback
21
22
  import warnings
22
23
  from asyncio import CancelledError
23
24
  from enum import Enum
25
+ from functools import cached_property
24
26
  from typing import TYPE_CHECKING, Any, AsyncIterator
25
27
 
26
28
  from airflow.exceptions import AirflowProviderDeprecationWarning
@@ -115,7 +117,6 @@ class KubernetesPodTrigger(BaseTrigger):
115
117
  self.on_finish_action = OnFinishAction(on_finish_action)
116
118
  self.should_delete_pod = self.on_finish_action == OnFinishAction.DELETE_POD
117
119
 
118
- self._hook: AsyncKubernetesHook | None = None
119
120
  self._since_time = None
120
121
 
121
122
  def serialize(self) -> tuple[str, dict[str, Any]]:
@@ -141,11 +142,10 @@ class KubernetesPodTrigger(BaseTrigger):
141
142
 
142
143
  async def run(self) -> AsyncIterator[TriggerEvent]: # type: ignore[override]
143
144
  """Get current pod status and yield a TriggerEvent."""
144
- hook = self._get_async_hook()
145
145
  self.log.info("Checking pod %r in namespace %r.", self.pod_name, self.pod_namespace)
146
146
  try:
147
147
  while True:
148
- pod = await hook.get_pod(
148
+ pod = await self.hook.get_pod(
149
149
  name=self.pod_name,
150
150
  namespace=self.pod_namespace,
151
151
  )
@@ -205,13 +205,13 @@ class KubernetesPodTrigger(BaseTrigger):
205
205
  # That means that task was marked as failed
206
206
  if self.get_logs:
207
207
  self.log.info("Outputting container logs...")
208
- await self._get_async_hook().read_logs(
208
+ await self.hook.read_logs(
209
209
  name=self.pod_name,
210
210
  namespace=self.pod_namespace,
211
211
  )
212
212
  if self.on_finish_action == OnFinishAction.DELETE_POD:
213
213
  self.log.info("Deleting pod...")
214
- await self._get_async_hook().delete_pod(
214
+ await self.hook.delete_pod(
215
215
  name=self.pod_name,
216
216
  namespace=self.pod_namespace,
217
217
  )
@@ -231,18 +231,22 @@ class KubernetesPodTrigger(BaseTrigger):
231
231
  "namespace": self.pod_namespace,
232
232
  "status": "error",
233
233
  "message": str(e),
234
+ "stack_trace": traceback.format_exc(),
234
235
  }
235
236
  )
236
237
 
237
238
  def _get_async_hook(self) -> AsyncKubernetesHook:
238
- if self._hook is None:
239
- self._hook = AsyncKubernetesHook(
240
- conn_id=self.kubernetes_conn_id,
241
- in_cluster=self.in_cluster,
242
- config_file=self.config_file,
243
- cluster_context=self.cluster_context,
244
- )
245
- return self._hook
239
+ # TODO: Remove this method when the min version of kubernetes provider is 7.12.0 in Google provider.
240
+ return AsyncKubernetesHook(
241
+ conn_id=self.kubernetes_conn_id,
242
+ in_cluster=self.in_cluster,
243
+ config_file=self.config_file,
244
+ cluster_context=self.cluster_context,
245
+ )
246
+
247
+ @cached_property
248
+ def hook(self) -> AsyncKubernetesHook:
249
+ return self._get_async_hook()
246
250
 
247
251
  def define_container_state(self, pod: V1Pod) -> ContainerState:
248
252
  pod_containers = pod.status.container_statuses
@@ -37,7 +37,7 @@ from kubernetes.stream import stream as kubernetes_stream
37
37
  from pendulum import DateTime
38
38
  from pendulum.parsing.exceptions import ParserError
39
39
  from typing_extensions import Literal
40
- from urllib3.exceptions import HTTPError as BaseHTTPError
40
+ from urllib3.exceptions import HTTPError, TimeoutError
41
41
 
42
42
  from airflow.exceptions import AirflowException, AirflowProviderDeprecationWarning
43
43
  from airflow.providers.cncf.kubernetes.pod_generator import PodDefaults
@@ -395,7 +395,7 @@ class PodManager(LoggingMixin):
395
395
  :meta private:
396
396
  """
397
397
 
398
- def consume_logs(*, since_time: DateTime | None = None) -> DateTime | None:
398
+ def consume_logs(*, since_time: DateTime | None = None) -> tuple[DateTime | None, Exception | None]:
399
399
  """
400
400
  Try to follow container logs until container completes.
401
401
 
@@ -404,7 +404,18 @@ class PodManager(LoggingMixin):
404
404
 
405
405
  Returns the last timestamp observed in logs.
406
406
  """
407
+ exception = None
407
408
  last_captured_timestamp = None
409
+ # We timeout connections after 30 minutes because otherwise they can get
410
+ # stuck forever. The 30 is somewhat arbitrary.
411
+ # As a consequence, a TimeoutError will be raised no more than 30 minutes
412
+ # after starting read.
413
+ connection_timeout = 60 * 30
414
+ # We set a shorter read timeout because that helps reduce *connection* timeouts
415
+ # (since the connection will be restarted periodically). And with read timeout,
416
+ # we don't need to worry about either duplicate messages or losing messages; we
417
+ # can safely resume from a few seconds later
418
+ read_timeout = 60 * 5
408
419
  try:
409
420
  logs = self.read_pod_logs(
410
421
  pod=pod,
@@ -415,6 +426,7 @@ class PodManager(LoggingMixin):
415
426
  ),
416
427
  follow=follow,
417
428
  post_termination_timeout=post_termination_timeout,
429
+ _request_timeout=(connection_timeout, read_timeout),
418
430
  )
419
431
  message_to_log = None
420
432
  message_timestamp = None
@@ -447,29 +459,37 @@ class PodManager(LoggingMixin):
447
459
  self._progress_callback(line)
448
460
  self.log.info("[%s] %s", container_name, message_to_log)
449
461
  last_captured_timestamp = message_timestamp
450
- except BaseHTTPError:
462
+ except TimeoutError as e:
463
+ # in case of timeout, increment return time by 2 seconds to avoid
464
+ # duplicate log entries
465
+ if val := (last_captured_timestamp or since_time):
466
+ return val.add(seconds=2), e
467
+ except HTTPError as e:
468
+ exception = e
451
469
  self.log.exception(
452
470
  "Reading of logs interrupted for container %r; will retry.",
453
471
  container_name,
454
472
  )
455
- return last_captured_timestamp or since_time
473
+ return last_captured_timestamp or since_time, exception
456
474
 
457
475
  # note: `read_pod_logs` follows the logs, so we shouldn't necessarily *need* to
458
476
  # loop as we do here. But in a long-running process we might temporarily lose connectivity.
459
477
  # So the looping logic is there to let us resume following the logs.
460
478
  last_log_time = since_time
461
479
  while True:
462
- last_log_time = consume_logs(since_time=last_log_time)
480
+ last_log_time, exc = consume_logs(since_time=last_log_time)
463
481
  if not self.container_is_running(pod, container_name=container_name):
464
482
  return PodLoggingStatus(running=False, last_log_time=last_log_time)
465
483
  if not follow:
466
484
  return PodLoggingStatus(running=True, last_log_time=last_log_time)
467
485
  else:
468
- self.log.warning(
469
- "Pod %s log read interrupted but container %s still running",
470
- pod.metadata.name,
471
- container_name,
472
- )
486
+ # a timeout is a normal thing and we ignore it and resume following logs
487
+ if not isinstance(exc, TimeoutError):
488
+ self.log.warning(
489
+ "Pod %s log read interrupted but container %s still running",
490
+ pod.metadata.name,
491
+ container_name,
492
+ )
473
493
  time.sleep(1)
474
494
 
475
495
  def _reconcile_requested_log_containers(
@@ -610,6 +630,7 @@ class PodManager(LoggingMixin):
610
630
  since_seconds: int | None = None,
611
631
  follow=True,
612
632
  post_termination_timeout: int = 120,
633
+ **kwargs,
613
634
  ) -> PodLogsConsumer:
614
635
  """Read log from the POD."""
615
636
  additional_kwargs = {}
@@ -618,6 +639,7 @@ class PodManager(LoggingMixin):
618
639
 
619
640
  if tail_lines:
620
641
  additional_kwargs["tail_lines"] = tail_lines
642
+ additional_kwargs.update(**kwargs)
621
643
 
622
644
  try:
623
645
  logs = self._client.read_namespaced_pod_log(
@@ -629,7 +651,7 @@ class PodManager(LoggingMixin):
629
651
  _preload_content=False,
630
652
  **additional_kwargs,
631
653
  )
632
- except BaseHTTPError:
654
+ except HTTPError:
633
655
  self.log.exception("There was an error reading the kubernetes API.")
634
656
  raise
635
657
 
@@ -658,7 +680,7 @@ class PodManager(LoggingMixin):
658
680
  return self._client.list_namespaced_event(
659
681
  namespace=pod.metadata.namespace, field_selector=f"involvedObject.name={pod.metadata.name}"
660
682
  )
661
- except BaseHTTPError as e:
683
+ except HTTPError as e:
662
684
  raise AirflowException(f"There was an error reading the kubernetes API: {e}")
663
685
 
664
686
  @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(), reraise=True)
@@ -666,7 +688,7 @@ class PodManager(LoggingMixin):
666
688
  """Read POD information."""
667
689
  try:
668
690
  return self._client.read_namespaced_pod(pod.metadata.name, pod.metadata.namespace)
669
- except BaseHTTPError as e:
691
+ except HTTPError as e:
670
692
  raise AirflowException(f"There was an error reading the kubernetes API: {e}")
671
693
 
672
694
  def await_xcom_sidecar_container_start(self, pod: V1Pod) -> None:
@@ -28,7 +28,7 @@ build-backend = "flit_core.buildapi"
28
28
 
29
29
  [project]
30
30
  name = "apache-airflow-providers-cncf-kubernetes"
31
- version = "7.11.0.rc1"
31
+ version = "7.12.0"
32
32
  description = "Provider package apache-airflow-providers-cncf-kubernetes for Apache Airflow"
33
33
  readme = "README.rst"
34
34
  authors = [
@@ -56,7 +56,7 @@ classifiers = [
56
56
  requires-python = "~=3.8"
57
57
  dependencies = [
58
58
  "aiofiles>=23.2.0",
59
- "apache-airflow>=2.6.0.dev0",
59
+ "apache-airflow>=2.6.0",
60
60
  "asgiref>=3.5.2",
61
61
  "cryptography>=2.0.0",
62
62
  "google-re2>=1.0",
@@ -65,8 +65,8 @@ dependencies = [
65
65
  ]
66
66
 
67
67
  [project.urls]
68
- "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.11.0"
69
- "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.11.0/changelog.html"
68
+ "Documentation" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0"
69
+ "Changelog" = "https://airflow.apache.org/docs/apache-airflow-providers-cncf-kubernetes/7.12.0/changelog.html"
70
70
  "Bug Tracker" = "https://github.com/apache/airflow/issues"
71
71
  "Source Code" = "https://github.com/apache/airflow"
72
72
  "Slack Chat" = "https://s.apache.org/airflow-slack"