1010import json
1111import logging
1212import os
13+ from typing import List , Optional
1314
1415from flask import current_app
1516from kubernetes import client
5960)
6061
6162from reana_workflow_controller .config import ( # isort:skip
63+ CONTAINER_IMAGE_ALIAS_PREFIXES ,
6264 IMAGE_PULL_SECRETS ,
6365 JOB_CONTROLLER_CONTAINER_PORT ,
6466 JOB_CONTROLLER_ENV_VARS ,
6567 JOB_CONTROLLER_SHUTDOWN_ENDPOINT ,
68+ REANA_INTERACTIVE_SESSIONS_DEFAULT_IMAGES ,
69+ REANA_INTERACTIVE_SESSIONS_ENVIRONMENTS ,
70+ REANA_INTERACTIVE_SESSIONS_RECOMMENDED_IMAGES ,
6671 REANA_RUNTIME_BATCH_TERMINATION_GRACE_PERIOD ,
6772 REANA_KUBERNETES_JOBS_MAX_USER_MEMORY_LIMIT ,
6873 REANA_KUBERNETES_JOBS_MEMORY_LIMIT ,
8186)
8287
8388
89+ def _container_image_aliases (
90+ image : str , prefixes = CONTAINER_IMAGE_ALIAS_PREFIXES
91+ ) -> List [str ]:
92+ """Return possible aliases for a docker image reference.
93+
94+ Aliases are obtained by adding/removing default prefixes like "docker.io/".
95+ Some of the returned aliases might not be valid docker image references,
96+ in particular when adding default prefixes to references that are already
97+ fully qualified.
98+
99+ Example: the returned aliases for `docker.io/library/ubuntu:24.04` are:
100+ - `docker.io/library/ubuntu:24.04`
101+ - `library/ubuntu:24.04`
102+ - `ubuntu:24.04`
103+ - `library/docker.io/library/ubuntu:24.04` (not valid)
104+ """
105+ aliases = [image ]
106+ for prefix in prefixes :
107+ if image .startswith (prefix ):
108+ # remove prefix
109+ aliases .append (image [len (prefix ) :])
110+ else :
111+ # add prefix
112+ aliases .append (prefix + image )
113+ return aliases
114+
115+
116+ def _validate_interactive_session_image (type_ : str , user_image : Optional [str ]) -> str :
117+ if type_ not in REANA_INTERACTIVE_SESSIONS_ENVIRONMENTS :
118+ raise REANAInteractiveSessionError (
119+ f"Missing environment configuration for { type_ } ."
120+ )
121+
122+ config = REANA_INTERACTIVE_SESSIONS_ENVIRONMENTS [type_ ]
123+ # recommended_images can be empty
124+ recommended_images = REANA_INTERACTIVE_SESSIONS_RECOMMENDED_IMAGES [type_ ]
125+ # default_image can be `None`
126+ default_image = REANA_INTERACTIVE_SESSIONS_DEFAULT_IMAGES [type_ ]
127+ image = user_image or default_image
128+
129+ if not image :
130+ raise REANAInteractiveSessionError ("Container image must be specified." )
131+
132+ if not config ["allow_custom" ]:
133+ # check if one of the aliases is in the recommended list
134+ aliases = _container_image_aliases (image )
135+ # normally only one alias should match, unless multiple aliases of the same
136+ # image are present in the recommended list
137+ allowed_alias = next (
138+ (alias for alias in aliases if alias in recommended_images ), None
139+ )
140+ if not allowed_alias :
141+ raise REANAInteractiveSessionError (
142+ f"Custom container image { image } is not allowed."
143+ )
144+ return allowed_alias
145+ else :
146+ return image
147+
148+
84149class WorkflowRunManager :
85150 """Interface which specifies how to manage workflow runs."""
86151
@@ -316,22 +381,26 @@ def start_batch_workflow_run(
316381 logging .error (msg , exc_info = True )
317382 raise e
318383
319- def start_interactive_session (self , interactive_session_type , ** kwargs ):
384+ def start_interactive_session (self , interactive_session_type , image = None , ** kwargs ):
320385 """Start an interactive workflow run.
321386
322387 :param interactive_session_type: One of the available interactive
323388 session types.
389+ :param image: Docker image to use for the interactive session.
324390 :return: Relative path to access the interactive session.
325391 """
392+ if interactive_session_type not in InteractiveSessionType .__members__ :
393+ raise REANAInteractiveSessionError (
394+ f"Interactive type { interactive_session_type } does not exist."
395+ )
396+
397+ validated_image = _validate_interactive_session_image (
398+ interactive_session_type , image
399+ )
400+
326401 action_completed = True
327402 kubernetes_objects = None
328403 try :
329- if interactive_session_type not in InteractiveSessionType .__members__ :
330- raise REANAInteractiveSessionError (
331- "Interactive type {} does not exist." .format (
332- interactive_session_type
333- )
334- )
335404 access_path = self ._generate_interactive_workflow_path ()
336405 workflow_run_name = self ._workflow_run_name_generator ("session" )
337406 kubernetes_objects = build_interactive_k8s_objects [
@@ -340,6 +409,7 @@ def start_interactive_session(self, interactive_session_type, **kwargs):
340409 workflow_run_name ,
341410 self .workflow .workspace_path ,
342411 access_path ,
412+ validated_image ,
343413 access_token = self .workflow .get_owner_access_token (),
344414 cvmfs_repos = self .retrieve_required_cvmfs_repos (),
345415 owner_id = self .workflow .owner_id ,
0 commit comments