Skip to content

Commit 806ea04

Browse files
committed
feat: add cloud storage via rclone
squashme: use the amalthea session cache
1 parent 1b981e9 commit 806ea04

File tree

12 files changed

+189
-96
lines changed

12 files changed

+189
-96
lines changed

bases/renku_data_services/data_api/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def register_all_handlers(app: Sanic, config: Config) -> Sanic:
142142
nb_config=config.nb_config,
143143
internal_gitlab_authenticator=config.gitlab_authenticator,
144144
git_repo=config.git_repositories_repo,
145+
rp_repo=config.rp_repo,
145146
)
146147
notebooks_new = NotebooksNewBP(
147148
name="notebooks",

components/renku_data_services/notebooks/api.spec.yaml

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -439,24 +439,27 @@ components:
439439
- registered
440440
type: object
441441
ErrorResponse:
442-
properties:
443-
error:
444-
"$ref": "#/components/schemas/ErrorResponseNested"
445-
required:
446-
- error
447442
type: object
448-
ErrorResponseNested:
449443
properties:
450-
code:
451-
type: integer
452-
detail:
453-
type: string
454-
message:
455-
type: string
444+
error:
445+
type: object
446+
properties:
447+
code:
448+
type: integer
449+
minimum: 0
450+
exclusiveMinimum: true
451+
example: 1404
452+
detail:
453+
type: string
454+
example: "A more detailed optional message showing what the problem was"
455+
message:
456+
type: string
457+
example: "Something went wrong - please try again later"
458+
required:
459+
- "code"
460+
- "message"
456461
required:
457-
- code
458-
- message
459-
type: object
462+
- "error"
460463
Generated:
461464
properties:
462465
enabled:

components/renku_data_services/notebooks/api/classes/cloud_storage/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,15 @@
66
class ICloudStorageRequest(Protocol):
77
"""The abstract class for cloud storage."""
88

9-
exists: bool
109
mount_folder: str
11-
source_folder: str
12-
bucket: str
10+
source_path: str
1311

1412
def get_manifest_patch(
1513
self,
1614
base_name: str,
1715
namespace: str,
18-
labels: dict[str, str] = {},
19-
annotations: dict[str, str] = {},
16+
labels: dict[str, str] | None = None,
17+
annotations: dict[str, str] | None = None,
2018
) -> list[dict[str, Any]]:
2119
"""The patches applied to a jupyter server to insert the storage in the session."""
2220
...

components/renku_data_services/notebooks/api/classes/k8s_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,13 @@ def __init__(self, url: str, server_type: type[_SessionType]):
351351
self.url = url
352352
self.client = httpx.AsyncClient()
353353
self.server_type: type[_SessionType] = server_type
354+
self.url_path_name = "servers"
355+
if server_type == AmaltheaSessionV1Alpha1:
356+
self.url_path_name = "sessions"
354357

355358
async def list_servers(self, safe_username: str) -> list[_SessionType]:
356359
"""List the jupyter servers."""
357-
url = urljoin(self.url, f"/users/{safe_username}/servers")
360+
url = urljoin(self.url, f"/users/{safe_username}/{self.url_path_name}")
358361
try:
359362
res = await self.client.get(url, timeout=10)
360363
except httpx.RequestError as err:
@@ -372,7 +375,7 @@ async def list_servers(self, safe_username: str) -> list[_SessionType]:
372375

373376
async def get_server(self, name: str) -> _SessionType | None:
374377
"""Get a specific jupyter server."""
375-
url = urljoin(self.url, f"/servers/{name}")
378+
url = urljoin(self.url, f"/{self.url_path_name}/{name}")
376379
try:
377380
res = await self.client.get(url, timeout=10)
378381
except httpx.RequestError as err:

components/renku_data_services/notebooks/api/schemas/cloud_storage.py

Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
from configparser import ConfigParser
44
from io import StringIO
5-
from pathlib import Path
6-
from typing import Any, Optional, Self
5+
from pathlib import PurePosixPath
6+
from typing import Any, Final, Optional, Self
77

8+
from kubernetes import client
89
from marshmallow import EXCLUDE, Schema, ValidationError, fields, validates_schema
910

1011
from renku_data_services.base_models import APIUser
1112
from renku_data_services.notebooks.api.classes.cloud_storage import ICloudStorageRequest
1213
from renku_data_services.notebooks.config import _NotebooksConfig
1314

15+
_sanitize_for_serialization = client.ApiClient().sanitize_for_serialization
16+
1417

1518
class RCloneStorageRequest(Schema):
1619
"""Request for RClone based storage."""
@@ -36,6 +39,8 @@ def validate_storage(self, data: dict, **kwargs: dict) -> None:
3639
class RCloneStorage(ICloudStorageRequest):
3740
"""RClone based storage."""
3841

42+
pvc_secret_annotation_name: Final[str] = "csi-rclone.dev/secretName"
43+
3944
def __init__(
4045
self,
4146
source_path: str,
@@ -60,7 +65,7 @@ async def storage_from_schema(
6065
user: APIUser,
6166
internal_gitlab_user: APIUser,
6267
project_id: int,
63-
work_dir: Path,
68+
work_dir: PurePosixPath,
6469
config: _NotebooksConfig,
6570
) -> Self:
6671
"""Create storage object from request."""
@@ -92,8 +97,73 @@ async def storage_from_schema(
9297
await config.storage_validator.validate_storage_configuration(configuration, source_path)
9398
return cls(source_path, configuration, readonly, mount_folder, name, config)
9499

100+
def pvc(
101+
self,
102+
base_name: str,
103+
namespace: str,
104+
labels: dict[str, str] | None = None,
105+
annotations: dict[str, str] | None = None,
106+
) -> client.V1PersistentVolumeClaim:
107+
"""The PVC for mounting cloud storage."""
108+
return client.V1PersistentVolumeClaim(
109+
metadata=client.V1ObjectMeta(
110+
name=base_name,
111+
namespace=namespace,
112+
annotations={self.pvc_secret_annotation_name: base_name} | (annotations or {}),
113+
labels={"name": base_name} | (labels or {}),
114+
),
115+
spec=client.V1PersistentVolumeClaimSpec(
116+
access_modes=["ReadOnlyMany" if self.readonly else "ReadWriteMany"],
117+
resources=client.V1VolumeResourceRequirements(requests={"storage": "10Gi"}),
118+
storage_class_name=self.config.cloud_storage.storage_class,
119+
),
120+
)
121+
122+
def volume_mount(self, base_name: str) -> client.V1VolumeMount:
123+
"""The volume mount for cloud storage."""
124+
return client.V1VolumeMount(
125+
mount_path=self.mount_folder,
126+
name=base_name,
127+
read_only=self.readonly,
128+
)
129+
130+
def volume(self, base_name: str) -> client.V1Volume:
131+
"""The volume entry for the statefulset specification."""
132+
return client.V1Volume(
133+
name=base_name,
134+
persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource(
135+
claim_name=base_name, read_only=self.readonly
136+
),
137+
)
138+
139+
def secret(
140+
self,
141+
base_name: str,
142+
namespace: str,
143+
labels: dict[str, str] | None = None,
144+
annotations: dict[str, str] | None = None,
145+
) -> client.V1Secret:
146+
"""The secret containing the configuration for the rclone csi driver."""
147+
return client.V1Secret(
148+
metadata=client.V1ObjectMeta(
149+
name=base_name,
150+
namespace=namespace,
151+
annotations=annotations,
152+
labels={"name": base_name} | (labels or {}),
153+
),
154+
string_data={
155+
"remote": self.name or base_name,
156+
"remotePath": self.source_path,
157+
"configData": self.config_string(self.name or base_name),
158+
},
159+
)
160+
95161
def get_manifest_patch(
96-
self, base_name: str, namespace: str, labels: dict = {}, annotations: dict = {}
162+
self,
163+
base_name: str,
164+
namespace: str,
165+
labels: dict[str, str] | None = None,
166+
annotations: dict[str, str] | None = None,
97167
) -> list[dict[str, Any]]:
98168
"""Get server manifest patch."""
99169
patches = []
@@ -104,57 +174,22 @@ def get_manifest_patch(
104174
{
105175
"op": "add",
106176
"path": f"/{base_name}-pv",
107-
"value": {
108-
"apiVersion": "v1",
109-
"kind": "PersistentVolumeClaim",
110-
"metadata": {
111-
"name": base_name,
112-
"labels": {"name": base_name},
113-
},
114-
"spec": {
115-
"accessModes": ["ReadOnlyMany" if self.readonly else "ReadWriteMany"],
116-
"resources": {"requests": {"storage": "10Gi"}},
117-
"storageClassName": self.config.cloud_storage.storage_class,
118-
},
119-
},
177+
"value": _sanitize_for_serialization(self.pvc(base_name, namespace, labels, annotations)),
120178
},
121179
{
122180
"op": "add",
123181
"path": f"/{base_name}-secret",
124-
"value": {
125-
"apiVersion": "v1",
126-
"kind": "Secret",
127-
"metadata": {
128-
"name": base_name,
129-
"labels": {"name": base_name},
130-
},
131-
"type": "Opaque",
132-
"stringData": {
133-
"remote": self.name or base_name,
134-
"remotePath": self.source_path,
135-
"configData": self.config_string(self.name or base_name),
136-
},
137-
},
182+
"value": _sanitize_for_serialization(self.secret(base_name, namespace, labels, annotations)),
138183
},
139184
{
140185
"op": "add",
141186
"path": "/statefulset/spec/template/spec/containers/0/volumeMounts/-",
142-
"value": {
143-
"mountPath": self.mount_folder,
144-
"name": base_name,
145-
"readOnly": self.readonly,
146-
},
187+
"value": _sanitize_for_serialization(self.volume_mount(base_name)),
147188
},
148189
{
149190
"op": "add",
150191
"path": "/statefulset/spec/template/spec/volumes/-",
151-
"value": {
152-
"name": base_name,
153-
"persistentVolumeClaim": {
154-
"claimName": base_name,
155-
"readOnly": self.readonly,
156-
},
157-
},
192+
"value": _sanitize_for_serialization(self.volume(base_name)),
158193
},
159194
],
160195
}

components/renku_data_services/notebooks/apispec.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2024-09-23T08:31:51+00:00
3+
# timestamp: 2024-09-23T14:43:54+00:00
44

55
from __future__ import annotations
66

@@ -34,10 +34,16 @@ class DefaultCullingThresholds(BaseAPISpec):
3434
registered: CullingThreshold
3535

3636

37-
class ErrorResponseNested(BaseAPISpec):
38-
code: int
39-
detail: Optional[str] = None
40-
message: str
37+
class Error(BaseAPISpec):
38+
code: int = Field(..., example=1404, gt=0)
39+
detail: Optional[str] = Field(
40+
None, example="A more detailed optional message showing what the problem was"
41+
)
42+
message: str = Field(..., example="Something went wrong - please try again later")
43+
44+
45+
class ErrorResponse(BaseAPISpec):
46+
error: Error
4147

4248

4349
class Generated(BaseAPISpec):
@@ -293,10 +299,6 @@ class SessionsImagesGetParametersQuery(BaseAPISpec):
293299
image_url: str
294300

295301

296-
class ErrorResponse(BaseAPISpec):
297-
error: ErrorResponseNested
298-
299-
300302
class LaunchNotebookRequest(BaseAPISpec):
301303
project_id: str
302304
launcher_id: str

0 commit comments

Comments
 (0)