168 lines
5.7 KiB
Python
168 lines
5.7 KiB
Python
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Portions derived from https://github.com/microsoft/autogen are under the MIT License.
|
|
# SPDX-License-Identifier: MIT
|
|
from __future__ import annotations
|
|
|
|
import atexit
|
|
import io
|
|
import logging
|
|
import secrets
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from types import TracebackType
|
|
from typing import Optional
|
|
|
|
import docker
|
|
|
|
from ...doc_utils import export_module
|
|
|
|
if sys.version_info >= (3, 11):
|
|
from typing import Self
|
|
else:
|
|
from typing_extensions import Self
|
|
|
|
from ..docker_commandline_code_executor import _wait_for_ready
|
|
from .base import JupyterConnectable, JupyterConnectionInfo
|
|
from .import_utils import require_jupyter_kernel_gateway_installed
|
|
from .jupyter_client import JupyterClient
|
|
|
|
|
|
@require_jupyter_kernel_gateway_installed()
|
|
@export_module("autogen.coding.jupyter")
|
|
class DockerJupyterServer(JupyterConnectable):
|
|
DEFAULT_DOCKERFILE = """FROM quay.io/jupyter/docker-stacks-foundation
|
|
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
|
|
USER ${NB_UID}
|
|
RUN mamba install --yes jupyter_kernel_gateway ipykernel && \
|
|
mamba clean --all -f -y && \
|
|
fix-permissions "${CONDA_DIR}" && \
|
|
fix-permissions "/home/${NB_USER}"
|
|
|
|
ENV TOKEN="UNSET"
|
|
CMD python -m jupyter kernelgateway --KernelGatewayApp.ip=0.0.0.0 \
|
|
--KernelGatewayApp.port=8888 \
|
|
--KernelGatewayApp.auth_token="${TOKEN}" \
|
|
--JupyterApp.answer_yes=true \
|
|
--JupyterWebsocketPersonality.list_kernels=true
|
|
|
|
EXPOSE 8888
|
|
|
|
WORKDIR "${HOME}"
|
|
"""
|
|
|
|
class GenerateToken:
|
|
pass
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
custom_image_name: Optional[str] = None,
|
|
container_name: Optional[str] = None,
|
|
auto_remove: bool = True,
|
|
stop_container: bool = True,
|
|
docker_env: dict[str, str] = {},
|
|
token: str | GenerateToken = GenerateToken(),
|
|
):
|
|
"""Start a Jupyter kernel gateway server in a Docker container.
|
|
|
|
Args:
|
|
custom_image_name (Optional[str], optional): Custom image to use. If this is None,
|
|
then the bundled image will be built and used. The default image is based on
|
|
quay.io/jupyter/docker-stacks-foundation and extended to include jupyter_kernel_gateway
|
|
container_name (Optional[str], optional): Name of the container to start.
|
|
A name will be generated if None.
|
|
auto_remove (bool, optional): If true the Docker container will be deleted
|
|
when it is stopped.
|
|
stop_container (bool, optional): If true the container will be stopped,
|
|
either by program exit or using the context manager
|
|
docker_env (Dict[str, str], optional): Extra environment variables to pass
|
|
to the running Docker container.
|
|
token (Union[str, GenerateToken], optional): Token to use for authentication.
|
|
If GenerateToken is used, a random token will be generated. Empty string
|
|
will be unauthenticated.
|
|
"""
|
|
if container_name is None:
|
|
container_name = f"autogen-jupyterkernelgateway-{uuid.uuid4()}"
|
|
|
|
client = docker.from_env()
|
|
if custom_image_name is None:
|
|
image_name = "autogen-jupyterkernelgateway"
|
|
# Make sure the image exists
|
|
try:
|
|
client.images.get(image_name)
|
|
except docker.errors.ImageNotFound:
|
|
# Build the image
|
|
# Get this script directory
|
|
here = Path(__file__).parent
|
|
dockerfile = io.BytesIO(self.DEFAULT_DOCKERFILE.encode("utf-8"))
|
|
logging.info(f"Image {image_name} not found. Building it now.")
|
|
client.images.build(path=here, fileobj=dockerfile, tag=image_name)
|
|
logging.info(f"Image {image_name} built successfully.")
|
|
else:
|
|
image_name = custom_image_name
|
|
# Check if the image exists
|
|
try:
|
|
client.images.get(image_name)
|
|
except docker.errors.ImageNotFound:
|
|
raise ValueError(f"Custom image {image_name} does not exist")
|
|
|
|
if isinstance(token, DockerJupyterServer.GenerateToken):
|
|
self._token = secrets.token_hex(32)
|
|
else:
|
|
self._token = token
|
|
|
|
# Run the container
|
|
env = {"TOKEN": self._token}
|
|
env.update(docker_env)
|
|
container = client.containers.run(
|
|
image_name,
|
|
detach=True,
|
|
auto_remove=auto_remove,
|
|
environment=env,
|
|
publish_all_ports=True,
|
|
name=container_name,
|
|
)
|
|
_wait_for_ready(container)
|
|
container_ports = container.ports
|
|
self._port = int(container_ports["8888/tcp"][0]["HostPort"])
|
|
self._container_id = container.id
|
|
|
|
def cleanup() -> None:
|
|
try:
|
|
inner_container = client.containers.get(container.id)
|
|
inner_container.stop()
|
|
except docker.errors.NotFound:
|
|
pass
|
|
|
|
atexit.unregister(cleanup)
|
|
|
|
if stop_container:
|
|
atexit.register(cleanup)
|
|
|
|
self._cleanup_func = cleanup
|
|
self._stop_container = stop_container
|
|
|
|
@property
|
|
def connection_info(self) -> JupyterConnectionInfo:
|
|
return JupyterConnectionInfo(host="127.0.0.1", use_https=False, port=self._port, token=self._token)
|
|
|
|
def stop(self) -> None:
|
|
self._cleanup_func()
|
|
|
|
def get_client(self) -> JupyterClient:
|
|
return JupyterClient(self.connection_info)
|
|
|
|
def __enter__(self) -> Self:
|
|
return self
|
|
|
|
def __exit__(
|
|
self, exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
|
|
) -> None:
|
|
self.stop()
|