Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,32 @@ After the last executor, the `filer` is called once more to process the outputs
and push them to remote locations from the PVC. The PVC is the scrubbed, deleted
and the taskmaster ends, completing the task.

┌─────────────────────────────────────────────────────────┐
│ Kubernetes │
│ │
│ ┌────────────────────────────┐ ┌───────────────────┐ │
│ │ Secret: ftp-secret │ │ ConfigMap/PVC │ │
│ │ - username │ │ - JSON_INPUT.gz │ │
│ │ - password │ │ │ │
│ └──────────▲─────────────────┘ └───────▲───────────┘ │
│ │ | │
│ │ | │
│ │ | │
│ ┌─────────┴────────────────────────────┴────────────┐ │
│ │ Job: taskmaster │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ Pod: taskmaster │ │ │
│ │ │ - Container: taskmaster │ │ │
│ │ │ - Env: TESK_FTP_USERNAME │ │ │
│ │ │ - Env: TESK_FTP_PASSWORD │ │ │
│ │ │ - Args: -f /jsoninput/JSON_INPUT.gz │ │ │
│ │ │ - Mounts: /podinfo │ │ │
│ │ │ /jsoninput │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘

Comment on lines +58 to +83

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just trying to understand k8s flow, please ignore, maybe will remove later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for me, but the rendered version does not look good to me:

Screenshot from 2024-08-05 10-55-34

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle it's nice to have such a schema in the docs.

I am, however, wondering about the FTP secret. Maybe this will become clearer to me after looking at the code below, but seeing it represented like this in the doc, I have the concern that FTP is somehow treated special, when, ideally, storage solutions should all be treated in an abstract manner, like previously discussed: abstract storage handler and individual implementations for different storage/file transfer solutions. And that should probably extend to managing secrets/credentials as well, no?

## Requirements

- A working [Kubernetes](https://kubernetes.io/) cluster version 1.9 and later.
Expand Down
1 change: 1 addition & 0 deletions compressed_data

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this used for? Consider giving it a more expressive (and less ambiguous - compressed could indicate sth like ZIP or GZ) name. Also consider giving it a file extension (.json, .txt ?). Finally, consider arranging test data in a separate folder data/ or tests/data/.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"inputs": ["{\"name\": null, \"description\": null, \"url\": \"s3://my-object-store/file1\", \"path\": \"/data/file1\", \"type\": \"FILE\", \"content\": null, \"streamable\": null}"], "outputs": ["{\"name\": null, \"description\": null, \"url\": \"s3://my-object-store/outfile-1\", \"path\": \"/data/outfile\", \"path_prefix\": null, \"type\": \"FILE\"}"], "volumes": ["/vol/A/"], "resources": {"disk_gb": 40.0}, "executors": [{"api_version": "batch/v1", "kind": "Job", "metadata": {"annotations": {"tes-task-name": "string"}, "creation_timestamp": null, "deletion_grace_period_seconds": null, "deletion_timestamp": null, "finalizers": null, "generate_name": null, "generation": null, "labels": {"job-type": "executor", "taskmaster-name": "task-fef96900-6da7-4672-a1ec-14bf3d720c8f", "executor-no": "0"}, "managed_fields": null, "name": "newname", "namespace": null, "owner_references": null, "resource_version": null, "self_link": null, "uid": null}, "spec": {"active_deadline_seconds": null, "backoff_limit": null, "backoff_limit_per_index": null, "completion_mode": null, "completions": null, "manual_selector": null, "max_failed_indexes": null, "parallelism": null, "pod_failure_policy": null, "pod_replacement_policy": null, "selector": null, "suspend": null, "template": {"metadata": {"annotations": null, "creation_timestamp": null, "deletion_grace_period_seconds": null, "deletion_timestamp": null, "finalizers": null, "generate_name": null, "generation": null, "labels": null, "managed_fields": null, "name": "newname", "namespace": null, "owner_references": null, "resource_version": null, "self_link": null, "uid": null}, "spec": {"active_deadline_seconds": null, "affinity": null, "automount_service_account_token": null, "containers": [{"args": null, "command": ["/bin/sh", "-c", " < /data/file1 > /tmp/stdout.log 2> /tmp/stderr.log"], "env": [{"name": "BLASTDB", "value": "/data/GRC38", "value_from": null}, {"name": "HMMERDB", "value": "/data/hmmer", "value_from": null}], "env_from": null, "image": null, "image_pull_policy": null, "lifecycle": null, "liveness_probe": null, "name": "newname", "ports": null, "readiness_probe": null, "resize_policy": null, "resources": {"claims": null, "limits": null, "requests": {"cpu": 4.0, "memory": 8589934592.0}}, "restart_policy": null, "security_context": null, "startup_probe": null, "stdin": null, "stdin_once": null, "termination_message_path": null, "termination_message_policy": null, "tty": null, "volume_devices": null, "volume_mounts": null, "working_dir": "/data/"}], "dns_config": null, "dns_policy": null, "enable_service_links": null, "ephemeral_containers": null, "host_aliases": null, "host_ipc": null, "host_network": null, "host_pid": null, "host_users": null, "hostname": null, "image_pull_secrets": null, "init_containers": null, "node_name": null, "node_selector": null, "os": null, "overhead": null, "preemption_policy": null, "priority": null, "priority_class_name": null, "readiness_gates": null, "resource_claims": null, "restart_policy": "Never", "runtime_class_name": null, "scheduler_name": null, "scheduling_gates": null, "security_context": null, "service_account": null, "service_account_name": null, "set_hostname_as_fqdn": null, "share_process_namespace": null, "subdomain": null, "termination_grace_period_seconds": null, "tolerations": null, "topology_spread_constraints": null, "volumes": null}}, "ttl_seconds_after_finished": null}, "status": null}]}
2 changes: 1 addition & 1 deletion deployment/charts/tesk/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ host_name: ""
#

