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

SwissDataScienceCenter / renku-data-services / 16369691288

18 Jul 2025 11:43AM UTC coverage: 87.186% (-0.04%) from 87.229%
16369691288

Pull #929

github

web-flow
Merge 03ce98685 into 8be58eb79
Pull Request #929: exp: session api proxy

8 of 21 new or added lines in 3 files covered. (38.1%)

13 existing lines in 5 files now uncovered.

21486 of 24644 relevant lines covered (87.19%)

1.53 hits per line

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

55.68
/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

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

11
from renku_data_services import base_models
2✔
12
from renku_data_services.base_api.auth import authenticate, authenticate_2
2✔
13
from renku_data_services.base_api.blueprint import BlueprintFactoryResponse, CustomBlueprint
2✔
14
from renku_data_services.base_models import AnonymousAPIUser, APIUser, AuthenticatedAPIUser, Authenticator
2✔
15
from renku_data_services.base_models.metrics import MetricsService
2✔
16
from renku_data_services.crc.db import ClusterRepository, ResourcePoolRepository
2✔
17
from renku_data_services.data_connectors.db import (
2✔
18
    DataConnectorRepository,
19
    DataConnectorSecretRepository,
20
)
21
from renku_data_services.errors import errors
2✔
22
from renku_data_services.notebooks import apispec, core
2✔
23
from renku_data_services.notebooks.api.amalthea_patches.init_containers import user_secrets_container
2✔
24
from renku_data_services.notebooks.api.schemas.config_server_options import ServerOptionsEndpointResponse
2✔
25
from renku_data_services.notebooks.api.schemas.logs import ServerLogs
2✔
26
from renku_data_services.notebooks.config import NotebooksConfig
2✔
27
from renku_data_services.notebooks.core_sessions import (
2✔
28
    get_auth_secret_anonymous,
29
    get_auth_secret_authenticated,
30
    get_culling,
31
    get_data_sources,
32
    get_extra_containers,
33
    get_extra_init_containers,
34
    get_gitlab_image_pull_secret,
35
    get_launcher_env_variables,
36
    patch_session,
37
    repositories_from_project,
38
    request_dc_secret_creation,
39
    request_session_secret_creation,
40
    requires_image_pull_secret,
41
    resources_from_resource_class,
42
    verify_launcher_env_variable_overrides,
43
)
44
from renku_data_services.notebooks.crs import (
2✔
45
    AmaltheaSessionSpec,
46
    AmaltheaSessionV1Alpha1,
47
    Authentication,
48
    AuthenticationType,
49
    ExtraVolume,
50
    ExtraVolumeMount,
51
    ImagePullPolicy,
52
    ImagePullSecret,
53
    Ingress,
54
    InitContainer,
55
    Metadata,
56
    ReconcileStrategy,
57
    Session,
58
    SessionEnvItem,
59
    Storage,
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✔
174
            except errors.MissingResourceError as err:
×
175
                raise exceptions.NotFound(message=err.message) from err
×
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) from err
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
    metrics: MetricsService
2✔
242
    cluster_repo: ClusterRepository
2✔
243

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

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

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

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

347
            (
×
348
                base_server_path,
349
                base_server_url,
350
                base_server_https_url,
351
                host,
352
                tls_secret,
353
                ingress_annotations,
354
            ) = await cluster.get_ingress_parameters(
355
                user, self.cluster_repo, self.nb_config.sessions.ingress, server_name
356
            )
357

358
            ui_path = f"{base_server_path}/{environment.default_url.lstrip('/')}"
×
359

360
            ingress = Ingress(
×
361
                host=host,
362
                ingressClassName=ingress_annotations.get("kubernetes.io/ingress.class"),
363
                annotations=ingress_annotations,
364
                tlsSecret=tls_secret,
365
                pathPrefix=base_server_path,
366
            )
367

368
            annotations: dict[str, str] = {
×
369
                "renku.io/project_id": str(launcher.project_id),
370
                "renku.io/launcher_id": body.launcher_id,
371
                "renku.io/resource_class_id": str(body.resource_class_id or default_resource_class.id),
372
            }
373
            if isinstance(user, AuthenticatedAPIUser):
×
374
                auth_secret = await get_auth_secret_authenticated(
×
375
                    self.nb_config, user, server_name, base_server_url, base_server_https_url, base_server_path
376
                )
377
            else:
378
                auth_secret = await get_auth_secret_anonymous(self.nb_config, server_name, request)
×
379
            if auth_secret.volume:
×
380
                extra_volumes.append(auth_secret.volume)
×
381

382
            image_pull_secret_name = None
×
383
            if isinstance(user, AuthenticatedAPIUser) and internal_gitlab_user.access_token is not None:
×
384
                needs_pull_secret = await requires_image_pull_secret(self.nb_config, image, internal_gitlab_user)
×
385

386
                if needs_pull_secret:
×
387
                    image_pull_secret_name = f"{server_name}-image-secret"
×
388

