33from __future__ import annotations
44
55from collections .abc import Callable
6+ from contextlib import AbstractAsyncContextManager , nullcontext
67from datetime import UTC , datetime
7- from pathlib import PurePosixPath
88from typing import Any
99
1010from sqlalchemy import select
1616from renku_data_services .authz .authz import Authz , ResourceType
1717from renku_data_services .authz .models import Scope
1818from renku_data_services .crc .db import ResourcePoolRepository
19- from renku_data_services .session import apispec , models
19+ from renku_data_services .session import models
2020from 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