Skip to content

Commit 72c23b8

Browse files
committed
feat: run notebooks in data service (#375)
Co-authored-by: Samuel Gaist <samuel.gaist@idiap.ch> squashme: resolve package version conflicts feat: update and expand apispec for environments chore: filter environments by owner type squashme: address comments feat!: expand environment specification This is a breaking change in the API. chore: add tests and minor fixes chore: test the global environments migration chore: fix tests chore: minor improvements to db session handling squashme: minor fix squashme: fixups for conflict resolutuion after merge squashme: fix failing tests chore: address comments feat: add command and args to environments squashme: notebooks changes This includes major edits to the notebooks code to work with the data service. chore: resolve changes from conflict resolution chore: do not use the complicated notebooks gitlab header The gitlab credentials header from the notebooks is really complicated. We used it here just to get the access token expiry. I modified the gateway to now pass in an extra header value to indicate the gitlab token expiry. squashme: handle per secret adoption in amalthea squashme: fix parsing of PosixPath in orm squashme: display the right status and state squashme: address comments from review pt1 refactor: make APIUser a frozen dataclass As there is no reason that these object shall be modified within the services, it simplifies its handling. squashme: address comments from review pt2 squashme: fixups from rebasing squashme: use PurePosixPath for workdir and mount squashme: add saved cloud storage model
1 parent 8878712 commit 72c23b8

14 files changed

Lines changed: 68 additions & 68 deletions

File tree

bases/renku_data_services/data_api/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def register_all_handlers(app: Sanic, config: Config) -> Sanic:
150150
nb_config=config.nb_config,
151151
project_repo=config.project_repo,
152152
session_repo=config.session_repo,
153+
storage_repo=config.storage_v2_repo,
153154
rp_repo=config.rp_repo,
154155
internal_gitlab_authenticator=config.gitlab_authenticator,
155156
)

components/renku_data_services/notebooks/api.spec.yaml

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -439,27 +439,24 @@ components:
439439
- registered
440440
type: object
441441
ErrorResponse:
442-
type: object
443442
properties:
444443
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"
444+
"$ref": "#/components/schemas/ErrorResponseNested"
461445
required:
462-
- "error"
446+
- error
447+
type: object
448+
ErrorResponseNested:
449+
properties:
450+
code:
451+
type: integer
452+
detail:
453+
type: string
454+
message:
455+
type: string
456+
required:
457+
- code
458+
- message
459+
type: object
463460
Generated:
464461
properties:
465462
enabled:

components/renku_data_services/notebooks/api/amalthea_patches/git_sidecar.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def main(server: "UserServer") -> list[dict[str, Any]]:
2222
commit_sha = getattr(server, "commit_sha", None)
2323

