diff --git a/jupyterhub/src/configs/hosted-workshop.py b/jupyterhub/src/configs/hosted-workshop.py index c7101a0..9c255b9 100644 --- a/jupyterhub/src/configs/hosted-workshop.py +++ b/jupyterhub/src/configs/hosted-workshop.py @@ -2,21 +2,7 @@ # deployment mode. In this mode authentication for JupyterHub is done # against the OpenShift cluster using OAuth. -# Work out the public server address for the OpenShift OAuth endpoint. -# Make sure the request is done in a session so the connection is closed -# and later calls against the REST API don't attempt to reuse it. This -# is just to avoid potential for any problems with connection reuse. - -from fnmatch import fnmatch - -from tornado import web, gen - -oauth_metadata_url = '%s/.well-known/oauth-authorization-server' % kubernetes_server_url - -with requests.Session() as session: - response = session.get(oauth_metadata_url, verify=False) - data = json.loads(response.content.decode('UTF-8')) - oauth_issuer_address = data['issuer'] +from tornado import web # Enable the OpenShift authenticator. Environments variables have # already been set from the hosted-workshop.sh script file. @@ -70,12 +56,6 @@ # don't loose their work. This is mounted on /opt/app-root, so we need # to copy the contents from the image into the persistent volume the # first time using an init container. -# -# Note that if a profiles list is used, there must still be a default -# terminal image setup we can use to run the init container. The image -# is what contains the script which copies the file into the persistent -# volume. Perhaps should use the JupyterHub image for the init container -# and add the script which performs the copy to this image. volume_size = os.environ.get('VOLUME_SIZE') @@ -199,23 +179,47 @@ @gen.coroutine def modify_pod_hook(spawner, pod): - # Set the session access token from the OpenShift login in - # both the terminal and console containers. We still mount - # the service account token still as well because the console - # needs the SSL certificate contained in it when accessing - # the cluster REST API. + hub = '%s-%s' % (application_name, namespace) + short_name = spawner.user.name + user_account_name = '%s-%s' % (hub, short_name) + hub_account_name = '%s-hub' % hub + + pod.spec.service_account_name = user_account_name + pod.spec.automount_service_account_token = True + + # Grab the OpenShift user access token from the login state. auth_state = yield spawner.user.get_auth_state() + access_token = auth_state['access_token'] + + # Ensure that a service account exists corresponding to the user. + # Need to do this as it may have been cleaned up if the session had + # expired and user wasn't logged out in the browser. + + owner_uid = yield create_service_account(spawner, pod) + + # If there are any exposed ports defined for the session, create + # a service object mapping to the pod for the ports, and create + # routes for each port. + + yield expose_service_ports(spawner, pod, owner_uid) + + # Before can continue, need to poll looking to see if the secret for + # the api token has been added to the service account. If don't do + # this then pod creation will fail immediately. To do this, must get + # the secrets from the service account and make sure they in turn + # exist. + + yield wait_on_service_account(user_account_name) + + # Set the session access token from the OpenShift login in + # both the terminal and console containers. pod.spec.containers[0].env.append( - dict(name='OPENSHIFT_TOKEN', value=auth_state['access_token'])) + dict(name='OPENSHIFT_TOKEN', value=access_token)) pod.spec.containers[-1].env.append( - dict(name='BRIDGE_K8S_AUTH_BEARER_TOKEN', - value=auth_state['access_token'])) - - pod.spec.service_account_name = '%s-%s-user' % (application_name, namespace) - pod.spec.automount_service_account_token = True + dict(name='BRIDGE_K8S_AUTH_BEARER_TOKEN', value=access_token)) # See if a template for the project name has been specified. # Try expanding the name, substituting the username. If the diff --git a/jupyterhub/src/configs/learning-portal.py b/jupyterhub/src/configs/learning-portal.py index 9d8737f..9c3d523 100644 --- a/jupyterhub/src/configs/learning-portal.py +++ b/jupyterhub/src/configs/learning-portal.py @@ -11,19 +11,16 @@ # '/restart' URL handler will cause any session to be restarted and they # will be given a new instance. -import time import functools import random import weakref -from tornado import gen, web +from tornado import web from jupyterhub.auth import Authenticator from jupyterhub.handlers import BaseHandler from jupyterhub.utils import url_path_join -from kubernetes.client.rest import ApiException - class AnonymousUser(object): def __init__(self, name): @@ -244,15 +241,13 @@ def authenticate(self, handler, data): @gen.coroutine def modify_pod_hook(spawner, pod): - # Create the service account. We know the user name is a UUID, but - # it is too long to use as is in project name, so we want to shorten. - hub = '%s-%s' % (application_name, namespace) short_name = spawner.user.name - project_name = '%s-%s' % (hub, short_name) user_account_name = '%s-%s' % (hub, short_name) hub_account_name = '%s-hub' % hub + project_name = '%s-%s' % (hub, short_name) + pod.spec.automount_service_account_token = True pod.spec.service_account_name = user_account_name diff --git a/jupyterhub/src/configs/terminal-server.py b/jupyterhub/src/configs/terminal-server.py index 4defaa0..88b4a01 100644 --- a/jupyterhub/src/configs/terminal-server.py +++ b/jupyterhub/src/configs/terminal-server.py @@ -2,21 +2,7 @@ # deployment mode. In this mode authentication for JupyterHub is done # against the OpenShift cluster using OAuth. -# Work out the public server address for the OpenShift OAuth endpoint. -# Make sure the request is done in a session so the connection is closed -# and later calls against the REST API don't attempt to reuse it. This -# is just to avoid potential for any problems with connection reuse. - -from fnmatch import fnmatch - -from tornado import web, gen - -oauth_metadata_url = '%s/.well-known/oauth-authorization-server' % kubernetes_server_url - -with requests.Session() as session: - response = session.get(oauth_metadata_url, verify=False) - data = json.loads(response.content.decode('UTF-8')) - oauth_issuer_address = data['issuer'] +from tornado import web # Enable the OpenShift authenticator. Environments variables have # already been set from the terminal-server.sh script file. @@ -70,12 +56,6 @@ # don't loose their work. This is mounted on /opt/app-root, so we need # to copy the contents from the image into the persistent volume the # first time using an init container. -# -# Note that if a profiles list is used, there must still be a default -# terminal image setup we can use to run the init container. The image -# is what contains the script which copies the file into the persistent -# volume. Perhaps should use the JupyterHub image for the init container -# and add the script which performs the copy to this image. volume_size = os.environ.get('VOLUME_SIZE') @@ -140,18 +120,44 @@ @gen.coroutine def modify_pod_hook(spawner, pod): - # Set the session access token from the OpenShift login in - # both the terminal and console containers. We still mount - # the service account token still as well because the console - # needs the SSL certificate contained in it when accessing - # the cluster REST API. + hub = '%s-%s' % (application_name, namespace) + short_name = spawner.user.name + user_account_name = '%s-%s' % (hub, short_name) + hub_account_name = '%s-hub' % hub + + pod.spec.service_account_name = user_account_name + pod.spec.automount_service_account_token = True + + # Grab the OpenShift user access token from the login state. auth_state = yield spawner.user.get_auth_state() + access_token = auth_state['access_token'] - pod.spec.containers[0].env.append( - dict(name='OPENSHIFT_TOKEN', value=auth_state['access_token'])) + # Ensure that a service account exists corresponding to the user. + # Need to do this as it may have been cleaned up if the session had + # expired and user wasn't logged out in the browser. - pod.spec.service_account_name = '%s-%s-user' % (application_name, namespace) + owner_uid = yield create_service_account(spawner, pod) + + # If there are any exposed ports defined for the session, create + # a service object mapping to the pod for the ports, and create + # routes for each port. + + yield expose_service_ports(spawner, pod, owner_uid) + + # Before can continue, need to poll looking to see if the secret for + # the api token has been added to the service account. If don't do + # this then pod creation will fail immediately. To do this, must get + # the secrets from the service account and make sure they in turn + # exist. + + yield wait_on_service_account(user_account_name) + + # Set the session access token from the OpenShift login in + # both the terminal and console containers. + + pod.spec.containers[0].env.append( + dict(name='OPENSHIFT_TOKEN', value=access_token)) # See if a template for the project name has been specified. # Try expanding the name, substituting the username. If the diff --git a/jupyterhub/src/configs/user-workspace.py b/jupyterhub/src/configs/user-workspace.py index 1b9164a..87ba02e 100644 --- a/jupyterhub/src/configs/user-workspace.py +++ b/jupyterhub/src/configs/user-workspace.py @@ -2,12 +2,7 @@ # deployment mode. In this mode authentication for JupyterHub is done # against a KeyCloak authentication server. -import string -import yaml - -from tornado import web, gen - -from kubernetes.client.rest import ApiException +from tornado import web # Configure standalone KeyCloak as the authentication provider for # users. Environments variables have already been set from the @@ -188,15 +183,13 @@ @gen.coroutine def modify_pod_hook(spawner, pod): - # Create the service account. We know the user name is a UUID, but - # it is too long to use as is in project name, so we want to shorten. - hub = '%s-%s' % (application_name, namespace) short_name = spawner.user.name - project_name = '%s-%s' % (hub, short_name) user_account_name = '%s-%s' % (hub, short_name) hub_account_name = '%s-hub' % hub + project_name = '%s-%s' % (hub, short_name) + pod.spec.automount_service_account_token = True pod.spec.service_account_name = user_account_name diff --git a/jupyterhub/src/jupyterhub_config.py b/jupyterhub/src/jupyterhub_config.py index 2f66c47..7cae357 100644 --- a/jupyterhub/src/jupyterhub_config.py +++ b/jupyterhub/src/jupyterhub_config.py @@ -1268,6 +1268,11 @@ def create_project_namespace(spawner, pod, project_name): @gen.coroutine def setup_project_namespace(spawner, pod, project_name, role, budget): + hub = '%s-%s' % (application_name, namespace) + short_name = spawner.user.name + user_account_name = '%s-%s' % (hub, short_name) + hub_account_name = '%s-hub' % hub + # Wait for project to exist before continuing. for _ in range(30): @@ -1298,11 +1303,6 @@ def setup_project_namespace(spawner, pod, project_name, role, budget): # delete project when done. Will fail if the project hasn't actually # been created yet. - hub = '%s-%s' % (application_name, namespace) - short_name = spawner.user.name - user_account_name = '%s-%s' % (hub, short_name) - hub_account_name = '%s-hub' % hub - try: text = role_binding_template.safe_substitute( configuration=configuration_type, namespace=namespace, @@ -1322,10 +1322,7 @@ def setup_project_namespace(spawner, pod, project_name, role, budget): raise # Create role binding in the project so the users service account - # can create resources in it. Need to give it 'admin' role and not - # just 'edit' so that can grant roles to service accounts in the - # project. This means it could though delete the project itself, and - # if do that can't create a new one as has no rights to do that. + # can create resources in it. try: text = role_binding_template.safe_substitute( @@ -1491,7 +1488,7 @@ def setup_project_namespace(spawner, pod, project_name, role, budget): extra_resources_loader = json.loads @gen.coroutine -def create_extra_resources(spawner, pod, project_name, project_uid, +def create_extra_resources(spawner, pod, project_name, owner_uid, user_account_name, short_name): if not extra_resources: @@ -1520,7 +1517,7 @@ def create_extra_resources(spawner, pod, project_name, project_uid, 'clusterrolebinding', 'namespace'): body['metadata']['ownerReferences'] = [dict( apiVersion='v1', kind='Namespace', blockOwnerDeletion=False, - controller=True, name=project_name, uid=project_uid)] + controller=True, name=project_name, uid=owner_uid)] if kind.lower() == 'namespace': service_account_name = 'system:serviceaccount:%s:%s-%s-hub' % ( diff --git a/templates/hosted-workshop-development.json b/templates/hosted-workshop-development.json index b603bd1..c1457c6 100644 --- a/templates/hosted-workshop-development.json +++ b/templates/hosted-workshop-development.json @@ -151,19 +151,6 @@ } } }, - { - "kind": "ServiceAccount", - "apiVersion": "v1", - "metadata": { - "name": "${APPLICATION_NAME}-${PROJECT_NAME}-user", - "namespace": "${PROJECT_NAME}", - "labels": { - "app": "${APPLICATION_NAME}-${PROJECT_NAME}", - "spawner": "hosted-workshop", - "class": "spawner" - } - } - }, { "kind": "OAuthClient", "apiVersion": "oauth.openshift.io/v1", diff --git a/templates/hosted-workshop-production.json b/templates/hosted-workshop-production.json index 3330fc4..8ea0378 100644 --- a/templates/hosted-workshop-production.json +++ b/templates/hosted-workshop-production.json @@ -72,7 +72,7 @@ }, { "name": "SPAWNER_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-spawner:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-spawner:6.1.0", "required": true }, { @@ -146,19 +146,6 @@ } } }, - { - "kind": "ServiceAccount", - "apiVersion": "v1", - "metadata": { - "name": "${APPLICATION_NAME}-${PROJECT_NAME}-user", - "namespace": "${PROJECT_NAME}", - "labels": { - "app": "${APPLICATION_NAME}-${PROJECT_NAME}", - "spawner": "hosted-workshop", - "class": "spawner" - } - } - }, { "kind": "OAuthClient", "apiVersion": "oauth.openshift.io/v1", diff --git a/templates/jumpbox-server-production.json b/templates/jumpbox-server-production.json index 26bfc7c..e140e52 100644 --- a/templates/jumpbox-server-production.json +++ b/templates/jumpbox-server-production.json @@ -45,12 +45,12 @@ }, { "name": "SPAWNER_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-spawner:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-spawner:6.1.0", "required": true }, { "name": "KEYCLOAK_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-keycloak:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-keycloak:6.1.0", "required": true }, { diff --git a/templates/learning-portal-production.json b/templates/learning-portal-production.json index 6b36144..b8cf658 100644 --- a/templates/learning-portal-production.json +++ b/templates/learning-portal-production.json @@ -77,7 +77,7 @@ }, { "name": "SPAWNER_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-spawner:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-spawner:6.1.0", "required": true }, { diff --git a/templates/terminal-server-development.json b/templates/terminal-server-development.json index 2e50f2b..46cbf89 100644 --- a/templates/terminal-server-development.json +++ b/templates/terminal-server-development.json @@ -151,19 +151,6 @@ } } }, - { - "kind": "ServiceAccount", - "apiVersion": "v1", - "metadata": { - "name": "${APPLICATION_NAME}-${PROJECT_NAME}-user", - "namespace": "${PROJECT_NAME}", - "labels": { - "app": "${APPLICATION_NAME}-${PROJECT_NAME}", - "spawner": "terminal-server", - "class": "spawner" - } - } - }, { "kind": "OAuthClient", "apiVersion": "oauth.openshift.io/v1", diff --git a/templates/terminal-server-production.json b/templates/terminal-server-production.json index d3071ac..9d3b235 100644 --- a/templates/terminal-server-production.json +++ b/templates/terminal-server-production.json @@ -72,7 +72,7 @@ }, { "name": "SPAWNER_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-spawner:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-spawner:6.1.0", "required": true }, { @@ -146,19 +146,6 @@ } } }, - { - "kind": "ServiceAccount", - "apiVersion": "v1", - "metadata": { - "name": "${APPLICATION_NAME}-${PROJECT_NAME}-user", - "namespace": "${PROJECT_NAME}", - "labels": { - "app": "${APPLICATION_NAME}-${PROJECT_NAME}", - "spawner": "terminal-server", - "class": "spawner" - } - } - }, { "kind": "OAuthClient", "apiVersion": "oauth.openshift.io/v1", diff --git a/templates/user-workspace-production.json b/templates/user-workspace-production.json index 4ad0197..4e98230 100644 --- a/templates/user-workspace-production.json +++ b/templates/user-workspace-production.json @@ -73,12 +73,12 @@ }, { "name": "SPAWNER_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-spawner:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-spawner:6.1.0", "required": true }, { "name": "KEYCLOAK_IMAGE", - "value": "quay.io/openshifthomeroom/workshop-keycloak:6.0.1", + "value": "quay.io/openshifthomeroom/workshop-keycloak:6.1.0", "required": true }, {