# 'openstack' or 's3'
storage: none
storage: s3

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: turn back to none.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏽

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be described in more detail somewhere. Also, if none is an allowed value, this should be listed, and not just openstack and s3.


# Configurable storage class.
storageClass:
Expand Down
72 changes: 72 additions & 0 deletions deployment/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,78 @@ custom:
tesResources_backend_parameters:
- VmSize
- ParamToRecogniseDataComingFromConfig
# Taskmaster configuration
taskmaster_template:
apiVersion: batch/v1
kind: Job
metadata:
name: taskmaster
labels:
app: taskmaster
spec:
template:
metadata:
name: taskmaster
spec:
serviceAccountName: default
containers:
- name: taskmaster
image: docker.io/elixircloud/tesk-core-taskmaster:v0.10.2
args:
- -f
- /jsoninput/JSON_INPUT.gz
env:
- name: TESK_FTP_USERNAME
valueFrom:
secretKeyRef:
name: ftp-secret
key: username
optional: true
- name: TESK_FTP_PASSWORD
valueFrom:
secretKeyRef:
name: ftp-secret
key: password
optional: true
volumeMounts:
- name: podinfo
mountPath: /podinfo
readOnly: true
- name: jsoninput
mountPath: /jsoninput
readOnly: true
volumes:
- name: podinfo
downwardAPI:
items:
- path: labels
fieldRef:
fieldPath: metadata.labels
restartPolicy: Never
# Taskmaster environment properties
taskmaster_env_properties:
# Taskmaster image name
imageName: docker.io/elixircloud/tesk-core-taskmaster
# Taskmaster image version
imageVersion: latest
# Filer image name
filerImageName: docker.io/elixircloud/tesk-core-filer
# Filer image version
filerImageVersion: latest
# Test FTP account settings
ftp:
# Name of the secret with FTP account credentials
secretName: account-secret
# If FTP account enabled (based on non-emptiness of secretName)
enabled: false
# If verbose (debug) mode of taskmaster is on (passes additional flag to taskmaster and sets image pull policy to Always)
debug: false
# Environment variables, that will be passed to taskmaster
environment:
key: value
# Service Account name for taskmaster
serviceAccountName: default

Comment thread
uniqueg marked this conversation as resolved.

# Logging configuration
# Cf. https://foca.readthedocs.io/en/latest/modules/foca.models.html#foca.models.config.LogConfig
Expand Down
1 change: 1 addition & 0 deletions encoded_data.txt

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, consider moving to dedicated data folder.

Also, consider using a file naming pattern that, when read left to right, goes from more generic to more specific, e.g., data_encoded.txt and data_compressed.txt instead of encoded_data.txt and compressed_data.txt. This helps keeping related data together when sorted alphanumerically.

