From 4f7b0fd880d49c6861aedd758dd53b080cd14fbd Mon Sep 17 00:00:00 2001 From: Tasko Olevski <16360283+olevski@users.noreply.github.com> Date: Wed, 20 May 2026 17:24:56 +0200 Subject: [PATCH] feat: add option to remove iframe from sessions For remote K8s clusters getting the iframes of sessions to work is fairly complicated dur to CORS/CSP. This is because the sesssion is running on one domain (for example dev.renku.ch) whereas the UI is running on another - like renkulab.io. For clusters we do not have control and are limited it is hard to properly configure these options. Also in some cases the configuration is required at the Jupyterlab or RStudio level which makes it hard to generalize. This is just a quick relief for currently active remote clusters in production being used right now where a lot of users are having trouble and really bad UX because the default button to open a session leads to a 50x page. We can think and work on a long term solution in the future so that we still have iframes or the same UI for remote sessions. --- components/renku_data_services/crc/db.py | 2 ++ components/renku_data_services/crc/models.py | 3 +++ components/renku_data_services/crc/orm.py | 8 ++++++++ components/renku_data_services/notebooks/api.spec.yaml | 4 ++++ components/renku_data_services/notebooks/apispec.py | 6 +++++- components/renku_data_services/notebooks/constants.py | 2 ++ components/renku_data_services/notebooks/core_sessions.py | 3 +++ components/renku_data_services/notebooks/crs.py | 7 ++++++- 8 files changed, 33 insertions(+), 2 deletions(-) diff --git a/components/renku_data_services/crc/db.py b/components/renku_data_services/crc/db.py index 8248345b3a..ac198e90fb 100644 --- a/components/renku_data_services/crc/db.py +++ b/components/renku_data_services/crc/db.py @@ -559,6 +559,8 @@ async def update_resource_pool( rp.hibernation_warning_period = update.hibernation_warning_period if update.platform is not None: rp.platform = update.platform + if update.skip_session_iframe is not None: + rp.skip_session_iframe = update.skip_session_iframe match (update.cluster_id, rp.cluster_id): case ResetType.Reset, x if x is not None: diff --git a/components/renku_data_services/crc/models.py b/components/renku_data_services/crc/models.py index b4040d4926..5566d90e3c 100644 --- a/components/renku_data_services/crc/models.py +++ b/components/renku_data_services/crc/models.py @@ -328,6 +328,7 @@ class UnsavedResourcePool: remote: RemoteConfigurationFirecrest | RemoteConfigurationRunai | None = None cluster_id: ClusterId | None = None platform: RuntimePlatform + skip_session_iframe: bool = False @dataclass(frozen=True, eq=True, kw_only=True) @@ -347,6 +348,7 @@ class ResourcePool: cluster: SavedClusterSettings | None = None platform: RuntimePlatform credits_used: int | None = None + skip_session_iframe: bool = False def get_resource_class(self, resource_class_id: int) -> ResourceClass | None: """Find a specific resource class in the resource pool by the resource class id.""" @@ -384,6 +386,7 @@ class ResourcePoolPatch: remote: RemoteConfigurationPatch | None = None cluster_id: ClusterId | ResetType | None = None platform: RuntimePlatform | None = None + skip_session_iframe: bool | None = None class RemoteConfigurationKind(StrEnum): diff --git a/components/renku_data_services/crc/orm.py b/components/renku_data_services/crc/orm.py index 67ccbd5ddd..713bb59b73 100644 --- a/components/renku_data_services/crc/orm.py +++ b/components/renku_data_services/crc/orm.py @@ -17,6 +17,9 @@ Table, literal, ) +from sqlalchemy import ( + false as sql_false, +) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column, relationship from sqlalchemy.schema import ForeignKey @@ -294,6 +297,9 @@ class ResourcePoolORM(BaseORM): Enum(models.RuntimePlatform, name="build_platform"), default=None, server_default=literal("linux_amd64") ) + skip_session_iframe: Mapped[bool] = mapped_column(default=False, server_default=sql_false()) + """If set to true it will cause the UI to not open the session in an iframe.""" + @classmethod def from_unsaved_model( cls, @@ -325,6 +331,7 @@ def from_unsaved_model( remote_json=remote_json, cluster_id=cluster.id if cluster else None, platform=new_resource_pool.platform, + skip_session_iframe=new_resource_pool.skip_session_iframe, ) def dump( @@ -362,6 +369,7 @@ def dump( cluster=cluster, platform=self.platform, credits_used=credits_used.value if credits_used else None, + skip_session_iframe=self.skip_session_iframe, ) def _dump_remote(self) -> models.RemoteConfigurationFirecrest | models.RemoteConfigurationRunai | None: diff --git a/components/renku_data_services/notebooks/api.spec.yaml b/components/renku_data_services/notebooks/api.spec.yaml index 6c0344e265..2401359f7b 100644 --- a/components/renku_data_services/notebooks/api.spec.yaml +++ b/components/renku_data_services/notebooks/api.spec.yaml @@ -268,6 +268,10 @@ components: $ref: "#/components/schemas/Ulid" resource_class_id: type: integer + skip_iframe: + type: boolean + default: false + description: If set to true the UI will not open the session in an iframe once it is ready required: - image - name diff --git a/components/renku_data_services/notebooks/apispec.py b/components/renku_data_services/notebooks/apispec.py index fed2dc7ac3..0594c46288 100644 --- a/components/renku_data_services/notebooks/apispec.py +++ b/components/renku_data_services/notebooks/apispec.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: api.spec.yaml -# timestamp: 2026-05-05T12:21:39+00:00 +# timestamp: 2026-05-20T15:08:05+00:00 from __future__ import annotations @@ -192,6 +192,10 @@ class SessionResponse(BaseAPISpec): pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$", ) resource_class_id: int + skip_iframe: bool = Field( + False, + description="If set to true the UI will not open the session in an iframe once it is ready", + ) class SessionListResponse(RootModel[List[SessionResponse]]): diff --git a/components/renku_data_services/notebooks/constants.py b/components/renku_data_services/notebooks/constants.py index d663041783..daec653f6f 100644 --- a/components/renku_data_services/notebooks/constants.py +++ b/components/renku_data_services/notebooks/constants.py @@ -6,3 +6,5 @@ AMALTHEA_SESSION_GVK: Final[GVK] = GVK(group="amalthea.dev", version="v1alpha1", kind="AmaltheaSession") JUPYTER_SESSION_GVK: Final[GVK] = GVK(group="amalthea.dev", version="v1alpha1", kind="JupyterServer") + +SKIP_SESSION_IFRAME_ANNOTATION = Final[str] = "renku.io/skip_session_iframe" diff --git a/components/renku_data_services/notebooks/core_sessions.py b/components/renku_data_services/notebooks/core_sessions.py index 297589ab7e..e08ee6cf3f 100644 --- a/components/renku_data_services/notebooks/core_sessions.py +++ b/components/renku_data_services/notebooks/core_sessions.py @@ -14,6 +14,7 @@ import httpx from kubernetes.client import V1ObjectMeta, V1Secret +from renku_data_services.notebooks.constants import SKIP_SESSION_IFRAME_ANNOTATION from sanic import Request from toml import dumps from ulid import ULID @@ -962,6 +963,7 @@ async def start_session( "renku.io/launcher_id": str(launcher_id), "renku.io/resource_class_id": str(resource_class.id), "renku.io/resource_pool_id": str(resource_pool.id), + SKIP_SESSION_IFRAME_ANNOTATION: str(resource_pool.skip_session_iframe), } # Authentication @@ -1240,6 +1242,7 @@ async def patch_session( annotations.update(session.spec.template.metadata.annotations) annotations["renku.io/resource_class_id"] = str(rc.id) annotations["renku.io/resource_pool_id"] = str(rp.id) + annotations[SKIP_SESSION_IFRAME_ANNOTATION] = rp.skip_session_iframe patch.spec.template = TemplatePatch(metadata=TemplateMetadataPatch(annotations=annotations)) if not patch.spec.session: patch.spec.session = AmaltheaSessionV1Alpha1SpecSessionPatch() diff --git a/components/renku_data_services/notebooks/crs.py b/components/renku_data_services/notebooks/crs.py index 34e4a7096f..ff5cb0679b 100644 --- a/components/renku_data_services/notebooks/crs.py +++ b/components/renku_data_services/notebooks/crs.py @@ -17,7 +17,11 @@ from renku_data_services.base_models.core import RESET, ResetType from renku_data_services.errors import errors from renku_data_services.notebooks import apispec -from renku_data_services.notebooks.constants import AMALTHEA_SESSION_GVK, JUPYTER_SESSION_GVK +from renku_data_services.notebooks.constants import ( + AMALTHEA_SESSION_GVK, + JUPYTER_SESSION_GVK, + SKIP_SESSION_IFRAME_ANNOTATION, +) from renku_data_services.notebooks.cr_amalthea_session import Affinity as _Affinity from renku_data_services.notebooks.cr_amalthea_session import ( Authentication, @@ -364,6 +368,7 @@ def as_apispec(self) -> apispec.SessionResponse: project_id=str(self.project_id), launcher_id=str(self.launcher_id), resource_class_id=self.resource_class_id(), + skip_iframe=self.metadata.annotations.get(SKIP_SESSION_IFRAME_ANNOTATION, False), ) def base_url(self) -> str | None: