Newer
Older
František Dvořák
committed
---
proxy:
service:
type: NodePort
ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
kubernetes.io/tls-acme: "true"
hosts:
- fullhub.eosc.zcu.cz
tls:
- hosts:
- fullhub.eosc.zcu.cz
secretName: acme-tls-fullhub
singleuser:
# keep resource limits in sync with:
# - profileList
storage:
František Dvořák
committed
extraVolumes:
- name: cvmfs-host
hostPath:
path: /cvmfs
type: Directory
- name: owncloud-home
empty_dir:
# - name: scratch
# ephemeral:
# volumeClaimTemplate:
# spec:
# accessModes: [ "ReadWriteOnce" ]
# storageClassName: local-path
# resources:
# requests:
# storage: "10Gi"
extraVolumeMounts:
- name: cvmfs-host
mountPath: "/cvmfs:shared"
mountPath: '/home/jovyan:shared'
# - name: scratch
# mountPath: '/scratch'
František Dvořák
committed
memory:
František Dvořák
committed
guarantee: 128M
cpu:
limit: 2
guarantee: .02
defaultUrl: "/lab"
image:
František Dvořák
committed
name: eginotebooks/single-user
tag: "sha-b94a3ef"
František Dvořák
committed
profileList:
- display_name: Small Environment - 2 vCPU / 4 GB RAM (non-collaboratice)
František Dvořák
committed
description: >
The notebook environment includes Python, R, Julia and Octave kernels. Non-collaborative.
František Dvořák
committed
default: true
kubespawner_override:
args:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'"
František Dvořák
committed
image: "eginotebooks/single-user:sha-b94a3ef"
- display_name: Small Environment - 2 vCPU / 4 GB RAM (collaboratice)
The notebook environment includes Python, R, Julia and Octave kernels. Collaborative.
kubespawner_override:
args:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'"
image: "valtri/single-user:jupyter-4e-collab"
cmd: jupyterhub-singleuser-webdav-wrapper
extraFiles:
wait-remote-home.sh:
mountPath: /usr/local/bin/jupyterhub-wait-remote-home
stringData: |-
#! /bin/sh
#
# Dirty hack to make remote mount on home directory working properly:
#
# 1) wait for webdav sidecar image to kick in
# 2) change directory to the mounted version of itself
# 3) launch notebook server
#
i=0
while ! grep '^webdav-fs: /home/jovyan ' /proc/mounts && test $i -lt 30; do
echo 'Waiting for ownClound mount...'
sleep 0.5
i=$((i+1))
done
singleuser-webdav-wrapper.sh:
mode: 0755
mountPath: /usr/local/bin/jupyterhub-singleuser-webdav-wrapper
stringData: |-
#! /bin/sh
/usr/local/bin/jupyterhub-wait-remote-home
cd .
exec jupyterhub-singleuser "$@"
František Dvořák
committed
hub:
services:
status:
url: "http://status-web/"
admin: true
František Dvořák
committed
image:
name: valtri/hub
František Dvořák
committed
config:
Authenticator:
enable_auth_state: true
admin_users:
# valtri@civ.zcu.cz
- c36b18fe-e03a-4a22-ab14-5965e0171410@eosc-federation.eu
František Dvořák
committed
allowed_groups:
- urn:geant:eosc-federation.eu:testing:group:eosc#testing.eosc-federation.eu
František Dvořák
committed
auto_login: true
claim_groups_key: "entitlements"
František Dvořák
committed
EGICheckinAuthenticator:
checkin_host: "{{ secret['checkin_host'] }}"
authorize_url: "https://{{ secret['checkin_host'] }}/OIDC/authorization"
token_url: "https://{{ secret['checkin_host'] }}/OIDC/token"
userdata_url: "https://{{ secret['checkin_host'] }}/OIDC/userinfo"
František Dvořák
committed
client_id: "{{ secret['client_id'] }}"
client_secret: "{{ secret['client_secret'] }}"
oauth_callback_url: "https://fullhub.eosc.zcu.cz/hub/oauth_callback"
openid_configuration_url: "https://proxy.testing.eosc-federation.eu/.well-known/openid-configuration"
scope: ["openid", "profile", "email", "offline_access", "entitlements"]
extra_authorize_params:
prompt: consent
František Dvořák
committed
JupyterHub:
admin_access: true
authenticate_prometheus: false
authenticator_class: egi_notebooks_hub.egiauthenticator.EGICheckinAuthenticator
# spawner_class: (in egi-notebooks-b2drop)
extraConfig:
egi-notebooks-welcome: |-
from egi_notebooks_hub.welcome import WelcomeHandler
c.JupyterHub.default_url = "/welcome"
c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)]
egi-notebooks-b2drop: |-
František Dvořák
committed
import base64
František Dvořák
committed
from jinja2 import BaseLoader
from jinja2 import Environment
from egi_notebooks_hub.onedata import OnedataSpawner
from kubernetes_asyncio.client.rest import ApiException
from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest
František Dvořák
committed
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
class B2DropSpawner(OnedataSpawner):
async def auth_state_hook(self, spawner, auth_state):
await super().auth_state_hook(spawner, auth_state)
self.b2drop_ready = False
self.b2drop_user = ""
self.b2drop_pwd = ""
try:
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
except ApiException:
return
if secret and secret.data:
self.b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
self.b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
self.b2drop_ready = (self.b2drop_user and self.b2drop_pwd)
def _render_options_form(self, profile_list):
# old:self._profile_list = self._init_profile_list(profile_list)
self._profile_list = self._get_initialized_profile_list(profile_list)
profile_form_template = Environment(loader=BaseLoader).from_string(
self.profile_form_template
)
return profile_form_template.render(profile_list=self._profile_list, b2drop_ready=self.b2drop_ready, b2drop_user=self.b2drop_user, b2drop_pwd=self.b2drop_pwd)
async def pre_spawn_hook(self, spawner):
await super(B2DropSpawner, self).pre_spawn_hook(spawner)
b2drop_user = self.user_options.get("b2drop-user", "")
b2drop_pwd = self.user_options.get("b2drop-pwd", "")
if not (b2drop_user and b2drop_pwd):
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
if secret and secret.data:
b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
if b2drop_user and b2drop_pwd:
volume_mounts = [
{"mountPath": "/owncloud:shared", "name": "owncloud-home"},
František Dvořák
committed
]
spawner.extra_containers.append(
{
"name": "b2drop",
"image": "eginotebooks/webdav-rclone-sidecar:sha-0a62679",
František Dvořák
committed
"env": [
{"name": "WEBDAV_URL", "value": "https://b2drop.eudat.eu/remote.php/webdav"},
{"name": "WEBDAV_PWD", "value": b2drop_pwd},
{"name": "WEBDAV_USER", "value": b2drop_user},
{"name": "WEBDAV_VENDOR", "value": "other"},
{"name": "MOUNT_PATH", "value": "/owncloud/b2drop"},
{"name": "MOUNT_WAIT_POINT", "value": "webdav-fs: /owncloud fuse.rclone"},
František Dvořák
committed
],
"resources": self.sidecar_resources,
"securityContext": {
"runAsUser": 1000,
"fsUser": 1000,
"fsGroup": 100,
František Dvořák
committed
"privileged": True,
"capabilities": {"add": ["SYS_ADMIN"]},
},
"volumeMounts": volume_mounts,
}
)
def options_from_form(self, formdata):
data = super(B2DropSpawner, self)._options_from_form(formdata)
data.update({'b2drop-user': formdata.get('b2drop-user', [None])[0],
'b2drop-pwd': formdata.get('b2drop-pwd', [None])[0]})
return data
class WebDavOIDCSpawner(B2DropSpawner):
# ownCloud Infinite Scale parameters
# (https://owncloud.dev/apis/http/graph/spaces/#list-my-spaces-get-medrives)
OCIS_URL = "https://ocis.aaitest.owncloud.works"
# personal space
OCIS_PERSONAL_SPACE = "/graph/v1.0/me/drives?%24filter=driveType+eq+personal"
# shared space
OCIS_SHARED_WITH_ME = "/graph/v1.0/me/drives?%24filter=driveType+eq+virtual"
# otter spaces
OCIS_SPACES = "/graph/v1.0/me/drives?%24filter=driveType+eq+project"
async def append_owncloud_sidecar(self, spawner, type, query, fallback_url=None, headers={}):
owncloud_url = fallback_url
http_client = AsyncHTTPClient()
req = HTTPRequest(
self.OCIS_URL + query,
headers=headers,
method="GET",
)
try:
resp = await http_client.fetch(req)
body = json.loads(resp.body.decode("utf8", "replace"))
self.log.debug("OCIS response: %s", body)
if "value" in body:
ocis_infos = body["value"]
if len(ocis_infos) >= 1 and "root" in ocis_infos[0]:
owncloud_url = ocis_infos[0]["root"].get("webDavUrl", None)
except HTTPClientError as e:
self.log.error("can't query ownCloud: %s", e)
self.log.info("ownCloud %s URL: %s", type, owncloud_url)
if owncloud_url is None:
return
if type == "home":
subpath = ""
else:
subpath = "/" + type.capitalize()
env = [
{"name": "WEBDAV_URL", "value": owncloud_url},
{"name": "WEBDAV_VENDOR", "value": "owncloud"},
# XXX: strict permissions needed for .local/share/jupyter/runtime/jupyter_cookie_secret
# quicker directory cache and polling
{"name": "MOUNT_OPTS", "value": "--file-perms=0600 --dir-perms=0770 --dir-cache-time=1m0s --poll-interval=0m20s"},
{"name": "MOUNT_PATH", "value": "/owncloud" + subpath},
# default mode is "full"
{"name": "VFS_CACHE_MODE", "value": "full"},
]
if type != "home":
env.append({"name": "MOUNT_WAIT_POINT", "value": "webdav-fs: /owncloud fuse.rclone"})
{"mountPath": "/owncloud:shared", "name": "owncloud-home"},
{"mountPath": self.token_mount_path, "name": self.token_secret_volume_name, "readOnly": True},
]
spawner.extra_containers.append(
{
"name": "owncloud-" + type,
"image": "eginotebooks/webdav-rclone-sidecar:sha-0a62679",
"args": ["bearer_token_command=cat " + self.token_path],
"resources": self.sidecar_resources,
"securityContext": {
"runAsUser": 1000,
"fsUser": 1000,
"fsGroup": 100,
"privileged": True,
"capabilities": {"add": ["SYS_ADMIN"]},
},
"volumeMounts": volume_mounts,
}
)
async def pre_spawn_hook(self, spawner):
await super(WebDavOIDCSpawner, self).pre_spawn_hook(spawner)
auth_state = await self.user.get_auth_state()
self.token_secret_volume_name = self._expand_user_properties(
self.token_secret_volume_name_template
)
self.token_path = os.path.join(self.token_mount_path, "access_token")
František Dvořák
committed
if auth_state:
access_token = auth_state.get("access_token", None)
headers = {
"Accept": "application/json",
"User-Agent": "JupyterHub",
"Authorization": "Bearer %s" % access_token,
}
František Dvořák
committed
await self.append_owncloud_sidecar(spawner, "home", self.OCIS_PERSONAL_SPACE, headers=headers)
await self.append_owncloud_sidecar(spawner, "shares", self.OCIS_SHARED_WITH_ME, headers=headers)
František Dvořák
committed
await self.append_owncloud_sidecar(spawner, "spaces", self.OCIS_SPACES, headers=headers)
else:
self.log.info("No auth state, skipping ownCloud")
c.JupyterHub.spawner_class = WebDavOIDCSpawner
František Dvořák
committed
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
c.B2DropSpawner.args = ["--FileCheckpoints.checkpoint_dir='/home/jovyan/.notebookCheckpoints'"]
c.B2DropSpawner.profile_form_template = """
<style>
/*
.profile divs holds two div tags: one for a radio button, and one
for the profile's content.
*/
#kubespawner-profiles-list .profile {
display: flex;
flex-direction: row;
font-weight: normal;
border-bottom: 1px solid #ccc;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .radio {
padding: 12px;
}
/* .option divs holds a label and a select tag */
#kubespawner-profiles-list .profile .option {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .option label {
font-weight: normal;
margin-right: 8px;
min-width: 96px;
}
</style>
<div class='form-group' id='kubespawner-profiles-list'>
{%- for profile in profile_list %}
{#- Wrap everything in a <label> so clicking anywhere selects the option #}
<label for='profile-item-{{ profile.slug }}' class='profile'>
<div class='radio'>
<input type='radio' name='profile' id='profile-item-{{ profile.slug }}' value='{{ profile.slug }}' {% if profile.default %}checked{% endif %} />
</div>
<div>
<h3>{{ profile.display_name }}</h3>
{%- if profile.description %}
<p>{{ profile.description }}</p>
{%- endif %}
{%- if profile.profile_options %}
<div>
{%- for k, option in profile.profile_options.items() %}
<div class='option'>
<label for='profile-option-{{profile.slug}}-{{k}}'>{{option.display_name}}</label>
<select name="profile-option-{{profile.slug}}-{{k}}" class="form-control">
{%- for k, choice in option['choices'].items() %}
<option value="{{ k }}" {% if choice.default %}selected{%endif %}>{{ choice.display_name }}</option>
{%- endfor %}
</select>
</div>
{%- endfor %}
</div>
{%- endif %}
</div>
</label>
{%- endfor %}
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="collabpsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
B2DROP connection
</a>
{%if b2drop_ready %}<span class="label label-success">Already configured!</span>{% endif %}
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<div class='form-group'>
<label for="b2drop-user" class="form-label">B2DROP app Username</label>
<input type="text" class="form-control" name="b2drop-user" id="b2drop-user" aria-describedby="b2drop-user-help" value="{{ b2drop_user }}">
<div id="b2drop-user-help" class="form-text">Create new app password at <a href="https://b2drop.eudat.eu/settings/user/security">B2DROP security configuration</a></div>
</div>
<div class='form-group'>
<label for="b2drop-pwd" class="form-label">B2DROP app Password</label>
<input type="password" class="form-control" name="b2drop-pwd" id="b2drop-pwd" value="{{ b2drop_pwd }}">
</div>
</div>
</div>
</div>
</div>
"""
{% endraw %}
extraFiles:
welcome.html:
mountPath: /usr/local/share/jupyterhub/templates/welcome.html
František Dvořák
committed
stringData: |-