• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 14382014257

10 Apr 2025 01:42PM UTC coverage: 86.576% (+0.2%) from 86.351%
14382014257

Pull #759

github

web-flow
Merge 470ff1568 into 74eb7d965
Pull Request #759: feat: add new service cache and migrations

412 of 486 new or added lines in 15 files covered. (84.77%)

18 existing lines in 6 files now uncovered.

20232 of 23369 relevant lines covered (86.58%)

1.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.85
/components/renku_data_services/notebooks/blueprints.py
1
"""Notebooks service API."""
2

3
from dataclasses import dataclass
2✔
4
from pathlib import PurePosixPath
2✔
5
from urllib.parse import urlparse
2✔
6

7
from sanic import Request, empty, exceptions, json
2✔
8
from sanic.response import HTTPResponse, JSONResponse
2✔
9
from sanic_ext import validate
2✔
10
from ulid import ULID
2✔
11

12
from renku_data_services import base_models
2✔
13
from renku_data_services.base_api.auth import authenticate, authenticate_2
2✔
14
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
15
from renku_data_services.base_models import AnonymousAPIUser, APIUser, AuthenticatedAPIUser, Authenticator
2✔
16
from renku_data_services.crc.db import ResourcePoolRepository
2✔
17
from renku_data_services.crc.models import GpuKind
2✔
18
from renku_data_services.data_connectors.db import (
2✔
19
    DataConnectorRepository,
20
    DataConnectorSecretRepository,
21
)
22
from renku_data_services.errors import errors
2✔
23
from renku_data_services.notebooks import apispec, core
2✔
24
from renku_data_services.notebooks.api.amalthea_patches.init_containers import user_secrets_container
2✔
25
from renku_data_services.notebooks.api.classes.repository import Repository
2✔
26
from renku_data_services.notebooks.api.schemas.config_server_options import ServerOptionsEndpointResponse
2✔
27
from renku_data_services.notebooks.api.schemas.logs import ServerLogs
2✔
28
from renku_data_services.notebooks.config import NotebooksConfig
2✔
29
from renku_data_services.notebooks.core_sessions import (
2✔
30
    get_auth_secret_anonymous,
31
    get_auth_secret_authenticated,
32
    get_culling,
33
    get_data_sources,
34
    get_extra_containers,
35
    get_extra_init_containers,
36
    get_gitlab_image_pull_secret,
37
    patch_session,
38
    request_dc_secret_creation,
39
    request_session_secret_creation,
40
    requires_image_pull_secret,
41
)
42
from renku_data_services.notebooks.crs import (
2✔
43
    AmaltheaSessionSpec,
44
    AmaltheaSessionV1Alpha1,
45
    Authentication,
46
    AuthenticationType,
47
    ExtraVolume,
48
    ExtraVolumeMount,
49
    ImagePullPolicy,
50
    ImagePullSecret,
51
    Ingress,
52
    InitContainer,
53
    Metadata,
54
    ReconcileStrategy,
55
    Resources,
56
    Session,
57
    SessionEnvItem,
58
    Storage,
59
    TlsSecret,
60
)
61
from renku_data_services.notebooks.errors.intermittent import AnonymousUserPatchError
2✔
62
from renku_data_services.notebooks.models import ExtraSecret
2✔
63
from renku_data_services.notebooks.util.kubernetes_ import (
2✔
64
    renku_2_make_server_name,
65
)
66
from renku_data_services.notebooks.utils import (
2✔
67
    node_affinity_from_resource_class,
68
    tolerations_from_resource_class,
69
)
70
from renku_data_services.project.db import ProjectRepository, ProjectSessionSecretRepository
2✔
71
from renku_data_services.repositories.db import GitRepositoriesRepository
2✔
72
from renku_data_services.session.db import SessionRepository
2✔
73
from renku_data_services.storage.db import StorageRepository
2✔
74
from renku_data_services.users.db import UserRepo
2✔
75

76

77
@dataclass(kw_only=True)
2✔
78
class NotebooksBP(CustomBlueprint):
2✔
79
    """Handlers for manipulating notebooks."""
80

81
    authenticator: Authenticator