Even better to add something more expressive to explain the use case of these files, i.e., data_USE_CASE_encoded.txt and data_USE_CASE_compressed.txt.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
H4sIAJUer2YC/9VWS3PbNhD+KxqeQ1N+xLYync4kbfqapIe0tzqDAYElhQivAKAsx+P/3gVJieDD7qWXXDTCt+A+v93FYya0bYLP3qz+yR7vMk0V3OFBN1K+Wt1lHDxzwgZhdAo3TsbjXeYv3xSFeshN+QVYyH0wDopKSDi/y+JFS8O2u1lwGmgqCg8WOtEvv39432HM6AA6pKZ8cEAVLeXJr6fs86tVZprwvzuOOqOD+TPO9+JBSKyDShxSA9OoOmf3RjYKOmcLPBRvixZ34E3jWCt5zLjwO1KX+P9qfbZ+QjkcgDXoWvvlY0atIHtwHoNCICtpYNtif47+ZDuhecT+MGU8Kgg0+tzqpVqbQGMqOjsBfB6o3+UxafEjzLHQdRYtMsx2vEmCQIcDVfYUHOZUQiurHWVALDhhOPGAVeN+4dqCikpoKsU3cMn9GjQ4GoD07ozhLtYek7QE2QXxxZR5THb0/5imGHkMTFEfwJ3Ca2OtoNpcb9br/JrTm/zq+uYip+fA8vOrsrrkNxdrdltlScpzbeK36zYrimpaAyeVAJmGerSg4b7920PeYn6GW+YeIyFIFXCgu2L3kmP9k6r2Eg+yIlLo3QA1gh87IF6wwLrqsiD2QDhQjvdhXo+Ssp2pojYlwjNwLCZBCsFhuMCMsn0lleGwJEisYIoaKtG6xH7CUiSCA6ko9g3vDKTxW+qolCCFVwmIpIofNA45ZqRgD2OZAysxvwoHxUw+N+8bzJTmAxAAvUe6tcl7oU+OwX4fDTFU4fslKq0wGyIk5aTYico0WGgPbi/QPmWsPQezA51SUgeKSk+T0tV+RFhMDO/Gbyl04bcxAzmLv6vVD6tkOa1+XBVB2cIHjvP+TJp6dTFA4FyE2uENet8ZOyb33Ye3f/3987uodE9l02Kd5l8//XR5e8JJ5YwaMjQo+O3jx/efFhVslQK3rKB3JcUQEQp5MDkSi4dZy0hRAXtgElJoj1zzHvebKeFFFlnjwognlIulT5FAyPKZ8fECZJIK5VM/cDSNtH9tsIH6y7aJm/Js3a47ZVxUe/v6drO5vHq9ucAF+tQZCNQtDQrWOOQaaZ8ch2QutvcbOw0Aiy/05EiMTtsHd47Cdu4mJuagTTk+E/7jxsSzkDZA93DAnonk9zO87Y0EvjcOHwI14cIN3MlainDtY6yVqJPBhNjUPOj41jr1W+zsxADYLQ5eHNpk1HG9dGt8IDjOqIcpKiybIBpCdHeCWsEnSONnNsZzMeE2VtVBmpA4TxZd1bjRJvO1heYLxCRfGRx/W6R4spAc4D5pCzrbVbgN3GicHRGCRMcWGZsfWqfG2b80e6ftMeN29ifsuzHhkBe4bBYNebYF3kic9It45M/EgxeaZTyWnxVMTUEgx0IS6kn1lae9taVx9TvDoHd+sqd8U3KjaNqPaWe9vI2Dkf0aHYGYRFM/EG9jHWKg+ChGC2HWdEegHS8hyKMFQit0gsQV5jGR6QbER0Vz+uzz07/JtQSDdQ0AAA==
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ requires = ["poetry-core"]

[tool.bandit]
skips = [
"B101", # Use of asserts is discouraged as it is removed during byte generation, we don't need that!
"B108", # Insecure usage of temp file/directory, false positive.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But are you sure that this will always be false positives?
If not, better to disable per line/block/module

"B321", # FTP-related functions are being called.
"B402", # A FTP-related module is being imported.
"B108" # Insecure usage of temp file/directory, false positive.
"B402" # A FTP-related module is being imported.
]

[tool.poetry]
Expand Down
18 changes: 14 additions & 4 deletions tesk/api/ga4gh/tes/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import logging

# from connexion import request # type: ignore
from foca.utils.logging import log_traffic # type: ignore

from tesk.api.ga4gh.tes.models import TesTask
from tesk.api.ga4gh.tes.service_info.service_info import ServiceInfo
from tesk.api.ga4gh.tes.task.create_task import CreateTesTask
from tesk.exceptions import BadRequest, InternalServerError

# Get logger instance
logger = logging.getLogger(__name__)
Expand All @@ -26,14 +28,22 @@ def CancelTask(id, *args, **kwargs) -> dict: # type: ignore

# POST /tasks
@log_traffic
def CreateTask(*args, **kwargs) -> dict: # type: ignore
def CreateTask(**kwargs) -> dict[str, str]: # type: ignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the type: ignore here after implementation?

