Skip to content

Commit 5e8e161

Browse files
feat: support kubernetes kernel cr
1 parent de3ae6a commit 5e8e161

File tree

3 files changed

+211
-1
lines changed

3 files changed

+211
-1
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ classifiers = [
2626
]
2727
dependencies = [
2828
"aiohttp[speedups]>=3.9.3",
29+
"jkclient>=0.0.5",
30+
"jupyter-client>=8.6.0",
2931
"python-dotenv>=1",
3032
"pydantic>=2",
3133
"requests>=2",
@@ -37,7 +39,6 @@ local = [
3739
# kernel dependencies
3840
"ipython>=8.18.1",
3941
"ipykernel>=6.26.0",
40-
"jupyter-client>=8.6.0",
4142
]
4243

4344
[project.urls]

src/pybox/kube.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from typing import Any
5+
6+
from dotenv import dotenv_values
7+
from jkclient import CreateKernelRequest, JupyterKernelClient, Kernel
8+
from jupyter_client import AsyncKernelClient, BlockingKernelClient
9+
10+
from pybox import LocalPyBox
11+
from pybox.base import BasePyBoxManager
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class KubePyBoxManager(BasePyBoxManager):
17+
"""Kubernetes kernel pybox, used to create a custom kernel and connect to it to execute code"""
18+
19+
def __init__(
20+
self,
21+
incluster: bool,
22+
env_file: str | None = None,
23+
kernel_env: dict[str, Any] | None = None,
24+
):
25+
self.env_file = env_file
26+
self.kernel_env = dotenv_values(env_file)
27+
if kernel_env:
28+
self.kernel_env.update(kernel_env)
29+
30+
self.client = JupyterKernelClient(incluster=incluster)
31+
32+
def start(self, kernel_id: str, cwd: str, **kwargs) -> LocalPyBox:
33+
"""Retrieve an existing kernel or create a new one in kubernetes
34+
35+
Args:
36+
kernel_id (str): kernel_id
37+
cwd (str): kernel_working_dir
38+
userid (str): kernel user
39+
40+
Returns:
41+
LocalPyBox: kubernetes kernel box
42+
"""
43+
env = self.kernel_env.copy()
44+
45+
if kernel_id:
46+
env["KERNEL_ID"] = kernel_id
47+
if cwd:
48+
env["KERNEL_WORKING_DIR"] = cwd
49+
if username := kwargs.pop("username", None):
50+
env["KERNEL_USERNAME"] = username
51+
52+
# Create kernel custom resource
53+
kernel_request = CreateKernelRequest(env=env)
54+
kernel: Kernel = self.client.create(kernel_request, **kwargs)
55+
56+
# New kernel clinet
57+
kernel_client = BlockingKernelClient()
58+
kernel_client.load_connection_info(kernel.conn_info)
59+
60+
return LocalPyBox(kernel_id=kernel_id, client=kernel_client)
61+
62+
async def astart(self, kernel_id: str, cwd: str, **kwargs) -> LocalPyBox:
63+
"""Retrieve an existing kernel or create a new one in kubernetes
64+
65+
Args:
66+
kernel_id (str): kubernetes kernel id
67+
cwd (str): kernel workdir
68+
69+
Returns:
70+
LocalPyBox: An iPython kernel that executes code.
71+
"""
72+
env = self.kernel_env.copy()
73+
74+
if kernel_id:
75+
env["KERNEL_ID"] = kernel_id
76+
if cwd:
77+
env["KERNEL_WORKING_DIR"] = cwd
78+
if username := kwargs.pop("username", None):
79+
env["KERNEL_USERNAME"] = username
80+
81+
# Create kernel custom resource
82+
kernel_request = CreateKernelRequest(env=env)
83+
kernel: Kernel = await self.client.acreate(kernel_request, **kwargs)
84+
85+
# New kernel clinet
86+
kernel_client = AsyncKernelClient()
87+
kernel_client.load_connection_info(kernel.conn_info)
88+
89+
return LocalPyBox(kernel_id=kernel_id, client=kernel_client)
90+
91+
def shutdown(self, kernel_id: str, **kwargs) -> None:
92+
"""Shutdown the kernel in kubernetes.
93+
94+
Args:
95+
kernel_id (str): kernel_id
96+
"""
97+
self.client.delete_by_kernel_id(kernel_id, **kwargs)
98+
99+
async def ashutdown(self, kernel_id: str, **kwargs) -> None:
100+
"""Shutdown the kubernetes kernel by kernel id.
101+
102+
Args:
103+
kernel_id (str): kubernetes kernel id
104+
"""
105+
return await self.client.adelete_by_kernel_id(kernel_id, **kwargs)

tests/test_kube.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from collections.abc import Iterator
2+
from uuid import uuid4
3+
4+
import pytest
5+
from pybox.kube import KubePyBoxManager
6+
7+
8+
@pytest.fixture(scope="module")
9+
def kube_manager() -> Iterator[KubePyBoxManager]:
10+
_mng = KubePyBoxManager(
11+
incluster=False,
12+
kernel_env={
13+
"KERNEL_USERNAME": "tablegpt",
14+
"KERNEL_NAMESPACE": "default",
15+
"KERNEL_IMAGE": "zjuici/tablegpt-kernel:0.1.1",
16+
"KERNEL_WORKING_DIR": "/mnt/data",
17+
"KERNEL_VOLUME_MOUNTS": [
18+
{"name": "shared-vol", "mountPath": "/mnt/data"},
19+
{"name": "ipython-profile-vol", "mountPath": "/opt/startup"},
20+
{
21+
"name": "kernel-launch-vol",
22+
"mountPath": "/usr/local/bin/bootstrap-kernel.sh",
23+
"subPath": "bootstrap-kernel.sh",
24+
},
25+
{
26+
"name": "kernel-launch-vol",
27+
"mountPath": "/usr/local/bin/kernel-launchers/python/scripts/launch_ipykernel.py",
28+
"subPath": "launch_ipykernel.py",
29+
},
30+
],
31+
"KERNEL_VOLUMES": [
32+
{
33+
"name": "shared-vol",
34+
"nfs": {
35+
"server": "10.0.0.29",
36+
"path": "/data/tablegpt-slim-py/data",
37+
},
38+
},
39+
{
40+
"name": "ipython-profile-vol",
41+
"configMap": {"name": "ipython-startup-scripts"},
42+
},
43+
{
44+
"name": "kernel-launch-vol",
45+
"configMap": {
46+
"defaultMode": 0o755,
47+
"name": "kernel-launch-scripts",
48+
},
49+
},
50+
],
51+
"KERNEL_STARTUP_SCRIPTS_PATH": "/opt/startup",
52+
"KERNEL_IDLE_TIMEOUT": "1800",
53+
},
54+
)
55+
yield _mng
56+
57+
58+
@pytest.mark.skip(reason="Start kernel cr need kubernetes environment")
59+
def test_start_with_user(kube_manager: KubePyBoxManager) -> None:
60+
61+
kernel_id = str(uuid4())
62+
box = kube_manager.start(
63+
kernel_id=kernel_id,
64+
cwd="/mnt/data",
65+
username="dev",
66+
)
67+
assert box.kernel_id == kernel_id
68+
69+
70+
@pytest.mark.skip(reason="Start kernel cr need kubernetes environment")
71+
def test_start_without_user(kube_manager: KubePyBoxManager) -> None:
72+
73+
kernel_id = str(uuid4())
74+
box = kube_manager.start(
75+
kernel_id=kernel_id,
76+
cwd="/mnt/data",
77+
)
78+
assert box.kernel_id == kernel_id
79+
80+
81+
@pytest.mark.skip(reason="Start kernel cr need kubernetes environment")
82+
@pytest.mark.asyncio
83+
async def test_start_async(kube_manager: KubePyBoxManager) -> None:
84+
kernel_id = str(uuid4())
85+
box = await kube_manager.astart(
86+
kernel_id=kernel_id,
87+
cwd="/mnt/data",
88+
)
89+
assert box.kernel_id == kernel_id
90+
91+
92+
@pytest.mark.skip(reason="Shutting down kernel cr need kubernetes environment")
93+
def test_shutdown_w_id(kube_manager: KubePyBoxManager) -> None:
94+
kube_manager.shutdown(kernel_id="1918a836-e941-4332-9e6f-dbfe91e5771a")
95+
96+
97+
@pytest.mark.skip(reason="Shutting down kernel cr need kubernetes environment")
98+
@pytest.mark.asyncio
99+
async def test_shutdown_async(kube_manager: KubePyBoxManager) -> None:
100+
await kube_manager.ashutdown(kernel_id="1918a836-e941-4332-9e6f-dbfe91e5771a")
101+
102+
103+
if __name__ == "__main__":
104+
pytest.main()

0 commit comments

Comments
 (0)