Skip to content

Commit cebb7b2

Browse files
committed
create task
1 parent 2429b0f commit cebb7b2

15 files changed

Lines changed: 899 additions & 677 deletions

poetry.lock

Lines changed: 500 additions & 533 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ version = "0.1.0"
3030
[tool.poetry.dependencies]
3131
boto3 = "1.34.104"
3232
foca = "^0.13.0"
33-
kubernetes = "^30.1.0"
33+
kubernetes = "^29.0.0"
3434
python = "^3.11"
3535
requests = ">=2.20.0"
3636
urllib3 = "^2.2.2"

tesk/api/ga4gh/tes/controllers.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from tesk.api.ga4gh.tes.models import TesTask
88
from tesk.api.ga4gh.tes.service_info.service_info import ServiceInfo
99
from tesk.api.ga4gh.tes.task.create_task import CreateTesTask
10-
from tesk.api.kubernetes.converter import TesKubernetesConverter
11-
from tesk.api.kubernetes.template import KubernetesTemplateSupplier
10+
from tesk.api.kubernetes.convert.converter import TesKubernetesConverter
11+
from tesk.api.kubernetes.convert.template import KubernetesTemplateSupplier
1212
from tesk.exceptions import BadRequest, InternalServerError
1313
from tesk.utils import get_custom_config
1414

@@ -43,7 +43,10 @@ def CreateTask(**kwargs) -> dict: # type: ignore
4343
if request_body is None:
4444
logger("Nothing recieved in request body.")
4545
raise BadRequest("No request body recieved.")
46-
CreateTesTask(TesTask(**request_body)).create_task()
46+
tes_task = TesTask(**request_body)
47+
namespace = "tesk"
48+
task_creater = CreateTesTask(tes_task, namespace)
49+
task_creater.response()
4750
except Exception as e:
4851
raise InternalServerError from e
4952

tesk/api/ga4gh/tes/task/create_task.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from tesk.api.ga4gh.tes.models import TesTask
66
from tesk.api.kubernetes.client_wrapper import KubernetesClientWrapper
77
from tesk.api.kubernetes.constants import Constants
8-
from tesk.api.kubernetes.converter import TesKubernetesConverter
9-
from tesk.exceptions import KubernetesError
8+
from tesk.api.kubernetes.convert.converter import TesKubernetesConverter
109
from tesk.constants import TeskConstants
10+
from tesk.exceptions import KubernetesError
1111

1212
logger = logging.getLogger(__name__)
1313

@@ -33,8 +33,9 @@ def __init__(self, task: TesTask, namespace=TeskConstants.tesk_namespace):
3333
def create_task(self):
3434
"""Create TES task."""
3535
attempts_no = 0
36-
while attempts_no > self.constants.job_create_attempts_no:
36+
while attempts_no < self.constants.job_create_attempts_no:
3737
try:
38+
attempts_no += 1
3839
resources = self.task.resources
3940

4041
if resources and resources.ram_gb:
@@ -49,28 +50,27 @@ def create_task(self):
4950
)
5051
)
5152

53+
task_master_config_map = (
54+
self.tes_kubernetes_converter.from_tes_task_to_k8s_config_map(
55+
self.task,
56+
task_master_job,
57+
# user
58+
)
59+
)
60+
_ = self.kubernetes_client_wrapper.create_config_map(
61+
task_master_config_map
62+
)
63+
created_job = self.kubernetes_client_wrapper.create_job(task_master_job)
64+
print(task_master_config_map)
5265
print(task_master_job)
53-
54-
# TODO: Create ConfigMap
55-
# TODO: Create Job
56-
# TODO Return created job
57-
# task_master_config_map = converter.from_tes_task_to_k8s_config_map(
58-
# task, user, task_master_job
59-
# )
60-
# created_config_map = kubernetes_client_wrapper.create_config_map(
61-
# task_master_config_map
62-
# )
63-
# created_job = kubernetes_client_wrapper.create_job(task_master_job)
64-
# return converter.from_k8s_job_to_tes_create_task_response(created_job)
66+
return created_job.metadata.name
6567

6668
except KubernetesError as e:
67-
# Handle Kubernetes specific exceptions
6869
if (
6970
not e.is_object_name_duplicated()
7071
or attempts_no >= self.constants.job_create_attempts_no
7172
):
7273
raise e
73-
attempts_no += 1
7474