"""Create task.

Args:
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
pass
try:
request_body = kwargs.get("body")
if request_body is None:
logger.error("Nothing received in request body.")
raise BadRequest("No request body received.")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connexion should handle BadRequest/400 errors automatically, unless that functionality is specifically disabled in the config (which it isn't). So no need for this code, I believe (can be easily tested).

tes_task = TesTask(**request_body)
namespace = "tesk"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just seen that this value is hardwired to tesk. In the java implementation, the TESK API is running in the same namespace that it will create the jobs, so you can deploy it in a shared cluster and call the namespace whatever you want.

I am not sure what others think, but this will be an issue for CSC

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't be an issue I plan on removing it from here, I'll add this to docs config or as env variable so when launching the api anyone can configure it.

return CreateTesTask(tes_task, namespace).response()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except when calling well known builtin functions, I think it improves readibility a lot to name params when passing args. It also helps prevent bugs that may arise from using positional args. Also, in this case, it avoids the need to define a variable just to be clear about what you are passing.

So consider using:

return CreateTesTask(task=tes_task, namespace="tesk").response()

except Exception as e:
raise InternalServerError from e
Comment on lines +37 to +46

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error handling looks wrong/weird to me:

  1. The generic catching of Exception means that every exception will raise a 500 error, no? So there is no chance of the route ever raising a 400, 401, 403 etc. error. Surely this is not what we want.
  2. FOCA already handles errors in application context appropriately. Specifically, all errors not listed in the exceptions dictionary are raised as 500 errors (see here), so this code is redundant.



# GET /tasks/service-info
Expand Down
2 changes: 1 addition & 1 deletion tesk/api/ga4gh/tes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ class TesResources(BaseModel):
example={"VmSize": "Standard_D64_v3"},
)
backend_parameters_strict: Optional[bool] = Field(
False,
default=False,
description="If set to true, backends should fail the task if any "
"backend_parameters\nkey/values are unsupported, otherwise, backends should "
"attempt to run the task",
Expand Down
1 change: 1 addition & 0 deletions tesk/api/ga4gh/tes/task/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Task API controller logic."""
87 changes: 87 additions & 0 deletions tesk/api/ga4gh/tes/task/create_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""TESK API module for creating a task."""

import logging

from tesk.api.ga4gh.tes.models import TesResources, TesTask
from tesk.api.kubernetes.client_wrapper import KubernetesClientWrapper
from tesk.api.kubernetes.constants import Constants
from tesk.api.kubernetes.convert.converter import TesKubernetesConverter
from tesk.constants import TeskConstants
from tesk.exceptions import KubernetesError

logger = logging.getLogger(__name__)


class CreateTesTask:
"""Create TES task."""

# TODO: Add user to the class when auth implemented in FOCA
def __init__(self, task: TesTask, namespace=TeskConstants.tesk_namespace):
"""Initialize the CreateTask class.

Args:
task: TES task to create.
namespace: Kubernetes namespace where the task is created.
"""
self.task = task
# self.user = user
self.kubernetes_client_wrapper = KubernetesClientWrapper()
self.namespace = namespace
self.tes_kubernetes_converter = TesKubernetesConverter(self.namespace)
self.constants = Constants()

def create_task(self) -> dict[str, str]:
"""Create TES task."""
attempts_no = 0
while attempts_no < self.constants.job_create_attempts_no:
try:
attempts_no += 1
resources = self.task.resources

minimum_ram_gb = self.kubernetes_client_wrapper.minimum_ram_gb()
if not self.task.resources:
self.task.resources = TesResources(cpu_cores=int(minimum_ram_gb))
if resources and resources.ram_gb and resources.ram_gb < minimum_ram_gb:
self.task.resources.ram_gb = minimum_ram_gb

task_master_job = (
self.tes_kubernetes_converter.from_tes_task_to_k8s_job(
self.task,
# self.user
)
)

task_master_config_map = (
self.tes_kubernetes_converter.from_tes_task_to_k8s_config_map(
self.task,
task_master_job,
# self.user
)
)

# Create ConfigMap and Job
_ = self.kubernetes_client_wrapper.create_config_map(
task_master_config_map
)
created_job = self.kubernetes_client_wrapper.create_job(task_master_job)

assert created_job.metadata is not None
assert created_job.metadata.name is not None

return {"id": created_job.metadata.name}

except KubernetesError as e:
if (
not e.is_object_name_duplicated()
or attempts_no >= self.constants.job_create_attempts_no
):
raise e

except Exception as exc:
logging.error("ERROR: In createTask", exc_info=True)
raise exc
return {} # Dummy return to silence mypy

def response(self) -> dict[str, str]:
"""Create response."""
return self.create_task()
1 change: 1 addition & 0 deletions tesk/api/kubernetes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Kubernetes API module."""
Loading