diff --git a/.s2i/bin/assemble b/.s2i/bin/assemble index 69025f1..b1493bf 100755 --- a/.s2i/bin/assemble +++ b/.s2i/bin/assemble @@ -23,6 +23,9 @@ npm install -g configurable-http-proxy cp /tmp/src/jupyterhub_config.py /opt/app-root/etc/ cp /tmp/src/jupyterhub_config.sh /opt/app-root/etc/ +cp /tmp/src/jupyterhub_config-*.py /opt/app-root/etc/ +cp /tmp/src/jupyterhub_config-*.sh /opt/app-root/etc/ + # This S2I assemble script is only used when creating the custom image. # For when running the image, or using it as a S2I builder, we use a second # set of custom S2I scripts. We now need to move these into the correct diff --git a/README.md b/README.md index 7ccf372..5f957c9 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,10 @@ To make it easier to deploy JupyterHub in OpenShift, templates are provided. To oc apply -f https://raw.githubusercontent.com/jupyter-on-openshift/jupyterhub-quickstart/master/templates/jupyterhub-builder.json oc apply -f https://raw.githubusercontent.com/jupyter-on-openshift/jupyterhub-quickstart/master/templates/jupyterhub-deployer.json oc apply -f https://raw.githubusercontent.com/jupyter-on-openshift/jupyterhub-quickstart/master/templates/jupyterhub-quickstart.json +oc apply -f https://raw.githubusercontent.com/jupyter-on-openshift/jupyterhub-quickstart/master/templates/jupyterhub-workspace.json ``` -This should result in the creation of the templates ``jupyterhub-builder``, ``jupyterhub-deployer`` and ``jupyterhub-quickstart``. +This should result in the creation of the templates ``jupyterhub-builder``, ``jupyterhub-deployer``, ``jupyterhub-quickstart`` and ``jupyterhub-workspace``. Creating the JupyterHub Deployment ---------------------------------- @@ -438,3 +439,43 @@ c.JupyterHub.services = [ ``` The ``cull-idle-servers`` program is provided with the JupyterHub image. Adjust the value for the timeout argument as necessary. + +Multi User Developer Workspace +------------------------------ + +The ``jupyterhub-workspace`` template combines a number of the above configuration options into one template. These include: + +* Authentication of users using OpenShift cluster OAuth provider. +* Optional specification of whitelisted users, including those who are admins. +* Optional allocation of a persistent storage volume for each user. +* Optional culling of idle sessions. + +Note that the template can only be used with Jupyter notebook images based on the ``s2i-minimal-notebook`` images. You can use official images from the Jupyter Project. + +Also, the ``jupyterhub-workspace`` template can only be deployed by a cluster admin, as it needs to create an ``oauthclient`` resource definition, which requires cluster admin access. + +To deploy the template and provide persistent storage and idle session culling you can use: + +``` +oc new-app --template jupyterhub-workspace --param VOLUME_SIZE=1Gi --param IDLE_TIMEOUT=3600 +``` + +To delete the deployment first use: + +``` +oc delete all,configmap,pvc,serviceaccount,rolebinding --selector app=jupyterhub +``` + +You then need to delete the ``oauthclient`` resource. Because this is a global resource, verify you are deleting the correct resource first by running: + +``` +oc get oauthclient --selector app=jupyterhub +``` + +If it is correct, then delete it using: + +``` +oc delete oauthclient --selector app=jupyterhub +``` + +If there is more than one resource matching the label selector, delete by name the one corresponding to the project you created the deployment in. The project name will be part of the resource name. diff --git a/build-configs/jupyterhub.json b/build-configs/jupyterhub.json index 9bc6ee8..c0bba1d 100644 --- a/build-configs/jupyterhub.json +++ b/build-configs/jupyterhub.json @@ -39,7 +39,7 @@ "type": "Git", "git": { "uri": "https://github.com/jupyter-on-openshift/jupyterhub-quickstart.git", - "ref": "3.3.1" + "ref": "3.4.0" } }, "strategy": { @@ -55,7 +55,7 @@ "output": { "to": { "kind": "ImageStreamTag", - "name": "jupyterhub:3.3.1" + "name": "jupyterhub:3.4.0" } } } diff --git a/image-streams/jupyterhub.json b/image-streams/jupyterhub.json index 79c19f5..f3bc929 100644 --- a/image-streams/jupyterhub.json +++ b/image-streams/jupyterhub.json @@ -14,10 +14,10 @@ }, "tags": [ { - "name": "3.3.1", + "name": "3.4.0", "from": { "kind": "DockerImage", - "name": "quay.io/jupyteronopenshift/jupyterhub:3.3.1" + "name": "quay.io/jupyteronopenshift/jupyterhub:3.4.0" } } ] diff --git a/jupyterhub_config-workspace.py b/jupyterhub_config-workspace.py new file mode 100644 index 0000000..2fea8a0 --- /dev/null +++ b/jupyterhub_config-workspace.py @@ -0,0 +1,154 @@ +# Authenticate users against OpenShift OAuth provider. + +c.JupyterHub.authenticator_class = "openshift" + +from oauthenticator.openshift import OpenShiftOAuthenticator +OpenShiftOAuthenticator.scope = ['user:full'] + +client_id = '%s-%s-users' % (application_name, namespace) +client_secret = os.environ['OAUTH_CLIENT_SECRET'] + +c.OpenShiftOAuthenticator.client_id = client_id +c.OpenShiftOAuthenticator.client_secret = client_secret +c.Authenticator.enable_auth_state = True + +c.CryptKeeper.keys = [ client_secret.encode('utf-8') ] + +c.OpenShiftOAuthenticator.oauth_callback_url = ( + 'https://%s/hub/oauth_callback' % public_hostname) + +# Add any additional JupyterHub configuration settings. + +c.KubeSpawner.extra_labels = { + 'spawner': 'workspace', + 'class': 'session', + 'user': '{username}' +} + +# Set up list of registered users and any users nominated as admins. + +if os.path.exists('/opt/app-root/configs/admin_users.txt'): + with open('/opt/app-root/configs/admin_users.txt') as fp: + content = fp.read().strip() + if content: + c.Authenticator.admin_users = set(content.split()) + +if os.path.exists('/opt/app-root/configs/user_whitelist.txt'): + with open('/opt/app-root/configs/user_whitelist.txt') as fp: + c.Authenticator.whitelist = set(fp.read().strip().split()) + +# For workshops we provide each user with a persistent volume so they +# 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. + +volume_size = os.environ.get('JUPYTERHUB_VOLUME_SIZE') + +if volume_size: + c.KubeSpawner.pvc_name_template = c.KubeSpawner.pod_name_template + + c.KubeSpawner.storage_pvc_ensure = True + + c.KubeSpawner.storage_capacity = volume_size + + c.KubeSpawner.storage_access_modes = ['ReadWriteOnce'] + + c.KubeSpawner.volumes.extend([ + { + 'name': 'data', + 'persistentVolumeClaim': { + 'claimName': c.KubeSpawner.pvc_name_template + } + } + ]) + + c.KubeSpawner.volume_mounts.extend([ + { + 'name': 'data', + 'mountPath': '/opt/app-root', + 'subPath': 'workspace' + } + ]) + + c.KubeSpawner.init_containers.extend([ + { + 'name': 'setup-volume', + 'image': '%s' % c.KubeSpawner.image_spec, + 'command': [ + '/opt/app-root/bin/setup-volume.sh', + '/opt/app-root', + '/mnt/workspace' + ], + "resources": { + "limits": { + "memory": os.environ.get('NOTEBOOK_MEMORY', '128Mi') + }, + "requests": { + "memory": os.environ.get('NOTEBOOK_MEMORY', '128Mi') + } + }, + 'volumeMounts': [ + { + 'name': 'data', + 'mountPath': '/mnt' + } + ] + } + ]) + +# Make modifications to pod based on user and type of session. + +from tornado import gen + +@gen.coroutine +def modify_pod_hook(spawner, pod): + 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'] + + # Set the session access token from the OpenShift login. + + 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 + # result is different then we use it, not if it is the same + # which would suggest it isn't unique. + + project = os.environ.get('OPENSHIFT_PROJECT') + + if project: + name = project.format(username=spawner.user.name) + if name != project: + pod.spec.containers[0].env.append( + dict(name='PROJECT_NAMESPACE', value=name)) + + # Ensure project is created if it doesn't exist. + + pod.spec.containers[0].env.append( + dict(name='OPENSHIFT_PROJECT', value=name)) + + return pod + +c.KubeSpawner.modify_pod_hook = modify_pod_hook + +# Setup culling of terminal instances if timeout parameter is supplied. + +idle_timeout = os.environ.get('JUPYTERHUB_IDLE_TIMEOUT') + +if idle_timeout and int(idle_timeout): + cull_idle_servers_cmd = ['/opt/app-root/bin/cull-idle-servers'] + + cull_idle_servers_cmd.append('--timeout=%s' % idle_timeout) + + c.JupyterHub.services.extend([ + { + 'name': 'cull-idle', + 'admin': True, + 'command': cull_idle_servers_cmd, + } + ]) diff --git a/jupyterhub_config-workspace.sh b/jupyterhub_config-workspace.sh new file mode 100644 index 0000000..9317201 --- /dev/null +++ b/jupyterhub_config-workspace.sh @@ -0,0 +1,7 @@ +KUBERNETES_SERVER_URL="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT" +OAUTH_METADATA_URL="$KUBERNETES_SERVER_URL/.well-known/oauth-authorization-server" +OAUTH_ISSUER_ADDRESS=`curl -ks $OAUTH_METADATA_URL | grep '"issuer":' | sed -e 's%.*https://%https://%' -e 's%",%%'` + +export OPENSHIFT_URL=$OAUTH_ISSUER_ADDRESS +export OPENSHIFT_REST_API_URL=$KUBERNETES_SERVER_URL +export OPENSHIFT_AUTH_API_URL=$OAUTH_ISSUER_ADDRESS diff --git a/jupyterhub_config.py b/jupyterhub_config.py index 2648b89..1befa1f 100644 --- a/jupyterhub_config.py +++ b/jupyterhub_config.py @@ -137,6 +137,16 @@ def resolve_image_name(name): # Define the default configuration for JupyterHub application. +c.Spawner.environment = dict() + +c.JupyterHub.services = [] + +c.KubeSpawner.init_containers = [] + +c.KubeSpawner.extra_containers = [] + +c.JupyterHub.extra_handlers = [] + c.JupyterHub.port = 8080 c.JupyterHub.hub_ip = '0.0.0.0' @@ -191,6 +201,11 @@ def resolve_image_name(name): if os.environ.get('JUPYTERHUB_NOTEBOOK_MEMORY'): c.Spawner.mem_limit = convert_size_to_bytes(os.environ['JUPYTERHUB_NOTEBOOK_MEMORY']) +notebook_interface = os.environ.get('JUPYTERHUB_NOTEBOOK_INTERFACE') + +if notebook_interface: + c.Spawner.environment['JUPYTER_NOTEBOOK_INTERFACE'] = notebook_interface + # Workaround bug in minishift where a service cannot be contacted from a # pod which backs the service. For further details see the minishift issue # https://github.com/minishift/minishift/issues/2400. @@ -234,6 +249,17 @@ def _wrapper_get_env(wrapped, instance, args, kwargs): return env +# Load configuration overrides based on configuration type. + +configuration_type = os.environ.get('CONFIGURATION_TYPE') + +if configuration_type: + config_file = '/opt/app-root/etc/jupyterhub_config-%s.py' % configuration_type + + if os.path.exists(config_file): + with open(config_file) as fp: + exec(compile(fp.read(), config_file, 'exec'), globals()) + # Load configuration included in the image. image_config_file = '/opt/app-root/src/.jupyter/jupyterhub_config.py' diff --git a/jupyterhub_config.sh b/jupyterhub_config.sh index 9650e39..af8fd9d 100644 --- a/jupyterhub_config.sh +++ b/jupyterhub_config.sh @@ -1,3 +1,9 @@ +if [ x"$CONFIGURATION_TYPE" != x"" ]; then + if [ -f /opt/app-root/etc/jupyterhub_config-$CONFIGURATION_TYPE.sh ]; then + . /opt/app-root/etc/jupyterhub_config-$CONFIGURATION_TYPE.sh + fi +fi + if [ -f /opt/app-root/src/.jupyter/jupyterhub_config.sh ]; then . /opt/app-root/src/.jupyter/jupyterhub_config.sh fi diff --git a/requirements.txt b/requirements.txt index 805c47d..3b28219 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ kubernetes==9.0.1 -jupyterhub==0.9.6 +jupyterhub==1.0.0 #jupyterhub-kubespawner==0.10.1 -git+https://github.com/jupyter-on-openshift/kubespawner.git@055f6f29fd4d7ee56d1e9cca1aacdd6f5fd19507#egg=jupyterhub-kubespawner +git+https://github.com/jupyterhub/kubespawner.git@a945ef01410867b39e0c174d362a8702bbaa15e9#egg=jupyterhub-kubespawner git+https://github.com/jupyterhub/wrapspawner.git@5f2b7075f77d0c1c49066682a8e8adad0dab76db jupyterhub-tmpauthenticator==0.6 oauthenticator==0.9.0 jupyterhub-ldapauthenticator==1.2.2 -psycopg2==2.8.3 -openshift==0.9.2 +psycopg2==2.8.4 +openshift==0.10.0 wrapt==1.11.2 diff --git a/templates/jupyterhub-builder.json b/templates/jupyterhub-builder.json index b79e90a..1166451 100644 --- a/templates/jupyterhub-builder.json +++ b/templates/jupyterhub-builder.json @@ -18,7 +18,7 @@ }, { "name": "BUILDER_IMAGE", - "value": "jupyterhub:3.3.1", + "value": "jupyterhub:3.4.0", "required": true }, { diff --git a/templates/jupyterhub-deployer.json b/templates/jupyterhub-deployer.json index 2fcc777..ad4daca 100644 --- a/templates/jupyterhub-deployer.json +++ b/templates/jupyterhub-deployer.json @@ -18,7 +18,7 @@ }, { "name": "JUPYTERHUB_IMAGE", - "value": "jupyterhub:3.3.1", + "value": "jupyterhub:3.4.0", "required": true }, { diff --git a/templates/jupyterhub-quickstart.json b/templates/jupyterhub-quickstart.json index f2a8259..5166fc7 100644 --- a/templates/jupyterhub-quickstart.json +++ b/templates/jupyterhub-quickstart.json @@ -18,7 +18,7 @@ }, { "name": "JUPYTERHUB_IMAGE", - "value": "jupyterhub:3.3.1", + "value": "jupyterhub:3.4.0", "required": true }, { @@ -75,6 +75,10 @@ "value": "512Mi", "required": true }, + { + "name": "NOTEBOOK_INTERFACE", + "value": "classic" + }, { "name": "NOTEBOOK_MEMORY", "description": "Amount of memory available to each notebook.", @@ -294,6 +298,10 @@ "name": "JUPYTERHUB_NOTEBOOK_MEMORY", "value": "${NOTEBOOK_MEMORY}" }, + { + "name": "JUPYTERHUB_NOTEBOOK_INTERFACE", + "value": "${NOTEBOOK_INTERFACE}" + }, { "name": "JUPYTERHUB_DATABASE_PASSWORD", "value": "${DATABASE_PASSWORD}" diff --git a/templates/jupyterhub-workspace.json b/templates/jupyterhub-workspace.json new file mode 100644 index 0000000..67ca885 --- /dev/null +++ b/templates/jupyterhub-workspace.json @@ -0,0 +1,562 @@ +{ + "kind": "Template", + "apiVersion": "template.openshift.io/v1", + "metadata": { + "name": "jupyterhub-workspace", + "annotations": { + "openshift.io/display-name": "JupyterHub Workspace", + "description": "Template for deploying a JupyterHub instance with cluster access.", + "iconClass": "icon-python", + "tags": "python,jupyter,jupyterhub" + } + }, + "parameters": [ + { + "name": "SPAWNER_NAMESPACE", + "value": "", + "required": true + }, + { + "name": "CLUSTER_SUBDOMAIN", + "value": "", + "required": true + }, + { + "name": "APPLICATION_NAME", + "value": "jupyterhub", + "required": true + }, + { + "name": "JUPYTERHUB_IMAGE", + "value": "jupyterhub:3.4.0", + "required": true + }, + { + "name": "NOTEBOOK_IMAGE", + "value": "s2i-minimal-notebook:3.6", + "required": true + }, + { + "name": "JUPYTERHUB_CONFIG", + "value": "", + "required": false + }, + { + "name": "JUPYTERHUB_ENVVARS", + "value": "", + "required": false + }, + { + "name": "ADMIN_USERS", + "value": "", + "required": false + }, + { + "name": "REGISTERED_USERS", + "value": "", + "required": false + }, + { + "name": "DATABASE_PASSWORD", + "generate": "expression", + "from": "[a-zA-Z0-9]{16}", + "required": true + }, + { + "name": "COOKIE_SECRET", + "generate": "expression", + "from": "[a-f0-9]{32}", + "required": true + }, + { + "name": "JUPYTERHUB_MEMORY", + "description": "Amount of memory available to JupyterHub.", + "value": "512Mi", + "required": true + }, + { + "name": "DATABASE_MEMORY", + "description": "Amount of memory available to PostgreSQL.", + "value": "512Mi", + "required": true + }, + { + "name": "NOTEBOOK_MEMORY", + "description": "Amount of memory available to each notebook.", + "value": "512Mi", + "required": true + }, + { + "name": "NOTEBOOK_INTERFACE", + "value": "classic" + }, + { + "name": "OPENSHIFT_PROJECT", + "value": "", + "required": false + }, + { + "name": "VOLUME_SIZE", + "description": "Amount of storage available to each user.", + "value": "" + }, + { + "name": "IDLE_TIMEOUT", + "description": "Time in seconds after which idle session is culled.", + "value": "" + }, + { + "name": "OAUTH_CLIENT_SECRET", + "generate": "expression", + "from": "[a-zA-Z0-9]{32}" + } + ], + "objects": [ + { + "kind": "OAuthClient", + "apiVersion": "oauth.openshift.io/v1", + "metadata": { + "name": "${APPLICATION_NAME}-${SPAWNER_NAMESPACE}-users", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "secret": "${OAUTH_CLIENT_SECRET}", + "grantMethod": "auto", + "redirectURIs": [ + "https://${APPLICATION_NAME}-${SPAWNER_NAMESPACE}.${CLUSTER_SUBDOMAIN}/hub/oauth_callback" + ] + }, + { + "kind": "ConfigMap", + "apiVersion": "v1", + "metadata": { + "name": "${APPLICATION_NAME}-cfg", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "data": { + "jupyterhub_config.py": "${JUPYTERHUB_CONFIG}", + "jupyterhub_config.sh": "${JUPYTERHUB_ENVVARS}", + "admin_users.txt": "${ADMIN_USERS}", + "user_whitelist.txt": "${REGISTERED_USERS}" + } + }, + { + "kind": "ServiceAccount", + "apiVersion": "v1", + "metadata": { + "name": "${APPLICATION_NAME}-hub", + "labels": { + "app": "${APPLICATION_NAME}" + }, + "annotations": { + "serviceaccounts.openshift.io/oauth-redirectreference.first": "{\"kind\":\"OAuthRedirectReference\",\"apiVersion\":\"v1\",\"reference\":{\"kind\":\"Route\",\"name\":\"${APPLICATION_NAME}\"}}", + "serviceaccounts.openshift.io/oauth-redirecturi.first": "hub/oauth_callback", + "serviceaccounts.openshift.io/oauth-want-challenges": "false" + } + } + }, + { + "kind": "RoleBinding", + "apiVersion": "authorization.openshift.io/v1", + "metadata": { + "name": "${APPLICATION_NAME}-edit", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "${APPLICATION_NAME}-hub" + } + ], + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "edit" + } + }, + { + "kind": "DeploymentConfig", + "apiVersion": "apps.openshift.io/v1", + "metadata": { + "name": "${APPLICATION_NAME}", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "spec": { + "strategy": { + "type": "Recreate" + }, + "triggers": [ + { + "type": "ConfigChange" + }, + { + "type": "ImageChange", + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "wait-for-database", + "jupyterhub" + ], + "from": { + "kind": "ImageStreamTag", + "name": "${JUPYTERHUB_IMAGE}" + } + } + } + ], + "replicas": 1, + "selector": { + "app": "${APPLICATION_NAME}", + "deploymentconfig": "${APPLICATION_NAME}" + }, + "template": { + "metadata": { + "annotations": { + "alpha.image.policy.openshift.io/resolve-names": "*" + }, + "labels": { + "app": "${APPLICATION_NAME}", + "deploymentconfig": "${APPLICATION_NAME}" + } + }, + "spec": { + "serviceAccountName": "${APPLICATION_NAME}-hub", + "initContainers": [ + { + "name": "wait-for-database", + "image": "${JUPYTERHUB_IMAGE}", + "command": [ "wait-for-database" ], + "resources": { + "limits": { + "memory": "${JUPYTERHUB_MEMORY}" + } + }, + "env": [ + { + "name": "JUPYTERHUB_DATABASE_PASSWORD", + "value": "${DATABASE_PASSWORD}" + }, + { + "name": "JUPYTERHUB_DATABASE_HOST", + "value": "${APPLICATION_NAME}-db" + }, + { + "name": "JUPYTERHUB_DATABASE_NAME", + "value": "postgres" + } + ] + } + ], + "containers": [ + { + "name": "jupyterhub", + "image": "${JUPYTERHUB_IMAGE}", + "ports": [ + { + "containerPort": 8080, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "memory": "${JUPYTERHUB_MEMORY}" + } + }, + "env": [ + { + "name": "CONFIGURATION_TYPE", + "value": "workspace" + }, + { + "name": "APPLICATION_NAME", + "value": "${APPLICATION_NAME}" + }, + { + "name": "JUPYTERHUB_NOTEBOOK_IMAGE", + "value": "${NOTEBOOK_IMAGE}" + }, + { + "name": "JUPYTERHUB_NOTEBOOK_MEMORY", + "value": "${NOTEBOOK_MEMORY}" + }, + { + "name": "JUPYTERHUB_NOTEBOOK_INTERFACE", + "value": "${NOTEBOOK_INTERFACE}" + }, + { + "name": "OPENSHIFT_PROJECT", + "value": "${OPENSHIFT_PROJECT}" + }, + { + "name": "JUPYTERHUB_VOLUME_SIZE", + "value": "${VOLUME_SIZE}" + }, + { + "name": "JUPYTERHUB_IDLE_TIMEOUT", + "value": "${IDLE_TIMEOUT}" + }, + { + "name": "JUPYTERHUB_DATABASE_PASSWORD", + "value": "${DATABASE_PASSWORD}" + }, + { + "name": "JUPYTERHUB_DATABASE_HOST", + "value": "${APPLICATION_NAME}-db" + }, + { + "name": "JUPYTERHUB_DATABASE_NAME", + "value": "postgres" + }, + { + "name": "JUPYTERHUB_COOKIE_SECRET", + "value": "${COOKIE_SECRET}" + }, + { + "name": "OAUTH_CLIENT_SECRET", + "value": "${OAUTH_CLIENT_SECRET}" + } + ], + "volumeMounts": [ + { + "name": "config", + "mountPath": "/opt/app-root/configs" + } + ] + } + ], + "volumes": [ + { + "name": "config", + "configMap": { + "name": "${APPLICATION_NAME}-cfg", + "defaultMode": 420 + } + } + ] + } + } + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "${APPLICATION_NAME}", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "spec": { + "ports": [ + { + "name": "8080-tcp", + "protocol": "TCP", + "port": 8080, + "targetPort": 8080 + }, + { + "name": "8081-tcp", + "protocol": "TCP", + "port": 8081, + "targetPort": 8081 + } + ], + "selector": { + "app": "${APPLICATION_NAME}", + "deploymentconfig": "${APPLICATION_NAME}" + } + } + }, + { + "kind": "Route", + "apiVersion": "route.openshift.io/v1", + "metadata": { + "name": "${APPLICATION_NAME}", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "spec": { + "host": "", + "to": { + "kind": "Service", + "name": "${APPLICATION_NAME}", + "weight": 100 + }, + "port": { + "targetPort": "8080-tcp" + }, + "tls": { + "termination": "edge", + "insecureEdgeTerminationPolicy": "Redirect" + } + } + }, + { + "kind": "PersistentVolumeClaim", + "apiVersion": "v1", + "metadata": { + "name": "${APPLICATION_NAME}-db", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "1Gi" + } + } + } + }, + { + "kind": "DeploymentConfig", + "apiVersion": "apps.openshift.io/v1", + "metadata": { + "name": "${APPLICATION_NAME}-db", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "spec": { + "replicas": 1, + "selector": { + "app": "${APPLICATION_NAME}", + "deploymentconfig": "${APPLICATION_NAME}-db" + }, + "strategy": { + "type": "Recreate" + }, + "template": { + "metadata": { + "labels": { + "app": "${APPLICATION_NAME}", + "deploymentconfig": "${APPLICATION_NAME}-db" + } + }, + "spec": { + "containers": [ + { + "name": "postgresql", + "env": [ + { + "name": "POSTGRESQL_USER", + "value": "jupyterhub" + }, + { + "name": "POSTGRESQL_PASSWORD", + "value": "${DATABASE_PASSWORD}" + }, + { + "name": "POSTGRESQL_DATABASE", + "value": "postgres" + } + ], + "livenessProbe": { + "tcpSocket": { + "port": 5432 + } + }, + "ports": [ + { + "containerPort": 5432, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "memory": "${DATABASE_MEMORY}" + } + }, + "readinessProbe": { + "exec": { + "command": [ + "/bin/sh", + "-i", + "-c", + "psql -h 127.0.0.1 -U $POSTGRESQL_USER -q -d $POSTGRESQL_DATABASE -c 'SELECT 1'" + ] + } + }, + "volumeMounts": [ + { + "mountPath": "/var/lib/pgsql/data", + "name": "data" + } + ] + } + ], + "volumes": [ + { + "name": "data", + "persistentVolumeClaim": { + "claimName": "${APPLICATION_NAME}-db" + } + }, + { + "name": "config", + "configMap": { + "name": "${APPLICATION_NAME}-cfg", + "defaultMode": 420 + } + } + ] + } + }, + "triggers": [ + { + "imageChangeParams": { + "automatic": true, + "containerNames": [ + "postgresql" + ], + "from": { + "kind": "ImageStreamTag", + "name": "postgresql:9.6", + "namespace": "openshift" + } + }, + "type": "ImageChange" + }, + { + "type": "ConfigChange" + } + ] + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "${APPLICATION_NAME}-db", + "labels": { + "app": "${APPLICATION_NAME}" + } + }, + "spec": { + "ports": [ + { + "name": "5432-tcp", + "protocol": "TCP", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "app": "${APPLICATION_NAME}", + "deploymentconfig": "${APPLICATION_NAME}-db" + } + } + } + ] +}