7575
except Exception as exc:
7676
logging.error("ERROR: In createTask", exc_info=True)

tesk/api/kubernetes/convert/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,57 @@
11
"""Module for converting TES tasks to Kubernetes jobs."""
22

3+
import base64
34
import gzip
45
import json
56
import logging
7+
from decimal import Decimal
68
from io import BytesIO
9+
from typing import Any
710

811
from kubernetes.client import (
912
V1ConfigMap,
1013
V1ConfigMapVolumeSource,
14+
V1Container,
15+
V1EnvVar,
1116
V1ObjectMeta,
17+
V1ResourceRequirements,
1218
V1Volume,
1319
)
1420
from kubernetes.client.models import V1Job
21+
from kubernetes.utils.quantity import parse_quantity # type: ignore
1522

1623
from tesk.api.ga4gh.tes.models import TesExecutor, TesResources, TesTask
1724
from tesk.api.kubernetes.constants import Constants, K8sConstants
18-
from tesk.api.kubernetes.template import KubernetesTemplateSupplier
25+
from tesk.api.kubernetes.convert.data.job import Job
26+
from tesk.api.kubernetes.convert.data.task import Task
27+
from tesk.api.kubernetes.convert.executor_command_wrapper import ExecutorCommandWrapper
28+
from tesk.api.kubernetes.convert.template import KubernetesTemplateSupplier
1929
from tesk.constants import TeskConstants
2030
from tesk.custom_config import TaskmasterEnvProperties
21-
from tesk.utils import get_taskmaster_env_property, get_taskmaster_template
31+
from tesk.utils import get_taskmaster_env_property, pydantic_model_list_json
2232

2333
logger = logging.getLogger(__name__)
2434

2535

2636
class TesKubernetesConverter:
37+
"""Convert TES requests to Kubernetes resources."""
38+
2739
def __init__(self, namespace=TeskConstants.tesk_namespace):
2840
"""Initialize the converter."""
2941
self.taskmaster_env_properties: TaskmasterEnvProperties = (
3042
get_taskmaster_env_property()
3143
)
44+
self.template_supplier = KubernetesTemplateSupplier(
45+
namespace=namespace
46+
# security_context=security_context
47+
)
3248
self.constants = Constants()
3349
self.k8s_constants = K8sConstants()
3450
self.namespace = namespace
3551

36-
# TODO: Add user to the mmethod when auth implemented in FOCA
52+
# TODO: Add user to the method when auth implemented in FOCA
3753
def from_tes_task_to_k8s_job(self, task: TesTask):
54+
"""Convert TES task to Kubernetes job."""
3855
taskmsater_job: V1Job = KubernetesTemplateSupplier(
3956
self.namespace
4057
).task_master_template()
@@ -48,6 +65,8 @@ def from_tes_task_to_k8s_job(self, task: TesTask):
4865
if taskmsater_job.metadata.labels is None:
4966
taskmsater_job.metadata.labels = {}
5067

68+
# taskmsater_job.metadata.name = task.name
69+
5170
taskmsater_job.metadata.annotations[self.constants.ann_testask_name_key] = (
5271
task.name
5372
)
@@ -84,21 +103,30 @@ def from_tes_task_to_k8s_job(self, task: TesTask):
84103
def from_tes_task_to_k8s_config_map(
85104
self,
86105
task: TesTask,
106+
job: V1Job,
87107
# user,
88-
job,
89-
):
108+
) -> V1ConfigMap:
109+
"""Create a Kubernetes ConfigMap from a TES task."""
90110
task_master_config_map = V1ConfigMap(
91111
metadata=V1ObjectMeta(name=job.metadata.name)
92112
)
113+
114+
task_master_config_map.metadata.labels = (
115+
task_master_config_map.metadata.labels or {}
116+
)
117+
task_master_config_map.metadata.annotations = (
118+
task_master_config_map.metadata.annotations or {}
119+
)
120+
93121
task_master_config_map.metadata.annotations[
94122
self.constants.ann_testask_name_key
95-
] = task["name"]
123+
] = task.name
96124
# task_master_config_map.metadata.labels[self.constants.label_userid_key] = user["username"]
97125