2424
volume_mount = {
25-
"mountPath": server.work_dir.absolute().as_posix(),
25+
"mountPath": server.work_dir.as_posix(),
2626
"name": "workspace",
2727
}
2828
if gl_project_path:
@@ -51,7 +51,7 @@ async def main(server: "UserServer") -> list[dict[str, Any]]:
5151
"env": [
5252
{
5353
"name": "GIT_RPC_MOUNT_PATH",
54-
"value": server.work_dir.absolute().as_posix(),
54+
"value": server.work_dir.as_posix(),
5555
},
5656
{
5757
"name": "GIT_RPC_PORT",

components/renku_data_services/notebooks/api/amalthea_patches/init_containers.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ async def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
3535
env = [
3636
{
3737
"name": f"{prefix}WORKSPACE_MOUNT_PATH",
38-
"value": server.workspace_mount_path.absolute().as_posix(),
38+
"value": server.workspace_mount_path.as_posix(),
3939
},
4040
{
4141
"name": f"{prefix}MOUNT_PATH",
42-
"value": server.work_dir.absolute().as_posix(),
42+
"value": server.work_dir.as_posix(),
4343
},
4444
{
4545
"name": f"{prefix}LFS_AUTO_FETCH",
@@ -134,7 +134,7 @@ async def git_clone_container_v2(server: "UserServer") -> dict[str, Any] | None:
134134
},
135135
"volumeMounts": [
136136
{
137-
"mountPath": server.workspace_mount_path.absolute().as_posix(),
137+
"mountPath": server.workspace_mount_path.as_posix(),
138138
"name": amalthea_session_work_volume,
139139
},
140140
*etc_cert_volume_mount,
@@ -161,11 +161,11 @@ async def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
161161
env = [
162162
{
163163
"name": f"{prefix}WORKSPACE_MOUNT_PATH",
164-
"value": server.workspace_mount_path.absolute().as_posix(),
164+
"value": server.workspace_mount_path.as_posix(),
165165
},
166166
{
167167
"name": f"{prefix}MOUNT_PATH",
168-
"value": server.work_dir.absolute().as_posix(),
168+
"value": server.work_dir.as_posix(),
169169
},
170170
{
171171
"name": f"{prefix}LFS_AUTO_FETCH",
@@ -260,7 +260,7 @@ async def git_clone_container(server: "UserServer") -> dict[str, Any] | None:
260260
},
261261
"volumeMounts": [
262262
{
263-
"mountPath": server.workspace_mount_path.absolute().as_posix(),
263+
"mountPath": server.workspace_mount_path.as_posix(),
264264
"name": "workspace",
265265
},
266266
*etc_cert_volume_mount,

components/renku_data_services/notebooks/api/amalthea_patches/jupyter_server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def env(server: "UserServer") -> list[dict[str, Any]]:
4343
"path": "/statefulset/spec/template/spec/containers/0/env/-",
4444
"value": {
4545
"name": "NOTEBOOK_DIR",
46-
"value": server.work_dir.absolute().as_posix(),
46+
"value": server.work_dir.as_posix(),
4747
},
4848
},
4949
{
@@ -53,7 +53,7 @@ def env(server: "UserServer") -> list[dict[str, Any]]:
5353
# relative to $HOME.
5454
"value": {
5555
"name": "MOUNT_PATH",
56-
"value": server.work_dir.absolute().as_posix(),
56+
"value": server.work_dir.as_posix(),
5757
},
5858
},
5959
{
@@ -223,7 +223,7 @@ def rstudio_env_variables(server: "UserServer") -> list[dict[str, Any]]:
223223
"path": "/statefulset/spec/template/spec/containers/0/volumeMounts/-",
224224
"value": {
225225
"name": secret_name,
226-
"mountPath": mount_location.absolute().as_posix(),
226+
"mountPath": mount_location.as_posix(),
227227
"subPath": mount_location.name,
228228
"readOnly": True,
229229
},

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55
from dataclasses import dataclass, field
66
from enum import Enum
7-
from pathlib import Path
7+
from pathlib import PurePosixPath
88
from typing import Any, Optional, Self, cast
99

1010
import requests
@@ -101,7 +101,7 @@ def get_image_config(self, image: "Image") -> Optional[dict[str, Any]]:
101101
return None
102102
return cast(dict[str, Any], res.json())
103103

104-
def image_workdir(self, image: "Image") -> Optional[Path]:
104+
def image_workdir(self, image: "Image") -> Optional[PurePosixPath]:
105105
"""Query the docker API to get the workdir of an image."""
106106
config = self.get_image_config(image)
107107
if config is None:
@@ -112,7 +112,7 @@ def image_workdir(self, image: "Image") -> Optional[Path]:
112112
workdir = nested_config.get("WorkingDir", "/")
113113
if workdir == "":
114114
workdir = "/"
115-
return Path(workdir)
115+
return PurePosixPath(workdir)
116116

117117
def with_oauth2_token(self, oauth2_token: str) -> "ImageRepoDockerAPI":
118118
"""Return a docker API instance with the token as authentication."""

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC
44
from collections.abc import Sequence
55
from itertools import chain
6-
from pathlib import Path
6+
from pathlib import PurePosixPath
77
from typing import Any
88
from urllib.parse import urljoin, urlparse
99

@@ -44,8 +44,8 @@ def __init__(
4444
user_secrets: K8sUserSecrets | None,
4545
cloudstorage: Sequence[ICloudStorageRequest],
4646
k8s_client: K8sClient,
47-
workspace_mount_path: Path,
48-
work_dir: Path,
47+
workspace_mount_path: PurePosixPath,
48+
work_dir: PurePosixPath,
4949
config: _NotebooksConfig,
5050
internal_gitlab_user: APIUser,
5151
using_default_image: bool = False,
@@ -204,7 +204,7 @@ async def _get_session_manifest(self) -> dict[str, Any]:
204204
"pvc": {
205205
"enabled": True,
206206
"storageClassName": self.config.sessions.storage.pvs_storage_class,
207-
"mountPath": self.workspace_mount_path.absolute().as_posix(),
207+
"mountPath": self.workspace_mount_path.as_posix(),
208208
},
209209
}
210210
else:
@@ -213,7 +213,7 @@ async def _get_session_manifest(self) -> dict[str, Any]:
213213
"size": storage_size,
214214
"pvc": {
215215
"enabled": False,
216-
"mountPath": self.workspace_mount_path.absolute().as_posix(),
216+
"mountPath": self.workspace_mount_path.as_posix(),
217217
},
218218
}
219219
# Authentication
@@ -256,7 +256,7 @@ async def _get_session_manifest(self) -> dict[str, Any]:
256256
"jupyterServer": {
257257
"defaultUrl": self.server_options.default_url,
258258
"image": self.image,
259-
"rootDir": self.work_dir.absolute().as_posix(),
259+
"rootDir": self.work_dir.as_posix(),
260260
"resources": self.server_options.to_k8s_resources(
261261
enforce_cpu_limits=self.config.sessions.enforce_cpu_limits
262262
),
@@ -375,8 +375,8 @@ def __init__(
375375
user_secrets: K8sUserSecrets | None,
376376
cloudstorage: Sequence[ICloudStorageRequest],
377377
k8s_client: K8sClient,
378-
workspace_mount_path: Path,
379-
work_dir: Path,
378+
workspace_mount_path: PurePosixPath,
379+
work_dir: PurePosixPath,
380380
config: _NotebooksConfig,
381381
gitlab_client: NotebooksGitlabClient,
382382
internal_gitlab_user: APIUser,
@@ -502,8 +502,8 @@ def __init__(
502502
user_secrets: K8sUserSecrets | None,
503503
cloudstorage: Sequence[ICloudStorageRequest],
504504
k8s_client: K8sClient,
505-
workspace_mount_path: Path,
506-
work_dir: Path,
505+
workspace_mount_path: PurePosixPath,
506+
work_dir: PurePosixPath,
507507
repositories: list[Repository],
508508
config: _NotebooksConfig,
509509
internal_gitlab_user: APIUser,

components/renku_data_services/notebooks/apispec.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,10 @@ class DefaultCullingThresholds(BaseAPISpec):
3434
registered: CullingThreshold
3535

3636

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
37+
class ErrorResponseNested(BaseAPISpec):
38+
code: int
39+
detail: Optional[str] = None
40+
message: str
4741

4842

4943
class Generated(BaseAPISpec):
@@ -299,6 +293,10 @@ class SessionsImagesGetParametersQuery(BaseAPISpec):
299293
image_url: str
300294

301295

296+
class ErrorResponse(BaseAPISpec):
297+
error: ErrorResponseNested
298+
299+
302300
class LaunchNotebookRequest(BaseAPISpec):
303301
project_id: str
304302
launcher_id: str

components/renku_data_services/storage/blueprints.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ async def _get(
5555
validator: RCloneValidator,
5656
query: apispec.StorageParams,
5757
) -> JSONResponse:
58-
storage: list[models.CloudStorage]
5958
storage = await self.storage_repo.get_storage(user=user, project_id=query.project_id)
6059

6160
return json([dump_storage_with_sensitive_fields(s, validator) for s in storage])
@@ -202,7 +201,6 @@ async def _get(
202201
validator: RCloneValidator,
203202
query: apispec.StorageV2Params,
204203
) -> JSONResponse:
205-
storage: list[models.CloudStorage]
206204
storage = await self.storage_v2_repo.get_storage(
207205
user=user, include_secrets=True, project_id=query.project_id
208206
)

components/renku_data_services/storage/db.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async def get_storage(
5656
name: str | None = None,
5757
include_secrets: bool = False,
5858
filter_by_access_level: bool = True,
59-
) -> list[models.CloudStorage]:
59+
) -> list[models.SavedCloudStorage]:
6060
"""Get a storage from the database."""
6161
async with self.session_maker() as session:
6262
if not project_id and not name and not id:
@@ -91,7 +91,7 @@ async def get_storage(
9191

9292
return [s.dump() for s in storage_orms if s.project_id in accessible_projects]
9393

94-
async def get_storage_by_id(self, storage_id: ULID, user: base_models.APIUser) -> models.CloudStorage:
94+
async def get_storage_by_id(self, storage_id: ULID, user: base_models.APIUser) -> models.SavedCloudStorage:
9595
"""Get a single storage by id."""
9696
storages = await self.get_storage(user, id=str(storage_id), include_secrets=True, filter_by_access_level=False)
9797

@@ -102,9 +102,7 @@ async def get_storage_by_id(self, storage_id: ULID, user: base_models.APIUser) -
102102

103103
return storages[0]
104104

105-
async def insert_storage(
106-
self, storage: models.UnsavedCloudStorage, user: base_models.APIUser
107-
) -> models.CloudStorage:
105+
async def insert_storage(self, storage: models.CloudStorage, user: base_models.APIUser) -> models.SavedCloudStorage:
108106
"""Insert a new cloud storage entry."""
109107
if not await self.filter_projects_by_access_level(user, [storage.project_id], authz_models.Role.OWNER):
110108
raise errors.ForbiddenError(message="User does not have access to this project")
@@ -118,7 +116,9 @@ async def insert_storage(
118116
session.add(orm)
119117
return orm.dump()
120118

121-
async def update_storage(self, storage_id: ULID, user: base_models.APIUser, **kwargs: dict) -> models.CloudStorage:
119+
async def update_storage(
120+
self, storage_id: ULID, user: base_models.APIUser, **kwargs: dict
121+
) -> models.SavedCloudStorage:
122122
"""Update a cloud storage entry."""
123123
async with self.session_maker() as session, session.begin():
124124
res = await session.execute(

0 commit comments

Comments
 (0)