Skip to content

Commit 1e21119

Browse files
committed
squashme: cleanup session launcher updates
1 parent 70dee1a commit 1e21119

File tree

2 files changed

+96
-59
lines changed

2 files changed

+96
-59
lines changed

components/renku_data_services/session/blueprints.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,35 @@ def patch(self) -> BlueprintFactoryResponse:
172172
async def _patch(
173173
_: Request, user: base_models.APIUser, launcher_id: ULID, body: apispec.SessionLauncherPatch
174174
) -> JSONResponse:
175-
body_dict = body.model_dump(exclude_none=True)
176-
launcher = await self.session_repo.update_launcher(user=user, launcher_id=launcher_id, **body_dict)
175+
body_dict = body.model_dump(exclude_none=True, mode="json")
176+
async with self.session_repo.session_maker() as session, session.begin():
177+
current_launcher = await self.session_repo.get_launcher(user, launcher_id)
178+
new_env: models.UnsavedEnvironment | None = None
179+
if (
180+
isinstance(body.environment, apispec.EnvironmentPatchInLauncher)
181+
and current_launcher.environment.environment_kind == models.EnvironmentKind.GLOBAL
182+
and body.environment.environment_kind == apispec.EnvironmentKind.CUSTOM
183+
):
184+
# This means that the global environment is being swapped for a custom one,
185+
# so we have to create a brand new environment, but we have to validate here.
186+
validated_env = apispec.EnvironmentPostInLauncher.model_validate(body_dict.pop("environment"))
187+
new_env = models.UnsavedEnvironment(
188+
name=validated_env.name,
189+
description=validated_env.description,
190+
container_image=validated_env.container_image,
191+
default_url=validated_env.default_url,
192+
port=validated_env.port,
193+
working_directory=PurePosixPath(validated_env.working_directory),
194+
mount_directory=PurePosixPath(validated_env.mount_directory),
195+
uid=validated_env.uid,
196+
gid=validated_env.gid,
197+
environment_kind=models.EnvironmentKind(validated_env.environment_kind.value),
198+
args=validated_env.args,
199+
command=validated_env.command,
200+
)
201+
launcher = await self.session_repo.update_launcher(
202+
user=user, launcher_id=launcher_id, new_custom_environment=new_env, session=session, **body_dict
203+
)
177204
return json(apispec.SessionLauncher.model_validate(launcher).model_dump(exclude_none=True, mode="json"))
178205

179206
return "/session_launchers/<launcher_id:ulid>", ["PATCH"], _patch

components/renku_data_services/session/db.py

Lines changed: 67 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from __future__ import annotations
44

55
from collections.abc import Callable
6+
from contextlib import AbstractAsyncContextManager, nullcontext
67
from datetime import UTC, datetime
7-
from pathlib import PurePosixPath
88
from typing import Any
99

1010
from sqlalchemy import select
@@ -16,7 +16,7 @@
1616
from renku_data_services.authz.authz import Authz, ResourceType
1717
from renku_data_services.authz.models import Scope
1818
from renku_data_services.crc.db import ResourcePoolRepository
19-
from renku_data_services.session import apispec, models
19+
from renku_data_services.session import models
2020
from renku_data_services.session import orm as schemas
2121

2222