2✔
82
    nb_config: NotebooksConfig
2✔
83
    git_repo: GitRepositoriesRepository
2✔
84
    internal_gitlab_authenticator: base_models.Authenticator
2✔
85
    rp_repo: ResourcePoolRepository
2✔
86
    user_repo: UserRepo
2✔
87
    storage_repo: StorageRepository
2✔
88

89
    def version(self) -> BlueprintFactoryResponse:
2✔
90
        """Return notebook services version."""
91

92
        async def _version(_: Request) -> JSONResponse:
2✔
93
            return json(core.notebooks_info(self.nb_config))
1✔
94

95
        return "/notebooks/version", ["GET"], _version
2✔
96

97
    def user_servers(self) -> BlueprintFactoryResponse:
2✔
98
        """Return a JSON of running servers for the user."""
99

100
        @authenticate(self.authenticator)
2✔
101
        async def _user_servers(
2✔
102
            request: Request, user: AnonymousAPIUser | AuthenticatedAPIUser, **query_params: dict
103
        ) -> JSONResponse:
104
            filter_attrs = list(filter(lambda x: x[1] is not None, request.get_query_args()))
1✔
105
            filtered_servers = await core.user_servers(self.nb_config, user, filter_attrs)
1✔
106
            return core.serialize_v1_servers(filtered_servers, self.nb_config)
1✔
107

108
        return "/notebooks/servers", ["GET"], _user_servers
2✔
109

110
    def user_server(self) -> BlueprintFactoryResponse:
2✔
111
        """Returns a user server based on its ID."""
112

113
        @authenticate(self.authenticator)
2✔
114
        async def _user_server(
2✔
115
            request: Request, user: AnonymousAPIUser | AuthenticatedAPIUser, server_name: str
116
        ) -> JSONResponse:
117
            server = await core.user_server(self.nb_config, user, server_name)
×
118
            return core.serialize_v1_server(server, self.nb_config)
×
119

120
        return "/notebooks/servers/<server_name>", ["GET"], _user_server
2✔
121

122
    def launch_notebook(self) -> BlueprintFactoryResponse:
2✔
123
        """Start a renku session."""
124

125
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
126
        @validate(json=apispec.LaunchNotebookRequestOld)
2✔
127
        async def _launch_notebook(
2✔
128
            request: Request,
129
            user: AnonymousAPIUser | AuthenticatedAPIUser,
130
            internal_gitlab_user: APIUser,
131
            body: apispec.LaunchNotebookRequestOld,
132
        ) -> JSONResponse:
133
            server, status_code = await core.launch_notebook(
1✔
134
                self.nb_config,
135
                user,
136
                internal_gitlab_user,
137
                body,
138
                user_repo=self.user_repo,
139
                storage_repo=self.storage_repo,
140
            )
141
            return core.serialize_v1_server(server, self.nb_config, status_code)
1✔
142

143
        return "/notebooks/servers", ["POST"], _launch_notebook
2✔
144

145
    def patch_server(self) -> BlueprintFactoryResponse:
2✔
146
        """Patch a user server by name based on the query param."""
147

148
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
149
        @validate(json=apispec.PatchServerRequest)
2✔
150
        async def _patch_server(
2✔
151
            request: Request,
152
            user: AnonymousAPIUser | AuthenticatedAPIUser,
153
            internal_gitlab_user: APIUser,
154
            server_name: str,
155
            body: apispec.PatchServerRequest,
156
        ) -> JSONResponse:
157
            if isinstance(user, AnonymousAPIUser):
1✔
158
                raise AnonymousUserPatchError()
×
159

160
            manifest = await core.patch_server(self.nb_config, user, internal_gitlab_user, server_name, body)
1✔
161
            return core.serialize_v1_server(manifest, self.nb_config)
1✔
162

163
        return "/notebooks/servers/<server_name>", ["PATCH"], _patch_server
2✔
164

165
    def stop_server(self) -> BlueprintFactoryResponse:
2✔
166
        """Stop user server by name."""
167

168
        @authenticate(self.authenticator)
2✔
169
        async def _stop_server(
2✔
170
            _: Request, user: AnonymousAPIUser | AuthenticatedAPIUser, server_name: str
171
        ) -> HTTPResponse:
