apache-airflow-providers-cncf-kubernetes 3.1.0__py3-none-any.whl → 10.10.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- airflow/providers/cncf/kubernetes/__init__.py +18 -23
- airflow/providers/cncf/kubernetes/backcompat/__init__.py +17 -0
- airflow/providers/cncf/kubernetes/backcompat/backwards_compat_converters.py +31 -49
- airflow/providers/cncf/kubernetes/callbacks.py +200 -0
- airflow/providers/cncf/kubernetes/cli/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/cli/kubernetes_command.py +195 -0
- airflow/providers/cncf/kubernetes/decorators/kubernetes.py +163 -0
- airflow/providers/cncf/kubernetes/decorators/kubernetes_cmd.py +118 -0
- airflow/providers/cncf/kubernetes/exceptions.py +37 -0
- airflow/providers/cncf/kubernetes/executors/__init__.py +17 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor.py +831 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor_types.py +91 -0
- airflow/providers/cncf/kubernetes/executors/kubernetes_executor_utils.py +736 -0
- airflow/providers/cncf/kubernetes/executors/local_kubernetes_executor.py +306 -0
- airflow/providers/cncf/kubernetes/get_provider_info.py +249 -50
- airflow/providers/cncf/kubernetes/hooks/kubernetes.py +846 -112
- airflow/providers/cncf/kubernetes/k8s_model.py +62 -0
- airflow/providers/cncf/kubernetes/kube_client.py +156 -0
- airflow/providers/cncf/kubernetes/kube_config.py +125 -0
- airflow/providers/cncf/kubernetes/kubernetes_executor_templates/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/kubernetes_executor_templates/basic_template.yaml +79 -0
- airflow/providers/cncf/kubernetes/kubernetes_helper_functions.py +165 -0
- airflow/providers/cncf/kubernetes/operators/custom_object_launcher.py +368 -0
- airflow/providers/cncf/kubernetes/operators/job.py +646 -0
- airflow/providers/cncf/kubernetes/operators/kueue.py +132 -0
- airflow/providers/cncf/kubernetes/operators/pod.py +1417 -0
- airflow/providers/cncf/kubernetes/operators/resource.py +191 -0
- airflow/providers/cncf/kubernetes/operators/spark_kubernetes.py +336 -35
- airflow/providers/cncf/kubernetes/pod_generator.py +592 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_image_template.yaml +68 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/dags_in_volume_template.yaml +74 -0
- airflow/providers/cncf/kubernetes/pod_template_file_examples/git_sync_template.yaml +95 -0
- airflow/providers/cncf/kubernetes/python_kubernetes_script.jinja2 +51 -0
- airflow/providers/cncf/kubernetes/python_kubernetes_script.py +92 -0
- airflow/providers/cncf/kubernetes/resource_convert/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/resource_convert/configmap.py +52 -0
- airflow/providers/cncf/kubernetes/resource_convert/env_variable.py +39 -0
- airflow/providers/cncf/kubernetes/resource_convert/secret.py +40 -0
- airflow/providers/cncf/kubernetes/secret.py +128 -0
- airflow/providers/cncf/kubernetes/sensors/spark_kubernetes.py +30 -14
- airflow/providers/cncf/kubernetes/template_rendering.py +81 -0
- airflow/providers/cncf/kubernetes/triggers/__init__.py +16 -0
- airflow/providers/cncf/kubernetes/triggers/job.py +176 -0
- airflow/providers/cncf/kubernetes/triggers/pod.py +344 -0
- airflow/providers/cncf/kubernetes/utils/__init__.py +3 -0
- airflow/providers/cncf/kubernetes/utils/container.py +118 -0
- airflow/providers/cncf/kubernetes/utils/delete_from.py +154 -0
- airflow/providers/cncf/kubernetes/utils/k8s_resource_iterator.py +46 -0
- airflow/providers/cncf/kubernetes/utils/pod_manager.py +887 -152
- airflow/providers/cncf/kubernetes/utils/xcom_sidecar.py +25 -16
- airflow/providers/cncf/kubernetes/version_compat.py +38 -0
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/METADATA +125 -0
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/RECORD +62 -0
- {apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info → apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info}/WHEEL +1 -2
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/entry_points.txt +3 -0
- apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/licenses/NOTICE +5 -0
- airflow/providers/cncf/kubernetes/backcompat/pod.py +0 -119
- airflow/providers/cncf/kubernetes/backcompat/pod_runtime_info_env.py +0 -56
- airflow/providers/cncf/kubernetes/backcompat/volume.py +0 -62
- airflow/providers/cncf/kubernetes/backcompat/volume_mount.py +0 -58
- airflow/providers/cncf/kubernetes/example_dags/example_kubernetes.py +0 -163
- airflow/providers/cncf/kubernetes/example_dags/example_spark_kubernetes.py +0 -66
- airflow/providers/cncf/kubernetes/example_dags/example_spark_kubernetes_spark_pi.yaml +0 -57
- airflow/providers/cncf/kubernetes/operators/kubernetes_pod.py +0 -622
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/METADATA +0 -452
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/NOTICE +0 -6
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/RECORD +0 -29
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/entry_points.txt +0 -3
- apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info/top_level.txt +0 -1
- /airflow/providers/cncf/kubernetes/{example_dags → decorators}/__init__.py +0 -0
- {apache_airflow_providers_cncf_kubernetes-3.1.0.dist-info → apache_airflow_providers_cncf_kubernetes-10.10.0rc1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
"""Classes for interacting with Kubernetes API."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from functools import reduce
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from kubernetes.client import models as k8s
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class K8SModel(ABC):
|
|
30
|
+
"""
|
|
31
|
+
Airflow Kubernetes models are here for backwards compatibility reasons only.
|
|
32
|
+
|
|
33
|
+
Ideally clients should use the kubernetes API
|
|
34
|
+
and the process of
|
|
35
|
+
|
|
36
|
+
client input -> Airflow k8s models -> k8s models
|
|
37
|
+
|
|
38
|
+
can be avoided. All of these models implement the
|
|
39
|
+
`attach_to_pod` method so that they integrate with the kubernetes client.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def attach_to_pod(self, pod: k8s.V1Pod) -> k8s.V1Pod:
|
|
44
|
+
"""
|
|
45
|
+
Attaches to pod.
|
|
46
|
+
|
|
47
|
+
:param pod: A pod to attach this Kubernetes object to
|
|
48
|
+
:return: The pod with the object attached
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def append_to_pod(pod: k8s.V1Pod, k8s_objects: list[K8SModel] | None):
|
|
53
|
+
"""
|
|
54
|
+
Attach additional specs to an existing pod object.
|
|
55
|
+
|
|
56
|
+
:param pod: A pod to attach a list of Kubernetes objects to
|
|
57
|
+
:param k8s_objects: a potential None list of K8SModels
|
|
58
|
+
:return: pod with the objects attached if they exist
|
|
59
|
+
"""
|
|
60
|
+
if not k8s_objects:
|
|
61
|
+
return pod
|
|
62
|
+
return reduce(lambda p, o: o.attach_to_pod(p), k8s_objects, pod)
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
"""Client for kubernetes communication."""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
import urllib3.util
|
|
24
|
+
|
|
25
|
+
from airflow.configuration import conf
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from kubernetes import client, config
|
|
31
|
+
from kubernetes.client import Configuration
|
|
32
|
+
from kubernetes.client.rest import ApiException
|
|
33
|
+
|
|
34
|
+
has_kubernetes = True
|
|
35
|
+
|
|
36
|
+
def _get_default_configuration() -> Configuration:
|
|
37
|
+
if hasattr(Configuration, "get_default_copy"):
|
|
38
|
+
return Configuration.get_default_copy()
|
|
39
|
+
return Configuration()
|
|
40
|
+
|
|
41
|
+
def _disable_verify_ssl() -> None:
|
|
42
|
+
configuration = _get_default_configuration()
|
|
43
|
+
configuration.verify_ssl = False
|
|
44
|
+
Configuration.set_default(configuration)
|
|
45
|
+
|
|
46
|
+
except ImportError as e:
|
|
47
|
+
# We need an exception class to be able to use it in ``except`` elsewhere
|
|
48
|
+
# in the code base
|
|
49
|
+
ApiException = BaseException
|
|
50
|
+
has_kubernetes = False
|
|
51
|
+
_import_err = e
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _enable_tcp_keepalive() -> None:
|
|
55
|
+
"""
|
|
56
|
+
Enable TCP keepalive mechanism.
|
|
57
|
+
|
|
58
|
+
This prevents urllib3 connection to hang indefinitely when idle connection
|
|
59
|
+
is time-outed on services like cloud load balancers or firewalls.
|
|
60
|
+
|
|
61
|
+
See https://github.com/apache/airflow/pull/11406 for detailed explanation.
|
|
62
|
+
|
|
63
|
+
Please ping @michalmisiewicz or @dimberman in the PR if you want to modify this function.
|
|
64
|
+
"""
|
|
65
|
+
import socket
|
|
66
|
+
|
|
67
|
+
from urllib3.connection import HTTPConnection, HTTPSConnection
|
|
68
|
+
|
|
69
|
+
tcp_keep_idle = conf.getint("kubernetes_executor", "tcp_keep_idle")
|
|
70
|
+
tcp_keep_intvl = conf.getint("kubernetes_executor", "tcp_keep_intvl")
|
|
71
|
+
tcp_keep_cnt = conf.getint("kubernetes_executor", "tcp_keep_cnt")
|
|
72
|
+
|
|
73
|
+
socket_options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
|
|
74
|
+
|
|
75
|
+
if hasattr(socket, "TCP_KEEPIDLE"):
|
|
76
|
+
socket_options.append((socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, tcp_keep_idle))
|
|
77
|
+
else:
|
|
78
|
+
log.debug("Unable to set TCP_KEEPIDLE on this platform")
|
|
79
|
+
|
|
80
|
+
if hasattr(socket, "TCP_KEEPINTVL"):
|
|
81
|
+
socket_options.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, tcp_keep_intvl))
|
|
82
|
+
else:
|
|
83
|
+
log.debug("Unable to set TCP_KEEPINTVL on this platform")
|
|
84
|
+
|
|
85
|
+
if hasattr(socket, "TCP_KEEPCNT"):
|
|
86
|
+
socket_options.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, tcp_keep_cnt))
|
|
87
|
+
else:
|
|
88
|
+
log.debug("Unable to set TCP_KEEPCNT on this platform")
|
|
89
|
+
|
|
90
|
+
# Cast both the default options and our socket options
|
|
91
|
+
socket_options_cast: list[tuple[int, int, int | bytes]] = [
|
|
92
|
+
(level, opt, val) for level, opt, val in socket_options
|
|
93
|
+
]
|
|
94
|
+
default_options_cast: list[tuple[int, int, int | bytes]] = [
|
|
95
|
+
(level, opt, val) for level, opt, val in HTTPSConnection.default_socket_options
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
# Then use the cast versions for both HTTPS and HTTP
|
|
99
|
+
HTTPSConnection.default_socket_options = default_options_cast + socket_options_cast
|
|
100
|
+
HTTPConnection.default_socket_options = default_options_cast + socket_options_cast
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_kube_client(
|
|
104
|
+
in_cluster: bool | None = None,
|
|
105
|
+
cluster_context: str | None = None,
|
|
106
|
+
config_file: str | None = None,
|
|
107
|
+
) -> client.CoreV1Api:
|
|
108
|
+
"""
|
|
109
|
+
Retrieve Kubernetes client.
|
|
110
|
+
|
|
111
|
+
:param in_cluster: whether we are in cluster
|
|
112
|
+
:param cluster_context: context of the cluster
|
|
113
|
+
:param config_file: configuration file
|
|
114
|
+
:return: kubernetes client
|
|
115
|
+
"""
|
|
116
|
+
if in_cluster is None:
|
|
117
|
+
in_cluster = conf.getboolean("kubernetes_executor", "in_cluster")
|
|
118
|
+
if not has_kubernetes:
|
|
119
|
+
raise _import_err
|
|
120
|
+
|
|
121
|
+
if conf.getboolean("kubernetes_executor", "enable_tcp_keepalive"):
|
|
122
|
+
_enable_tcp_keepalive()
|
|
123
|
+
|
|
124
|
+
configuration = _get_default_configuration()
|
|
125
|
+
api_client_retry_configuration = conf.getjson(
|
|
126
|
+
"kubernetes_executor", "api_client_retry_configuration", fallback={}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if not conf.getboolean("kubernetes_executor", "verify_ssl"):
|
|
130
|
+
_disable_verify_ssl()
|
|
131
|
+
|
|
132
|
+
if isinstance(api_client_retry_configuration, dict):
|
|
133
|
+
configuration.retries = urllib3.util.Retry(**api_client_retry_configuration)
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError("api_client_retry_configuration should be a dictionary")
|
|
136
|
+
|
|
137
|
+
if in_cluster:
|
|
138
|
+
config.load_incluster_config(client_configuration=configuration)
|
|
139
|
+
else:
|
|
140
|
+
if cluster_context is None:
|
|
141
|
+
cluster_context = conf.get("kubernetes_executor", "cluster_context", fallback=None)
|
|
142
|
+
if config_file is None:
|
|
143
|
+
config_file = conf.get("kubernetes_executor", "config_file", fallback=None)
|
|
144
|
+
config.load_kube_config(
|
|
145
|
+
config_file=config_file, context=cluster_context, client_configuration=configuration
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not conf.getboolean("kubernetes_executor", "verify_ssl"):
|
|
149
|
+
configuration.verify_ssl = False
|
|
150
|
+
|
|
151
|
+
ssl_ca_cert = conf.get("kubernetes_executor", "ssl_ca_cert")
|
|
152
|
+
if ssl_ca_cert:
|
|
153
|
+
configuration.ssl_ca_cert = ssl_ca_cert
|
|
154
|
+
|
|
155
|
+
api_client = client.ApiClient(configuration=configuration)
|
|
156
|
+
return client.CoreV1Api(api_client)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import warnings
|
|
20
|
+
|
|
21
|
+
from airflow.configuration import conf
|
|
22
|
+
from airflow.exceptions import AirflowConfigException, AirflowProviderDeprecationWarning
|
|
23
|
+
from airflow.settings import AIRFLOW_HOME
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KubeConfig:
|
|
27
|
+
"""Configuration for Kubernetes."""
|
|
28
|
+
|
|
29
|
+
core_section = "core"
|
|
30
|
+
kubernetes_section = "kubernetes_executor"
|
|
31
|
+
logging_section = "logging"
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
configuration_dict = conf.as_dict(display_sensitive=True)
|
|
35
|
+
self.core_configuration = configuration_dict[self.core_section]
|
|
36
|
+
self.airflow_home = AIRFLOW_HOME
|
|
37
|
+
self.dags_folder = conf.get(self.core_section, "dags_folder")
|
|
38
|
+
self.parallelism = conf.getint(self.core_section, "parallelism")
|
|
39
|
+
self.pod_template_file = conf.get(self.kubernetes_section, "pod_template_file", fallback=None)
|
|
40
|
+
|
|
41
|
+
self.delete_worker_pods = conf.getboolean(self.kubernetes_section, "delete_worker_pods")
|
|
42
|
+
self.delete_worker_pods_on_failure = conf.getboolean(
|
|
43
|
+
self.kubernetes_section, "delete_worker_pods_on_failure"
|
|
44
|
+
)
|
|
45
|
+
self.worker_pod_pending_fatal_container_state_reasons = []
|
|
46
|
+
if conf.get(self.kubernetes_section, "worker_pod_pending_fatal_container_state_reasons", fallback=""):
|
|
47
|
+
self.worker_pod_pending_fatal_container_state_reasons = [
|
|
48
|
+
r.strip()
|
|
49
|
+
for r in conf.get(
|
|
50
|
+
self.kubernetes_section, "worker_pod_pending_fatal_container_state_reasons"
|
|
51
|
+
).split(",")
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
self.worker_pods_creation_batch_size = conf.getint(
|
|
55
|
+
self.kubernetes_section, "worker_pods_creation_batch_size"
|
|
56
|
+
)
|
|
57
|
+
self.worker_container_repository = conf.get(self.kubernetes_section, "worker_container_repository")
|
|
58
|
+
if self.worker_container_repository:
|
|
59
|
+
warnings.warn(
|
|
60
|
+
"Configuration 'worker_container_repository' is deprecated. "
|
|
61
|
+
"Use 'pod_template_file' to specify the container image repository instead.",
|
|
62
|
+
AirflowProviderDeprecationWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
self.worker_container_tag = conf.get(self.kubernetes_section, "worker_container_tag")
|
|
66
|
+
if self.worker_container_tag:
|
|
67
|
+
warnings.warn(
|
|
68
|
+
"Configuration 'worker_container_tag' is deprecated. "
|
|
69
|
+
"Use 'pod_template_file' to specify the container image tag instead.",
|
|
70
|
+
AirflowProviderDeprecationWarning,
|
|
71
|
+
stacklevel=2,
|
|
72
|
+
)
|
|
73
|
+
if self.worker_container_repository and self.worker_container_tag:
|
|
74
|
+
self.kube_image = f"{self.worker_container_repository}:{self.worker_container_tag}"
|
|
75
|
+
else:
|
|
76
|
+
self.kube_image = None
|
|
77
|
+
|
|
78
|
+
# The Kubernetes Namespace in which the Scheduler and Webserver reside. Note
|
|
79
|
+
# that if your
|
|
80
|
+
# cluster has RBAC enabled, your scheduler may need service account permissions to
|
|
81
|
+
# create, watch, get, and delete pods in this namespace.
|
|
82
|
+
self.kube_namespace = conf.get(self.kubernetes_section, "namespace")
|
|
83
|
+
if self.kube_namespace and self.kube_namespace != "default":
|
|
84
|
+
warnings.warn(
|
|
85
|
+
"Configuration 'namespace' is deprecated. "
|
|
86
|
+
"Use 'pod_template_file' to specify the namespace instead.",
|
|
87
|
+
AirflowProviderDeprecationWarning,
|
|
88
|
+
stacklevel=2,
|
|
89
|
+
)
|
|
90
|
+
self.multi_namespace_mode = conf.getboolean(self.kubernetes_section, "multi_namespace_mode")
|
|
91
|
+
if self.multi_namespace_mode and conf.get(
|
|
92
|
+
self.kubernetes_section, "multi_namespace_mode_namespace_list"
|
|
93
|
+
):
|
|
94
|
+
self.multi_namespace_mode_namespace_list = conf.get(
|
|
95
|
+
self.kubernetes_section, "multi_namespace_mode_namespace_list"
|
|
96
|
+
).split(",")
|
|
97
|
+
else:
|
|
98
|
+
self.multi_namespace_mode_namespace_list = None
|
|
99
|
+
# The Kubernetes Namespace in which pods will be created by the executor. Note
|
|
100
|
+
# that if your
|
|
101
|
+
# cluster has RBAC enabled, your workers may need service account permissions to
|
|
102
|
+
# interact with cluster components.
|
|
103
|
+
self.executor_namespace = conf.get(self.kubernetes_section, "namespace")
|
|
104
|
+
|
|
105
|
+
self.kube_client_request_args = conf.getjson(
|
|
106
|
+
self.kubernetes_section, "kube_client_request_args", fallback={}
|
|
107
|
+
)
|
|
108
|
+
if not isinstance(self.kube_client_request_args, dict):
|
|
109
|
+
raise AirflowConfigException(
|
|
110
|
+
f"[{self.kubernetes_section}] 'kube_client_request_args' expected a JSON dict, got "
|
|
111
|
+
+ type(self.kube_client_request_args).__name__
|
|
112
|
+
)
|
|
113
|
+
if self.kube_client_request_args:
|
|
114
|
+
if "_request_timeout" in self.kube_client_request_args and isinstance(
|
|
115
|
+
self.kube_client_request_args["_request_timeout"], list
|
|
116
|
+
):
|
|
117
|
+
self.kube_client_request_args["_request_timeout"] = tuple(
|
|
118
|
+
self.kube_client_request_args["_request_timeout"]
|
|
119
|
+
)
|
|
120
|
+
self.delete_option_kwargs = conf.getjson(self.kubernetes_section, "delete_option_kwargs", fallback={})
|
|
121
|
+
if not isinstance(self.delete_option_kwargs, dict):
|
|
122
|
+
raise AirflowConfigException(
|
|
123
|
+
f"[{self.kubernetes_section}] 'delete_option_kwargs' expected a JSON dict, got "
|
|
124
|
+
+ type(self.delete_option_kwargs).__name__
|
|
125
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
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.
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
kind: Pod
|
|
19
|
+
apiVersion: v1
|
|
20
|
+
metadata:
|
|
21
|
+
name: placeholder-name-dont-delete
|
|
22
|
+
namespace: placeholder-name-dont-delete
|
|
23
|
+
labels:
|
|
24
|
+
mylabel: foo
|
|
25
|
+
spec:
|
|
26
|
+
containers:
|
|
27
|
+
- name: base
|
|
28
|
+
image: placeholder-name-dont-delete
|
|
29
|
+
env:
|
|
30
|
+
- name: AIRFLOW__CORE__EXECUTOR
|
|
31
|
+
value: LocalExecutor
|
|
32
|
+
- name: AIRFLOW_HOME
|
|
33
|
+
value: /opt/airflow
|
|
34
|
+
- name: AIRFLOW__CORE__DAGS_FOLDER
|
|
35
|
+
value: /opt/airflow/dags
|
|
36
|
+
- name: AIRFLOW__CORE__FERNET_KEY
|
|
37
|
+
valueFrom:
|
|
38
|
+
secretKeyRef:
|
|
39
|
+
name: airflow-fernet-key
|
|
40
|
+
key: fernet-key
|
|
41
|
+
- name: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN
|
|
42
|
+
valueFrom:
|
|
43
|
+
secretKeyRef:
|
|
44
|
+
name: airflow-airflow-metadata
|
|
45
|
+
key: connection
|
|
46
|
+
- name: foo
|
|
47
|
+
value: bar
|
|
48
|
+
resources: {}
|
|
49
|
+
volumeMounts:
|
|
50
|
+
- name: airflow-logs
|
|
51
|
+
mountPath: /opt/airflow/logs
|
|
52
|
+
- name: airflow-config
|
|
53
|
+
readOnly: true
|
|
54
|
+
mountPath: /opt/airflow/airflow.cfg
|
|
55
|
+
subPath: airflow.cfg
|
|
56
|
+
- name: airflow-config
|
|
57
|
+
readOnly: true
|
|
58
|
+
mountPath: /opt/airflow/config/airflow_local_settings.py
|
|
59
|
+
subPath: airflow_local_settings.py
|
|
60
|
+
terminationMessagePath: /dev/termination-log
|
|
61
|
+
terminationMessagePolicy: File
|
|
62
|
+
imagePullPolicy: IfNotPresent
|
|
63
|
+
volumes:
|
|
64
|
+
- name: airflow-logs
|
|
65
|
+
emptyDir: {}
|
|
66
|
+
- name: airflow-config
|
|
67
|
+
configMap:
|
|
68
|
+
name: airflow-airflow-config
|
|
69
|
+
defaultMode: 420
|
|
70
|
+
restartPolicy: Never
|
|
71
|
+
terminationGracePeriodSeconds: 30
|
|
72
|
+
serviceAccountName: airflow-worker
|
|
73
|
+
serviceAccount: airflow-worker
|
|
74
|
+
securityContext:
|
|
75
|
+
runAsUser: 50000
|
|
76
|
+
fsGroup: 50000
|
|
77
|
+
imagePullSecrets:
|
|
78
|
+
- name: airflow-registry
|
|
79
|
+
schedulerName: default-scheduler
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import secrets
|
|
21
|
+
import string
|
|
22
|
+
from functools import cache
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
import pendulum
|
|
26
|
+
from kubernetes.client.rest import ApiException
|
|
27
|
+
from slugify import slugify
|
|
28
|
+
|
|
29
|
+
from airflow.configuration import conf
|
|
30
|
+
from airflow.providers.cncf.kubernetes.backcompat import get_logical_date_key
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from airflow.models.taskinstancekey import TaskInstanceKey
|
|
34
|
+
|
|
35
|
+
log = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
alphanum_lower = string.ascii_lowercase + string.digits
|
|
38
|
+
|
|
39
|
+
POD_NAME_MAX_LENGTH = 63 # Matches Linux kernel's HOST_NAME_MAX default value minus 1.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def rand_str(num):
|
|
43
|
+
"""
|
|
44
|
+
Generate random lowercase alphanumeric string of length num.
|
|
45
|
+
|
|
46
|
+
:meta private:
|
|
47
|
+
"""
|
|
48
|
+
return "".join(secrets.choice(alphanum_lower) for _ in range(num))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def add_unique_suffix(*, name: str, rand_len: int = 8, max_len: int = POD_NAME_MAX_LENGTH) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Add random string to pod or job name while staying under max length.
|
|
54
|
+
|
|
55
|
+
:param name: name of the pod or job
|
|
56
|
+
:param rand_len: length of the random string to append
|
|
57
|
+
:param max_len: maximum length of the pod name
|
|
58
|
+
:meta private:
|
|
59
|
+
"""
|
|
60
|
+
suffix = "-" + rand_str(rand_len)
|
|
61
|
+
return name[: max_len - len(suffix)].strip("-.") + suffix
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_unique_id(
|
|
65
|
+
dag_id: str | None = None,
|
|
66
|
+
task_id: str | None = None,
|
|
67
|
+
*,
|
|
68
|
+
max_length: int = POD_NAME_MAX_LENGTH,
|
|
69
|
+
unique: bool = True,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Generate unique pod or job ID given a dag_id and / or task_id.
|
|
73
|
+
|
|
74
|
+
:param dag_id: DAG ID
|
|
75
|
+
:param task_id: Task ID
|
|
76
|
+
:param max_length: max number of characters
|
|
77
|
+
:param unique: whether a random string suffix should be added
|
|
78
|
+
:return: A valid identifier for a kubernetes pod name
|
|
79
|
+
"""
|
|
80
|
+
if not (dag_id or task_id):
|
|
81
|
+
raise ValueError("Must supply either dag_id or task_id.")
|
|
82
|
+
name = ""
|
|
83
|
+
if dag_id:
|
|
84
|
+
name += dag_id
|
|
85
|
+
if task_id:
|
|
86
|
+
if name:
|
|
87
|
+
name += "-"
|
|
88
|
+
name += task_id
|
|
89
|
+
base_name = slugify(name, lowercase=True)[:max_length].strip(".-")
|
|
90
|
+
if unique:
|
|
91
|
+
return add_unique_suffix(name=base_name, rand_len=8, max_len=max_length)
|
|
92
|
+
return base_name
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def annotations_to_key(annotations: dict[str, str]) -> TaskInstanceKey:
|
|
96
|
+
"""Build a TaskInstanceKey based on pod annotations."""
|
|
97
|
+
log.debug("Creating task key for annotations %s", annotations)
|
|
98
|
+
dag_id = annotations["dag_id"]
|
|
99
|
+
task_id = annotations["task_id"]
|
|
100
|
+
try_number = int(annotations["try_number"])
|
|
101
|
+
annotation_run_id = annotations.get("run_id")
|
|
102
|
+
map_index = int(annotations.get("map_index", -1))
|
|
103
|
+
|
|
104
|
+
# Compat: Look up the run_id from the TI table!
|
|
105
|
+
from airflow.models.dagrun import DagRun
|
|
106
|
+
from airflow.models.taskinstance import TaskInstance, TaskInstanceKey
|
|
107
|
+
from airflow.settings import Session
|
|
108
|
+
|
|
109
|
+
logical_date_key = get_logical_date_key()
|
|
110
|
+
|
|
111
|
+
if not annotation_run_id and logical_date_key in annotations:
|
|
112
|
+
logical_date = pendulum.parse(annotations[logical_date_key])
|
|
113
|
+
# Do _not_ use create-session, we don't want to expunge
|
|
114
|
+
if Session is None:
|
|
115
|
+
raise RuntimeError("Session not configured. Call configure_orm() first.")
|
|
116
|
+
session = Session()
|
|
117
|
+
|
|
118
|
+
task_instance_run_id = (
|
|
119
|
+
session.query(TaskInstance.run_id)
|
|
120
|
+
.join(TaskInstance.dag_run)
|
|
121
|
+
.filter(
|
|
122
|
+
TaskInstance.dag_id == dag_id,
|
|
123
|
+
TaskInstance.task_id == task_id,
|
|
124
|
+
getattr(DagRun, logical_date_key) == logical_date,
|
|
125
|
+
)
|
|
126
|
+
.scalar()
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
task_instance_run_id = annotation_run_id
|
|
130
|
+
|
|
131
|
+
return TaskInstanceKey(
|
|
132
|
+
dag_id=dag_id,
|
|
133
|
+
task_id=task_id,
|
|
134
|
+
run_id=task_instance_run_id,
|
|
135
|
+
try_number=try_number,
|
|
136
|
+
map_index=map_index,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@cache
|
|
141
|
+
def get_logs_task_metadata() -> bool:
|
|
142
|
+
return conf.getboolean("kubernetes_executor", "logs_task_metadata")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def annotations_for_logging_task_metadata(annotation_set):
|
|
146
|
+
if get_logs_task_metadata():
|
|
147
|
+
annotations_for_logging = annotation_set
|
|
148
|
+
else:
|
|
149
|
+
annotations_for_logging = "<omitted>"
|
|
150
|
+
return annotations_for_logging
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def should_retry_creation(exception: BaseException) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Check if an Exception indicates a transient error and warrants retrying.
|
|
156
|
+
|
|
157
|
+
This function is needed for preventing 'No agent available' error. The error appears time to time
|
|
158
|
+
when users try to create a Resource or Job. This issue is inside kubernetes and in the current moment
|
|
159
|
+
has no solution. Like a temporary solution we decided to retry Job or Resource creation request each
|
|
160
|
+
time when this error appears.
|
|
161
|
+
More about this issue here: https://github.com/cert-manager/cert-manager/issues/6457
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(exception, ApiException):
|
|
164
|
+
return str(exception.status) == "500"
|
|
165
|
+
return False
|