98-
if "tags" in task and "GROUP_NAME" in task["tags"]:
126+
if "tags" in task and "GROUP_NAME" in task.tags:
99127
task_master_config_map.metadata.labels[
100128
self.constants.label_groupname_key
101-
] = task["tags"]["GROUP_NAME"]
129+
] = task.tags["GROUP_NAME"]
102130
# elif user["is_member"]:
103131
# task_master_config_map.metadata.labels[self.constants.label_groupname_key] = user[
104132
# "any_group"
@@ -107,30 +135,42 @@ def from_tes_task_to_k8s_config_map(
107135
executors_as_jobs = [
108136
self.from_tes_executor_to_k8s_job(
109137
task_master_config_map.metadata.name,
110-
task["name"],
138+
task.name,
111139
executor,
112140
idx,
113-
task["resources"],
141+
task.resources,
114142
# user,
115143
)
116-
for idx, executor in enumerate(task["executors"])
144+
for idx, executor in enumerate(task.executors)
117145
]
118146

119-
task_master_input = {
120-
"inputs": task.inputs or [],
121-
"outputs": task.outputs or [],
147+
task_master_input: dict[str, Any] = {
148+
"inputs": pydantic_model_list_json(task.inputs) or [],
149+
"outputs": pydantic_model_list_json(task.outputs) or [],
122150
"volumes": task.volumes or [],
123-
"resources": {"disk_gb": task.resources.disk_gb or 10.0},
151+
"resources": {"disk_gb": float(task.resources.disk_gb) or 10.0},
124152
}
125-
task_master_input[self.constants.taskmaster_input_exec_key] = executors_as_jobs
153+
task_master_input[self.constants.taskmaster_input_exec_key] = [
154+
exec_job.to_dict() for exec_job in executors_as_jobs
155+
]
156+
157+
def decimal_to_float(obj):
158+
if isinstance(obj, Decimal):
159+
return float(obj)
160+
raise TypeError
126161

127-
task_master_input_as_json = json.dumps(task_master_input)
162+
taskmaster_input_as_json = json.loads(
163+
json.dumps(task_master_input, default=decimal_to_float)
164+
)
128165
try:
129166
with BytesIO() as obj:
130167
with gzip.GzipFile(fileobj=obj, mode="wb") as gzip_file:
131-
gzip_file.write(task_master_input_as_json.encode("utf-8"))
168+
json_data = json.dumps(taskmaster_input_as_json)
169+
gzip_file.write(json_data.encode("utf-8"))
132170
task_master_config_map.binary_data = {
133-
f"{self.constants.taskmaster_input}.gz": obj.getvalue()
171+
f"{self.constants.taskmaster_input}.gz": base64.b64encode(
172+
obj.getvalue()
173+
).decode("utf-8")
134174
}
135175
except Exception as e:
136176
logger.info(
@@ -140,7 +180,6 @@ def from_tes_task_to_k8s_config_map(
140180

141181
return task_master_config_map
142182

143-
144183
def from_tes_executor_to_k8s_job(
145184
self,
146185
generated_task_id: str,
@@ -151,38 +190,58 @@ def from_tes_executor_to_k8s_job(
151190
# user: User
152191
) -> V1Job:
153192
# Get new template executor Job object
154-
job = self.executor_template_supplier()
155-
193+
job: V1Job = self.template_supplier.executor_template()
194+
156195
# Set executors name based on taskmaster's job name
157-
Job(job).change_job_name(Task(generated_task_id).get_executor_name(executor_index))
158-
196+
Job(job).change_job_name(
197+
# Task(job, generated_task_id).get_executor_name(executor_index)
198+
"newname"
199+
)
200+
159201
# Put arbitrary labels and annotations
160202
job.metadata.labels = job.metadata.labels or {}
161-
job.metadata.labels["taskId"] = generated_task_id
162-
job.metadata.labels["execNo"] = str(executor_index)
163-
job.metadata.labels["userId"] = user.username
164-
203+
job.metadata.labels[self.constants.label_testask_id_key] = generated_task_id
204+
job.metadata.labels[self.constants.label_execno_key] = str(executor_index)
205+
# job.metadata.labels[self.constants.label_userid_key] = user.username
206+
165207
job.metadata.annotations = job.metadata.annotations or {}
166-
job.metadata.annotations["tesTaskName"] = tes_task_name
167-
168-
container = job.spec.template.spec.containers[0]
169-
208+
job.metadata.annotations[self.constants.ann_testask_name_key] = tes_task_name
209+
210+
container: V1Container = job.spec.template.spec.containers[0]
211+
212+
# TODO: Not sure what to do with this
170213
# Convert potential TRS URI into docker image
171-
container.image = self.trs_client.get_docker_image_for_tool_version_uri(executor.image)
172-
214+
# container.image = self.trs_client.get_docker_image_for_tool_version_uri(
215+
# executor.image
216+
# )
217+
218+
if not container.command:
219+
container.command = []
173220
# Map executor's command to job container's command
174-
for command in ExecutorCommandWrapper(executor).get_commands_with_stream_redirects():
175-
container.add_command_item(command)
176-
221+
for command in ExecutorCommandWrapper(
222+
executor
223+
).get_commands_with_stream_redirects():
224+
container.command.append(command)
225+
177226
if executor.env:
178-
container.env = [V1EnvVar(name=key, value=value) for key, value in executor.env.items()]
179-
227+
container.env = [
228+
V1EnvVar(name=key, value=value) for key, value in executor.env.items()
229+
]
230+
else:
231+
container.env = []
232+
180233
container.working_dir = executor.workdir
181-
234+
235+
container.resources = V1ResourceRequirements(requests={})
236+
182237
if resources.cpu_cores:
183-
container.resources.requests['cpu'] = QuantityFormatter().parse(str(resources.cpu_cores))
184-
238+
container.resources.requests["cpu"] = parse_quantity(
239+
str(resources.cpu_cores)
240+
)
241+
185242
if resources.ram_gb:
186-
container.resources.requests['memory'] = QuantityFormatter().parse(f"{resources.ram_gb:.6f}Gi")
187-
243+
container.resources.requests["memory"] = parse_quantity(
244+
f"{resources.ram_gb:.6f}Gi"
245+
)
246+
188247
return job

tesk/api/kubernetes/convert/data/__init__.py

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""A container for a single Kubernetes job object (can be both a taskmaster and an executor) and its list of worker pods (Kubernetes Pod objects)."""
2+
3+
from typing import List, Optional
4+
5+
from kubernetes.client import V1Job, V1Pod
6+
7+
8+
class Job:
9+
"""A container for a single Kubernetes job object (can be both a taskmaster and an executor) and its list of worker pods (Kubernetes Pod objects)"""
10+
11+
def __init__(self, job: V1Job):
12+
"""Initializes the Job with a Kubernetes job object."""
13+
self.job: V1Job = job
14+
self.pods: List[V1Pod] = []
15+
16+
def get_job(self) -> V1Job:
17+
"""Returns the Kubernetes job object."""
18+
return self.job
19+
20+
def add_pod(self, pod: V1Pod):
21+
"""Adds a single pod to the list (without any duplication checks)."""
22+
# TODO: This doesn't take care of duplication, use set on id or name
23+
self.pods.append(pod)
24+
25+
def has_pods(self) -> bool:
26+
"""Checks if the job has any pods."""
27+
return bool(self.pods)
28+
29+
def get_first_pod(self) -> Optional[V1Pod]:
30+
"""Returns arbitrarily chosen pod from the list (currently the first one added) or None if the job has no pods."""
31+
if not self.has_pods():
32+
return None
33+
return self.pods[0]
34+
35+
def get_pods(self) -> List[V1Pod]:
36+
"""Returns the list of job pods in the order of addition to the list or an empty list if no pods."""
37+
return self.pods
38+
39+
def change_job_name(self, new_name: str):
40+
"""Changes the job name, as well as the names in its metadata and container specs."""
41+
self.job.metadata.name = new_name
42+
self.job.spec.template.metadata.name = new_name
43+
if self.job.spec.template.spec.containers:
44+
self.job.spec.template.spec.containers[0].name = new_name
45+
46+
def get_job_name(self) -> Optional[str]:
47+
"""Returns the job name."""
48+
return self.job.metadata.name

0 commit comments

Comments
 (0)