container-hub 0.2.0__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.
- container_hub/__init__.py +34 -0
- container_hub/carriers/__init__.py +0 -0
- container_hub/carriers/docker/__init__.py +0 -0
- container_hub/carriers/docker/backend.py +178 -0
- container_hub/carriers/docker/tests/__init__.py +0 -0
- container_hub/carriers/docker/tests/test_docker_backend.py +122 -0
- container_hub/carriers/kubernetes/__init__.py +0 -0
- container_hub/carriers/kubernetes/backend.py +278 -0
- container_hub/carriers/kubernetes/tests/__init__.py +0 -0
- container_hub/carriers/kubernetes/tests/test_kubernetes_backend.py +78 -0
- container_hub/exceptions.py +6 -0
- container_hub/models.py +287 -0
- container_hub/version.py +3 -0
- container_hub-0.2.0.dist-info/METADATA +338 -0
- container_hub-0.2.0.dist-info/RECORD +19 -0
- container_hub-0.2.0.dist-info/WHEEL +5 -0
- container_hub-0.2.0.dist-info/licenses/AUTHORS.rst +13 -0
- container_hub-0.2.0.dist-info/licenses/LICENSE +22 -0
- container_hub-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .exceptions import InvalidConfiguration
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_backend(settings, prefix="CONTAINER_HUB"):
|
|
5
|
+
"""
|
|
6
|
+
Get a backend based on simple-settings or Django settings object.
|
|
7
|
+
|
|
8
|
+
Uses `CONTAINER_HUB_CARRIER` = ["docker", "kubernetes"] to determine
|
|
9
|
+
the backend.
|
|
10
|
+
"""
|
|
11
|
+
if not hasattr(settings, f"{prefix}_CARRIER"):
|
|
12
|
+
raise InvalidConfiguration(f"{prefix}_CARRIER is a mandatory setting")
|
|
13
|
+
opt = str(getattr(settings, f"{prefix}_CARRIER")).lower()
|
|
14
|
+
|
|
15
|
+
if opt == "docker":
|
|
16
|
+
from container_hub.carriers.docker.backend import (
|
|
17
|
+
DockerBackend,
|
|
18
|
+
DockerBackendConfig,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return DockerBackend(DockerBackendConfig.from_settings(settings, prefix))
|
|
22
|
+
elif opt == "kubernetes":
|
|
23
|
+
from container_hub.carriers.kubernetes.backend import (
|
|
24
|
+
KubernetesBackend,
|
|
25
|
+
KubernetesBackendConfig,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return KubernetesBackend(
|
|
29
|
+
KubernetesBackendConfig.from_settings(settings, prefix)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
raise InvalidConfiguration(
|
|
33
|
+
f"Unknown carrier {opt} option, should either be 'docker' or 'kubernetes'"
|
|
34
|
+
)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
|
|
6
|
+
from docker import DockerClient
|
|
7
|
+
from docker.errors import APIError, ContainerError, DockerException, NotFound
|
|
8
|
+
from docker.types import Mount
|
|
9
|
+
|
|
10
|
+
from container_hub.exceptions import CarrierError
|
|
11
|
+
from container_hub.models import ContainerConfig, DockerBackendConfig
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DockerBackend:
|
|
17
|
+
"""
|
|
18
|
+
Backend for starting Docker instances via Docker
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: DockerBackendConfig):
|
|
22
|
+
self.config = config
|
|
23
|
+
|
|
24
|
+
@cached_property
|
|
25
|
+
def client(self):
|
|
26
|
+
return DockerClient(base_url=self.config.client_url)
|
|
27
|
+
|
|
28
|
+
def container_hosts(self) -> Dict[str, str]:
|
|
29
|
+
"""
|
|
30
|
+
Always localhost, so return emtpy dict
|
|
31
|
+
"""
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
def container_list(self) -> List[str]:
|
|
35
|
+
"""
|
|
36
|
+
Returns a list of simulation_ids
|
|
37
|
+
"""
|
|
38
|
+
lc = self.client.containers.list(filters={"label": "simulation_id"})
|
|
39
|
+
return [x.name.lstrip("simulation-") for x in lc]
|
|
40
|
+
|
|
41
|
+
def container_ips(self) -> Dict[str, str]:
|
|
42
|
+
"""
|
|
43
|
+
Return list of ip addresses
|
|
44
|
+
"""
|
|
45
|
+
d = {}
|
|
46
|
+
containers = self.client.containers.list(filters={"label": "simulation_id"})
|
|
47
|
+
for container in containers:
|
|
48
|
+
try:
|
|
49
|
+
ip_address = container.attrs["NetworkSettings"]["Networks"][
|
|
50
|
+
self.config.network_name
|
|
51
|
+
]["IPAddress"]
|
|
52
|
+
except KeyError:
|
|
53
|
+
continue
|
|
54
|
+
d[container.name] = ip_address
|
|
55
|
+
return d
|
|
56
|
+
|
|
57
|
+
def up(self, container_config: ContainerConfig) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Create container based on simulation and threedimodel.
|
|
60
|
+
|
|
61
|
+
:returns the container id
|
|
62
|
+
"""
|
|
63
|
+
name = f"simulation-{container_config.sim_uid}"
|
|
64
|
+
result_path = container_config.base_result_path / Path(name)
|
|
65
|
+
labels = dict([(x.name, x.value) for x in container_config.labels])
|
|
66
|
+
labels.update({"simulation_id": f"{container_config.sim_uid}"})
|
|
67
|
+
|
|
68
|
+
_envs = dict([(f"{x.name}", f"{x.value}") for x in container_config.envs])
|
|
69
|
+
_envs.update({"RESULT_PATH": f"{result_path.as_posix()}"})
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
container_config.container_log_level is not None
|
|
73
|
+
and "LOG_LEVEL" not in _envs
|
|
74
|
+
):
|
|
75
|
+
_envs.update(
|
|
76
|
+
{f"LOG_LEVEL": f"{container_config.container_log_level.value}"}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if container_config.debugpy_port is not None:
|
|
80
|
+
ports = {
|
|
81
|
+
f"{container_config.debugpy_port}/tcp": container_config.debugpy_port
|
|
82
|
+
}
|
|
83
|
+
_envs.update(
|
|
84
|
+
{f"DEBUGPY": "1", "DEBUGPY_PORT": str(container_config.debugpy_port)}
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
ports = {}
|
|
88
|
+
|
|
89
|
+
cmd = (
|
|
90
|
+
f"python service.py {container_config.redis_host} {container_config.model_config} "
|
|
91
|
+
f"{container_config.sim_uid} {container_config.sim_ref_datetime.isoformat()} "
|
|
92
|
+
f"{container_config.end_time} {container_config.duration} {container_config.start_mode} "
|
|
93
|
+
f"{container_config.pause_timeout} {container_config.max_rate} {container_config.clean_up_files}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
skip_model_mount: bool = all(
|
|
97
|
+
[
|
|
98
|
+
x
|
|
99
|
+
for x in [
|
|
100
|
+
container_config.gridadmin_download_url,
|
|
101
|
+
container_config.tables_download_url,
|
|
102
|
+
]
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if container_config.gridadmin_download_url is not None:
|
|
107
|
+
cmd += f" {container_config.gridadmin_download_url}"
|
|
108
|
+
if container_config.tables_download_url is not None:
|
|
109
|
+
cmd += f" {container_config.tables_download_url}"
|
|
110
|
+
|
|
111
|
+
logger.debug("cmd %s", cmd)
|
|
112
|
+
logger.debug("Envs %s", _envs)
|
|
113
|
+
|
|
114
|
+
mounts: List[Mount] = []
|
|
115
|
+
for mount in container_config.mount_points:
|
|
116
|
+
if skip_model_mount and mount.mount_path == "/models":
|
|
117
|
+
# Skip mounting models
|
|
118
|
+
continue
|
|
119
|
+
mounts.append(
|
|
120
|
+
Mount(
|
|
121
|
+
mount.mount_path,
|
|
122
|
+
mount.local_path,
|
|
123
|
+
type="bind",
|
|
124
|
+
read_only=mount.read_only,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
container = self.client.containers.run(
|
|
130
|
+
image=container_config.image_name,
|
|
131
|
+
command=cmd,
|
|
132
|
+
name=name,
|
|
133
|
+
network=self.config.network_name,
|
|
134
|
+
mounts=mounts,
|
|
135
|
+
environment=_envs,
|
|
136
|
+
detach=True,
|
|
137
|
+
labels=labels,
|
|
138
|
+
ports=ports,
|
|
139
|
+
)
|
|
140
|
+
except (DockerException, APIError, ContainerError) as err:
|
|
141
|
+
logger.error(err)
|
|
142
|
+
raise CarrierError(err)
|
|
143
|
+
|
|
144
|
+
# double check everything went right
|
|
145
|
+
try:
|
|
146
|
+
self.client.containers.get(container.id)
|
|
147
|
+
except (APIError, NotFound) as err:
|
|
148
|
+
logger.error(
|
|
149
|
+
f"simulation container exited prematurely. Could not retrieve "
|
|
150
|
+
f"the container if though it should be running {err}"
|
|
151
|
+
)
|
|
152
|
+
raise CarrierError(err)
|
|
153
|
+
|
|
154
|
+
logger.info(f"Started simulation container {container.name}")
|
|
155
|
+
return container.id
|
|
156
|
+
|
|
157
|
+
def down(self, sim_uid: str):
|
|
158
|
+
try:
|
|
159
|
+
container = self.client.containers.get(f"simulation-{sim_uid}")
|
|
160
|
+
container_id = container.id
|
|
161
|
+
except (APIError, NotFound) as err:
|
|
162
|
+
logger.error(
|
|
163
|
+
f"Could not get the simulation container, error message: {err}"
|
|
164
|
+
)
|
|
165
|
+
raise CarrierError(err)
|
|
166
|
+
try:
|
|
167
|
+
if not self.config.debug:
|
|
168
|
+
container.remove(force=True)
|
|
169
|
+
else:
|
|
170
|
+
container.kill()
|
|
171
|
+
except APIError as err:
|
|
172
|
+
logger.error(
|
|
173
|
+
f"Could not kill/remove the "
|
|
174
|
+
f"simulation container, error message: {err}"
|
|
175
|
+
)
|
|
176
|
+
raise CarrierError(err)
|
|
177
|
+
logger.info(f"Removed container for simulation {sim_uid}")
|
|
178
|
+
return container_id
|
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from docker.types import Mount
|
|
6
|
+
|
|
7
|
+
from container_hub.carriers.docker.backend import DockerBackend
|
|
8
|
+
from container_hub.models import (
|
|
9
|
+
ContainerConfig,
|
|
10
|
+
DockerBackendConfig,
|
|
11
|
+
EnvVar,
|
|
12
|
+
Label,
|
|
13
|
+
LogLevel,
|
|
14
|
+
MountPoint,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def docker_backend():
|
|
20
|
+
config = DockerBackendConfig(
|
|
21
|
+
"client_url",
|
|
22
|
+
"my_network",
|
|
23
|
+
)
|
|
24
|
+
return DockerBackend(config)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_container_list(docker_backend: DockerBackend):
|
|
28
|
+
with patch("container_hub.carriers.docker.backend.DockerClient") as client:
|
|
29
|
+
container = MagicMock()
|
|
30
|
+
container.name = "simulation-112"
|
|
31
|
+
client().containers.list.return_value = [container]
|
|
32
|
+
containers = docker_backend.container_list()
|
|
33
|
+
assert containers == [
|
|
34
|
+
"112",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_container_ips(docker_backend: DockerBackend):
|
|
39
|
+
with patch("container_hub.carriers.docker.backend.DockerClient") as client:
|
|
40
|
+
container = MagicMock()
|
|
41
|
+
container.name = "simulation-112"
|
|
42
|
+
container.attrs = {
|
|
43
|
+
"NetworkSettings": {
|
|
44
|
+
"Networks": {
|
|
45
|
+
docker_backend.config.network_name: {"IPAddress": "127.0.0.1"}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
client().containers.list.return_value = [container]
|
|
50
|
+
ip_addresses = docker_backend.container_ips()
|
|
51
|
+
assert ip_addresses == {"simulation-112": "127.0.0.1"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_up(docker_backend: DockerBackend):
|
|
55
|
+
dt = datetime.now()
|
|
56
|
+
container_config = ContainerConfig(
|
|
57
|
+
"my_image",
|
|
58
|
+
"base_result_path",
|
|
59
|
+
12,
|
|
60
|
+
dt,
|
|
61
|
+
3600,
|
|
62
|
+
3600,
|
|
63
|
+
0,
|
|
64
|
+
"initialize",
|
|
65
|
+
"/model.ini",
|
|
66
|
+
2,
|
|
67
|
+
512,
|
|
68
|
+
[EnvVar("env", "1")],
|
|
69
|
+
[Label("name", "value")],
|
|
70
|
+
0,
|
|
71
|
+
True,
|
|
72
|
+
"gridadmin_url",
|
|
73
|
+
"tables_download_url",
|
|
74
|
+
[MountPoint("/local", "/mnt", False)],
|
|
75
|
+
"redis1",
|
|
76
|
+
LogLevel.debug,
|
|
77
|
+
5678,
|
|
78
|
+
)
|
|
79
|
+
with patch("container_hub.carriers.docker.backend.DockerClient") as client:
|
|
80
|
+
container = MagicMock()
|
|
81
|
+
container.id = 10
|
|
82
|
+
client().containers.run.return_value = container
|
|
83
|
+
container_id = docker_backend.up(container_config)
|
|
84
|
+
assert container_id == 10
|
|
85
|
+
|
|
86
|
+
# Check all params for DockerClient().containers.run
|
|
87
|
+
to_check = {
|
|
88
|
+
"image": "my_image",
|
|
89
|
+
"command": f"python service.py redis1 /model.ini 12 {dt.isoformat()} 3600 3600 initialize 0 0 True gridadmin_url tables_download_url",
|
|
90
|
+
"name": "simulation-12",
|
|
91
|
+
"network": "my_network",
|
|
92
|
+
"mounts": [
|
|
93
|
+
Mount(
|
|
94
|
+
**{
|
|
95
|
+
"target": "/mnt",
|
|
96
|
+
"source": "/local",
|
|
97
|
+
"type": "bind",
|
|
98
|
+
"read_only": False,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
],
|
|
102
|
+
"environment": {
|
|
103
|
+
"env": "1",
|
|
104
|
+
"RESULT_PATH": "base_result_path/simulation-12",
|
|
105
|
+
"LOG_LEVEL": "DEBUG",
|
|
106
|
+
"DEBUGPY": "1",
|
|
107
|
+
"DEBUGPY_PORT": "5678",
|
|
108
|
+
},
|
|
109
|
+
"ports": {"5678/tcp": 5678},
|
|
110
|
+
"detach": True,
|
|
111
|
+
"labels": {"name": "value", "simulation_id": "12"},
|
|
112
|
+
}
|
|
113
|
+
assert client().containers.run.call_args[1] == to_check
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_down(docker_backend: DockerBackend):
|
|
117
|
+
with patch("container_hub.carriers.docker.backend.DockerClient") as client:
|
|
118
|
+
container = MagicMock()
|
|
119
|
+
container.id = 112
|
|
120
|
+
client().containers.get.return_value = container
|
|
121
|
+
container_id = docker_backend.down("112")
|
|
122
|
+
assert container_id == 112
|
|
File without changes
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
from typing import Any, Dict, List, Union
|
|
3
|
+
|
|
4
|
+
from hikaru.model.rel_1_28 import (
|
|
5
|
+
Affinity,
|
|
6
|
+
ConfigMapVolumeSource,
|
|
7
|
+
Container,
|
|
8
|
+
ContainerPort,
|
|
9
|
+
EmptyDirVolumeSource,
|
|
10
|
+
EnvVar,
|
|
11
|
+
HostAlias,
|
|
12
|
+
HostPathVolumeSource,
|
|
13
|
+
Job,
|
|
14
|
+
JobSpec,
|
|
15
|
+
LocalObjectReference,
|
|
16
|
+
NodeAffinity,
|
|
17
|
+
NodeSelector,
|
|
18
|
+
NodeSelectorRequirement,
|
|
19
|
+
NodeSelectorTerm,
|
|
20
|
+
ObjectMeta,
|
|
21
|
+
PodSpec,
|
|
22
|
+
PodTemplateSpec,
|
|
23
|
+
ResourceRequirements,
|
|
24
|
+
SecretVolumeSource,
|
|
25
|
+
Volume,
|
|
26
|
+
VolumeMount,
|
|
27
|
+
)
|
|
28
|
+
from kubernetes import config as client_config
|
|
29
|
+
from kubernetes.client import (
|
|
30
|
+
ApiClient,
|
|
31
|
+
ApiException,
|
|
32
|
+
BatchV1Api,
|
|
33
|
+
Configuration,
|
|
34
|
+
CoreV1Api,
|
|
35
|
+
)
|
|
36
|
+
from kubernetes.client.models import V1DeleteOptions, V1Job, V1JobList, V1JobStatus
|
|
37
|
+
|
|
38
|
+
from container_hub.exceptions import CarrierError
|
|
39
|
+
from container_hub.models import (
|
|
40
|
+
KubernetesBackendConfig,
|
|
41
|
+
KubernetesContainer,
|
|
42
|
+
KubernetesJobConfig,
|
|
43
|
+
MountPointType,
|
|
44
|
+
NodeAffinityConfig,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Accessing host (laptop) from within k8s cluster
|
|
48
|
+
K3S_HOST_DNS_NAME = "host.k3d.internal"
|
|
49
|
+
MINIKUBE_HOST_DNS_NAME = "host.minikube.internal"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class KubernetesBackend:
|
|
53
|
+
"""
|
|
54
|
+
Backend for starting Docker instances via Docker
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, config: KubernetesBackendConfig, in_cluster=True):
|
|
58
|
+
self.config = config
|
|
59
|
+
self.in_cluster = in_cluster
|
|
60
|
+
|
|
61
|
+
@cached_property
|
|
62
|
+
def configuration(self) -> Configuration:
|
|
63
|
+
configuration = Configuration(host=self.config.client_url)
|
|
64
|
+
if self.in_cluster:
|
|
65
|
+
client_config.load_incluster_config(configuration)
|
|
66
|
+
|
|
67
|
+
return configuration
|
|
68
|
+
|
|
69
|
+
def container_hosts(self) -> Dict[str, str]:
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
def get_job_status(self, job_name: str) -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Get a specific pod ip, based on job_name (simulation-xxx)
|
|
75
|
+
"""
|
|
76
|
+
job: Union[V1Job, None] = None
|
|
77
|
+
with ApiClient(self.configuration) as api_client:
|
|
78
|
+
api = BatchV1Api(api_client)
|
|
79
|
+
try:
|
|
80
|
+
job: V1Job = api.read_namespaced_job(
|
|
81
|
+
job_name, namespace=self.config.namespace
|
|
82
|
+
)
|
|
83
|
+
except ApiException:
|
|
84
|
+
job = None
|
|
85
|
+
|
|
86
|
+
if job is None:
|
|
87
|
+
raise CarrierError("unknown Job")
|
|
88
|
+
|
|
89
|
+
if job.status is None:
|
|
90
|
+
raise CarrierError("Job has no status")
|
|
91
|
+
|
|
92
|
+
job_status: V1JobStatus = job.status
|
|
93
|
+
return job_status.to_dict()
|
|
94
|
+
|
|
95
|
+
def pod_ip(self, job_name: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Get a specific pod ip, based on job_name (simulation-xxx)
|
|
98
|
+
"""
|
|
99
|
+
with ApiClient(self.configuration) as api_client:
|
|
100
|
+
api = CoreV1Api(api_client)
|
|
101
|
+
results = api.list_namespaced_pod(
|
|
102
|
+
namespace=self.config.namespace, label_selector=f"job-name={job_name}"
|
|
103
|
+
)
|
|
104
|
+
if len(results.items) != 1:
|
|
105
|
+
raise CarrierError(f"Incorrect number of pods returned: {results}")
|
|
106
|
+
return results.items[0].status.pod_ip
|
|
107
|
+
|
|
108
|
+
def container_ips(self) -> Dict[str, str]:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
def container_list(self) -> List[str]:
|
|
112
|
+
with ApiClient(self.configuration) as api_client:
|
|
113
|
+
jobs: List[Job] = []
|
|
114
|
+
api = BatchV1Api(api_client)
|
|
115
|
+
job_list = None
|
|
116
|
+
while job_list is None or job_list.metadata._continue is not None:
|
|
117
|
+
job_list: V1JobList = api.list_namespaced_job(
|
|
118
|
+
namespace=self.config.namespace
|
|
119
|
+
)
|
|
120
|
+
jobs += [x for x in job_list.items]
|
|
121
|
+
return [
|
|
122
|
+
job.metadata.name.lstrip("simulation-")
|
|
123
|
+
for job in jobs
|
|
124
|
+
if job.metadata.name.startswith("simulation-")
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
def up(self, job_config: KubernetesJobConfig) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Create Kubernetes job for simulation
|
|
130
|
+
"""
|
|
131
|
+
job = get_simulation_job(job_config)
|
|
132
|
+
with ApiClient(self.configuration) as api_client:
|
|
133
|
+
job = job.create(namespace=self.config.namespace, client=api_client)
|
|
134
|
+
return job_config.name
|
|
135
|
+
|
|
136
|
+
def down(self, sim_uid: str):
|
|
137
|
+
"""Remove the given app."""
|
|
138
|
+
name = f"simulation-{sim_uid}"
|
|
139
|
+
with ApiClient(self.configuration) as api_client:
|
|
140
|
+
api = BatchV1Api(api_client)
|
|
141
|
+
api.delete_namespaced_job(
|
|
142
|
+
namespace=self.config.namespace,
|
|
143
|
+
name=name,
|
|
144
|
+
grace_period_seconds=0,
|
|
145
|
+
propagation_policy="Background",
|
|
146
|
+
body=V1DeleteOptions(propagation_policy="Background"),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_node_affinity(node_affinity_cfg: NodeAffinityConfig) -> Affinity:
|
|
151
|
+
return Affinity(
|
|
152
|
+
nodeAffinity=NodeAffinity(
|
|
153
|
+
requiredDuringSchedulingIgnoredDuringExecution=NodeSelector(
|
|
154
|
+
nodeSelectorTerms=[
|
|
155
|
+
NodeSelectorTerm(
|
|
156
|
+
matchExpressions=[
|
|
157
|
+
NodeSelectorRequirement(
|
|
158
|
+
key=node_affinity_cfg.key,
|
|
159
|
+
operator=node_affinity_cfg.operator,
|
|
160
|
+
values=node_affinity_cfg.values,
|
|
161
|
+
)
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_simulation_job(cfg: KubernetesJobConfig) -> Job:
|
|
171
|
+
return Job(
|
|
172
|
+
apiVersion="batch/v1",
|
|
173
|
+
kind="Job",
|
|
174
|
+
metadata=ObjectMeta(
|
|
175
|
+
name=cfg.name,
|
|
176
|
+
annotations={x.name: x.value for x in cfg.annotations},
|
|
177
|
+
labels={"app": cfg.name},
|
|
178
|
+
),
|
|
179
|
+
spec=JobSpec(
|
|
180
|
+
template=PodTemplateSpec(
|
|
181
|
+
metadata=ObjectMeta(
|
|
182
|
+
annotations={x.name: x.value for x in cfg.annotations},
|
|
183
|
+
labels={"app": cfg.name},
|
|
184
|
+
),
|
|
185
|
+
spec=PodSpec(
|
|
186
|
+
affinity=get_node_affinity(cfg.node_affinity)
|
|
187
|
+
if cfg.node_affinity is not None
|
|
188
|
+
else None,
|
|
189
|
+
serviceAccountName=cfg.service_account_name,
|
|
190
|
+
imagePullSecrets=[
|
|
191
|
+
LocalObjectReference(name=cfg.regcred_secret_name)
|
|
192
|
+
]
|
|
193
|
+
if cfg.regcred_secret_name is not None
|
|
194
|
+
else None,
|
|
195
|
+
hostAliases=[
|
|
196
|
+
HostAlias(x.ip_address, x.hostnames) for x in cfg.host_aliases
|
|
197
|
+
],
|
|
198
|
+
containers=[
|
|
199
|
+
get_container(cfg.redis_config),
|
|
200
|
+
get_container(cfg.scheduler_config),
|
|
201
|
+
get_container(cfg.simulation_config),
|
|
202
|
+
],
|
|
203
|
+
volumes=[
|
|
204
|
+
Volume(
|
|
205
|
+
name=mount.name,
|
|
206
|
+
hostPath=HostPathVolumeSource(
|
|
207
|
+
path=mount.local_path, type="Directory"
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
for mount in cfg.mount_points
|
|
211
|
+
if mount.type == MountPointType.HOSTPATH
|
|
212
|
+
]
|
|
213
|
+
+ [
|
|
214
|
+
Volume(
|
|
215
|
+
name=mount.name,
|
|
216
|
+
emptyDir=EmptyDirVolumeSource(sizeLimit=mount.size_limit),
|
|
217
|
+
)
|
|
218
|
+
for mount in cfg.mount_points
|
|
219
|
+
if mount.type == MountPointType.EMPTYDIR
|
|
220
|
+
]
|
|
221
|
+
+ [
|
|
222
|
+
Volume(
|
|
223
|
+
name=mount.name,
|
|
224
|
+
configMap=ConfigMapVolumeSource(name=mount.local_path),
|
|
225
|
+
)
|
|
226
|
+
for mount in cfg.mount_points
|
|
227
|
+
if mount.type == MountPointType.CONFIGMAP
|
|
228
|
+
]
|
|
229
|
+
+ [
|
|
230
|
+
Volume(
|
|
231
|
+
name=mount.name,
|
|
232
|
+
secret=SecretVolumeSource(secretName=mount.local_path),
|
|
233
|
+
)
|
|
234
|
+
for mount in cfg.mount_points
|
|
235
|
+
if mount.type == MountPointType.SECRET
|
|
236
|
+
],
|
|
237
|
+
restartPolicy="Never",
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
backoffLimit=0,
|
|
241
|
+
completions=1,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_container(cfg: KubernetesContainer) -> Container:
|
|
247
|
+
"""
|
|
248
|
+
Get k8s container API resource
|
|
249
|
+
"""
|
|
250
|
+
# Might ne needed for local dev:
|
|
251
|
+
# hostAliases=[HostAlias(f"{HOST_IP}", ["minio"])],
|
|
252
|
+
|
|
253
|
+
return Container(
|
|
254
|
+
name=cfg.name,
|
|
255
|
+
image=cfg.image,
|
|
256
|
+
imagePullPolicy="IfNotPresent",
|
|
257
|
+
resources=ResourceRequirements(
|
|
258
|
+
limits=cfg.resources.limits.to_dict(),
|
|
259
|
+
requests=cfg.resources.requests.to_dict(),
|
|
260
|
+
)
|
|
261
|
+
if cfg.resources is not None
|
|
262
|
+
else ResourceRequirements(),
|
|
263
|
+
args=cfg.args,
|
|
264
|
+
env=[
|
|
265
|
+
EnvVar(
|
|
266
|
+
name=envvar.name,
|
|
267
|
+
value=envvar.value,
|
|
268
|
+
)
|
|
269
|
+
for envvar in cfg.envs
|
|
270
|
+
],
|
|
271
|
+
volumeMounts=[
|
|
272
|
+
VolumeMount(
|
|
273
|
+
mountPath=mount.mount_path, name=mount.name, readOnly=mount.read_only
|
|
274
|
+
)
|
|
275
|
+
for mount in cfg.mount_points
|
|
276
|
+
],
|
|
277
|
+
ports=[ContainerPort(containerPort=port) for port in cfg.ports],
|
|
278
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from simple_settings import LazySettings
|
|
5
|
+
|
|
6
|
+
from container_hub.carriers.kubernetes.backend import KubernetesBackend
|
|
7
|
+
from container_hub.models import KubernetesBackendConfig, KubernetesJobConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def kubernetes_backend():
|
|
12
|
+
config = KubernetesBackendConfig(
|
|
13
|
+
"http://kubernetes/",
|
|
14
|
+
"threedi",
|
|
15
|
+
)
|
|
16
|
+
return KubernetesBackend(config, in_cluster=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def kubernetes_simple_settings():
|
|
21
|
+
yield LazySettings("tests.test_files.kubernetes_settings")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_container_list(kubernetes_backend: KubernetesBackend):
|
|
25
|
+
with patch("container_hub.carriers.kubernetes.backend.ApiClient") as client:
|
|
26
|
+
job = MagicMock()
|
|
27
|
+
job.metadata.name = "simulation-112"
|
|
28
|
+
job_list = MagicMock()
|
|
29
|
+
job_list.items = [job]
|
|
30
|
+
job_list.metadata._continue = None
|
|
31
|
+
client().__enter__().call_api.return_value = job_list
|
|
32
|
+
hosts = kubernetes_backend.container_list()
|
|
33
|
+
assert hosts == ["112"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_job_status(kubernetes_backend: KubernetesBackend):
|
|
37
|
+
with patch("container_hub.carriers.kubernetes.backend.ApiClient") as client:
|
|
38
|
+
job = MagicMock()
|
|
39
|
+
job.metadata.name = "simulation-112"
|
|
40
|
+
job.status = MagicMock()
|
|
41
|
+
job.status.active = 1
|
|
42
|
+
job.status.succeeded = None
|
|
43
|
+
job.status.failed = 1
|
|
44
|
+
client().__enter__().call_api.return_value = job
|
|
45
|
+
job_status = kubernetes_backend.get_job_status()
|
|
46
|
+
assert job_status == {"active": 1, "succeeded": None, "failed": 1}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_pod_ip(kubernetes_backend: KubernetesBackend):
|
|
50
|
+
with patch("container_hub.carriers.kubernetes.backend.ApiClient") as client:
|
|
51
|
+
pod = MagicMock()
|
|
52
|
+
pod.status.pod_ip = "127.0.0.1"
|
|
53
|
+
pod_list = MagicMock()
|
|
54
|
+
pod_list.items = [pod]
|
|
55
|
+
client().__enter__().call_api.return_value = pod_list
|
|
56
|
+
pod_ip = kubernetes_backend.pod_ip("simulation-1")
|
|
57
|
+
assert pod_ip == "127.0.0.1"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_up(kubernetes_backend: KubernetesBackend, kubernetes_simple_settings):
|
|
61
|
+
job_config = KubernetesJobConfig.from_settings(
|
|
62
|
+
"simulation-1", kubernetes_simple_settings
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
with patch("container_hub.carriers.kubernetes.backend.ApiClient") as client:
|
|
66
|
+
|
|
67
|
+
def call_api(*args, body=None, **kwargs):
|
|
68
|
+
return (body, 200, {})
|
|
69
|
+
|
|
70
|
+
client().__enter__().call_api = call_api
|
|
71
|
+
name = kubernetes_backend.up(job_config)
|
|
72
|
+
assert name == "simulation-1"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_down(kubernetes_backend: KubernetesBackend):
|
|
76
|
+
with patch("container_hub.carriers.kubernetes.backend.ApiClient") as client:
|
|
77
|
+
kubernetes_backend.down("simulation-1")
|
|
78
|
+
client().__enter__().call_api.assert_called()
|