Skip to content

Commit

Permalink
Switch to JupyterHub service (#75)
Browse files Browse the repository at this point in the history
* Switch to service

* Update UI tests

* Update readme
  • Loading branch information
trungleduc committed Mar 26, 2024
1 parent e5ed825 commit 466f6fb
Show file tree
Hide file tree
Showing 52 changed files with 992 additions and 265 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ ui-tests/test-results
lib/

# Hatch version
_version.py
_version.py
156 changes: 138 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![Github Actions Status](https://github.com/plasmabio/tljh-repo2docker/workflows/Tests/badge.svg)

TLJH plugin to build and use Docker images as user environments. The Docker images are built using [`repo2docker`](https://repo2docker.readthedocs.io/en/latest/).
TLJH plugin providing a JupyterHub service to build and use Docker images as user environments. The Docker images are built using [`repo2docker`](https://repo2docker.readthedocs.io/en/latest/).

## Requirements

Expand All @@ -29,64 +29,184 @@ curl https://tljh.jupyter.org/bootstrap.py
| sudo python3 - \
--version 1.0.0 \
--admin test:test \
--plugin git+https://github.com/plasmabio/tljh-repo2docker@master
--plugin tljh-repo2docker
```

Refer to [The Littlest JupyterHub documentation](http://tljh.jupyter.org/en/latest/topic/customizing-installer.html?highlight=plugins#installing-tljh-plugins)
for more info on installing TLJH plugins.

## Configuration

This Python package is designed for deployment as [a service managed by JupyterHub](https://jupyterhub.readthedocs.io/en/stable/reference/services.html#launching-a-hub-managed-service). The service runs its own Tornado server. Requests will be forwarded to it by the JupyterHub internal proxy from the standard URL `https://{my-hub-url}/services/my-service/`.

The available settings for this service are:

- `port`: Port of the service; defaults to 6789
- `ip`: Internal IP of the service; defaults to 127.0.0.1
- `default_memory_limit`: Default memory limit of a user server; defaults to `None`
- `default_cpu_limit`: Default CPU limit of a user server; defaults to `None`
- `machine_profiles`: Instead of entering directly the CPU and Memory value, `tljh-repo2docker` can be configured with pre-defined machine profiles and users can only choose from the available option; defaults to `[]`

Here is an example of registering `tljh_repo2docker`'s service with JupyterHub

```python
# jupyterhub_config.py

from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE

c.JupyterHub.services.extend(
[
{
"name": "tljh_repo2docker",
"url": "http://127.0.0.1:6789", # URL must match the `ip` and `port` config
"command": [
sys.executable,
"-m",
"tljh_repo2docker",
"--ip",
"127.0.0.1",
"--port",
"6789"
],
"oauth_no_confirm": True,
}
]
)
# Set required scopes for the service and users
c.JupyterHub.load_roles = [
{
"description": "Role for tljh_repo2docker service",
"name": "tljh-repo2docker-service",
"scopes": ["read:users", "read:servers", "read:roles:users"],
"services": ["tljh_repo2docker"],
},
{
"name": "user",
"scopes": [
"self",
# access to the serve page
"access:services!service=tljh_repo2docker",
],
},
]
```

By default, only users with an admin role can access the environment builder page and APIs, by leveraging the RBAC system of JupyterHub, non-admin users can also be granted the access right.

Here is an example of the configuration

```python
# jupyterhub_config.py

from tljh_repo2docker import TLJH_R2D_ADMIN_SCOPE

c.JupyterHub.services.extend(
[
{
"name": "tljh_repo2docker",
"url": "http://127.0.0.1:6789",
"command": [
sys.executable,
"-m",
"tljh_repo2docker",
"--ip",
"127.0.0.1",
"--port",
"6789"
],
"oauth_no_confirm": True,
"oauth_client_allowed_scopes": [
TLJH_R2D_ADMIN_SCOPE, # Allows this service to check if users have its admin scope.
],
}
]
)

c.JupyterHub.custom_scopes = {
TLJH_R2D_ADMIN_SCOPE: {
"description": "Admin access to tljh_repo2docker",
},
}

c.JupyterHub.load_roles = [
... # Other role settings
{
"name": 'tljh-repo2docker-service-admin',
"users": ["alice"],
"scopes": [TLJH_R2D_ADMIN_SCOPE],
},
]

```

## Usage

### List the environments

The _Environments_ page shows the list of built environments, as well as the ones currently being built:

![environments](https://user-images.githubusercontent.com/591645/80962805-056df500-8e0e-11ea-81ab-6efc1c97432d.png)
![environments](https://raw.githubusercontent.com/plasmabio/tljh-repo2docker/master/ui-tests/tests/ui.test.ts-snapshots/environment-list-linux.png)

### Add a new environment

Just like on [Binder](https://mybinder.org), new environments can be added by clicking on the _Add New_ button and providing a URL to the repository. Optional names, memory, and CPU limits can also be set for the environment:

![add-new](https://user-images.githubusercontent.com/591645/80963115-9fce3880-8e0e-11ea-890b-c9b928f7edb1.png)
![add-new](https://raw.githubusercontent.com/plasmabio/tljh-repo2docker/master/ui-tests/tests/ui.test.ts-snapshots/environment-dialog-linux.png)

### Follow the build logs

Clicking on the _Logs_ button will open a new dialog with the build logs:

![logs](https://user-images.githubusercontent.com/591645/82306574-86f18580-99bf-11ea-984b-4749ddde15e7.png)
![logs](https://raw.githubusercontent.com/plasmabio/tljh-repo2docker/master/ui-tests/tests/ui.test.ts-snapshots/environment-console-linux.png)

### Select an environment

Once ready, the environments can be selected from the JupyterHub spawn page:

![select-env](https://user-images.githubusercontent.com/591645/81152248-10e22d00-8f82-11ea-9b5f-5831d8f7d085.png)
![select-env](https://raw.githubusercontent.com/plasmabio/tljh-repo2docker/master/ui-tests/tests/ui.test.ts-snapshots/servers-dialog-linux.png)

### Private Repositories

`tljh-repo2docker` also supports building environments from private repositories.

It is possible to provide the `username` and `password` in the `Credentials` section of the form:

![image](https://user-images.githubusercontent.com/591645/107362654-51567480-6ad9-11eb-93be-74d3b1c37828.png)
![image](https://raw.githubusercontent.com/plasmabio/tljh-repo2docker/master/ui-tests/tests/ui.test.ts-snapshots/environment-dialog-linux.png)

On GitHub and GitLab, a user might have to first create an access token with `read` access to use as the password:

![image](https://user-images.githubusercontent.com/591645/107350843-39c3bf80-6aca-11eb-8b82-6fa95ba4c7e4.png)

### Set CPU and Memory via machine profiles
### Machine profiles

Instead of entering directly the CPU and Memory value, `tljh-repo2docker` can be configured with pre-defined machine profiles and users can only choose from the available options. The following snippet will add 3 machines with labels `Small`, `Medium` and `Large` to the profile list:
Instead of entering directly the CPU and Memory value, `tljh-repo2docker` can be configured with pre-defined machine profiles and users can only choose from the available options. The following configuration will add 3 machines with labels Small, Medium and Large to the profile list:

```python
from tljh.configurer import apply_config, load_config

tljh_config = load_config()
tljh_config["limits"]["machine_profiles"] = [
{"label": "Small", "cpu": 2, "memory": 2},
{"label": "Medium", "cpu": 4, "memory": 4},
{"label": "Large", "cpu": 8, "memory": 8},
]
apply_config(tljh_config, c)
c.JupyterHub.services.extend(
[
{
"name": "tljh_repo2docker",
"url": "http://127.0.0.1:6789",
"command": [
sys.executable,
"-m",
"tljh_repo2docker",
"--ip",
"127.0.0.1",
"--port",
"6789",
"--machine_profiles",
'{"label": "Small", "cpu": 2, "memory": 2}',
"--machine_profiles",
'{"label": "Medium", "cpu": 4, "memory": 4}',
"--machine_profiles",
'{"label": "Large", "cpu": 8, "memory": 8}'

],
"oauth_no_confirm": True,
}
]
)
```

![image](https://github.com/plasmabio/tljh-repo2docker/assets/4451292/c1f0231e-a02d-41dc-85e0-97a97ffa0311)
Expand Down
77 changes: 59 additions & 18 deletions jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,75 @@
and overrides some of the default values from the plugin.
"""

import getpass

from jupyterhub.auth import DummyAuthenticator
from tljh.configurer import apply_config, load_config
from tljh_repo2docker import tljh_custom_jupyterhub_config

c.JupyterHub.services = []
from tljh_repo2docker import tljh_custom_jupyterhub_config, TLJH_R2D_ADMIN_SCOPE
import sys

tljh_config = load_config()

# set default limits in the TLJH config in memory
# tljh_config["limits"]["memory"] = "2G"
# tljh_config["limits"]["cpu"] = 2

# set CPU and memory based on machine profiles
tljh_config["limits"]["machine_profiles"] = [
{"label": "Small", "cpu": 2, "memory": 2},
{"label": "Medium", "cpu": 4, "memory": 4},
{"label": "Large", "cpu": 8, "memory": 8},
]

apply_config(tljh_config, c)

tljh_custom_jupyterhub_config(c)

c.JupyterHub.authenticator_class = DummyAuthenticator

user = getpass.getuser()
c.Authenticator.admin_users = {user, "alice"}
c.JupyterHub.allow_named_servers = True
c.JupyterHub.ip = "0.0.0.0"

c.JupyterHub.services.extend(
[
{
"name": "tljh_repo2docker",
"url": "http://127.0.0.1:6789",
"command": [
sys.executable,
"-m",
"tljh_repo2docker",
"--ip",
"127.0.0.1",
"--port",
"6789",
"--machine_profiles",
'{"label": "Small", "cpu": 2, "memory": 2}',
"--machine_profiles",
'{"label": "Medium", "cpu": 4, "memory": 4}',
"--machine_profiles",
'{"label": "Large", "cpu": 8, "memory": 8}'

],
"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:servers", "read:roles:users"],
"services": ["tljh_repo2docker"],
},
{
"name": 'tljh-repo2docker-service-admin',
"users": ["alice"],
"scopes": [TLJH_R2D_ADMIN_SCOPE],
},
{
"name": "user",
"scopes": [
"self",
# access to the env page
"access:services!service=tljh_repo2docker",
],
},
]
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dependencies = [
"aiodocker~=0.19",
"dockerspawner~=12.1",
"jupyter_client>=6.1,<8",
"httpx"
]
dynamic = ["version"]
license = {file = "LICENSE"}
Expand Down
5 changes: 4 additions & 1 deletion src/common/AxiosContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createContext, useContext } from 'react';
import { AxiosClient } from './axiosclient';

export const AxiosContext = createContext<AxiosClient>(new AxiosClient({}));
export const AxiosContext = createContext<{
hubClient: AxiosClient;
serviceClient: AxiosClient;
}>({ hubClient: new AxiosClient({}), serviceClient: new AxiosClient({}) });

export const useAxios = () => {
return useContext(AxiosContext);
Expand Down
6 changes: 4 additions & 2 deletions src/common/JupyterhubContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { createContext, useContext } from 'react';

export interface IJupyterhubData {
baseUrl: string;
prefix: string;
servicePrefix: string;
hubPrefix: string;
user: string;
adminAccess: boolean;
xsrfToken: string;
}
export const JupyterhubContext = createContext<IJupyterhubData>({
baseUrl: '',
prefix: '',
servicePrefix: '',
hubPrefix: '',
user: '',
adminAccess: false,
xsrfToken: ''
Expand Down
14 changes: 11 additions & 3 deletions src/environments/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@ export interface IAppProps {
}
export default function App(props: IAppProps) {
const jhData = useJupyterhub();
const axios = useMemo(() => {
const baseUrl = jhData.baseUrl;

const hubClient = useMemo(() => {
const baseUrl = jhData.hubPrefix;
const xsrfToken = jhData.xsrfToken;
return new AxiosClient({ baseUrl, xsrfToken });
}, [jhData]);

const serviceClient = useMemo(() => {
const baseUrl = jhData.servicePrefix;
const xsrfToken = jhData.xsrfToken;
return new AxiosClient({ baseUrl, xsrfToken });
}, [jhData]);

return (
<ThemeProvider theme={customTheme}>
<AxiosContext.Provider value={axios}>
<AxiosContext.Provider value={{ hubClient, serviceClient }}>
<ScopedCssBaseline>
<Stack sx={{ padding: 1 }} spacing={1}>
<NewEnvironmentDialog
Expand Down
4 changes: 2 additions & 2 deletions src/environments/LogDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ function _EnvironmentLogButton(props: IEnvironmentLogButton) {

terminal.open(divRef.current);
fitAddon.fit();
const { baseUrl, xsrfToken } = jhData;
const { servicePrefix, xsrfToken } = jhData;

let logsUrl = urlJoin(
baseUrl,
servicePrefix,
'api',
'environments',
props.image,
Expand Down
2 changes: 1 addition & 1 deletion src/environments/NewEnvironmentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ function _NewEnvironmentDialog(props: INewEnvironmentDialogProps) {
data.memory = data.memory ?? '2';
data.username = data.username ?? '';
data.password = data.password ?? '';
const response = await axios.request({
const response = await axios.serviceClient.request({
method: 'post',
prefix: API_PREFIX,
path: ENV_PREFIX,
Expand Down
Loading

0 comments on commit 466f6fb

Please sign in to comment.