172
            try:
1✔
173
                await core.stop_server(self.nb_config, user, server_name)
1✔
UNCOV
174
            except errors.MissingResourceError as err:
×
UNCOV
175
                raise exceptions.NotFound(message=err.message)
×
176
            return HTTPResponse(status=204)
1✔
177

178
        return "/notebooks/servers/<server_name>", ["DELETE"], _stop_server
2✔
179

180
    def server_options(self) -> BlueprintFactoryResponse:
2✔
181
        """Return a set of configurable server options."""
182

183
        async def _server_options(request: Request) -> JSONResponse:
2✔
184
            return json(ServerOptionsEndpointResponse().dump(core.server_options(self.nb_config)))
1✔
185

186
        return "/notebooks/server_options", ["GET"], _server_options
2✔
187

188
    def server_logs(self) -> BlueprintFactoryResponse:
2✔
189
        """Return the logs of the running server."""
190

191
        @authenticate(self.authenticator)
2✔
192
        async def _server_logs(
2✔
193
            request: Request, user: AnonymousAPIUser | AuthenticatedAPIUser, server_name: str
194
        ) -> JSONResponse:
195
            args: dict[str, str | int] = request.get_args()
1✔
196
            max_lines = int(args.get("max_lines", 250))
1✔
197
            try:
1✔
198
                logs = await core.server_logs(self.nb_config, user, server_name, max_lines)
1✔
199
            except errors.MissingResourceError as err:
1✔
200
                raise exceptions.NotFound(message=err.message)
1✔
201
            return json(ServerLogs().dump(logs))
1✔
202

203
        return "/notebooks/logs/<server_name>", ["GET"], _server_logs
2✔
204

205
    def check_docker_image(self) -> BlueprintFactoryResponse:
2✔
206
        """Return the availability of the docker image."""
207

208
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
209
        @validate(query=apispec.NotebooksImagesGetParametersQuery)
2✔
210
        async def _check_docker_image(
2✔
211
            request: Request,
212
            user: AnonymousAPIUser | AuthenticatedAPIUser,
213
            internal_gitlab_user: APIUser,
214
            query: apispec.NotebooksImagesGetParametersQuery,
215
        ) -> HTTPResponse:
216
            image_url = request.get_args().get("image_url")
1✔
217
            if not isinstance(image_url, str):
1✔
218
                raise ValueError("required string of image url")
×
219

220
            status = 200 if await core.docker_image_exists(self.nb_config, image_url, internal_gitlab_user) else 404
1✔
221
            return HTTPResponse(status=status)
1✔
222

223
        return "/notebooks/images", ["GET"], _check_docker_image
2✔
224

225

226
@dataclass(kw_only=True)
2✔
227
class NotebooksNewBP(CustomBlueprint):
2✔
228
    """Handlers for manipulating notebooks for the new Amalthea operator."""
229

230
    authenticator: base_models.Authenticator
2✔
231
    internal_gitlab_authenticator: base_models.Authenticator
2✔
232
    nb_config: NotebooksConfig
2✔
233
    project_repo: ProjectRepository
2✔
234
    project_session_secret_repo: ProjectSessionSecretRepository
2✔
235
    session_repo: SessionRepository
2✔
236
    rp_repo: ResourcePoolRepository
2✔
237
    storage_repo: StorageRepository
2✔
238
    user_repo: UserRepo
2✔
239
    data_connector_repo: DataConnectorRepository
2✔
240
    data_connector_secret_repo: DataConnectorSecretRepository
2✔
241

242
    def start(self) -> BlueprintFactoryResponse:
2✔
243
        """Start a session with the new operator."""
244

245
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
246
        @validate(json=apispec.SessionPostRequest)
2✔
247
        async def _handler(
2✔
248
            request: Request,
249
            user: AuthenticatedAPIUser | AnonymousAPIUser,
250
            internal_gitlab_user: APIUser,
251
            body: apispec.SessionPostRequest,
252
        ) -> JSONResponse:
253
            # gitlab_client = NotebooksGitlabClient(self.nb_config.git.url, internal_gitlab_user.access_token)