389
                    image_secret = get_gitlab_image_pull_secret(
×
390
                        self.nb_config, user, image_pull_secret_name, internal_gitlab_user.access_token
391
                    )
392
                    secrets_to_create.append(image_secret)
×
393

394
            secrets_to_create.append(auth_secret)
×
395

396
            # Raise an error if there are invalid environment variables in the request body
397
            verify_launcher_env_variable_overrides(launcher, body)
×
398
            env = [
×
399
                SessionEnvItem(name="RENKU_BASE_URL_PATH", value=base_server_path),
400
                SessionEnvItem(name="RENKU_BASE_URL", value=base_server_url),
401
                SessionEnvItem(name="RENKU_MOUNT_DIR", value=storage_mount.as_posix()),
402
                SessionEnvItem(name="RENKU_SESSION", value="1"),
403
                SessionEnvItem(name="RENKU_SESSION_IP", value="0.0.0.0"),  # nosec B104
404
                SessionEnvItem(name="RENKU_SESSION_PORT", value=f"{environment.port}"),
405
                SessionEnvItem(name="RENKU_WORKING_DIR", value=work_dir.as_posix()),
406
            ]
407
            launcher_env_variables = get_launcher_env_variables(launcher, body)
×
408
            if launcher_env_variables:
×
409
                env.extend(launcher_env_variables)
×
410

411
            storage_class = await cluster.get_storage_class(
×
412
                user, self.cluster_repo, self.nb_config.sessions.storage.pvs_storage_class
413
            )
414
            manifest = AmaltheaSessionV1Alpha1(
×
415
                metadata=Metadata(name=server_name, annotations=annotations),
416
                spec=AmaltheaSessionSpec(
417
                    imagePullSecrets=[ImagePullSecret(name=image_pull_secret_name, adopt=True)]
418
                    if image_pull_secret_name
419
                    else [],
420
                    codeRepositories=[],
421
                    hibernated=False,
422
                    reconcileStrategy=ReconcileStrategy.whenFailedOrHibernated,
423
                    priorityClassName=resource_class.quota,
424
                    session=Session(
425
                        image=image,
426
                        imagePullPolicy=ImagePullPolicy.Always,
427
                        urlPath=ui_path,
428
                        port=environment.port,
429
                        storage=Storage(
430
                            className=storage_class,
431
                            size=str(body.disk_storage) + "G",
432
                            mountPath=storage_mount.as_posix(),
433
                        ),
434
                        workingDir=work_dir.as_posix(),
435
                        runAsUser=environment.uid,
436
                        runAsGroup=environment.gid,
437
                        resources=resources_from_resource_class(resource_class),
438
                        extraVolumeMounts=extra_volume_mounts,
439
                        command=environment.command,
440
                        args=environment.args,
441
                        shmSize="1G",
442
                        env=env,
443
                    ),
444
                    ingress=ingress,
445
                    extraContainers=extra_containers,
446
                    initContainers=extra_init_containers,
447
                    extraVolumes=extra_volumes,
448
                    culling=get_culling(user, resource_pool, self.nb_config),
449
                    authentication=Authentication(
450
                        enabled=True,
451
                        type=AuthenticationType.oauth2proxy
452
                        if isinstance(user, AuthenticatedAPIUser)
453
                        else AuthenticationType.token,
454
                        secretRef=auth_secret.key_ref("auth"),
455
                        extraVolumeMounts=[auth_secret.volume_mount] if auth_secret.volume_mount else [],
456
                    ),
457
                    dataSources=data_sources,
458
                    tolerations=tolerations_from_resource_class(
459
                        resource_class, self.nb_config.sessions.tolerations_model
460
                    ),
461
                    affinity=node_affinity_from_resource_class(resource_class, self.nb_config.sessions.affinity_model),
462
                ),
463
            )
464
            for s in secrets_to_create:
×
465
                await self.nb_config.k8s_v2_client.create_secret(s.secret, cluster)
×
466
            try:
×
467
                manifest = await self.nb_config.k8s_v2_client.create_session(manifest, user)
×
468
            except Exception as err:
×
469
                for s in secrets_to_create:
×
470
                    await self.nb_config.k8s_v2_client.delete_secret(s.secret.metadata.name, cluster)
×
471
                raise errors.ProgrammingError(message="Could not start the amalthea session") from err
×
472
            else:
473
                try:
×
474
                    await request_session_secret_creation(user, self.nb_config, manifest, session_secrets)
×
475
                    await request_dc_secret_creation(user, self.nb_config, manifest, enc_secrets)
×
476
                except Exception:
×
477
                    await self.nb_config.k8s_v2_client.delete_session(server_name, user.id)
×
478
                    raise
×
479

480
            await self.metrics.user_requested_session_launch(
×
481
                user=user,
482
                metadata={
483
                    "cpu": int(resource_class.cpu * 1000),
484
                    "memory": resource_class.memory,
485
                    "gpu": resource_class.gpu,
486
                    "storage": body.disk_storage,
487
                    "resource_class_id": resource_class.id,
488
                    "resource_pool_id": resource_pool.id or "",
489
                    "resource_class_name": f"{resource_pool.name}.{resource_class.name}",
490
                    "session_id": server_name,
491
                },
492
            )
