├── README.md ├── images └── jupyter-hub │ ├── Dockerfile │ ├── jupyterhub_config.py │ └── requirements.txt └── jupyter-hub ├── jupyter-hub ├── deployment.yaml ├── pvc.yaml ├── rbac.yaml └── service.yaml └── proxy ├── deployment.yaml └── service.yaml /README.md: -------------------------------------------------------------------------------- 1 | # Manual deployment of JupyterHub on Kubernetes for a single machine 2 | 3 | This repository provides source code for Deploying a simple JupyterHub on Kubernetes for a single machine. 4 | 5 | It contains 6 | - Dockerfile files (`images` directory) for building Docker images 7 | - `yaml` files (`jupyter-hub` directory) for deploying some components of JupyterHub on Kubernetes via `kubectl` commands. 8 | 9 | This repository is for the post at https://medium.com/@kienmn97/manually-deploy-jupyterhub-on-kubernetes-for-a-single-machine-dbcd9c9e50a4 10 | 11 | Update KubeSpawner service account in order to mount Kubernetes service account secrets to single user notebook: https://medium.com/@kienmn97/mounting-kubernetes-service-account-secrets-for-single-user-jupyter-notebook-pod-29163e527ad3 12 | 13 | Update PersistentVolume and PersistentVolumeClaim for storage: https://medium.com/@kienmn97/persistent-storage-in-jupyterhub-on-kubernetes-cluster-running-on-minikube-4b469bdb1b86 -------------------------------------------------------------------------------- /images/jupyter-hub/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ARG JUPYTERHUB_VERSION=1.1.0 4 | 5 | RUN apt-get update && \ 6 | apt-get install -y --no-install-recommends \ 7 | git \ 8 | vim \ 9 | less \ 10 | python3 \ 11 | python3-dev \ 12 | python3-pip \ 13 | python3-setuptools \ 14 | python3-wheel \ 15 | libssl-dev \ 16 | libcurl4-openssl-dev \ 17 | build-essential \ 18 | sqlite3 \ 19 | curl \ 20 | dnsutils \ 21 | $(bash -c 'if [[ $JUPYTERHUB_VERSION == "git"* ]]; then \ 22 | # workaround for https://bugs.launchpad.net/ubuntu/+source/nodejs/+bug/1794589 23 | echo nodejs=8.10.0~dfsg-2ubuntu0.2 nodejs-dev=8.10.0~dfsg-2ubuntu0.2 npm; \ 24 | fi') \ 25 | && \ 26 | apt-get purge && apt-get clean 27 | 28 | ARG NB_USER=jovyan 29 | ARG NB_UID=1000 30 | ARG HOME=/home/jovyan 31 | 32 | ENV LANG C.UTF-8 33 | 34 | RUN adduser --disabled-password \ 35 | --gecos "Default user" \ 36 | --uid ${NB_UID} \ 37 | --home ${HOME} \ 38 | --force-badname \ 39 | ${NB_USER} 40 | 41 | ADD requirements.txt /tmp/requirements.txt 42 | ADD jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py 43 | 44 | RUN PYCURL_SSL_LIBRARY=openssl pip3 install --no-cache-dir \ 45 | -r /tmp/requirements.txt \ 46 | $(bash -c 'if [[ $JUPYTERHUB_VERSION == "git"* ]]; then \ 47 | echo ${JUPYTERHUB_VERSION}; \ 48 | else \ 49 | echo jupyterhub==${JUPYTERHUB_VERSION}; \ 50 | fi') 51 | 52 | WORKDIR /srv/jupyterhub 53 | 54 | # So we can actually write a db file here 55 | RUN chown ${NB_USER}:${NB_USER} /srv/jupyterhub 56 | 57 | # JupyterHub API port 58 | EXPOSE 8081 59 | 60 | USER ${NB_USER} 61 | #CMD ["jupyterhub"] 62 | CMD ["jupyterhub", "--config", "/srv/jupyterhub/jupyterhub_config.py"] -------------------------------------------------------------------------------- /images/jupyter-hub/jupyterhub_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | from tornado.httpclient import AsyncHTTPClient 6 | from kubernetes import client 7 | from jupyterhub.utils import url_path_join 8 | 9 | # # Make sure that modules placed in the same directory as the jupyterhub config are added to the pythonpath 10 | # configuration_directory = os.path.dirname(os.path.realpath(__file__)) 11 | # sys.path.insert(0, configuration_directory) 12 | 13 | # from z2jh import get_config, set_config_if_not_none 14 | 15 | # # Configure JupyterHub to use the curl backend for making HTTP requests, 16 | # # rather than the pure-python implementations. The default one starts 17 | # # being too slow to make a large number of requests to the proxy API 18 | # # at the rate required. 19 | # AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") 20 | 21 | c.JupyterHub.spawner_class = 'kubespawner.KubeSpawner' 22 | 23 | # Connect to a proxy running in a different pod 24 | c.ConfigurableHTTPProxy.api_url = 'http://{}:{}'.format(os.environ['PROXY_API_SERVICE_HOST'], int(os.environ['PROXY_API_SERVICE_PORT'])) 25 | c.ConfigurableHTTPProxy.should_start = False 26 | 27 | # # Do not shut down user pods when hub is restarted 28 | # c.JupyterHub.cleanup_servers = False 29 | 30 | # # Check that the proxy has routes appropriately setup 31 | c.JupyterHub.last_activity_interval = 60 32 | 33 | # # Don't wait at all before redirecting a spawning user to the progress page 34 | # c.JupyterHub.tornado_settings = { 35 | # 'slow_spawn_timeout': 0, 36 | # } 37 | 38 | 39 | # def camelCaseify(s): 40 | # """convert snake_case to camelCase 41 | 42 | # For the common case where some_value is set from someValue 43 | # so we don't have to specify the name twice. 44 | # """ 45 | # return re.sub(r"_([a-z])", lambda m: m.group(1).upper(), s) 46 | 47 | 48 | # # configure the hub db connection 49 | # db_type = get_config('hub.db.type') 50 | # if db_type == 'sqlite-pvc': 51 | # c.JupyterHub.db_url = "sqlite:///jupyterhub.sqlite" 52 | # elif db_type == "sqlite-memory": 53 | # c.JupyterHub.db_url = "sqlite://" 54 | # else: 55 | # set_config_if_not_none(c.JupyterHub, "db_url", "hub.db.url") 56 | 57 | 58 | # for trait, cfg_key in ( 59 | # # Max number of servers that can be spawning at any one time 60 | # ('concurrent_spawn_limit', None), 61 | # # Max number of servers to be running at one time 62 | # ('active_server_limit', None), 63 | # # base url prefix 64 | # ('base_url', None), 65 | # ('allow_named_servers', None), 66 | # ('named_server_limit_per_user', None), 67 | # ('authenticate_prometheus', None), 68 | # ('redirect_to_server', None), 69 | # ('shutdown_on_logout', None), 70 | # ('template_paths', None), 71 | # ('template_vars', None), 72 | # ): 73 | # if cfg_key is None: 74 | # cfg_key = camelCaseify(trait) 75 | # set_config_if_not_none(c.JupyterHub, trait, 'hub.' + cfg_key) 76 | 77 | c.JupyterHub.ip = os.environ['PROXY_PUBLIC_SERVICE_HOST'] 78 | c.JupyterHub.port = int(os.environ['PROXY_PUBLIC_SERVICE_PORT']) 79 | 80 | # # the hub should listen on all interfaces, so the proxy can access it 81 | c.JupyterHub.hub_ip = '0.0.0.0' 82 | 83 | # # implement common labels 84 | # # this duplicates the jupyterhub.commonLabels helper 85 | # common_labels = c.KubeSpawner.common_labels = {} 86 | # common_labels['app'] = get_config( 87 | # "nameOverride", 88 | # default=get_config("Chart.Name", "jupyterhub"), 89 | # ) 90 | # common_labels['heritage'] = "jupyterhub" 91 | # chart_name = get_config('Chart.Name') 92 | # chart_version = get_config('Chart.Version') 93 | # if chart_name and chart_version: 94 | # common_labels['chart'] = "{}-{}".format( 95 | # chart_name, chart_version.replace('+', '_'), 96 | # ) 97 | # release = get_config('Release.Name') 98 | # if release: 99 | # common_labels['release'] = release 100 | 101 | # c.KubeSpawner.namespace = os.environ.get('POD_NAMESPACE', 'default') 102 | 103 | # # Max number of consecutive failures before the Hub restarts itself 104 | # # requires jupyterhub 0.9.2 105 | # set_config_if_not_none( 106 | # c.Spawner, 107 | # 'consecutive_failure_limit', 108 | # 'hub.consecutiveFailureLimit', 109 | # ) 110 | 111 | # for trait, cfg_key in ( 112 | # ('start_timeout', None), 113 | # ('image_pull_policy', 'image.pullPolicy'), 114 | # ('events_enabled', 'events'), 115 | # ('extra_labels', None), 116 | # ('extra_annotations', None), 117 | # ('uid', None), 118 | # ('fs_gid', None), 119 | # ('service_account', 'serviceAccountName'), 120 | # ('storage_extra_labels', 'storage.extraLabels'), 121 | # ('tolerations', 'extraTolerations'), 122 | # ('node_selector', None), 123 | # ('node_affinity_required', 'extraNodeAffinity.required'), 124 | # ('node_affinity_preferred', 'extraNodeAffinity.preferred'), 125 | # ('pod_affinity_required', 'extraPodAffinity.required'), 126 | # ('pod_affinity_preferred', 'extraPodAffinity.preferred'), 127 | # ('pod_anti_affinity_required', 'extraPodAntiAffinity.required'), 128 | # ('pod_anti_affinity_preferred', 'extraPodAntiAffinity.preferred'), 129 | # ('lifecycle_hooks', None), 130 | # ('init_containers', None), 131 | # ('extra_containers', None), 132 | # ('mem_limit', 'memory.limit'), 133 | # ('mem_guarantee', 'memory.guarantee'), 134 | # ('cpu_limit', 'cpu.limit'), 135 | # ('cpu_guarantee', 'cpu.guarantee'), 136 | # ('extra_resource_limits', 'extraResource.limits'), 137 | # ('extra_resource_guarantees', 'extraResource.guarantees'), 138 | # ('environment', 'extraEnv'), 139 | # ('profile_list', None), 140 | # ('extra_pod_config', None), 141 | # ): 142 | # if cfg_key is None: 143 | # cfg_key = camelCaseify(trait) 144 | # set_config_if_not_none(c.KubeSpawner, trait, 'singleuser.' + cfg_key) 145 | 146 | # image = get_config("singleuser.image.name") 147 | # if image: 148 | # tag = get_config("singleuser.image.tag") 149 | # if tag: 150 | # image = "{}:{}".format(image, tag) 151 | 152 | # c.KubeSpawner.image = image 153 | 154 | c.KubeSpawner.image = "jupyter/pyspark-notebook:latest" 155 | 156 | c.KubeSpawner.service_account = "hub" 157 | 158 | # if get_config('singleuser.imagePullSecret.enabled'): 159 | # c.KubeSpawner.image_pull_secrets = 'singleuser-image-credentials' 160 | 161 | # # scheduling: 162 | # if get_config('scheduling.userScheduler.enabled'): 163 | # c.KubeSpawner.scheduler_name = os.environ['HELM_RELEASE_NAME'] + "-user-scheduler" 164 | # if get_config('scheduling.podPriority.enabled'): 165 | # c.KubeSpawner.priority_class_name = os.environ['HELM_RELEASE_NAME'] + "-default-priority" 166 | 167 | # # add node-purpose affinity 168 | # match_node_purpose = get_config('scheduling.userPods.nodeAffinity.matchNodePurpose') 169 | # if match_node_purpose: 170 | # node_selector = dict( 171 | # matchExpressions=[ 172 | # dict( 173 | # key="hub.jupyter.org/node-purpose", 174 | # operator="In", 175 | # values=["user"], 176 | # ) 177 | # ], 178 | # ) 179 | # if match_node_purpose == 'prefer': 180 | # c.KubeSpawner.node_affinity_preferred.append( 181 | # dict( 182 | # weight=100, 183 | # preference=node_selector, 184 | # ), 185 | # ) 186 | # elif match_node_purpose == 'require': 187 | # c.KubeSpawner.node_affinity_required.append(node_selector) 188 | # elif match_node_purpose == 'ignore': 189 | # pass 190 | # else: 191 | # raise ValueError("Unrecognized value for matchNodePurpose: %r" % match_node_purpose) 192 | 193 | # # add dedicated-node toleration 194 | # for key in ( 195 | # 'hub.jupyter.org/dedicated', 196 | # # workaround GKE not supporting / in initial node taints 197 | # 'hub.jupyter.org_dedicated', 198 | # ): 199 | # c.KubeSpawner.tolerations.append( 200 | # dict( 201 | # key=key, 202 | # operator='Equal', 203 | # value='user', 204 | # effect='NoSchedule', 205 | # ) 206 | # ) 207 | 208 | # # Configure dynamically provisioning pvc 209 | # storage_type = get_config('singleuser.storage.type') 210 | 211 | # if storage_type == 'dynamic': 212 | # pvc_name_template = get_config('singleuser.storage.dynamic.pvcNameTemplate') 213 | # c.KubeSpawner.pvc_name_template = pvc_name_template 214 | # volume_name_template = get_config('singleuser.storage.dynamic.volumeNameTemplate') 215 | # c.KubeSpawner.storage_pvc_ensure = True 216 | # set_config_if_not_none(c.KubeSpawner, 'storage_class', 'singleuser.storage.dynamic.storageClass') 217 | # set_config_if_not_none(c.KubeSpawner, 'storage_access_modes', 'singleuser.storage.dynamic.storageAccessModes') 218 | # set_config_if_not_none(c.KubeSpawner, 'storage_capacity', 'singleuser.storage.capacity') 219 | 220 | # # Add volumes to singleuser pods 221 | # c.KubeSpawner.volumes = [ 222 | # { 223 | # 'name': volume_name_template, 224 | # 'persistentVolumeClaim': { 225 | # 'claimName': pvc_name_template 226 | # } 227 | # } 228 | # ] 229 | # c.KubeSpawner.volume_mounts = [ 230 | # { 231 | # 'mountPath': get_config('singleuser.storage.homeMountPath'), 232 | # 'name': volume_name_template 233 | # } 234 | # ] 235 | # elif storage_type == 'static': 236 | # pvc_claim_name = get_config('singleuser.storage.static.pvcName') 237 | # c.KubeSpawner.volumes = [{ 238 | # 'name': 'home', 239 | # 'persistentVolumeClaim': { 240 | # 'claimName': pvc_claim_name 241 | # } 242 | # }] 243 | 244 | # c.KubeSpawner.volume_mounts = [{ 245 | # 'mountPath': get_config('singleuser.storage.homeMountPath'), 246 | # 'name': 'home', 247 | # 'subPath': get_config('singleuser.storage.static.subPath') 248 | # }] 249 | 250 | # c.KubeSpawner.volumes.extend(get_config('singleuser.storage.extraVolumes', [])) 251 | # c.KubeSpawner.volume_mounts.extend(get_config('singleuser.storage.extraVolumeMounts', [])) 252 | 253 | # Mount volume for storage 254 | pvc_name_template = 'claim-{username}' 255 | c.KubeSpawner.pvc_name_template = pvc_name_template 256 | volume_name_template = 'volume-{username}' 257 | 258 | c.KubeSpawner.storage_pvc_ensure = True 259 | c.KubeSpawner.storage_class = 'standard' 260 | c.KubeSpawner.storage_access_modes = ['ReadWriteOnce'] 261 | c.KubeSpawner.storage_capacity = '200Mi' 262 | 263 | # Add volumes to singleuser pods 264 | c.KubeSpawner.volumes = [ 265 | { 266 | 'name': volume_name_template, 267 | 'persistentVolumeClaim': { 268 | 'claimName': pvc_name_template 269 | } 270 | } 271 | ] 272 | c.KubeSpawner.volume_mounts = [ 273 | { 274 | 'mountPath': '/home/jovyan', 275 | 'name': volume_name_template 276 | } 277 | ] 278 | 279 | # # Gives spawned containers access to the API of the hub 280 | c.JupyterHub.hub_connect_ip = os.environ['HUB_SERVICE_HOST'] 281 | c.JupyterHub.hub_connect_port = int(os.environ['HUB_SERVICE_PORT']) 282 | 283 | # # Allow switching authenticators easily 284 | # auth_type = get_config('auth.type') 285 | # email_domain = 'local' 286 | 287 | # common_oauth_traits = ( 288 | # ('client_id', None), 289 | # ('client_secret', None), 290 | # ('oauth_callback_url', 'callbackUrl'), 291 | # ) 292 | 293 | c.JupyterHub.authenticator_class = 'jupyterhub.auth.DummyAuthenticator' 294 | c.DummyAuthenticator.password = "some_password" 295 | 296 | # if auth_type == 'google': 297 | # c.JupyterHub.authenticator_class = 'oauthenticator.GoogleOAuthenticator' 298 | # for trait, cfg_key in common_oauth_traits + ( 299 | # ('hosted_domain', None), 300 | # ('login_service', None), 301 | # ): 302 | # if cfg_key is None: 303 | # cfg_key = camelCaseify(trait) 304 | # set_config_if_not_none(c.GoogleOAuthenticator, trait, 'auth.google.' + cfg_key) 305 | # email_domain = get_config('auth.google.hostedDomain') 306 | # elif auth_type == 'github': 307 | # c.JupyterHub.authenticator_class = 'oauthenticator.github.GitHubOAuthenticator' 308 | # for trait, cfg_key in common_oauth_traits + ( 309 | # ('github_organization_whitelist', 'orgWhitelist'), 310 | # ): 311 | # if cfg_key is None: 312 | # cfg_key = camelCaseify(trait) 313 | # set_config_if_not_none(c.GitHubOAuthenticator, trait, 'auth.github.' + cfg_key) 314 | # elif auth_type == 'cilogon': 315 | # c.JupyterHub.authenticator_class = 'oauthenticator.CILogonOAuthenticator' 316 | # for trait, cfg_key in common_oauth_traits: 317 | # if cfg_key is None: 318 | # cfg_key = camelCaseify(trait) 319 | # set_config_if_not_none(c.CILogonOAuthenticator, trait, 'auth.cilogon.' + cfg_key) 320 | # elif auth_type == 'gitlab': 321 | # c.JupyterHub.authenticator_class = 'oauthenticator.gitlab.GitLabOAuthenticator' 322 | # for trait, cfg_key in common_oauth_traits + ( 323 | # ('gitlab_group_whitelist', None), 324 | # ('gitlab_project_id_whitelist', None), 325 | # ('gitlab_url', None), 326 | # ): 327 | # if cfg_key is None: 328 | # cfg_key = camelCaseify(trait) 329 | # set_config_if_not_none(c.GitLabOAuthenticator, trait, 'auth.gitlab.' + cfg_key) 330 | # elif auth_type == 'azuread': 331 | # c.JupyterHub.authenticator_class = 'oauthenticator.azuread.AzureAdOAuthenticator' 332 | # for trait, cfg_key in common_oauth_traits + ( 333 | # ('tenant_id', None), 334 | # ('username_claim', None), 335 | # ): 336 | # if cfg_key is None: 337 | # cfg_key = camelCaseify(trait) 338 | 339 | # set_config_if_not_none(c.AzureAdOAuthenticator, trait, 'auth.azuread.' + cfg_key) 340 | # elif auth_type == 'mediawiki': 341 | # c.JupyterHub.authenticator_class = 'oauthenticator.mediawiki.MWOAuthenticator' 342 | # for trait, cfg_key in common_oauth_traits + ( 343 | # ('index_url', None), 344 | # ): 345 | # if cfg_key is None: 346 | # cfg_key = camelCaseify(trait) 347 | # set_config_if_not_none(c.MWOAuthenticator, trait, 'auth.mediawiki.' + cfg_key) 348 | # elif auth_type == 'globus': 349 | # c.JupyterHub.authenticator_class = 'oauthenticator.globus.GlobusOAuthenticator' 350 | # for trait, cfg_key in common_oauth_traits + ( 351 | # ('identity_provider', None), 352 | # ): 353 | # if cfg_key is None: 354 | # cfg_key = camelCaseify(trait) 355 | # set_config_if_not_none(c.GlobusOAuthenticator, trait, 'auth.globus.' + cfg_key) 356 | # elif auth_type == 'hmac': 357 | # c.JupyterHub.authenticator_class = 'hmacauthenticator.HMACAuthenticator' 358 | # c.HMACAuthenticator.secret_key = bytes.fromhex(get_config('auth.hmac.secretKey')) 359 | # elif auth_type == 'dummy': 360 | # c.JupyterHub.authenticator_class = 'dummyauthenticator.DummyAuthenticator' 361 | # set_config_if_not_none(c.DummyAuthenticator, 'password', 'auth.dummy.password') 362 | # elif auth_type == 'tmp': 363 | # c.JupyterHub.authenticator_class = 'tmpauthenticator.TmpAuthenticator' 364 | # elif auth_type == 'lti': 365 | # c.JupyterHub.authenticator_class = 'ltiauthenticator.LTIAuthenticator' 366 | # set_config_if_not_none(c.LTIAuthenticator, 'consumers', 'auth.lti.consumers') 367 | # elif auth_type == 'ldap': 368 | # c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator' 369 | # c.LDAPAuthenticator.server_address = get_config('auth.ldap.server.address') 370 | # set_config_if_not_none(c.LDAPAuthenticator, 'server_port', 'auth.ldap.server.port') 371 | # set_config_if_not_none(c.LDAPAuthenticator, 'use_ssl', 'auth.ldap.server.ssl') 372 | # set_config_if_not_none(c.LDAPAuthenticator, 'allowed_groups', 'auth.ldap.allowedGroups') 373 | # set_config_if_not_none(c.LDAPAuthenticator, 'bind_dn_template', 'auth.ldap.dn.templates') 374 | # set_config_if_not_none(c.LDAPAuthenticator, 'lookup_dn', 'auth.ldap.dn.lookup') 375 | # set_config_if_not_none(c.LDAPAuthenticator, 'lookup_dn_search_filter', 'auth.ldap.dn.search.filter') 376 | # set_config_if_not_none(c.LDAPAuthenticator, 'lookup_dn_search_user', 'auth.ldap.dn.search.user') 377 | # set_config_if_not_none(c.LDAPAuthenticator, 'lookup_dn_search_password', 'auth.ldap.dn.search.password') 378 | # set_config_if_not_none(c.LDAPAuthenticator, 'lookup_dn_user_dn_attribute', 'auth.ldap.dn.user.dnAttribute') 379 | # set_config_if_not_none(c.LDAPAuthenticator, 'escape_userdn', 'auth.ldap.dn.user.escape') 380 | # set_config_if_not_none(c.LDAPAuthenticator, 'valid_username_regex', 'auth.ldap.dn.user.validRegex') 381 | # set_config_if_not_none(c.LDAPAuthenticator, 'user_search_base', 'auth.ldap.dn.user.searchBase') 382 | # set_config_if_not_none(c.LDAPAuthenticator, 'user_attribute', 'auth.ldap.dn.user.attribute') 383 | # elif auth_type == 'custom': 384 | # # full_class_name looks like "myauthenticator.MyAuthenticator". 385 | # # To create a docker image with this class availabe, you can just have the 386 | # # following Dockerfile: 387 | # # FROM jupyterhub/k8s-hub:v0.4 388 | # # RUN pip3 install myauthenticator 389 | # full_class_name = get_config('auth.custom.className') 390 | # c.JupyterHub.authenticator_class = full_class_name 391 | # auth_class_name = full_class_name.rsplit('.', 1)[-1] 392 | # auth_config = c[auth_class_name] 393 | # auth_config.update(get_config('auth.custom.config') or {}) 394 | # else: 395 | # raise ValueError("Unhandled auth type: %r" % auth_type) 396 | 397 | # set_config_if_not_none(c.OAuthenticator, 'scope', 'auth.scopes') 398 | 399 | # set_config_if_not_none(c.Authenticator, 'enable_auth_state', 'auth.state.enabled') 400 | 401 | # # Enable admins to access user servers 402 | # set_config_if_not_none(c.JupyterHub, 'admin_access', 'auth.admin.access') 403 | # set_config_if_not_none(c.Authenticator, 'admin_users', 'auth.admin.users') 404 | # set_config_if_not_none(c.Authenticator, 'whitelist', 'auth.whitelist.users') 405 | 406 | # c.JupyterHub.services = [] 407 | 408 | # if get_config('cull.enabled', False): 409 | # cull_cmd = [ 410 | # 'python3', 411 | # '/etc/jupyterhub/cull_idle_servers.py', 412 | # ] 413 | # base_url = c.JupyterHub.get('base_url', '/') 414 | # cull_cmd.append( 415 | # '--url=http://127.0.0.1:8081' + url_path_join(base_url, 'hub/api') 416 | # ) 417 | 418 | # cull_timeout = get_config('cull.timeout') 419 | # if cull_timeout: 420 | # cull_cmd.append('--timeout=%s' % cull_timeout) 421 | 422 | # cull_every = get_config('cull.every') 423 | # if cull_every: 424 | # cull_cmd.append('--cull-every=%s' % cull_every) 425 | 426 | # cull_concurrency = get_config('cull.concurrency') 427 | # if cull_concurrency: 428 | # cull_cmd.append('--concurrency=%s' % cull_concurrency) 429 | 430 | # if get_config('cull.users'): 431 | # cull_cmd.append('--cull-users') 432 | 433 | # if get_config('cull.removeNamedServers'): 434 | # cull_cmd.append('--remove-named-servers') 435 | 436 | # cull_max_age = get_config('cull.maxAge') 437 | # if cull_max_age: 438 | # cull_cmd.append('--max-age=%s' % cull_max_age) 439 | 440 | # c.JupyterHub.services.append({ 441 | # 'name': 'cull-idle', 442 | # 'admin': True, 443 | # 'command': cull_cmd, 444 | # }) 445 | 446 | # for name, service in get_config('hub.services', {}).items(): 447 | # # jupyterhub.services is a list of dicts, but 448 | # # in the helm chart it is a dict of dicts for easier merged-config 449 | # service.setdefault('name', name) 450 | # # handle camelCase->snake_case of api_token 451 | # api_token = service.pop('apiToken', None) 452 | # if api_token: 453 | # service['api_token'] = api_token 454 | # c.JupyterHub.services.append(service) 455 | 456 | 457 | # set_config_if_not_none(c.Spawner, 'cmd', 'singleuser.cmd') 458 | # set_config_if_not_none(c.Spawner, 'default_url', 'singleuser.defaultUrl') 459 | 460 | # cloud_metadata = get_config('singleuser.cloudMetadata', {}) 461 | 462 | # if not cloud_metadata.get('enabled', False): 463 | # # Use iptables to block access to cloud metadata by default 464 | # network_tools_image_name = get_config('singleuser.networkTools.image.name') 465 | # network_tools_image_tag = get_config('singleuser.networkTools.image.tag') 466 | # ip_block_container = client.V1Container( 467 | # name="block-cloud-metadata", 468 | # image=f"{network_tools_image_name}:{network_tools_image_tag}", 469 | # command=[ 470 | # 'iptables', 471 | # '-A', 'OUTPUT', 472 | # '-d', cloud_metadata.get('ip', '169.254.169.254'), 473 | # '-j', 'DROP' 474 | # ], 475 | # security_context=client.V1SecurityContext( 476 | # privileged=True, 477 | # run_as_user=0, 478 | # capabilities=client.V1Capabilities(add=['NET_ADMIN']) 479 | # ) 480 | # ) 481 | 482 | # c.KubeSpawner.init_containers.append(ip_block_container) 483 | 484 | 485 | # if get_config('debug.enabled', False): 486 | # c.JupyterHub.log_level = 'DEBUG' 487 | # c.Spawner.debug = True 488 | 489 | 490 | # extra_config = get_config('hub.extraConfig', {}) 491 | # if isinstance(extra_config, str): 492 | # from textwrap import indent, dedent 493 | # msg = dedent( 494 | # """ 495 | # hub.extraConfig should be a dict of strings, 496 | # but found a single string instead. 497 | 498 | # extraConfig as a single string is deprecated 499 | # as of the jupyterhub chart version 0.6. 500 | 501 | # The keys can be anything identifying the 502 | # block of extra configuration. 503 | 504 | # Try this instead: 505 | 506 | # hub: 507 | # extraConfig: 508 | # myConfig: | 509 | # {} 510 | 511 | # This configuration will still be loaded, 512 | # but you are encouraged to adopt the nested form 513 | # which enables easier merging of multiple extra configurations. 514 | # """ 515 | # ) 516 | # print( 517 | # msg.format( 518 | # indent(extra_config, ' ' * 10).lstrip() 519 | # ), 520 | # file=sys.stderr 521 | # ) 522 | # extra_config = {'deprecated string': extra_config} 523 | 524 | # for key, config_py in sorted(extra_config.items()): 525 | # print("Loading extra config: %s" % key) 526 | # exec(config_py) -------------------------------------------------------------------------------- /images/jupyter-hub/requirements.txt: -------------------------------------------------------------------------------- 1 | # JupyterHub's version is defined in chartpress.yaml 2 | 3 | ## Authenticators 4 | jupyterhub-dummyauthenticator==0.3.* 5 | jupyterhub-firstuseauthenticator==0.12.* 6 | jupyterhub-tmpauthenticator==0.6.* 7 | jupyterhub-ltiauthenticator==0.4.* 8 | jupyterhub-ldapauthenticator==1.3.* 9 | jupyterhub-hmacauthenticator==0.1.* 10 | jupyterhub-nativeauthenticator==0.0.5 11 | mwoauth==0.3.7 12 | globus_sdk[jwt]==1.8.* 13 | nullauthenticator==1.0.* 14 | oauthenticator==0.11.* 15 | 16 | ## Spawners 17 | jupyterhub-kubespawner==0.11.* 18 | kubernetes==10.0.* 19 | 20 | ## Other dependencies 21 | pymysql==0.9.* 22 | psycopg2-binary==2.8.* 23 | pycurl==7.43.0.* 24 | statsd==3.3.* 25 | cryptography==2.8.* 26 | 27 | ## Useful tools 28 | 29 | # py-spy is useful for profiling running hubs 30 | py-spy -------------------------------------------------------------------------------- /jupyter-hub/jupyter-hub/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hub 5 | labels: 6 | component: jupyter 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | component: jupyter 12 | template: 13 | metadata: 14 | labels: 15 | component: jupyter 16 | spec: 17 | serviceAccountName: hub 18 | containers: 19 | - name: jupyter-hub 20 | #image: kienmn97/jupyter-hub:1.1.0 21 | image: kienmn97/jupyter-hub-spark-notebook:1.1.0 22 | imagePullPolicy: Always 23 | command: ["jupyterhub", "--config", "/srv/jupyterhub/jupyterhub_config.py"] 24 | ports: 25 | - containerPort: 8081 26 | env: 27 | - name: CONFIGPROXY_AUTH_TOKEN 28 | value: "f7a5ba56603e8f4e4f1a1eb85b551b9e70ff863f76cb5f56e72aa23f84e31b5c" 29 | volumeMounts: 30 | # - mountPath: /srv/jupyterhub/jupyterhub_config.py 31 | # subPath: jupyterhub_config.py 32 | # name: config 33 | #- mountPath: /etc/jupyterhub/config/ 34 | # name: config 35 | #- mountPath: /etc/jupyterhub/secret/ 36 | # name: secret 37 | - mountPath: /svr/jupyterhub/ 38 | name: hub-db-dir 39 | resources: 40 | requests: 41 | cpu: 100m 42 | volumes: 43 | # - name: config 44 | # configMap: 45 | # name: hub-config 46 | #- name: config 47 | # configMap: 48 | # name: hub-config 49 | #- name: secret 50 | # secret: 51 | # secretName: hub-secret 52 | - name: hub-db-dir 53 | persistentVolumeClaim: 54 | claimName: hub-db-dir 55 | -------------------------------------------------------------------------------- /jupyter-hub/jupyter-hub/pvc.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: hub-db-dir 5 | labels: 6 | component: jupyter 7 | spec: 8 | storageClassName: "standard" 9 | accessModes: 10 | - ReadWriteOnce 11 | resources: 12 | requests: 13 | storage: "200Mi" -------------------------------------------------------------------------------- /jupyter-hub/jupyter-hub/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: hub 5 | labels: 6 | component: jupyter 7 | --- 8 | kind: Role 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | metadata: 11 | name: hub 12 | labels: 13 | component: jupyter 14 | rules: 15 | - apiGroups: [""] # "" indicates the core API group 16 | resources: ["pods", "persistentvolumeclaims"] 17 | verbs: ["get", "watch", "list", "create", "delete"] 18 | - apiGroups: [""] # "" indicates the core API group 19 | resources: ["events"] 20 | verbs: ["get", "watch", "list"] 21 | --- 22 | kind: RoleBinding 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | metadata: 25 | name: hub 26 | labels: 27 | component: jupyter 28 | subjects: 29 | - kind: ServiceAccount 30 | name: hub 31 | namespace: 32 | roleRef: 33 | kind: Role 34 | name: hub 35 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /jupyter-hub/jupyter-hub/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: hub 5 | labels: 6 | component: jupyter 7 | spec: 8 | type: ClusterIP 9 | ports: 10 | - protocol: TCP 11 | port: 8081 12 | targetPort: 8081 13 | selector: 14 | component: jupyter -------------------------------------------------------------------------------- /jupyter-hub/proxy/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: proxy 5 | labels: 6 | component: proxy 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | component: proxy 12 | #strategy: 13 | # {{- .Values.proxy.deploymentStrategy | toYaml | trimSuffix "\n" | nindent 4 }} 14 | template: 15 | metadata: 16 | labels: 17 | component: proxy 18 | # {{- /* Changes here will cause the Deployment to restart the pods. */}} 19 | # {{- include "jupyterhub.matchLabels" . | nindent 8 }} 20 | # hub.jupyter.org/network-access-hub: "true" 21 | # hub.jupyter.org/network-access-singleuser: "true" 22 | # {{- if .Values.proxy.labels }} 23 | # {{- .Values.proxy.labels | toYaml | trimSuffix "\n" | nindent 8 }} 24 | # {{- end }} 25 | # annotations: 26 | # This lets us autorestart when the secret changes! 27 | # checksum/hub-secret: {{ include (print $.Template.BasePath "/hub/secret.yaml") . | sha256sum }} 28 | # checksum/proxy-secret: {{ include (print $.Template.BasePath "/proxy/secret.yaml") . | sha256sum }} 29 | # {{- if .Values.proxy.annotations }} 30 | # {{- .Values.proxy.annotations | toYaml | trimSuffix "\n" | nindent 8 }} 31 | # {{- end }} 32 | spec: 33 | containers: 34 | - name: chp 35 | image: jupyterhub/configurable-http-proxy:4.2.1 36 | command: 37 | - configurable-http-proxy 38 | - --ip=0.0.0.0 39 | - --api-ip=0.0.0.0 40 | - --api-port=8001 41 | - --default-target=http://$(HUB_SERVICE_HOST):$(HUB_SERVICE_PORT) 42 | - --error-target=http://$(HUB_SERVICE_HOST):$(HUB_SERVICE_PORT)/hub/error 43 | - --port=8000 44 | - --log-level=debug 45 | # resources: 46 | # {{- .Values.proxy.chp.resources | toYaml | trimSuffix "\n" | nindent 12 }} 47 | securityContext: 48 | # Don't allow any process to execute as root inside the container 49 | allowPrivilegeEscalation: false 50 | env: 51 | - name: CONFIGPROXY_AUTH_TOKEN 52 | value: "f7a5ba56603e8f4e4f1a1eb85b551b9e70ff863f76cb5f56e72aa23f84e31b5c" 53 | # {{- with .Values.proxy.chp.image.pullPolicy }} 54 | # imagePullPolicy: {{ . }} 55 | # {{- end }} 56 | ports: 57 | - containerPort: 8000 58 | name: proxy-public 59 | - containerPort: 8001 60 | name: api -------------------------------------------------------------------------------- /jupyter-hub/proxy/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: proxy-api 5 | labels: 6 | component: proxy 7 | spec: 8 | selector: 9 | component: proxy 10 | ports: 11 | - protocol: TCP 12 | port: 8001 13 | targetPort: 8001 14 | --- 15 | apiVersion: v1 16 | kind: Service 17 | metadata: 18 | name: proxy-public 19 | labels: 20 | component: proxy 21 | spec: 22 | selector: 23 | component: proxy 24 | ports: 25 | - name: https 26 | port: 443 27 | protocol: TCP 28 | - name: http 29 | port: 80 30 | protocol: TCP 31 | targetPort: 80 32 | targetPort: 8000 33 | # allow proxy.service.nodePort for http 34 | #{{- if .Values.proxy.service.nodePorts.http }} 35 | #nodePort: {{ .Values.proxy.service.nodePorts.http }} 36 | #{{- end }} 37 | type: LoadBalancer 38 | --------------------------------------------------------------------------------