254
            launcher = await self.session_repo.get_launcher(user, ULID.from_str(body.launcher_id))
×
255
            project = await self.project_repo.get_project(user=user, project_id=launcher.project_id)
×
256
            server_name = renku_2_make_server_name(
×
257
                user=user, project_id=str(launcher.project_id), launcher_id=body.launcher_id
258
            )
259
            existing_session = await self.nb_config.k8s_v2_client.get_server(server_name, user.id)
×
260
            if existing_session is not None and existing_session.spec is not None:
×
261
                return json(existing_session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
262
            environment = launcher.environment
×
263
            image = environment.container_image
×
264
            image_workdir = await core.docker_image_workdir(
×
265
                self.nb_config, environment.container_image, internal_gitlab_user
266
            )
267
            default_resource_class = await self.rp_repo.get_default_resource_class()
×
268
            if default_resource_class.id is None:
×
269
                raise errors.ProgrammingError(message="The default resource class has to have an ID", quiet=True)
×
270
            if body.resource_class_id is None:
×
271
                resource_pool = await self.rp_repo.get_default_resource_pool()
×
272
                resource_class = resource_pool.get_default_resource_class()
×
273
                if not resource_class and len(resource_pool.classes) > 0:
×
274
                    resource_class = resource_pool.classes[0]
×
275
                if not resource_class or not resource_class.id:
×
276
                    raise errors.ProgrammingError(message="There cannot find any resource classes in the default pool.")
×
277
            else:
278
                resource_pool = await self.rp_repo.get_resource_pool_from_class(user, body.resource_class_id)
×
279
                resource_class = resource_pool.get_resource_class(body.resource_class_id)
×
280
                if not resource_class or not resource_class.id:
×
281
                    raise errors.MissingResourceError(
×
282
                        message=f"The resource class with ID {body.resource_class_id} does not exist."
283
                    )
284
            await self.nb_config.crc_validator.validate_class_storage(user, resource_class.id, body.disk_storage)
×
285
            work_dir_fallback = PurePosixPath("/home/jovyan")
×
286
            work_dir = environment.working_directory or image_workdir or work_dir_fallback
×
287
            storage_mount_fallback = work_dir / "work"
×
288
            storage_mount = launcher.environment.mount_directory or storage_mount_fallback
×
289
            secrets_mount_directory = storage_mount / project.secrets_mount_directory
×
290
            session_secrets = await self.project_session_secret_repo.get_all_session_secrets_from_project(
×
291
                user=user, project_id=project.id
292
            )
293
            data_connectors_stream = self.data_connector_secret_repo.get_data_connectors_with_secrets(user, project.id)
×
294
            git_providers = await self.nb_config.git_provider_helper.get_providers(user=user)
×
295
            repositories: list[Repository] = []
×
296
            for repo in project.repositories:
×
297
                found_provider_id: str | None = None
×
298
                for provider in git_providers:
×
299
                    if urlparse(provider.url).netloc == urlparse(repo).netloc:
×
300
                        found_provider_id = provider.id
×
301
                        break
×
302
                repositories.append(Repository(url=repo, provider=found_provider_id))
×
303

304
            # User secrets
305
            extra_volume_mounts: list[ExtraVolumeMount] = []
×
306
            extra_volumes: list[ExtraVolume] = []
×
307
            extra_init_containers: list[InitContainer] = []
×
308
            user_secrets_container_patches = user_secrets_container(
×
309
                user=user,
310
                config=self.nb_config,
311
                secrets_mount_directory=secrets_mount_directory.as_posix(),
312
                k8s_secret_name=f"{server_name}-secrets",
313
                session_secrets=session_secrets,
314
            )
315
            if user_secrets_container_patches is not None:
×
316
                (init_container_session_secret, volumes_session_secret, volume_mounts_session_secret) = (
×
317
                    user_secrets_container_patches
318
                )
319
                extra_volumes.extend(volumes_session_secret)
×
320
                extra_volume_mounts.extend(volume_mounts_session_secret)
×
321
                extra_init_containers.append(init_container_session_secret)
×
322

323
            secrets_to_create: list[ExtraSecret] = []
×
324
            data_sources, data_secrets, enc_secrets = await get_data_sources(
×
325
                nb_config=self.nb_config,
326
                server_name=server_name,
327
                user=user,
328
                data_connectors_stream=data_connectors_stream,
329
                work_dir=work_dir,
330
                cloud_storage_overrides=body.cloudstorage or [],
331
                user_repo=self.user_repo,
332
            )
333
            secrets_to_create.extend(data_secrets)
×
334
            extra_init_containers_dc, extra_init_volumes_dc = await get_extra_init_containers(
×
335
                self.nb_config,
336
                user,
337
                repositories,
338
                git_providers,
339
                storage_mount,
340
                work_dir,
341
                uid=environment.uid,
342
                gid=environment.gid,
343
            )
344
            extra_containers = await get_extra_containers(self.nb_config, user, repositories, git_providers)
×
345
            extra_volumes.extend(extra_init_volumes_dc)
×
346
            extra_init_containers.extend(extra_init_containers_dc)
×
347

348
            base_server_url = self.nb_config.sessions.ingress.base_url(server_name)
×
349
            base_server_path = self.nb_config.sessions.ingress.base_path(server_name)
×
350
            ui_path: str = (
×
351
                f"{base_server_path.rstrip('/')}/{environment.default_url.lstrip('/')}"
352
                if len(environment.default_url) > 0
353
                else base_server_path
354
            )
355
            annotations: dict[str, str] = {
×
356
                "renku.io/project_id": str(launcher.project_id),
357
                "renku.io/launcher_id": body.launcher_id,
358
                "renku.io/resource_class_id": str(body.resource_class_id or default_resource_class.id),
359
            }
360
            requests: dict[str, str | int] = {
×
361
                "cpu": str(round(resource_class.cpu * 1000)) + "m",
362
                "memory": f"{resource_class.memory}Gi",
363
            }
364
            limits: dict[str, str | int] = {"memory": f"{resource_class.memory}Gi"}
×
365
            if resource_class.gpu > 0:
×
366
                gpu_name = GpuKind.NVIDIA.value + "/gpu"
×
367
                requests[gpu_name] = resource_class.gpu
×
368
                limits[gpu_name] = resource_class.gpu
×
369
            if isinstance(user, AuthenticatedAPIUser):
×
370
                auth_secret = await get_auth_secret_authenticated(self.nb_config, user, server_name)
×
371
            else:
372
                auth_secret = await get_auth_secret_anonymous(self.nb_config, server_name, request)
×
373
            if auth_secret.volume:
×
374
                extra_volumes.append(auth_secret.volume)
×
375

376
            image_pull_secret_name = None
×
377
            if isinstance(user, AuthenticatedAPIUser) and internal_gitlab_user.access_token is not None:
×
378
                needs_pull_secret = await requires_image_pull_secret(self.nb_config, image, internal_gitlab_user)
×
379

380
                if needs_pull_secret:
×
381
                    image_pull_secret_name = f"{server_name}-image-secret"
×
382

383
                    image_secret = get_gitlab_image_pull_secret(
×
384
                        self.nb_config, user, image_pull_secret_name, internal_gitlab_user.access_token
385
                    )
386
                    secrets_to_create.append(image_secret)
×
387

388
            secrets_to_create.append(auth_secret)
×
389

390
            manifest = AmaltheaSessionV1Alpha1(
×
391
                metadata=Metadata(name=server_name, annotations=annotations),
392
                spec=AmaltheaSessionSpec(
393
                    imagePullSecrets=[ImagePullSecret(name=image_pull_secret_name, adopt=True)]
394
                    if image_pull_secret_name
395
                    else [],
396
                    codeRepositories=[],
397
                    hibernated=False,
398
                    reconcileStrategy=ReconcileStrategy.whenFailedOrHibernated,
399
                    priorityClassName=resource_class.quota,
400
                    session=Session(
401
                        image=image,
402
                        imagePullPolicy=ImagePullPolicy.Always,
403
                        urlPath=ui_path,
404
                        port=environment.port,
405
                        storage=Storage(
406
                            className=self.nb_config.sessions.storage.pvs_storage_class,
407
                            size=str(body.disk_storage) + "G",
408
                            mountPath=storage_mount.as_posix(),
409
                        ),
410
                        workingDir=work_dir.as_posix(),
411
                        runAsUser=environment.uid,
412
                        runAsGroup=environment.gid,
413
                        resources=Resources(requests=requests, limits=limits if len(limits) > 0 else None),
414
                        extraVolumeMounts=extra_volume_mounts,
415
                        command=environment.command,
416
                        args=environment.args,
417
                        shmSize="1G",
418
                        env=[
419
                            SessionEnvItem(name="RENKU_BASE_URL_PATH", value=base_server_path),
420
                            SessionEnvItem(name="RENKU_BASE_URL", value=base_server_url),
421
                            SessionEnvItem(name="RENKU_MOUNT_DIR", value=storage_mount.as_posix()),
422
                            SessionEnvItem(name="RENKU_SESSION", value="1"),
423
                            SessionEnvItem(name="RENKU_SESSION_IP", value="0.0.0.0"),  # nosec B104
424
                            SessionEnvItem(name="RENKU_SESSION_PORT", value=f"{environment.port}"),
425
                            SessionEnvItem(name="RENKU_WORKING_DIR", value=work_dir.as_posix()),
426
                        ],
427
                    ),
428
                    ingress=Ingress(
429
                        host=self.nb_config.sessions.ingress.host,
430
                        ingressClassName=self.nb_config.sessions.ingress.annotations.get("kubernetes.io/ingress.class"),
431
                        annotations=self.nb_config.sessions.ingress.annotations,
432
                        tlsSecret=TlsSecret(adopt=False, name=self.nb_config.sessions.ingress.tls_secret)
433
                        if self.nb_config.sessions.ingress.tls_secret is not None
434
                        else None,
435
                        pathPrefix=base_server_path,
436
                    ),
437
                    extraContainers=extra_containers,
438
                    initContainers=extra_init_containers,
439
                    extraVolumes=extra_volumes,
440
                    culling=get_culling(resource_pool, self.nb_config),
441
                    authentication=Authentication(
442
                        enabled=True,
443
                        type=AuthenticationType.oauth2proxy
444
                        if isinstance(user, AuthenticatedAPIUser)
445
                        else AuthenticationType.token,
446
                        secretRef=auth_secret.key_ref("auth"),
447
                        extraVolumeMounts=[auth_secret.volume_mount] if auth_secret.volume_mount else [],
448
                    ),
449
                    dataSources=data_sources,
450
                    tolerations=tolerations_from_resource_class(
451
                        resource_class, self.nb_config.sessions.tolerations_model
452
                    ),
453
                    affinity=node_affinity_from_resource_class(resource_class, self.nb_config.sessions.affinity_model),
454
                ),
455
            )
456
            for s in secrets_to_create:
×
457
                await self.nb_config.k8s_v2_client.create_secret(s.secret)
×
458
            try:
×
459
                manifest = await self.nb_config.k8s_v2_client.create_server(manifest, user.id)
×
460
            except Exception:
×
461
                for s in secrets_to_create:
×
462
                    await self.nb_config.k8s_v2_client.delete_secret(s.secret.metadata.name)
×
463
                raise errors.ProgrammingError(message="Could not start the amalthea session")
×
464
            else:
465
                try:
×
466
                    await request_session_secret_creation(user, self.nb_config, manifest, session_secrets)
×
467
                    await request_dc_secret_creation(user, self.nb_config, manifest, enc_secrets)
×
468
                except Exception:
×
469
                    await self.nb_config.k8s_v2_client.delete_server(server_name, user.id)
×
470
                    raise
×
471

472
            return json(manifest.as_apispec().model_dump(mode="json", exclude_none=True), 201)
×
473

474
        return "/sessions", ["POST"], _handler
2✔
475

476
    def get_all(self) -> BlueprintFactoryResponse:
2✔
477
        """Get all sessions for a user."""
478

479
        @authenticate(self.authenticator)
2✔
480
        async def _handler(_: Request, user: AuthenticatedAPIUser | AnonymousAPIUser) -> HTTPResponse:
2✔
481
            sessions = await self.nb_config.k8s_v2_client.list_servers(user.id)
×
482
            output: list[dict] = []
×
483
            for session in sessions:
×
484
                output.append(session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
485
            return json(output)
×
486

487
        return "/sessions", ["GET"], _handler
2✔
488

489
    def get_one(self) -> BlueprintFactoryResponse:
2✔
490
        """Get a specific session for a user."""
491

492
        @authenticate(self.authenticator)
2✔
493
        async def _handler(_: Request, user: AuthenticatedAPIUser | AnonymousAPIUser, session_id: str) -> HTTPResponse:
2✔
494
            session = await self.nb_config.k8s_v2_client.get_server(session_id, user.id)
×
495
            if session is None:
×
496
                raise errors.ValidationError(message=f"The session with ID {session_id} does not exist.", quiet=True)
×
497
            return json(session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
498

499
        return "/sessions/<session_id>", ["GET"], _handler
2✔
500

501
    def delete(self) -> BlueprintFactoryResponse:
2✔
502
        """Fully delete a session with the new operator."""
503

504
        @authenticate(self.authenticator)
2✔
505
        async def _handler(_: Request, user: AuthenticatedAPIUser | AnonymousAPIUser, session_id: str) -> HTTPResponse:
2✔
506
            await self.nb_config.k8s_v2_client.delete_server(session_id, user.id)
×
507
            return empty()
×
508

509
        return "/sessions/<session_id>", ["DELETE"], _handler
2✔
510

511
    def patch(self) -> BlueprintFactoryResponse:
2✔
512
        """Patch a session."""
513

514
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
515
        @validate(json=apispec.SessionPatchRequest)
2✔
516
        async def _handler(
2✔
517
            _: Request,
518
            user: AuthenticatedAPIUser | AnonymousAPIUser,
519
            internal_gitlab_user: APIUser,
520
            session_id: str,
521
            body: apispec.SessionPatchRequest,
522
        ) -> HTTPResponse:
523
            new_session = await patch_session(
×
524
                body,
525
                session_id,
526
                self.nb_config,
527
                user,
528
                internal_gitlab_user,
529
                rp_repo=self.rp_repo,
530
                project_repo=self.project_repo,
531
            )
532
            return json(new_session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
533

534
        return "/sessions/<session_id>", ["PATCH"], _handler
2✔
535

536
    def logs(self) -> BlueprintFactoryResponse:
2✔
537
        """Get logs from the session."""
538

539
        @authenticate(self.authenticator)
2✔
540
        @validate(query=apispec.SessionsSessionIdLogsGetParametersQuery)
2✔
541
        async def _handler(
2✔
542
            _: Request,
543
            user: AuthenticatedAPIUser | AnonymousAPIUser,
544
            session_id: str,
545
            query: apispec.SessionsSessionIdLogsGetParametersQuery,
546
        ) -> HTTPResponse:
547
            logs = await self.nb_config.k8s_v2_client.get_server_logs(session_id, user.id, query.max_lines)
×
548
            return json(apispec.SessionLogsResponse.model_validate(logs).model_dump(exclude_none=True))
×
549

550
        return "/sessions/<session_id>/logs", ["GET"], _handler
2✔
551

552
    def check_docker_image(self) -> BlueprintFactoryResponse:
2✔
553
        """Return the availability of the docker image."""
554

555
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
556
        @validate(query=apispec.SessionsImagesGetParametersQuery)
2✔
557
        async def _check_docker_image(
2✔
558
            request: Request,
559
            user: AnonymousAPIUser | AuthenticatedAPIUser,
560
            internal_gitlab_user: APIUser,
561
            query: apispec.SessionsImagesGetParametersQuery,
562
        ) -> HTTPResponse:
563
            image_url = request.get_args().get("image_url")
×
564
            if not isinstance(image_url, str):
×
565
                raise ValueError("required string of image url")
×
566

567
            status = 200 if await core.docker_image_exists(self.nb_config, image_url, internal_gitlab_user) else 404
×
568
            return HTTPResponse(status=status)
×
569

570
        return "/sessions/images", ["GET"], _check_docker_image
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc