diff --git a/example/DEPLOYMENT.md b/example/DEPLOYMENT.md new file mode 100644 index 0000000..63d2e2f --- /dev/null +++ b/example/DEPLOYMENT.md @@ -0,0 +1,92 @@ +# Deploy `tljh_repo2docker` as a hub-managed service + +A guide to help you add `tljh_repo2docker` service to a JupyterHub deployment from scratch. + +> [!NOTE] +> In this guide, we assume you have experience with setting up JupyterHub on Kubernetes. We will use [Zero to JupyterHub](https://z2jh.jupyter.org/en/latest/index.html#zero-to-jupyterhub-with-kubernetes) as the base JupyterHub deployment. + +## Create a new JupyterHub docker image. + +Since we are going to use `tljh_repo2docker` as a hub-managed service, the python package needs to be installed into the base JupyterHub image. We will build a custom image based on the [zero-to-jupyterhub image](https://quay.io/repository/jupyterhub/k8s-hub). Here is a minimal docker file to build the custom image: + +```docker +FROM quay.io/jupyterhub/k8s-hub:3.3.7 + +USER root +RUN python3 -m pip install "tljh-repo2docker>=2.0.0a1" + +USER jovyan +``` + +Then you can build and push the image to the registry of your choice: + +```bash +docker build . -t k8s-hub-tljh:xxx +``` +## Installing JupyterHub and tljh_repo2docker with Helm chart. + +With a Kubernetes cluster available and Helm installed, we can install our custom JupyterHub image in the Kubernetes cluster using the JupyterHub Helm chart. You can find a chart template at [example/tljh_r2d](example/tljh_r2d). + +### Using local build-backend via `repo2docker`. + +To build images using the local docker engine, modify the `value.yaml` file with the following content: + +```yaml +# values.yaml +jupyterhub: + enabled: true + hub: + image: + name: k8s-hub-tljh + tag: xxx + extraFiles: + my_jupyterhub_config: + mountPath: /usr/local/etc/jupyterhub/jupyterhub_config.d/jupyterhub_config.py +``` + +Now install the chart with the JupyterHub config from `jupyterhub_config_local.py`: + +```bash +helm upgrade --install tljh . --wait --set-file jupyterhub.hub.extraFiles.my_jupyterhub_config.stringData=./jupyterhub_config_local.py +``` + +### Using binderhub service as build-backend. + +To use binderhub as the build-backend, you need to deploy [binderhub service](https://binderhub-service.readthedocs.io/en/latest/tutorials/install.html) and config `tljh-repo2docker` to use this service. Here is an example of the configuration: + +```yaml +#values.yaml + +jupyterhub: + enabled: true + hub: + image: + name: k8s-hub-binhderhub-tljh + tag: xxx + config: + BinderSpawner: + auth_enabled: true + extraFiles: + my_jupyterhub_config: + mountPath: /usr/local/etc/jupyterhub/jupyterhub_config.d/jupyterhub_config.py + +binderhub-service: + config: + BinderHub: + base_url: /services/binder + use_registry: true + image_prefix: "" + enable_api_only_mode: true + + buildPodsRegistryCredentials: + server: "https://index.docker.io/v1/" # Set image registry for pushing images + +``` + +Now install the chart with the JupyterHub config from `jupyterhub_config_binderhub.py` and with image registry credentials passed via command line: + +```bash +helm upgrade --install tljh . --wait --set-file jupyterhub.hub.extraFiles.my_jupyterhub_config.stringData=./jupyterhub_config_binderhub.py --set binderhub-service.buildPodsRegistryCredentials.password=xxx --set binderhub-service.buildPodsRegistryCredentials.username=xxx +``` + +Now you should have a working instance of JupyterHub with `tljh_repo2docker` service deployed. \ No newline at end of file diff --git a/example/Dockerfile b/example/Dockerfile new file mode 100644 index 0000000..447d374 --- /dev/null +++ b/example/Dockerfile @@ -0,0 +1,6 @@ +FROM quay.io/jupyterhub/k8s-hub:3.3.7 + +USER root +RUN python3 -m pip install "tljh-repo2docker>=2.0.0a1" + +USER jovyan \ No newline at end of file diff --git a/example/tljh_r2d/.helmignore b/example/tljh_r2d/.helmignore new file mode 100644 index 0000000..691fa13 --- /dev/null +++ b/example/tljh_r2d/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ \ No newline at end of file diff --git a/example/tljh_r2d/Chart.lock b/example/tljh_r2d/Chart.lock new file mode 100644 index 0000000..1fc7a4e --- /dev/null +++ b/example/tljh_r2d/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: jupyterhub + repository: https://hub.jupyter.org/helm-chart/ + version: 3.3.7 +- name: binderhub-service + repository: https://2i2c.org/binderhub-service + version: 0.1.0-0.dev.git.201.h161a088 +digest: sha256:db05fc18bdb7cbcf06ea8881ed72391383ed0524def10d76b62b9d1880abac51 +generated: "2024-05-02T20:34:11.347162518+02:00" diff --git a/example/tljh_r2d/Chart.yaml b/example/tljh_r2d/Chart.yaml new file mode 100644 index 0000000..c2022ad --- /dev/null +++ b/example/tljh_r2d/Chart.yaml @@ -0,0 +1,34 @@ +apiVersion: v2 +name: tljh_r2d +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" + +dependencies: + - name: jupyterhub + version: "3.3.7" + repository: https://hub.jupyter.org/helm-chart/ + condition: jupyterhub.enabled + # Optional binderhub build backend + - name: binderhub-service + version : "0.1.0-0.dev.git.201.h161a088" + repository: https://2i2c.org/binderhub-service \ No newline at end of file diff --git a/example/tljh_r2d/jupyterhub_config_binderhub.py b/example/tljh_r2d/jupyterhub_config_binderhub.py new file mode 100644 index 0000000..71c2384 --- /dev/null +++ b/example/tljh_r2d/jupyterhub_config_binderhub.py @@ -0,0 +1,182 @@ +""" +This file is only used for local development +and overrides some of the default values from the plugin. +""" + +from kubespawner import KubeSpawner +from pathlib import Path +from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE +from tornado import web +from traitlets import Bool, Unicode +from traitlets.config import Configurable +import sys + + +""" +Helpers for creating BinderSpawners +This file is defined in binderhub/binderspawner_mixin.py and is copied to here +""" + + +class BinderSpawnerMixin(Configurable): + """ + Mixin to convert a JupyterHub container spawner to a BinderHub spawner + + Container spawner must support the following properties that will be set + via spawn options: + - image: Container image to launch + - token: JupyterHub API token + """ + + def __init__(self, *args, **kwargs): + # Is this right? Is it possible to having multiple inheritance with both + # classes using traitlets? + # https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way + # https://github.com/ipython/traitlets/pull/175 + super().__init__(*args, **kwargs) + + auth_enabled = Bool( + False, + help=""" + Enable authenticated binderhub setup. + + Requires `jupyterhub-singleuser` to be available inside the repositories + being built. + """, + config=True, + ) + + cors_allow_origin = Unicode( + "", + help=""" + Origins that can access the spawned notebooks. + + Sets the Access-Control-Allow-Origin header in the spawned + notebooks. Set to '*' to allow any origin to access spawned + notebook servers. + + See also BinderHub.cors_allow_origin in binderhub config + for controlling CORS policy for the BinderHub API endpoint. + """, + config=True, + ) + + def get_args(self): + if self.auth_enabled: + args = super().get_args() + else: + args = [ + "--ip=0.0.0.0", + f"--port={self.port}", + f"--NotebookApp.base_url={self.server.base_url}", + f"--NotebookApp.token={self.user_options['token']}", + "--NotebookApp.trust_xheaders=True", + ] + if self.default_url: + args.append(f"--NotebookApp.default_url={self.default_url}") + + if self.cors_allow_origin: + args.append("--NotebookApp.allow_origin=" + self.cors_allow_origin) + # allow_origin=* doesn't properly allow cross-origin requests to single files + # see https://github.com/jupyter/notebook/pull/5898 + if self.cors_allow_origin == "*": + args.append("--NotebookApp.allow_origin_pat=.*") + args += self.args + # ServerApp compatibility: duplicate NotebookApp args + for arg in list(args): + if arg.startswith("--NotebookApp."): + args.append(arg.replace("--NotebookApp.", "--ServerApp.")) + return args + + def start(self): + if not self.auth_enabled: + if "token" not in self.user_options: + raise web.HTTPError(400, "token required") + if "image" not in self.user_options: + raise web.HTTPError(400, "image required") + if "image" in self.user_options: + self.image = self.user_options["image"] + return super().start() + + def get_env(self): + env = super().get_env() + if "repo_url" in self.user_options: + env["BINDER_REPO_URL"] = self.user_options["repo_url"] + for key in ( + "binder_ref_url", + "binder_launch_host", + "binder_persistent_request", + "binder_request", + ): + if key in self.user_options: + env[key.upper()] = self.user_options[key] + return env + + +class BinderSpawner(BinderSpawnerMixin, KubeSpawner): + pass + + +HERE = Path(__file__).parent + +c.JupyterHub.spawner_class = BinderSpawner + +c.JupyterHub.allow_named_servers = True + +c.JupyterHub.services.extend( + [ + {"name": "binder", "url": "http://tljh-binderhub-service"}, + { + "name": "tljhrepo2docker", + "url": "http://r2d-svc:6789", + "command": [ + sys.executable, + "-m", + "tljh_repo2docker", + "--ip", + "0.0.0.0", + "--port", + "6789", + "--TljhRepo2Docker.binderhub_url", + "http://tljh-binderhub-service/services/binder", + ], + "oauth_no_confirm": True, + "oauth_client_allowed_scopes": [ + TLJH_R2D_ADMIN_SCOPE, + ], + }, + ] +) + +c.JupyterHub.custom_scopes = { + TLJH_R2D_ADMIN_SCOPE: { + "description": "Admin access to myservice", + }, +} + +c.JupyterHub.load_roles = [ + { + "description": "Role for tljh_repo2docker service", + "name": "tljh-repo2docker-service", + "scopes": [ + "read:users", + "read:roles:users", + "admin:servers", + "access:services!service=binder", + ], + "services": ["tljhrepo2docker"], + }, + { + "name": "tljh-repo2docker-service-admin", + "users": [], # List of users having admin right on tljh-repo2docker + "scopes": [TLJH_R2D_ADMIN_SCOPE], + }, + { + "name": "user", + "scopes": [ + "self", + # access to the env page + "access:services!service=tljhrepo2docker", + ], + }, +] diff --git a/example/tljh_r2d/jupyterhub_config_local.py b/example/tljh_r2d/jupyterhub_config_local.py new file mode 100644 index 0000000..90de665 --- /dev/null +++ b/example/tljh_r2d/jupyterhub_config_local.py @@ -0,0 +1,66 @@ +""" +This file is only used for local development +and overrides some of the default values from the plugin. +""" + +from pathlib import Path +from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE +import sys + + +HERE = Path(__file__).parent + +c.JupyterHub.allow_named_servers = True +c.JupyterHub.services.extend( + [ + { + "name": "tljhrepo2docker", + "url": "http://r2d-svc:6789", + "command": [ + sys.executable, + "-m", + "tljh_repo2docker", + "--ip", + "0.0.0.0", + "--port", + "6789", + ], + "oauth_no_confirm": True, + "oauth_client_allowed_scopes": [ + TLJH_R2D_ADMIN_SCOPE, + ], + }, + ] +) + +c.JupyterHub.custom_scopes = { + TLJH_R2D_ADMIN_SCOPE: { + "description": "Admin access to myservice", + }, +} + +c.JupyterHub.load_roles = [ + { + "description": "Role for tljh_repo2docker service", + "name": "tljh-repo2docker-service", + "scopes": [ + "read:users", + "read:roles:users", + "admin:servers", + ], + "services": ["tljhrepo2docker"], + }, + { + "name": "tljh-repo2docker-service-admin", + "users": [], # List of users having admin right on tljh-repo2docker + "scopes": [TLJH_R2D_ADMIN_SCOPE], + }, + { + "name": "user", + "scopes": [ + "self", + # access to the env page + "access:services!service=tljhrepo2docker", + ], + }, +] diff --git a/example/tljh_r2d/templates/svc.yaml b/example/tljh_r2d/templates/svc.yaml new file mode 100644 index 0000000..654942b --- /dev/null +++ b/example/tljh_r2d/templates/svc.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: r2d-svc +spec: + clusterIP: None + selector: + app: jupyterhub + component: hub \ No newline at end of file diff --git a/example/tljh_r2d/values.yaml b/example/tljh_r2d/values.yaml new file mode 100644 index 0000000..5bafb56 --- /dev/null +++ b/example/tljh_r2d/values.yaml @@ -0,0 +1,23 @@ +jupyterhub: + enabled: true + hub: + image: + name: k8s-hub-binhderhub-tljh + tag: xxx + config: + BinderSpawner: + auth_enabled: true + extraFiles: + my_jupyterhub_config: + mountPath: /usr/local/etc/jupyterhub/jupyterhub_config.d/jupyterhub_config.py + +binderhub-service: + config: + BinderHub: + base_url: /services/binder + use_registry: true + image_prefix: "" + enable_api_only_mode: true + + buildPodsRegistryCredentials: + server: "https://index.docker.io/v1/"