diff --git a/appstore/api/v1/k8s_service.py b/appstore/api/v1/k8s_service.py new file mode 100644 index 00000000..1c858312 --- /dev/null +++ b/appstore/api/v1/k8s_service.py @@ -0,0 +1,80 @@ +import base64 +import os +from kubernetes import client, config +from app.models.user import UserType + +class KubernetesService: + def __init__(self): + self.api_instance = self.get_v1_client() + + @staticmethod + def get_v1_client() -> client.CoreV1Api: + try: + config.load_incluster_config() + except: + config.load_kube_config() + + return client.CoreV1Api() + + def get_current_namespace(self): + # This will exist if ran in-cluster with a service account + ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + if os.path.exists(ns_path): + with open(ns_path, "r") as f: + return f.read().strip() + try: + # Doesn't work when ran in-cluster (there is no kubeconfig) + contexts, current_context = config.list_kube_config_contexts() + return current_context["context"]["namespace"] + except KeyError: + return "default" + + def create_credential_secret(self, course_name: str, onyen: str, password: str, user_type: UserType): + current_namespace = self.get_current_namespace() + + secret_name = self._compute_credential_secret_name(course_name, onyen) + secret_data = { + "onyen": onyen, + "password": password, + "class": course_name, + "user_type": user_type.value, + } + encoded_secret_data = { + key: base64.b64encode(value.encode()).decode() for (key, value) in secret_data.items() + } + + secret = client.V1Secret( + api_version="v1", + kind="Secret", + metadata=client.V1ObjectMeta( + name=secret_name, + namespace=current_namespace + ), + type="Opaque", + data=encoded_secret_data + ) + + self.api_instance.create_namespaced_secret( + namespace=current_namespace, + body=secret + ) + + def delete_credential_secret(self, course_name: str, onyen: str): + current_namespace = self.get_current_namespace() + + secret_name = self._compute_credential_secret_name(course_name, onyen) + self.api_instance.delete_namespaced_secret( + namespace=current_namespace, + name=secret_name + ) + + def get_autogen_password(self, course_name: str, onyen: str) -> str: + current_namespace = self.get_current_namespace() + secret_name = self._compute_credential_secret_name(course_name, onyen) + secret = self.api_instance.read_namespaced_secret(secret_name, current_namespace) + return base64.decode(secret.data["password"]).decode("utf-8") + + @staticmethod + def _compute_credential_secret_name(course_name: str, onyen: str) -> str: + # Secret names are subject to RFC 1123 meaning they cannot contain uppercase characters, spaces, or underscores. + return f"{course_name.lower().replace(' ', '-')}-{onyen.lower()}-credential-secret" \ No newline at end of file diff --git a/appstore/api/v1/views.py b/appstore/api/v1/views.py index 66d46423..34caac58 100644 --- a/appstore/api/v1/views.py +++ b/appstore/api/v1/views.py @@ -35,6 +35,7 @@ InstanceModifySerializer, EmptySerializer, ) +from .k8s_service import KubernetesService from urllib.parse import urljoin @@ -600,6 +601,8 @@ def create(self, request): env = {} if settings.GRADER_API_URL is not None: env["GRADER_API_URL"] = settings.GRADER_API_URL + if settings.EDUHELX_CLASS_NAME is not None: + env["USER_AUTOGEN_PASSWORD"] = KubernetesService().get_autogen_password(settings.EDUHELX_CLASS_NAME, username) host = get_host(request) system = tycho.start(principal, app_id, resource_request.resources, host, env) diff --git a/appstore/appstore/settings/base.py b/appstore/appstore/settings/base.py index 48931e2b..9a71e246 100644 --- a/appstore/appstore/settings/base.py +++ b/appstore/appstore/settings/base.py @@ -132,6 +132,7 @@ ] GRADER_API_URL = os.environ.get("GRADER_API_URL", None) +EDUHELX_CLASS_NAME = os.environ.get("EDUHELX_CLASS_NAME", None) SESSION_IDLE_TIMEOUT = int(os.environ.get("DJANGO_SESSION_IDLE_TIMEOUT", 300)) EXPORTABLE_ENV = os.environ.get("EXPORTABLE_ENV",None)