493
            return json(manifest.as_apispec().model_dump(mode="json", exclude_none=True), 201)
×
494

495
        return "/sessions", ["POST"], _handler
2✔
496

497
    def get_all(self) -> BlueprintFactoryResponse:
2✔
498
        """Get all sessions for a user."""
499

500
        @authenticate(self.authenticator)
2✔
501
        async def _handler(_: Request, user: AuthenticatedAPIUser | AnonymousAPIUser) -> HTTPResponse:
2✔
502
            sessions = await self.nb_config.k8s_v2_client.list_sessions(user.id)
×
503
            output: list[dict] = []
×
504
            for session in sessions:
×
505
                output.append(session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
506
            return json(output)
×
507

508
        return "/sessions", ["GET"], _handler
2✔
509

510
    def get_one(self) -> BlueprintFactoryResponse:
2✔
511
        """Get a specific session for a user."""
512

513
        @authenticate(self.authenticator)
2✔
514
        async def _handler(_: Request, user: AuthenticatedAPIUser | AnonymousAPIUser, session_id: str) -> HTTPResponse:
2✔
515
            session = await self.nb_config.k8s_v2_client.get_session(session_id, user.id)
×
516
            if session is None:
×
517
                raise errors.ValidationError(message=f"The session with ID {session_id} does not exist.", quiet=True)
×
518
            return json(session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
519

520
        return "/sessions/<session_id>", ["GET"], _handler
2✔
521

522
    def delete(self) -> BlueprintFactoryResponse:
2✔
523
        """Fully delete a session with the new operator."""
524

525
        @authenticate(self.authenticator)
2✔
526
        async def _handler(_: Request, user: AuthenticatedAPIUser | AnonymousAPIUser, session_id: str) -> HTTPResponse:
2✔
527
            await self.nb_config.k8s_v2_client.delete_session(session_id, user.id)
×
528
            await self.metrics.session_stopped(user, metadata={"session_id": session_id})
×
529
            return empty()
×
530

531
        return "/sessions/<session_id>", ["DELETE"], _handler
2✔
532

533
    def patch(self) -> BlueprintFactoryResponse:
2✔
534
        """Patch a session."""
535

536
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
537
        @validate(json=apispec.SessionPatchRequest)
2✔
538
        async def _handler(
2✔
539
            _: Request,
540
            user: AuthenticatedAPIUser | AnonymousAPIUser,
541
            internal_gitlab_user: APIUser,
542
            session_id: str,
543
            body: apispec.SessionPatchRequest,
544
        ) -> HTTPResponse:
545
            new_session = await patch_session(
×
546
                body,
547
                session_id,
548
                self.nb_config,
549
                user,
550
                internal_gitlab_user,
551
                rp_repo=self.rp_repo,
552
                project_repo=self.project_repo,
553
                metrics=self.metrics,
554
            )
555
            return json(new_session.as_apispec().model_dump(exclude_none=True, mode="json"))
×
556

557
        return "/sessions/<session_id>", ["PATCH"], _handler
2✔
558

559
    def logs(self) -> BlueprintFactoryResponse:
2✔
560
        """Get logs from the session."""
561

562
        @authenticate(self.authenticator)
2✔
563
        @validate(query=apispec.SessionsSessionIdLogsGetParametersQuery)
2✔
564
        async def _handler(
2✔
565
            _: Request,
566
            user: AuthenticatedAPIUser | AnonymousAPIUser,
567
            session_id: str,
568
            query: apispec.SessionsSessionIdLogsGetParametersQuery,
569
        ) -> HTTPResponse:
570
            logs = await self.nb_config.k8s_v2_client.get_session_logs(session_id, user.id, query.max_lines)
×
571
            return json(apispec.SessionLogsResponse.model_validate(logs).model_dump(exclude_none=True))
×
572

573
        return "/sessions/<session_id>/logs", ["GET"], _handler
2✔
574

575
    def check_docker_image(self) -> BlueprintFactoryResponse:
2✔
576
        """Return the availability of the docker image."""
577

578
        @authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
2✔
579
        @validate(query=apispec.SessionsImagesGetParametersQuery)
2✔
580
        async def _check_docker_image(
2✔
581
            request: Request,
582
            user: AnonymousAPIUser | AuthenticatedAPIUser,
583
            internal_gitlab_user: APIUser,
584
            query: apispec.SessionsImagesGetParametersQuery,
585
        ) -> HTTPResponse:
586
            image_url = request.get_args().get("image_url")
×
587
            if not isinstance(image_url, str):
×
588
                raise ValueError("required string of image url")
×
589

590
            status = 200 if await core.docker_image_exists(self.nb_config, image_url, internal_gitlab_user) else 404
×
591
            return HTTPResponse(status=status)
×
592

593
        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