Skip to content

Commit 283586b

Browse files
authored
✨ Allow connection to multiple dask-gateways (ITISFoundation#2652)
1 parent 77e6477 commit 283586b

File tree

51 files changed

+1746
-577
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1746
-577
lines changed

.codecov.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
codecov:
2-
require_ci_to_pass: yes
3-
branch: master
2+
require_ci_to_pass: true
3+
branch: master
44

55
coverage:
66
precision: 1
@@ -32,7 +32,6 @@ coverage:
3232
paths:
3333
- services
3434

35-
3635
patch:
3736
default:
3837
informational: true
@@ -53,4 +52,4 @@ parsers:
5352
comment:
5453
layout: "reach,diff,flags,tree"
5554
behavior: default
56-
require_changes: no
55+
require_changes: false

.vscode/launch.template.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@
55
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
66
"version": "0.2.0",
77
"configurations": [
8+
{
9+
"name": "Python: Run Test",
10+
"type": "python",
11+
"request": "launch",
12+
"module": "pytest",
13+
"args": [
14+
"-sx",
15+
"--log-cli-level=INFO",
16+
"--ff",
17+
"--keep-docker-up",
18+
"--setup-show",
19+
"${file}"
20+
],
21+
"cwd": "${workspaceFolder}",
22+
"console": "integratedTerminal",
23+
"justMyCode": false
24+
},
825
{
926
"name": "Python: Remote Attach api-server",
1027
"type": "python",

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ info: ## displays setup information
512512
@echo ' python : $(shell python3 --version)'
513513
@echo ' node : $(shell node --version 2> /dev/null || echo ERROR nodejs missing)'
514514
@echo ' docker : $(shell docker --version)'
515+
@echo ' docker buildx : $(shell docker buildx version)'
515516
@echo ' docker-compose: $(shell docker-compose --version)'
516517

517518

api/specs/webserver/components/schemas/cluster.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ JupyterHubTokenAuthentication:
211211
type: string
212212
enum: [jupyterhub]
213213
default: jupyterhub
214+
api_token:
215+
type: string
216+
required:
217+
- api_token
214218
additionalProperties: false
215219

216220
ClusterAccessRights:
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
from typing import Any, Dict, Literal, Optional, Union
2+
3+
from pydantic import AnyUrl, BaseModel, Extra, Field, HttpUrl, validator
4+
from pydantic.types import NonNegativeInt
5+
from simcore_postgres_database.models.clusters import ClusterType
6+
7+
from .users import GroupID
8+
9+
10+
class ClusterAccessRights(BaseModel):
11+
read: bool = Field(..., description="allows to run pipelines on that cluster")
12+
write: bool = Field(..., description="allows to modify the cluster")
13+
delete: bool = Field(..., description="allows to delete a cluster")
14+
15+
class Config:
16+
extra = Extra.forbid
17+
18+
19+
CLUSTER_ADMIN_RIGHTS = ClusterAccessRights(read=True, write=True, delete=True)
20+
CLUSTER_MANAGER_RIGHTS = ClusterAccessRights(read=True, write=True, delete=False)
21+
CLUSTER_USER_RIGHTS = ClusterAccessRights(read=True, write=False, delete=False)
22+
CLUSTER_NO_RIGHTS = ClusterAccessRights(read=False, write=False, delete=False)
23+
24+
25+
class BaseAuthentication(BaseModel):
26+
type: str
27+
28+
class Config:
29+
extra = Extra.forbid
30+
31+
32+
class SimpleAuthentication(BaseAuthentication):
33+
type: Literal["simple"] = "simple"
34+
username: str
35+
password: str
36+
37+
class Config(BaseAuthentication.Config):
38+
schema_extra = {
39+
"examples": [
40+
{
41+
"type": "simple",
42+
"username": "someuser",
43+
"password": "somepassword",
44+
},
45+
]
46+
}
47+
48+
49+
class KerberosAuthentication(BaseAuthentication):
50+
type: Literal["kerberos"] = "kerberos"
51+
# NOTE: the entries here still need to be defined
52+
class Config(BaseAuthentication.Config):
53+
schema_extra = {
54+
"examples": [
55+
{
56+
"type": "kerberos",
57+
},
58+
]
59+
}
60+
61+
62+
class JupyterHubTokenAuthentication(BaseAuthentication):
63+
type: Literal["jupyterhub"] = "jupyterhub"
64+
api_token: str
65+
66+
class Config(BaseAuthentication.Config):
67+
schema_extra = {
68+
"examples": [
69+
{"type": "jupyterhub", "api_token": "some_jupyterhub_token"},
70+
]
71+
}
72+
73+
74+
class NoAuthentication(BaseAuthentication):
75+
type: Literal["none"] = "none"
76+
77+
78+
InternalClusterAuthentication = NoAuthentication
79+
ExternalClusterAuthentication = Union[
80+
SimpleAuthentication, KerberosAuthentication, JupyterHubTokenAuthentication
81+
]
82+
ClusterAuthentication = Union[
83+
ExternalClusterAuthentication,
84+
InternalClusterAuthentication,
85+
]
86+
87+
88+
class BaseCluster(BaseModel):
89+
name: str = Field(..., description="The human readable name of the cluster")
90+
description: Optional[str] = None
91+
type: ClusterType
92+
owner: GroupID
93+
thumbnail: Optional[HttpUrl] = Field(
94+
None,
95+
description="url to the image describing this cluster",
96+
examples=["https://placeimg.com/171/96/tech/grayscale/?0.jpg"],
97+
)
98+
endpoint: AnyUrl
99+
authentication: ClusterAuthentication = Field(
100+
..., description="Dask gateway authentication"
101+
)
102+
access_rights: Dict[GroupID, ClusterAccessRights] = Field(default_factory=dict)
103+
104+
class Config:
105+
extra = Extra.forbid
106+
use_enum_values = True
107+
108+
def to_clusters_db(self, only_update: bool) -> Dict[str, Any]:
109+
db_model = self.dict(
110+
by_alias=True,
111+
exclude={"id", "access_rights"},
112+
exclude_unset=only_update,
113+
exclude_none=only_update,
114+
)
115+
return db_model
116+
117+
118+
class Cluster(BaseCluster):
119+
id: NonNegativeInt = Field(..., description="The cluster ID")
120+
121+
class Config(BaseCluster.Config):
122+
schema_extra = {
123+
"examples": [
124+
{
125+
"id": 432,
126+
"name": "My awesome cluster",
127+
"type": ClusterType.ON_PREMISE,
128+
"owner": 12,
129+
"endpoint": "https://registry.osparc-development.fake.dev",
130+
"authentication": {
131+
"type": "simple",
132+
"username": "someuser",
133+
"password": "somepassword",
134+
},
135+
},
136+
{
137+
"id": 432546,
138+
"name": "My AWS cluster",
139+
"description": "a AWS cluster administered by me",
140+
"type": ClusterType.AWS,
141+
"owner": 154,
142+
"endpoint": "https://registry.osparc-development.fake.dev",
143+
"authentication": {"type": "kerberos"},
144+
"access_rights": {
145+
154: CLUSTER_ADMIN_RIGHTS,
146+
12: CLUSTER_MANAGER_RIGHTS,
147+
7899: CLUSTER_USER_RIGHTS,
148+
},
149+
},
150+
{
151+
"id": 325436,
152+
"name": "My AWS cluster",
153+
"description": "a AWS cluster administered by me",
154+
"type": ClusterType.AWS,
155+
"owner": 2321,
156+
"endpoint": "https://registry.osparc-development.fake2.dev",
157+
"authentication": {
158+
"type": "jupyterhub",
159+
"api_token": "some_fake_token",
160+
},
161+
"access_rights": {
162+
154: CLUSTER_ADMIN_RIGHTS,
163+
12: CLUSTER_MANAGER_RIGHTS,
164+
7899: CLUSTER_USER_RIGHTS,
165+
},
166+
},
167+
]
168+
}
169+
170+
@validator("access_rights", always=True, pre=True)
171+
@classmethod
172+
def check_owner_has_access_rights(cls, v, values):
173+
owner_gid = values["owner"]
174+
# check owner is in the access rights, if not add it
175+
if owner_gid not in v:
176+
v[owner_gid] = CLUSTER_ADMIN_RIGHTS
177+
# check owner has full access
178+
if v[owner_gid] != CLUSTER_ADMIN_RIGHTS:
179+
raise ValueError("the cluster owner access rights are incorrectly set")
180+
return v
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from contextlib import suppress
2+
from typing import Any, Dict, Type
3+
4+
import pytest
5+
from models_library.clusters import (
6+
CLUSTER_ADMIN_RIGHTS,
7+
CLUSTER_MANAGER_RIGHTS,
8+
CLUSTER_USER_RIGHTS,
9+
Cluster,
10+
)
11+
from pydantic import BaseModel, ValidationError
12+
13+
14+
@pytest.mark.parametrize(
15+
"model_cls",
16+
(Cluster,),
17+
)
18+
def test_cluster_access_rights_correctly_created_when_owner_access_rights_not_present(
19+
model_cls: Type[BaseModel], model_cls_examples: Dict[str, Dict[str, Any]]
20+
):
21+
for example in model_cls_examples.values():
22+
owner_gid = example["owner"]
23+
# remove the owner from the access rights if any
24+
example.get("access_rights", {}).pop(owner_gid, None)
25+
26+
instance = model_cls(**example)
27+
assert instance.access_rights[owner_gid] == CLUSTER_ADMIN_RIGHTS # type: ignore
28+
29+
30+
@pytest.mark.parametrize(
31+
"model_cls",
32+
(Cluster,),
33+
)
34+
def test_cluster_fails_when_owner_has_no_admin_rights(
35+
model_cls: Type[BaseModel], model_cls_examples: Dict[str, Dict[str, Any]]
36+
):
37+
for example in model_cls_examples.values():
38+
owner_gid = example["owner"]
39+
# ensure there are access rights
40+
example.setdefault("access_rights", {})
41+
# set the owner with manager rights
42+
example["access_rights"][owner_gid] = CLUSTER_MANAGER_RIGHTS
43+
with pytest.raises(ValidationError):
44+
model_cls(**example)
45+
46+
# set the owner with user rights
47+
example["access_rights"][owner_gid] = CLUSTER_USER_RIGHTS
48+
with pytest.raises(ValidationError):
49+
model_cls(**example)
50+
51+
52+
@pytest.mark.parametrize(
53+
"model_cls",
54+
(Cluster,),
55+
)
56+
def test_export_clusters_to_db(
57+
model_cls: Type[BaseModel], model_cls_examples: Dict[str, Dict[str, Any]]
58+
):
59+
for example in model_cls_examples.values():
60+
owner_gid = example["owner"]
61+
# remove the owner from the access rights if any
62+
with suppress(KeyError):
63+
example.get("access_rights", {}).pop(owner_gid)
64+
instance = model_cls(**example)
65+
66+
# for inserts
67+
cluster_db_dict = instance.to_clusters_db(only_update=True) # type: ignore
68+
keys_not_in_db = ["id", "access_rights"]
69+
70+
assert list(cluster_db_dict.keys()) == [
71+
x for x in example if x not in keys_not_in_db
72+
]

packages/models-library/tests/test_utils_misc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pytest
1414
from models_library.utils.misc import extract_examples
1515
from pydantic import BaseModel
16+
from pydantic.json import pydantic_encoder
1617

1718
NameClassPair = Tuple[str, Type[BaseModel]]
1819

@@ -48,6 +49,6 @@ def test_extract_examples(class_name, model_cls):
4849
# check if correct examples
4950
for example in examples:
5051
print(class_name)
51-
print(json.dumps(example, indent=2))
52+
print(json.dumps(example, default=pydantic_encoder, indent=2))
5253
model_instance = model_cls.parse_obj(example)
5354
assert isinstance(model_instance, model_cls)

services/dask-sidecar/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ COPY --chown=scu:scu services/dask-sidecar/docker services/dask-sidecar/docker
125125
# make sure dask worker is started as ``dask-worker --dashboard-address 8787``.
126126
# Otherwise the worker will take random ports to serve the /health entrypoint.
127127
HEALTHCHECK \
128-
--interval=30s \
129-
--timeout=15s \
128+
--interval=10s \
129+
--timeout=5s \
130130
--start-period=5s \
131-
--retries=3 \
131+
--retries=5 \
132132
CMD ["curl", "-Lf", "http://127.0.0.1:8787/health"]
133133

134134
ENTRYPOINT [ "/bin/sh", "services/dask-sidecar/docker/entrypoint.sh" ]

0 commit comments

Comments
 (0)