Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to JupyterHub service #75

Merged
merged 3 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading