2017-12-30 01:08:35 +03:00
|
|
|
# Licensed to the Apache Software Foundation (ASF) under one
|
|
|
|
# or more contributor license agreements. See the NOTICE file
|
|
|
|
# distributed with this work for additional information
|
|
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
|
|
# to you under the Apache License, Version 2.0 (the
|
|
|
|
# "License"); you may not use this file except in compliance
|
|
|
|
# with the License. You may obtain a copy of the License at
|
2017-12-07 18:41:05 +03:00
|
|
|
#
|
2017-12-30 01:08:35 +03:00
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
2017-12-07 18:41:05 +03:00
|
|
|
#
|
2017-12-30 01:08:35 +03:00
|
|
|
# Unless required by applicable law or agreed to in writing,
|
|
|
|
# software distributed under the License is distributed on an
|
|
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
|
# KIND, either express or implied. See the License for the
|
|
|
|
# specific language governing permissions and limitations
|
|
|
|
# under the License.
|
2019-09-05 00:24:31 +03:00
|
|
|
"""
|
|
|
|
This module provides an interface between the previous Pod
|
|
|
|
API and outputs a kubernetes.client.models.V1Pod.
|
|
|
|
The advantage being that the full Kubernetes API
|
|
|
|
is supported and no serialization need be written.
|
|
|
|
"""
|
|
|
|
import copy
|
2020-08-26 17:53:37 +03:00
|
|
|
import datetime
|
2020-05-17 00:13:58 +03:00
|
|
|
import hashlib
|
2020-02-11 20:47:32 +03:00
|
|
|
import os
|
2020-05-17 00:13:58 +03:00
|
|
|
import re
|
2019-09-17 14:16:32 +03:00
|
|
|
import uuid
|
2020-09-09 01:56:59 +03:00
|
|
|
import warnings
|
2020-02-11 20:47:32 +03:00
|
|
|
from functools import reduce
|
2020-09-17 18:40:20 +03:00
|
|
|
from typing import List, Optional, Union
|
2019-09-17 14:16:32 +03:00
|
|
|
|
2020-02-11 20:47:32 +03:00
|
|
|
import yaml
|
2020-08-26 17:53:37 +03:00
|
|
|
from dateutil import parser
|
2020-07-06 12:37:22 +03:00
|
|
|
from kubernetes.client import models as k8s
|
2020-02-11 20:47:32 +03:00
|
|
|
from kubernetes.client.api_client import ApiClient
|
2019-08-29 05:31:56 +03:00
|
|
|
|
2020-02-11 20:47:32 +03:00
|
|
|
from airflow.exceptions import AirflowConfigException
|
2020-09-17 18:40:20 +03:00
|
|
|
from airflow.kubernetes.pod_generator_deprecated import PodGenerator as PodGeneratorDeprecated
|
2020-01-09 23:39:05 +03:00
|
|
|
from airflow.version import version as airflow_version
|
|
|
|
|
|
|
|
MAX_POD_ID_LEN = 253
|
|
|
|
|
2020-05-17 00:13:58 +03:00
|
|
|
MAX_LABEL_LEN = 63
|
|
|
|
|
2017-12-07 18:41:05 +03:00
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
class PodDefaults:
|
|
|
|
"""
|
2019-12-03 18:02:20 +03:00
|
|
|
Static defaults for Pods
|
2019-09-05 00:24:31 +03:00
|
|
|
"""
|
2020-09-21 13:45:06 +03:00
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
XCOM_MOUNT_PATH = '/airflow/xcom'
|
|
|
|
SIDECAR_CONTAINER_NAME = 'airflow-xcom-sidecar'
|
2019-09-09 15:42:06 +03:00
|
|
|
XCOM_CMD = 'trap "exit 0" INT; while true; do sleep 30; done;'
|
2019-09-05 00:24:31 +03:00
|
|
|
VOLUME_MOUNT = k8s.V1VolumeMount(
|
|
|
|
name='xcom',
|
|
|
|
mount_path=XCOM_MOUNT_PATH
|
|
|
|
)
|
|
|
|
VOLUME = k8s.V1Volume(
|
|
|
|
name='xcom',
|
|
|
|
empty_dir=k8s.V1EmptyDirVolumeSource()
|
|
|
|
)
|
|
|
|
SIDECAR_CONTAINER = k8s.V1Container(
|
|
|
|
name=SIDECAR_CONTAINER_NAME,
|
2019-09-09 15:42:06 +03:00
|
|
|
command=['sh', '-c', XCOM_CMD],
|
|
|
|
image='alpine',
|
2019-09-16 00:15:53 +03:00
|
|
|
volume_mounts=[VOLUME_MOUNT],
|
|
|
|
resources=k8s.V1ResourceRequirements(
|
|
|
|
requests={
|
|
|
|
"cpu": "1m",
|
|
|
|
}
|
|
|
|
),
|
2019-09-05 00:24:31 +03:00
|
|
|
)
|
2017-12-27 17:30:04 +03:00
|
|
|
|
|
|
|
|
2020-05-17 00:13:58 +03:00
|
|
|
def make_safe_label_value(string):
|
|
|
|
"""
|
|
|
|
Valid label values must be 63 characters or less and must be empty or begin and
|
|
|
|
end with an alphanumeric character ([a-z0-9A-Z]) with dashes (-), underscores (_),
|
|
|
|
dots (.), and alphanumerics between.
|
|
|
|
|
|
|
|
If the label value is greater than 63 chars once made safe, or differs in any
|
|
|
|
way from the original value sent to this function, then we need to truncate to
|
|
|
|
53 chars, and append it with a unique hash.
|
|
|
|
"""
|
|
|
|
safe_label = re.sub(r"^[^a-z0-9A-Z]*|[^a-zA-Z0-9_\-\.]|[^a-z0-9A-Z]*$", "", string)
|
|
|
|
|
|
|
|
if len(safe_label) > MAX_LABEL_LEN or string != safe_label:
|
|
|
|
safe_hash = hashlib.md5(string.encode()).hexdigest()[:9]
|
|
|
|
safe_label = safe_label[:MAX_LABEL_LEN - len(safe_hash) - 1] + "-" + safe_hash
|
|
|
|
|
|
|
|
return safe_label
|
|
|
|
|
|
|
|
|
2020-08-26 17:53:37 +03:00
|
|
|
def datetime_to_label_safe_datestring(datetime_obj: datetime.datetime) -> str:
|
|
|
|
"""
|
|
|
|
Kubernetes doesn't like ":" in labels, since ISO datetime format uses ":" but
|
|
|
|
not "_" let's
|
|
|
|
replace ":" with "_"
|
|
|
|
|
|
|
|
:param datetime_obj: datetime.datetime object
|
|
|
|
:return: ISO-like string representing the datetime
|
|
|
|
"""
|
|
|
|
return datetime_obj.isoformat().replace(":", "_").replace('+', '_plus_')
|
|
|
|
|
|
|
|
|
|
|
|
def label_safe_datestring_to_datetime(string: str) -> datetime.datetime:
|
|
|
|
"""
|
|
|
|
Kubernetes doesn't permit ":" in labels. ISO datetime format uses ":" but not
|
|
|
|
"_", let's
|
|
|
|
replace ":" with "_"
|
|
|
|
|
|
|
|
:param string: str
|
|
|
|
:return: datetime.datetime object
|
|
|
|
"""
|
|
|
|
return parser.parse(string.replace('_plus_', '+').replace("_", ":"))
|
|
|
|
|
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
class PodGenerator:
|
|
|
|
"""
|
|
|
|
Contains Kubernetes Airflow Worker configuration logic
|
|
|
|
|
|
|
|
Represents a kubernetes pod and manages execution of a single pod.
|
2020-01-09 23:39:05 +03:00
|
|
|
Any configuration that is container specific gets applied to
|
|
|
|
the first container in the list of containers.
|
|
|
|
|
|
|
|
:param pod: The fully specified pod. Mutually exclusive with `path_or_string`
|
|
|
|
:type pod: Optional[kubernetes.client.models.V1Pod]
|
2020-02-11 20:47:32 +03:00
|
|
|
:param pod_template_file: Path to YAML file. Mutually exclusive with `pod`
|
|
|
|
:type pod_template_file: Optional[str]
|
2020-01-09 23:39:05 +03:00
|
|
|
:param extract_xcom: Whether to bring up a container for xcom
|
|
|
|
:type extract_xcom: bool
|
2019-09-05 00:24:31 +03:00
|
|
|
"""
|
2020-09-21 13:45:06 +03:00
|
|
|
|
2019-09-17 14:16:32 +03:00
|
|
|
def __init__( # pylint: disable=too-many-arguments,too-many-locals
|
2019-09-05 00:24:31 +03:00
|
|
|
self,
|
2020-01-09 23:39:05 +03:00
|
|
|
pod: Optional[k8s.V1Pod] = None,
|
2020-02-11 20:47:32 +03:00
|
|
|
pod_template_file: Optional[str] = None,
|
2020-09-17 18:40:20 +03:00
|
|
|
extract_xcom: bool = True
|
2019-09-05 00:24:31 +03:00
|
|
|
):
|
2020-09-17 18:40:20 +03:00
|
|
|
if not pod_template_file and not pod:
|
|
|
|
raise AirflowConfigException("Podgenerator requires either a "
|
|
|
|
"`pod` or a `pod_template_file` argument")
|
|
|
|
if pod_template_file and pod:
|
|
|
|
raise AirflowConfigException("Cannot pass both `pod` "
|
|
|
|
"and `pod_template_file` arguments")
|
2020-01-09 23:39:05 +03:00
|
|
|
|
2020-02-11 20:47:32 +03:00
|
|
|
if pod_template_file:
|
|
|
|
self.ud_pod = self.deserialize_model_file(pod_template_file)
|
|
|
|
else:
|
|
|
|
self.ud_pod = pod
|
2020-01-09 23:39:05 +03:00
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
# Attach sidecar
|
|
|
|
self.extract_xcom = extract_xcom
|
|
|
|
|
|
|
|
def gen_pod(self) -> k8s.V1Pod:
|
2019-09-17 14:16:32 +03:00
|
|
|
"""Generates pod"""
|
2019-09-05 00:24:31 +03:00
|
|
|
result = self.ud_pod
|
|
|
|
|
2020-01-09 23:39:05 +03:00
|
|
|
result.metadata.name = self.make_unique_pod_id(result.metadata.name)
|
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
if self.extract_xcom:
|
2020-09-17 18:40:20 +03:00
|
|
|
result = self.add_xcom_sidecar(result)
|
2019-09-05 00:24:31 +03:00
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
@staticmethod
|
2020-09-17 18:40:20 +03:00
|
|
|
def add_xcom_sidecar(pod: k8s.V1Pod) -> k8s.V1Pod:
|
2019-09-17 14:16:32 +03:00
|
|
|
"""Adds sidecar"""
|
2019-09-05 00:24:31 +03:00
|
|
|
pod_cp = copy.deepcopy(pod)
|
2020-01-09 23:39:05 +03:00
|
|
|
pod_cp.spec.volumes = pod.spec.volumes or []
|
2019-09-05 00:24:31 +03:00
|
|
|
pod_cp.spec.volumes.insert(0, PodDefaults.VOLUME)
|
2020-01-09 23:39:05 +03:00
|
|
|
pod_cp.spec.containers[0].volume_mounts = pod_cp.spec.containers[0].volume_mounts or []
|
2019-09-05 00:24:31 +03:00
|
|
|
pod_cp.spec.containers[0].volume_mounts.insert(0, PodDefaults.VOLUME_MOUNT)
|
|
|
|
pod_cp.spec.containers.append(PodDefaults.SIDECAR_CONTAINER)
|
|
|
|
|
|
|
|
return pod_cp
|
|
|
|
|
|
|
|
@staticmethod
|
2020-09-17 18:40:20 +03:00
|
|
|
def from_obj(obj) -> Optional[Union[dict, k8s.V1Pod]]:
|
2019-09-17 14:16:32 +03:00
|
|
|
"""Converts to pod from obj"""
|
2020-09-09 01:56:59 +03:00
|
|
|
if obj is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
k8s_legacy_object = obj.get("KubernetesExecutor", None)
|
|
|
|
k8s_object = obj.get("pod_override", None)
|
|
|
|
|
|
|
|
if k8s_legacy_object and k8s_object:
|
|
|
|
raise AirflowConfigException("Can not have both a legacy and new"
|
|
|
|
"executor_config object. Please delete the KubernetesExecutor"
|
|
|
|
"dict and only use the pod_override kubernetes.client.models.V1Pod"
|
|
|
|
"object.")
|
|
|
|
if not k8s_object and not k8s_legacy_object:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if isinstance(k8s_object, k8s.V1Pod):
|
|
|
|
return k8s_object
|
|
|
|
elif isinstance(k8s_legacy_object, dict):
|
|
|
|
warnings.warn('Using a dictionary for the executor_config is deprecated and will soon be removed.'
|
|
|
|
'please use a `kubernetes.client.models.V1Pod` class with a "pod_override" key'
|
|
|
|
' instead. ',
|
|
|
|
category=DeprecationWarning)
|
|
|
|
return PodGenerator.from_legacy_obj(obj)
|
|
|
|
else:
|
|
|
|
raise TypeError(
|
|
|
|
'Cannot convert a non-kubernetes.client.models.V1Pod'
|
|
|
|
'object into a KubernetesExecutorConfig')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def from_legacy_obj(obj) -> Optional[k8s.V1Pod]:
|
|
|
|
"""Converts to pod from obj"""
|
2019-09-05 00:24:31 +03:00
|
|
|
if obj is None:
|
2020-01-09 23:39:05 +03:00
|
|
|
return None
|
2019-09-05 00:24:31 +03:00
|
|
|
|
2019-12-03 18:02:20 +03:00
|
|
|
# We do not want to extract constant here from ExecutorLoader because it is just
|
|
|
|
# A name in dictionary rather than executor selection mechanism and it causes cyclic import
|
|
|
|
namespaced = obj.get("KubernetesExecutor", {})
|
2019-09-05 00:24:31 +03:00
|
|
|
|
2020-01-09 23:39:05 +03:00
|
|
|
if not namespaced:
|
|
|
|
return None
|
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
resources = namespaced.get('resources')
|
|
|
|
|
|
|
|
if resources is None:
|
|
|
|
requests = {
|
|
|
|
'cpu': namespaced.get('request_cpu'),
|
2020-02-27 13:25:24 +03:00
|
|
|
'memory': namespaced.get('request_memory'),
|
|
|
|
'ephemeral-storage': namespaced.get('ephemeral-storage')
|
2017-12-27 17:30:04 +03:00
|
|
|
}
|
2019-09-05 00:24:31 +03:00
|
|
|
limits = {
|
|
|
|
'cpu': namespaced.get('limit_cpu'),
|
2020-02-27 13:25:24 +03:00
|
|
|
'memory': namespaced.get('limit_memory'),
|
|
|
|
'ephemeral-storage': namespaced.get('ephemeral-storage')
|
2017-12-07 18:41:05 +03:00
|
|
|
}
|
2019-09-05 00:24:31 +03:00
|
|
|
all_resources = list(requests.values()) + list(limits.values())
|
|
|
|
if all(r is None for r in all_resources):
|
|
|
|
resources = None
|
|
|
|
else:
|
|
|
|
resources = k8s.V1ResourceRequirements(
|
|
|
|
requests=requests,
|
|
|
|
limits=limits
|
|
|
|
)
|
2020-02-11 20:47:32 +03:00
|
|
|
namespaced['resources'] = resources
|
2020-09-17 18:40:20 +03:00
|
|
|
return PodGeneratorDeprecated(**namespaced).gen_pod()
|
2017-12-07 18:41:05 +03:00
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
@staticmethod
|
2020-01-09 23:39:05 +03:00
|
|
|
def reconcile_pods(base_pod: k8s.V1Pod, client_pod: Optional[k8s.V1Pod]) -> k8s.V1Pod:
|
2018-05-14 22:58:39 +03:00
|
|
|
"""
|
2019-09-05 00:24:31 +03:00
|
|
|
:param base_pod: has the base attributes which are overwritten if they exist
|
|
|
|
in the client pod and remain if they do not exist in the client_pod
|
|
|
|
:type base_pod: k8s.V1Pod
|
|
|
|
:param client_pod: the pod that the client wants to create.
|
|
|
|
:type client_pod: k8s.V1Pod
|
|
|
|
:return: the merged pods
|
|
|
|
|
2020-01-09 23:39:05 +03:00
|
|
|
This can't be done recursively as certain fields some overwritten, and some concatenated.
|
2018-05-14 22:58:39 +03:00
|
|
|
"""
|
2020-01-09 23:39:05 +03:00
|
|
|
if client_pod is None:
|
|
|
|
return base_pod
|
2018-05-14 22:58:39 +03:00
|
|
|
|
2019-09-05 00:24:31 +03:00
|
|
|
client_pod_cp = copy.deepcopy(client_pod)
|
2020-01-09 23:39:05 +03:00
|
|
|
client_pod_cp.spec = PodGenerator.reconcile_specs(base_pod.spec, client_pod_cp.spec)
|
2020-08-11 17:49:44 +03:00
|
|
|
client_pod_cp.metadata = PodGenerator.reconcile_metadata(base_pod.metadata, client_pod_cp.metadata)
|
2020-01-09 23:39:05 +03:00
|
|
|
client_pod_cp = merge_objects(base_pod, client_pod_cp)
|
2019-09-05 00:24:31 +03:00
|
|
|
|
|
|
|
return client_pod_cp
|
2020-01-09 23:39:05 +03:00
|
|
|
|
2020-08-11 17:49:44 +03:00
|
|
|
@staticmethod
|
|
|
|
def reconcile_metadata(base_meta, client_meta):
|
|
|
|
"""
|
|
|
|
Merge kubernetes Metadata objects
|
|
|
|
:param base_meta: has the base attributes which are overwritten if they exist
|
|
|
|
in the client_meta and remain if they do not exist in the client_meta
|
|
|
|
:type base_meta: k8s.V1ObjectMeta
|
|
|
|
:param client_meta: the spec that the client wants to create.
|
|
|
|
:type client_meta: k8s.V1ObjectMeta
|
|
|
|
:return: the merged specs
|
|
|
|
"""
|
|
|
|
if base_meta and not client_meta:
|
|
|
|
return base_meta
|
|
|
|
if not base_meta and client_meta:
|
|
|
|
return client_meta
|
|
|
|
elif client_meta and base_meta:
|
|
|
|
client_meta.labels = merge_objects(base_meta.labels, client_meta.labels)
|
|
|
|
client_meta.annotations = merge_objects(base_meta.annotations, client_meta.annotations)
|
|
|
|
extend_object_field(base_meta, client_meta, 'managed_fields')
|
|
|
|
extend_object_field(base_meta, client_meta, 'finalizers')
|
|
|
|
extend_object_field(base_meta, client_meta, 'owner_references')
|
|
|
|
return merge_objects(base_meta, client_meta)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2020-01-09 23:39:05 +03:00
|
|
|
@staticmethod
|
|
|
|
def reconcile_specs(base_spec: Optional[k8s.V1PodSpec],
|
|
|
|
client_spec: Optional[k8s.V1PodSpec]) -> Optional[k8s.V1PodSpec]:
|
|
|
|
"""
|
|
|
|
:param base_spec: has the base attributes which are overwritten if they exist
|
|
|
|
in the client_spec and remain if they do not exist in the client_spec
|
|
|
|
:type base_spec: k8s.V1PodSpec
|
|
|
|
:param client_spec: the spec that the client wants to create.
|
|
|
|
:type client_spec: k8s.V1PodSpec
|
|
|
|
:return: the merged specs
|
|
|
|
"""
|
|
|
|
if base_spec and not client_spec:
|
|
|
|
return base_spec
|
|
|
|
if not base_spec and client_spec:
|
|
|
|
return client_spec
|
|
|
|
elif client_spec and base_spec:
|
|
|
|
client_spec.containers = PodGenerator.reconcile_containers(
|
|
|
|
base_spec.containers, client_spec.containers
|
|
|
|
)
|
|
|
|
merged_spec = extend_object_field(base_spec, client_spec, 'volumes')
|
|
|
|
return merge_objects(base_spec, merged_spec)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def reconcile_containers(base_containers: List[k8s.V1Container],
|
|
|
|
client_containers: List[k8s.V1Container]) -> List[k8s.V1Container]:
|
|
|
|
"""
|
|
|
|
:param base_containers: has the base attributes which are overwritten if they exist
|
|
|
|
in the client_containers and remain if they do not exist in the client_containers
|
|
|
|
:type base_containers: List[k8s.V1Container]
|
|
|
|
:param client_containers: the containers that the client wants to create.
|
|
|
|
:type client_containers: List[k8s.V1Container]
|
|
|
|
:return: the merged containers
|
|
|
|
|
|
|
|
The runs recursively over the list of containers.
|
|
|
|
"""
|
|
|
|
if not base_containers:
|
|
|
|
return client_containers
|
|
|
|
if not client_containers:
|
|
|
|
return base_containers
|
|
|
|
|
|
|
|
client_container = client_containers[0]
|
|
|
|
base_container = base_containers[0]
|
|
|
|
client_container = extend_object_field(base_container, client_container, 'volume_mounts')
|
|
|
|
client_container = extend_object_field(base_container, client_container, 'env')
|
|
|
|
client_container = extend_object_field(base_container, client_container, 'env_from')
|
|
|
|
client_container = extend_object_field(base_container, client_container, 'ports')
|
|
|
|
client_container = extend_object_field(base_container, client_container, 'volume_devices')
|
|
|
|
client_container = merge_objects(base_container, client_container)
|
|
|
|
|
|
|
|
return [client_container] + PodGenerator.reconcile_containers(
|
|
|
|
base_containers[1:], client_containers[1:]
|
|
|
|
)
|
|
|
|
|
|
|
|
@staticmethod
|
2020-09-11 20:47:59 +03:00
|
|
|
def construct_pod( # pylint: disable=too-many-arguments
|
2020-01-09 23:39:05 +03:00
|
|
|
dag_id: str,
|
|
|
|
task_id: str,
|
|
|
|
pod_id: str,
|
|
|
|
try_number: int,
|
2020-09-11 20:47:59 +03:00
|
|
|
kube_image: str,
|
2020-08-26 17:53:37 +03:00
|
|
|
date: datetime.datetime,
|
2020-01-09 23:39:05 +03:00
|
|
|
command: List[str],
|
2020-09-11 20:47:59 +03:00
|
|
|
pod_override_object: Optional[k8s.V1Pod],
|
|
|
|
base_worker_pod: k8s.V1Pod,
|
2020-01-09 23:39:05 +03:00
|
|
|
namespace: str,
|
|
|
|
worker_uuid: str
|
|
|
|
) -> k8s.V1Pod:
|
|
|
|
"""
|
|
|
|
Construct a pod by gathering and consolidating the configuration from 3 places:
|
|
|
|
- airflow.cfg
|
|
|
|
- executor_config
|
|
|
|
- dynamic arguments
|
|
|
|
"""
|
2020-09-17 18:40:20 +03:00
|
|
|
try:
|
|
|
|
image = pod_override_object.spec.containers[0].image # type: ignore
|
|
|
|
if not image:
|
|
|
|
image = kube_image
|
|
|
|
except Exception: # pylint: disable=W0703
|
|
|
|
image = kube_image
|
|
|
|
|
|
|
|
dynamic_pod = k8s.V1Pod(
|
|
|
|
metadata=k8s.V1ObjectMeta(
|
|
|
|
namespace=namespace,
|
|
|
|
annotations={
|
|
|
|
'dag_id': dag_id,
|
|
|
|
'task_id': task_id,
|
|
|
|
'execution_date': date.isoformat(),
|
|
|
|
'try_number': str(try_number),
|
|
|
|
},
|
|
|
|
name=PodGenerator.make_unique_pod_id(pod_id),
|
|
|
|
labels={
|
|
|
|
'airflow-worker': worker_uuid,
|
|
|
|
'dag_id': dag_id,
|
|
|
|
'task_id': task_id,
|
|
|
|
'execution_date': datetime_to_label_safe_datestring(date),
|
|
|
|
'try_number': str(try_number),
|
|
|
|
'airflow_version': airflow_version.replace('+', '-'),
|
|
|
|
'kubernetes_executor': 'True',
|
|
|
|
}
|
|
|
|
),
|
|
|
|
spec=k8s.V1PodSpec(
|
|
|
|
containers=[
|
|
|
|
k8s.V1Container(
|
|
|
|
name="base",
|
|
|
|
command=command,
|
|
|
|
image=image,
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
2020-01-09 23:39:05 +03:00
|
|
|
|
2020-02-11 20:47:32 +03:00
|
|
|
# Reconcile the pods starting with the first chronologically,
|
2020-09-11 20:47:59 +03:00
|
|
|
# Pod from the pod_template_File -> Pod from executor_config arg -> Pod from the K8s executor
|
|
|
|
pod_list = [base_worker_pod, pod_override_object, dynamic_pod]
|
2020-02-11 20:47:32 +03:00
|
|
|
|
|
|
|
return reduce(PodGenerator.reconcile_pods, pod_list)
|
|
|
|
|
2020-09-09 01:56:59 +03:00
|
|
|
@staticmethod
|
|
|
|
def serialize_pod(pod: k8s.V1Pod):
|
|
|
|
"""
|
2020-09-17 18:40:20 +03:00
|
|
|
|
2020-09-09 01:56:59 +03:00
|
|
|
Converts a k8s.V1Pod into a jsonified object
|
2020-09-17 18:40:20 +03:00
|
|
|
|
2020-09-09 01:56:59 +03:00
|
|
|
@param pod:
|
|
|
|
@return:
|
|
|
|
"""
|
|
|
|
api_client = ApiClient()
|
|
|
|
return api_client.sanitize_for_serialization(pod)
|
|
|
|
|
2020-02-11 20:47:32 +03:00
|
|
|
@staticmethod
|
|
|
|
def deserialize_model_file(path: str) -> k8s.V1Pod:
|
|
|
|
"""
|
|
|
|
:param path: Path to the file
|
|
|
|
:return: a kubernetes.client.models.V1Pod
|
|
|
|
|
|
|
|
Unfortunately we need access to the private method
|
|
|
|
``_ApiClient__deserialize_model`` from the kubernetes client.
|
|
|
|
This issue is tracked here; https://github.com/kubernetes-client/python/issues/977.
|
|
|
|
"""
|
|
|
|
if os.path.exists(path):
|
|
|
|
with open(path) as stream:
|
|
|
|
pod = yaml.safe_load(stream)
|
|
|
|
else:
|
|
|
|
pod = yaml.safe_load(path)
|
|
|
|
|
|
|
|
# pylint: disable=protected-access
|
2020-09-09 01:56:59 +03:00
|
|
|
return PodGenerator.deserialize_model_dict(pod)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def deserialize_model_dict(pod_dict: dict) -> k8s.V1Pod:
|
|
|
|
"""
|
|
|
|
Deserializes python dictionary to k8s.V1Pod
|
|
|
|
@param pod_dict:
|
|
|
|
@return:
|
|
|
|
"""
|
|
|
|
api_client = ApiClient()
|
|
|
|
return api_client._ApiClient__deserialize_model( # pylint: disable=W0212
|
|
|
|
pod_dict, k8s.V1Pod)
|
2020-01-09 23:39:05 +03:00
|
|
|
|
|
|
|
@staticmethod
|
2020-09-17 18:40:20 +03:00
|
|
|
def make_unique_pod_id(pod_id):
|
2020-06-21 11:34:41 +03:00
|
|
|
r"""
|
2020-01-09 23:39:05 +03:00
|
|
|
Kubernetes pod names must be <= 253 chars and must pass the following regex for
|
|
|
|
validation
|
|
|
|
``^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$``
|
|
|
|
|
2020-09-17 18:40:20 +03:00
|
|
|
:param pod_id: a dag_id with only alphanumeric characters
|
2020-01-09 23:39:05 +03:00
|
|
|
:return: ``str`` valid Pod name of appropriate length
|
|
|
|
"""
|
2020-09-17 18:40:20 +03:00
|
|
|
if not pod_id:
|
2020-01-09 23:39:05 +03:00
|
|
|
return None
|
|
|
|
|
|
|
|
safe_uuid = uuid.uuid4().hex
|
2020-09-17 18:40:20 +03:00
|
|
|
safe_pod_id = pod_id[:MAX_POD_ID_LEN - len(safe_uuid) - 1] + "-" + safe_uuid
|
2020-01-09 23:39:05 +03:00
|
|
|
|
|
|
|
return safe_pod_id
|
|
|
|
|
|
|
|
|
|
|
|
def merge_objects(base_obj, client_obj):
|
|
|
|
"""
|
|
|
|
:param base_obj: has the base attributes which are overwritten if they exist
|
|
|
|
in the client_obj and remain if they do not exist in the client_obj
|
|
|
|
:param client_obj: the object that the client wants to create.
|
|
|
|
:return: the merged objects
|
|
|
|
"""
|
|
|
|
if not base_obj:
|
|
|
|
return client_obj
|
|
|
|
if not client_obj:
|
|
|
|
return base_obj
|
|
|
|
|
|
|
|
client_obj_cp = copy.deepcopy(client_obj)
|
|
|
|
|
2020-08-11 17:49:44 +03:00
|
|
|
if isinstance(base_obj, dict) and isinstance(client_obj_cp, dict):
|
2020-08-20 02:06:56 +03:00
|
|
|
base_obj_cp = copy.deepcopy(base_obj)
|
|
|
|
base_obj_cp.update(client_obj_cp)
|
|
|
|
return base_obj_cp
|
2020-08-11 17:49:44 +03:00
|
|
|
|
2020-01-09 23:39:05 +03:00
|
|
|
for base_key in base_obj.to_dict().keys():
|
|
|
|
base_val = getattr(base_obj, base_key, None)
|
|
|
|
if not getattr(client_obj, base_key, None) and base_val:
|
2020-08-11 17:49:44 +03:00
|
|
|
if not isinstance(client_obj_cp, dict):
|
|
|
|
setattr(client_obj_cp, base_key, base_val)
|
|
|
|
else:
|
|
|
|
client_obj_cp[base_key] = base_val
|
2020-01-09 23:39:05 +03:00
|
|
|
return client_obj_cp
|
|
|
|
|
|
|
|
|
|
|
|
def extend_object_field(base_obj, client_obj, field_name):
|
|
|
|
"""
|
|
|
|
:param base_obj: an object which has a property `field_name` that is a list
|
|
|
|
:param client_obj: an object which has a property `field_name` that is a list.
|
|
|
|
A copy of this object is returned with `field_name` modified
|
|
|
|
:param field_name: the name of the list field
|
|
|
|
:type field_name: str
|
|
|
|
:return: the client_obj with the property `field_name` being the two properties appended
|
|
|
|
"""
|
|
|
|
client_obj_cp = copy.deepcopy(client_obj)
|
|
|
|
base_obj_field = getattr(base_obj, field_name, None)
|
|
|
|
client_obj_field = getattr(client_obj, field_name, None)
|
|
|
|
|
|
|
|
if (not isinstance(base_obj_field, list) and base_obj_field is not None) or \
|
|
|
|
(not isinstance(client_obj_field, list) and client_obj_field is not None):
|
|
|
|
raise ValueError("The chosen field must be a list.")
|
|
|
|
|
|
|
|
if not base_obj_field:
|
|
|
|
return client_obj_cp
|
|
|
|
if not client_obj_field:
|
|
|
|
setattr(client_obj_cp, field_name, base_obj_field)
|
|
|
|
return client_obj_cp
|
|
|
|
|
|
|
|
appended_fields = base_obj_field + client_obj_field
|
|
|
|
setattr(client_obj_cp, field_name, appended_fields)
|
|
|
|
return client_obj_cp
|