diff --git a/session-manager/handlers/application_git.py b/session-manager/handlers/application_git.py new file mode 100644 index 00000000..46c74d59 --- /dev/null +++ b/session-manager/handlers/application_git.py @@ -0,0 +1,70 @@ +import random +import string + +from .operator_config import OPERATOR_API_GROUP, INGRESS_DOMAIN, INGRESS_PROTOCOL + + +def git_workshop_spec_patches(application_properties): + characters = string.ascii_letters + string.digits + + git_host = f"git-$(session_namespace).{INGRESS_DOMAIN}" + git_username = "$(session_namespace)" + git_password = "".join(random.sample(characters, 32)) + + return { + "spec": { + "session": { + "ingresses": [ + {"name": "git", "port": 10087, "authentication": {"type": "none"}} + ], + "variables": [ + { + "name": "git_protocol", + "value": INGRESS_PROTOCOL, + }, + { + "name": "git_host", + "value": git_host, + }, + { + "name": "git_username", + "value": git_username, + }, + { + "name": "git_password", + "value": git_password, + }, + ], + "env": [ + { + "name": "GIT_PROTOCOL", + "value": INGRESS_PROTOCOL, + }, + { + "name": "GIT_HOST", + "value": git_host, + }, + { + "name": "GIT_USERNAME", + "value": git_username, + }, + { + "name": "GIT_PASSWORD", + "value": git_password, + }, + ], + } + } + } + + +def git_environment_objects_list(application_properties): + return [] + + +def git_session_objects_list(application_properties): + return [] + + +def git_pod_template_spec_patches(application_properties): + return {} diff --git a/session-manager/handlers/application_vcluster.py b/session-manager/handlers/application_vcluster.py index 3ff9ee83..ababa8be 100644 --- a/session-manager/handlers/application_vcluster.py +++ b/session-manager/handlers/application_vcluster.py @@ -1,6 +1,22 @@ from .operator_config import OPERATOR_API_GROUP +def vcluster_workshop_spec_patches(application_properties): + return { + "spec": { + "session": { + "applications": {"console": {"vendor": "octant"}}, + "variables": [ + { + "name": "vcluster_secret", + "value": "$(session_namespace)-vc-kubeconfig", + }, + ], + } + } + } + + def vcluster_environment_objects_list(application_properties): return [] @@ -34,7 +50,7 @@ def vcluster_session_objects_list(application_properties): "targetNamespaces": { "nameSelector": {"matchNames": ["$(workshop_namespace)"]} }, - "targetSecret": {"name": "$(session_namespace)-vc-kubeconfig"}, + "targetSecret": {"name": "$(vcluster_secret)"}, } ] }, @@ -290,11 +306,7 @@ def vcluster_pod_template_spec_patches(application_properties): "volumes": [ { "name": "kubeconfig", - "secret": {"secretName": "$(session_namespace)-vc-kubeconfig"}, + "secret": {"secretName": "$(vcluster_secret)"}, } ], } - - -def vcluster_workshop_config_patches(application_properties): - return {"spec": {"session": {"applications": {"console": {"vendor": "octant"}}}}} diff --git a/session-manager/handlers/applications.py b/session-manager/handlers/applications.py index 16ef66b9..f1dafe6b 100644 --- a/session-manager/handlers/applications.py +++ b/session-manager/handlers/applications.py @@ -1,21 +1,43 @@ +from .application_git import ( + git_workshop_spec_patches, + git_environment_objects_list, + git_session_objects_list, + git_pod_template_spec_patches, +) from .application_vcluster import ( + vcluster_workshop_spec_patches, vcluster_environment_objects_list, vcluster_session_objects_list, vcluster_pod_template_spec_patches, - vcluster_workshop_config_patches, ) registered_applications = { + "git": dict( + workshop_spec_patches=git_workshop_spec_patches, + environment_objects_list=git_environment_objects_list, + session_objects_list=git_session_objects_list, + pod_template_spec_patches=git_pod_template_spec_patches, + ), "vcluster": dict( + workshop_spec_patches=vcluster_workshop_spec_patches, environment_objects_list=vcluster_environment_objects_list, session_objects_list=vcluster_session_objects_list, pod_template_spec_patches=vcluster_pod_template_spec_patches, - workshop_config_patches=vcluster_workshop_config_patches, - ) + + ), } +def workshop_spec_patches(application, application_properties): + handler = registered_applications.get(application, {}).get( + "workshop_spec_patches" + ) + if handler: + return handler(application_properties) + return {} + + def environment_objects_list(application, application_properties): handler = registered_applications.get(application, {}).get( "environment_objects_list" @@ -41,8 +63,4 @@ def pod_template_spec_patches(application, application_properties): return {} -def workshop_config_patches(application, application_properties): - handler = registered_applications.get(application, {}).get("workshop_config_patches") - if handler: - return handler(application_properties) - return {} + diff --git a/session-manager/handlers/workshopenvironment.py b/session-manager/handlers/workshopenvironment.py index 26ce86bf..8861a8d1 100644 --- a/session-manager/handlers/workshopenvironment.py +++ b/session-manager/handlers/workshopenvironment.py @@ -7,7 +7,7 @@ from .objects import create_from_dict, Workshop, SecretCopier from .helpers import substitute_variables, smart_overlay_merge, Applications -from .applications import environment_objects_list, workshop_config_patches +from .applications import environment_objects_list, workshop_spec_patches from .operator_config import ( OPERATOR_API_GROUP, @@ -46,27 +46,26 @@ id=OPERATOR_STATUS_KEY, ) def workshop_environment_create(name, meta, spec, patch, logger, **_): - # Use the name of the custom resource as the name of the namespace - # under which the workshop environment is created and any workshop - # instances are created. + # Use the name of the custom resource as the name of the namespace under + # which the workshop environment is created and any workshop instances are + # created. environment_name = name workshop_namespace = environment_name - # Can optionally be passed name of the training portal via a label - # when the workshop environment is created as a child to a training - # portal. + # Can optionally be passed name of the training portal via a label when the + # workshop environment is created as a child to a training portal. portal_name = meta.get("labels", {}).get( f"training.{OPERATOR_API_GROUP}/portal.name", "" ) - # The name of the workshop to be deployed can differ and is taken - # from the specification of the workspace. Lookup the workshop - # resource definition and ensure it exists. Later we will stash a - # copy of this in the status of the custom resource, and we will use - # this copy to avoid being affected by changes in the original after - # the creation of the workshop environment. + # The name of the workshop to be deployed can differ and is taken from the + # specification of the workshop environment. Lookup the workshop resource + # definition and ensure it exists. Later we will stash a copy of this in the + # status of the custom resource, and we will use this copy to avoid being + # affected by changes in the original after the creation of the workshop + # environment. workshop_name = spec["workshop"]["name"] @@ -88,15 +87,25 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): workshop_generation = workshop_instance.obj["metadata"]["generation"] workshop_spec = workshop_instance.obj.get("spec", {}) - # Create a wrapper for determining if applications enabled and what - # configuration they provide. + # Create a wrapper for determining what applications are enabled and what + # configuration they provide. This includes allowing applications to patch + # the workshop config. As an application could enable another application + # because it requires it, we calculate the list of applications again after + # patching. It is the modified version of the config which gets saved in + # the status so that it can be used later by the workshop session. applications = Applications(workshop_spec["session"].get("applications", {})) - # Create the namespace for everything related to this workshop. When - # pod security admission controller is being used, need to set the whole - # namespace as requiring privilged as we need to run docker in docker in - # this namespace. + for application in applications: + if applications.is_enabled(application): + workshop_config_patch = workshop_spec_patches( + application, applications.properties(application) + ) + smart_overlay_merge(workshop_spec, workshop_config_patch.get("spec", {})) + + applications = Applications(workshop_spec["session"].get("applications", {})) + + # Create the namespace for everything related to this workshop. namespace_body = { "apiVersion": "v1", @@ -131,7 +140,9 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): raise kopf.TemporaryError(f"Namespace {workshop_namespace} already exists.") raise - # Apply pod security policies to whole namespace if enabled. + # When using the pod security admission controller, we need to set the whole + # namespace as requiring privilged as we need to run docker in docker in + # this namespace. if CLUSTER_SECURITY_POLICY_ENGINE == "pod-security-policies": psp_role_binding_body = { @@ -166,8 +177,8 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): # Delete any limit ranges applied to the namespace so they don't cause # issues with workshop instance deployments or any workshop deployments. # This can be an issue where namespace/project templates apply them - # automatically to a namespace. The problem is that we may do this query - # too quickly and they may not have been created as yet. + # automatically to a namespace. The problem is that we may do this query too + # quickly and they may not have been created as yet. for limit_range in pykube.LimitRange.objects( api, namespace=workshop_namespace @@ -178,10 +189,10 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): pass # Delete any resource quotas applied to the namespace so they don't cause - # issues with workshop instance deploymemnts or any workshop resources. - # This can be an issue where namespace/project templates apply them - # automatically to a namespace. The problem is that we may do this query - # too quickly and they may not have been created as yet. + # issues with workshop instance deploymemnts or any workshop resources. This + # can be an issue where namespace/project templates apply them automatically + # to a namespace. The problem is that we may do this query too quickly and + # they may not have been created as yet. for resource_quota in pykube.ResourceQuota.objects( api, namespace=workshop_namespace @@ -246,9 +257,9 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): NetworkPolicy(api, network_policy_body).create() - # Create a config map in the workshop namespace which contains the - # details about the workshop. This will be mounted into workshop - # instances so they can derive information to configure themselves. + # Create a config map in the workshop namespace which contains the details + # about the workshop. This will be mounted into workshop instances so they + # can derive information to configure themselves. workshop_config = { "spec": { @@ -262,18 +273,6 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): } } - for application in applications: - if applications.is_enabled(application): - workshop_config_patch = workshop_config_patches( - application, applications.properties(application) - ) - smart_overlay_merge(workshop_config, workshop_config_patch) - - if applications.is_enabled("git"): - workshop_config["spec"]["session"]["ingresses"].append( - {"name": "git", "port": 10087, "authentication": {"type": "none"}} - ) - config_map_body = { "apiVersion": "v1", "kind": "ConfigMap", @@ -523,6 +522,15 @@ def workshop_environment_create(name, meta, spec, patch, logger, **_): storage_class=CLUSTER_STORAGE_CLASS, ) + application_variables_list = workshop_spec.get("session").get("variables", []) + + application_variables_list = substitute_variables( + application_variables_list, environment_variables + ) + + for variable in application_variables_list: + environment_variables[variable["name"]] = variable["value"] + if workshop_spec.get("environment", {}).get("objects"): objects = [] diff --git a/session-manager/handlers/workshopsession.py b/session-manager/handlers/workshopsession.py index 87bd28b5..8b77496c 100644 --- a/session-manager/handlers/workshopsession.py +++ b/session-manager/handlers/workshopsession.py @@ -13,7 +13,11 @@ from .objects import create_from_dict, WorkshopEnvironment from .helpers import substitute_variables, smart_overlay_merge, Applications -from .applications import session_objects_list, pod_template_spec_patches +from .applications import ( + session_objects_list, + pod_template_spec_patches, + workshop_spec_patches, +) from .operator_config import ( resolve_workshop_image, @@ -901,10 +905,18 @@ def workshop_session_create(name, meta, spec, status, patch, logger, **_): ] # Create a wrapper for determining if applications enabled and what - # configuration they provide. + # configuration they provide. Apply any patches to the workshop config + # required by enabled applications. applications = Applications(workshop_spec["session"].get("applications", {})) + # for application in applications: + # if applications.is_enabled(application): + # workshop_config_patch = workshop_spec_patches( + # application, applications.properties(application) + # ) + # smart_overlay_merge(workshop_spec, workshop_config_patch) + # Calculate the hostname to be used for this workshop session. session_hostname = f"{session_namespace}.{INGRESS_DOMAIN}" @@ -958,15 +970,6 @@ def resolve_security_policy(name): applications.properties("registry")["password"] = registry_password applications.properties("registry")["secret"] = registry_secret - if applications.is_enabled("git"): - git_host = f"git-{session_namespace}.{INGRESS_DOMAIN}" - git_username = session_namespace - git_password = "".join(random.sample(characters, 32)) - - applications.properties("git")["host"] = git_host - applications.properties("git")["username"] = git_username - applications.properties("git")["password"] = git_password - # Determine if any secrets being copied into the workshop environment # namespace exist. This is done before creating the session namespace so we # can fail with a transient error and try again later. Note that we don't @@ -1172,7 +1175,10 @@ def resolve_security_policy(name): pykube.PersistentVolumeClaim(api, persistent_volume_claim_body).create() - # List of variables that can be replaced in session objects etc. + # List of variables that can be replaced in session objects etc. For those + # set by applications they are passed through from when the workshop + # environment was processed. We need to substitute and session variables + # in those before add them to the final set of session variables. session_variables = dict( image_repository=IMAGE_REPOSITORY, @@ -1190,6 +1196,15 @@ def resolve_security_policy(name): storage_class=CLUSTER_STORAGE_CLASS, ) + application_variables_list = workshop_spec.get("session").get("variables", []) + + application_variables_list = substitute_variables( + application_variables_list, session_variables + ) + + for variable in application_variables_list: + session_variables[variable["name"]] = variable["value"] + if applications.is_enabled("registry"): session_variables.update( dict( @@ -1200,16 +1215,6 @@ def resolve_security_policy(name): ) ) - if applications.is_enabled("git"): - session_variables.update( - dict( - git_protocol=INGRESS_PROTOCOL, - git_host=git_host, - git_username=git_username, - git_password=git_password, - ) - ) - # Create any secondary namespaces required for the session. namespaces = [] @@ -2460,32 +2465,6 @@ def _apply_environment_patch(patch): kopf.adopt(object_body) create_from_dict(object_body) - if applications.is_enabled("git"): - additional_env.append( - { - "name": "GIT_PROTOCOL", - "value": INGRESS_PROTOCOL, - } - ) - additional_env.append( - { - "name": "GIT_HOST", - "value": git_host, - } - ) - additional_env.append( - { - "name": "GIT_USERNAME", - "value": git_username, - } - ) - additional_env.append( - { - "name": "GIT_PASSWORD", - "value": git_password, - } - ) - # Apply any additional environment variables to the deployment. _apply_environment_patch(additional_env) @@ -2565,15 +2544,6 @@ def _apply_environment_patch(patch): # Suffix use is deprecated. See prior note. ingress_hostnames.append(f"{session_namespace}-editor.{INGRESS_DOMAIN}") - if applications.is_enabled("git"): - ingresses.append( - {"name": "git", "port": 10087, "authentication": {"type": "none"}} - ) - - ingress_hostnames.append(f"git-{session_namespace}.{INGRESS_DOMAIN}") - # Suffix use is deprecated. See prior note. - ingress_hostnames.append(f"{session_namespace}-git.{INGRESS_DOMAIN}") - for ingress in ingresses: ingress_hostnames.append( f"{ingress['name']}-{session_namespace}.{INGRESS_DOMAIN}"