Skip to content

Commit 39f981c

Browse files
olevskiPanaetius
andcommitted
fix: preserve the default url in the session spec (#514)
Co-authored-by: Ralf Grubenmann <ralf.grubenmann@sdsc.ethz.ch>
1 parent c33365b commit 39f981c

4 files changed

Lines changed: 49 additions & 12 deletions

File tree

components/renku_data_services/notebooks/blueprints.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
from dataclasses import dataclass
66
from pathlib import PurePosixPath
7-
from typing import Any
7+
from typing import Any, cast
88
from urllib.parse import urljoin, urlparse
99

1010
import httpx
@@ -267,7 +267,7 @@ def start(self) -> BlueprintFactoryResponse:
267267
@authenticate_2(self.authenticator, self.internal_gitlab_authenticator)
268268
@validate(json=apispec.SessionPostRequest)
269269
async def _handler(
270-
_: Request,
270+
request: Request,
271271
user: AuthenticatedAPIUser | AnonymousAPIUser,
272272
internal_gitlab_user: APIUser,
273273
body: apispec.SessionPostRequest,
@@ -381,14 +381,19 @@ async def _handler(
381381

382382
base_server_url = self.nb_config.sessions.ingress.base_url(server_name)
383383
base_server_path = self.nb_config.sessions.ingress.base_path(server_name)
384+
ui_path: str = (
385+
f"{base_server_path.rstrip("/")}/{environment.default_url.lstrip("/")}"
386+
if len(environment.default_url) > 0
387+
else base_server_path
388+
)
384389
annotations: dict[str, str] = {
385390
"renku.io/project_id": str(launcher.project_id),
386391
"renku.io/launcher_id": body.launcher_id,
387392
"renku.io/resource_class_id": str(body.resource_class_id or default_resource_class.id),
388393
}
389394
requests: dict[str, str | int] = {
390395
"cpu": str(round(resource_class.cpu * 1000)) + "m",
391-
"memory": resource_class.memory,
396+
"memory": f"{resource_class.memory}Gi",
392397
}
393398
if resource_class.gpu > 0:
394399
gpu_name = GpuKind.NVIDIA.value + "/gpu"
@@ -409,7 +414,7 @@ async def _handler(
409414
hibernated=False,
410415
session=Session(
411416
image=image,
412-
urlPath=base_server_path,
417+
urlPath=ui_path,
413418
port=environment.port,
414419
storage=Storage(
415420
className=self.nb_config.sessions.storage.pvs_storage_class,
@@ -436,6 +441,7 @@ async def _handler(
436441
tlsSecret=TlsSecret(adopt=False, name=self.nb_config.sessions.ingress.tls_secret)
437442
if self.nb_config.sessions.ingress.tls_secret is not None
438443
else None,
444+
pathPrefix=base_server_path,
439445
),
440446
extraContainers=extra_containers,
441447
initContainers=session_init_containers,
@@ -454,7 +460,14 @@ async def _handler(
454460
else AuthenticationType.token,
455461
secretRef=SecretRefKey(name=server_name, key="auth", adopt=True),
456462
extraVolumeMounts=[
457-
ExtraVolumeMount(name="renku-authorized-emails", mountPath="/authorized_emails")
463+
# NOTE: Without subpath k8s keeps updating the secret and this can lead to
464+
# the oauth2proxy restarting intermittently even when the secret does not change
465+
# because the oauth2proxy watches this file and restarts on changes
466+
ExtraVolumeMount(
467+
name="renku-authorized-emails",
468+
mountPath="/authorized_emails",
469+
subPath="authorized_emails",
470+
)
458471
]
459472
if isinstance(user, AuthenticatedAPIUser)
460473
else [],
@@ -477,19 +490,31 @@ async def _handler(
477490
"redirect_url": urljoin(base_server_url + "/", "oauth2/callback"),
478491
"cookie_path": base_server_path,
479492
"proxy_prefix": parsed_proxy_url.path,
480-
"authenticated_emails_file": "/authorized_emails/authorized_emails",
493+
"authenticated_emails_file": "/authorized_emails",
481494
"client_secret": self.nb_config.sessions.oidc.client_secret,
482495
"cookie_secret": base64.urlsafe_b64encode(os.urandom(32)).decode(),
483496
"insecure_oidc_allow_unverified_email": self.nb_config.sessions.oidc.allow_unverified_email,
484497
}
485498
)
486499
secret_data["authorized_emails"] = user.email
487500
else:
501+
# NOTE: We extract the session cookie value here in order to avoid creating a cookie.
502+
# The gateway encrypts and signs cookies so the user ID injected in the request headers does not
503+
# match the value of the session cookie.
504+
session_id = cast(str | None, request.cookies.get(self.nb_config.session_id_cookie_name))
505+
if not session_id:
506+
raise errors.UnauthorizedError(
507+
message=f"You have to have a renku session cookie at {self.nb_config.session_id_cookie_name} "
508+
"in order to launch an anonymous session."
509+
)
510+
# NOTE: Amalthea looks for the token value first in the cookie and then in the authorization header
488511
secret_data["auth"] = safe_dump(
489512
{
490-
"token": user.id,
491-
"cookie_key": "Renku-Auth-Anon-Id",
492-
"verbose": True,
513+
"authproxy": {
514+
"token": session_id,
515+
"cookie_key": self.nb_config.session_id_cookie_name,
516+
"verbose": True,
517+
}
493518
}
494519
)
495520
secrets_to_create.append(V1Secret(metadata=V1ObjectMeta(name=server_name), string_data=secret_data))

components/renku_data_services/notebooks/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class NotebooksConfig:
121121
session_get_endpoint_annotations: _ServersGetEndpointAnnotations = field(
122122
default_factory=_ServersGetEndpointAnnotations
123123
)
124+
session_id_cookie_name: str = "_renku_session" # NOTE: This cookie name is set and controlled by the gateway
124125

125126
@classmethod
126127
def from_env(cls, db_config: DBConfig) -> Self:

components/renku_data_services/notebooks/cr_amalthea_session.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: <stdin>
3-
# timestamp: 2024-10-24T01:41:50+00:00
3+
# timestamp: 2024-11-05T01:48:51+00:00
44

55
from __future__ import annotations
66

@@ -2487,6 +2487,10 @@ class Ingress(BaseCRD):
24872487
annotations: Optional[Dict[str, str]] = None
24882488
host: str
24892489
ingressClassName: Optional[str] = None
2490+
pathPrefix: str = Field(
2491+
default="/",
2492+
description="The path prefix that will be used in the ingress. If this is explicitly set, then the\nurlPath value should be a subpath of this value.",
2493+
)
24902494
tlsSecret: Optional[TlsSecret] = Field(
24912495
default=None,
24922496
description="The name of the TLS secret, same as what is specified in a regular Kubernetes Ingress.",
@@ -3116,7 +3120,7 @@ class Session(BaseCRD):
31163120
)
31173121
image: str
31183122
port: int = Field(
3119-
...,
3123+
default=8000,
31203124
description="The TCP port on the pod where the session can be accessed.\nIf the session has authentication enabled then the ingress and service will point to the authentication container\nand the authentication proxy container will proxy to this port. If authentication is disabled then the ingress and service\nroute directly to this port. Note that renku reserves the highest TCP value 65535 to run the authentication proxy.",
31213125
gt=0,
31223126
lt=65535,
@@ -3137,7 +3141,7 @@ class Session(BaseCRD):
31373141
storage: Storage = {}
31383142
urlPath: str = Field(
31393143
default="/",
3140-
description="The path where the session can be accessed. If an ingress is enabled then this will be\nthe path prefix for the ingress.",
3144+
description="The path where the session can be accessed, if an ingress is used this should be a subpath\nof the ingress.pathPrefix field. For example if the pathPrefix is /foo, this should be /foo or /foo/bar,\nbut it cannot be /baz.",
31413145
)
31423146
workingDir: Optional[str] = Field(
31433147
default=None,
@@ -3278,6 +3282,10 @@ class Status(BaseCRD):
32783282
default=None,
32793283
description="Counts of the total and ready containers, can represent either regular or init containers.",
32803284
)
3285+
error: Optional[str] = Field(
3286+
default=None,
3287+
description="If the state is failed then the message will contain information about what went wrong, otherwise it is empty",
3288+
)
32813289
failingSince: Optional[datetime] = None
32823290
hibernatedSince: Optional[datetime] = None
32833291
idle: bool = False

components/renku_data_services/notebooks/crs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ def as_apispec(self) -> apispec.SessionResponse:
239239
total_containers=total_containers,
240240
will_hibernate_at=will_hibernate_at,
241241
will_delete_at=will_delete_at,
242+
message=self.status.error,
242243
),
243244
url=url,
244245
project_id=str(self.project_id),
@@ -256,6 +257,8 @@ def base_url(self) -> str | None:
256257
scheme = "https" if self.spec and self.spec.ingress and self.spec.ingress.tlsSecret else "http"
257258
host = self.spec.ingress.host
258259
path = self.spec.session.urlPath if self.spec.session.urlPath else "/"
260+
if not path.endswith("/"):
261+
path += "/"
259262
params = None
260263
query = None
261264
fragment = None

0 commit comments

Comments
 (0)