@@ -297,20 +297,34 @@ async def insert_launcher(
297297
return launcher.dump()
298298

299299
async def update_launcher(
300-
self, user: base_models.APIUser, launcher_id: ULID, **kwargs: Any
300+
self,
301+
user: base_models.APIUser,
302+
launcher_id: ULID,
303+
new_custom_environment: models.UnsavedEnvironment | None,
304+
session: AsyncSession | None = None,
305+
**kwargs: Any,
301306
) -> models.SessionLauncher:
302307
"""Update a session launcher entry."""
303308
if not user.is_authenticated or user.id is None:
304309
raise errors.UnauthorizedError(message="You do not have the required permissions for this operation.")
305310

306-
async with self.session_maker() as session, session.begin():
311+
session_ctx: AbstractAsyncContextManager = nullcontext()
312+
tx: AbstractAsyncContextManager = nullcontext()
313+
if not session:
314+
session = self.session_maker()
315+
session_ctx = session
316+
if not session.in_transaction():
317+
tx = session.begin()
318+
319+
async with session_ctx, tx:
307320
res = await session.scalars(
308321
select(schemas.SessionLauncherORM).where(schemas.SessionLauncherORM.id == launcher_id)
309322
)
310323
launcher = res.one_or_none()
311324
if launcher is None:
312325
raise errors.MissingResourceError(
313-
message=f"Session launcher with id '{launcher_id}' does not exist or you do not have access to it." # noqa: E501
326+
message=f"Session launcher with id '{launcher_id}' does not "
327+
"exist or you do not have access to it."
314328
)
315329

316330
authorized = await self.project_authz.has_permission(
@@ -349,11 +363,24 @@ async def update_launcher(
349363
]:
350364
setattr(launcher, key, value)
351365

352-
env_payload: dict = kwargs.get("environment", {})
353-
if len(env_payload.keys()) == 1 and "id" in env_payload and isinstance(env_payload["id"], str):
354-
# The environment ID is being changed or set
366+
await self.__update_launcher_environment(user, launcher, session, new_custom_environment, **kwargs)
367+
return launcher.dump()
368+
369+
async def __update_launcher_environment(
370+
self,
371+
user: base_models.APIUser,
372+
launcher: schemas.SessionLauncherORM,
373+
session: AsyncSession,
374+
new_custom_environment: models.UnsavedEnvironment | None,
375+
**kwargs: Any,
376+
) -> None:
377+
current_env_kind = launcher.environment.environment_kind
378+
match new_custom_environment, current_env_kind, kwargs:
379+
case None, _, {"id": env_id, **nothing_else} if len(nothing_else) == 0:
380+
# The environment in the launcher is set via ID, the new ID has to refer
381+
# to an environment that is GLOBAL.
355382
old_environment = launcher.environment
356-
new_environment_id = ULID.from_str(env_payload["id"])
383+
new_environment_id = ULID.from_str(env_id)
357384
res_env = await session.scalars(
358385
select(schemas.EnvironmentORM).where(schemas.EnvironmentORM.id == new_environment_id)
359386
)
@@ -376,54 +403,37 @@ async def update_launcher(
376403
# We remove the custom environment to avoid accumulating custom environments that are not associated
377404
# with any launchers.
378405
await session.delete(old_environment)
379-
else:
380-
# Fields other than the environment ID are being updated
381-
if launcher.environment.environment_kind == models.EnvironmentKind.GLOBAL:
382-
# A global environment is being replaced with a custom one
383-
if env_payload.get("environment_kind") == models.EnvironmentKind.GLOBAL:
384-
raise errors.ValidationError(
385-
message="When one global environment is being replaced with another in a "
386-
"launcher only the new global environment ID should be specfied",
387-
quiet=True,
388-
)
389-
env_payload["environment_kind"] = models.EnvironmentKind.CUSTOM.value
390-
env_payload_valid = apispec.EnvironmentPostInLauncher.model_validate(env_payload)
391-
new_unsaved_env = models.UnsavedEnvironment(
392-
name=env_payload_valid.name,
393-
description=env_payload_valid.description,
394-
container_image=env_payload_valid.container_image,
395-
default_url=env_payload_valid.default_url,
396-
port=env_payload_valid.port,
397-
working_directory=PurePosixPath(env_payload_valid.working_directory),
398-
mount_directory=PurePosixPath(env_payload_valid.mount_directory),
399-
uid=env_payload_valid.uid,
400-
gid=env_payload_valid.gid,
401-
environment_kind=models.EnvironmentKind(env_payload_valid.environment_kind.value),
402-
args=env_payload_valid.args,
403-
command=env_payload_valid.command,
404-
)
405-
new_env = await self.__insert_environment(user, session, new_unsaved_env)
406-
launcher.environment = new_env
407-
else:
408-
# Fields on the environment attached to the launcher are being changed.
409-
for key, val in env_payload.items():
410-
# NOTE: Only some fields can be updated.
411-
if key in [
412-
"name",
413-
"description",
414-
"container_image",
415-
"default_url",
416-
"port",
417-
"working_directory",
418-
"mount_directory",
419-
"uid",
420-
"gid",
421-
"args",
422-
"command",
423-
]:
424-
setattr(launcher.environment, key, val)
425-
426-
return launcher.dump()
406+
case None, models.EnvironmentKind.CUSTOM, {**rest} if (
407+
rest.get("environment_kind") is None
408+
or rest.get("environment_kind") == models.EnvironmentKind.CUSTOM.value
409+
):
410+
# Custom environment being updated
411+
for key, val in rest.items():
412+
# NOTE: Only some fields can be updated.
413+
if key in [
414+
"name",
415+
"description",
416+
"container_image",
417+
"default_url",
418+
"port",
419+
"working_directory",
420+
"mount_directory",
421+
"uid",
422+
"gid",
423+
"args",
424+
"command",
425+
]:
426+
setattr(launcher.environment, key, val)
427+
case models.UnsavedEnvironment(), models.EnvironmentKind.GLOBAL, {**nothing_else} if (
428+
len(nothing_else) == 0 and new_custom_environment.environment_kind == models.EnvironmentKind.CUSTOM
429+
):
430+
# Global environment replaced by a custom one
431+
new_env = await self.__insert_environment(user, session, new_custom_environment)
432+
launcher.environment = new_env
433+
case _:
434+
raise errors.ValidationError(
435+
message="Encountered an invalid payload for updating a launcher environment", quiet=True
436+
)
427437

428438
async def delete_launcher(self, user: base_models.APIUser, launcher_id: ULID) -> None:
429439
"""Delete a session launcher entry."""

0 commit comments

Comments
 (0)