-
Notifications
You must be signed in to change notification settings - Fork 1
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
DM-37758: Replace Cachemachine with JupyterLab Controller #199
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
"""Client for the JupyterLab Controller service.""" | ||
|
||
from __future__ import annotations | ||
|
||
from aiohttp import ClientSession | ||
|
||
from .config import config | ||
from .exceptions import ControllerError | ||
from .models.jupyter import ControllerImage, ControllerImages | ||
|
||
|
||
class ControllerClient: | ||
"""Query the JupyterLab Controller service for image information. | ||
|
||
The JupyterLab Controller is canonical for the available images and | ||
details such as which image is recommended and what the latest weeklies | ||
are. This client queries it and returns the image that matches some | ||
selection criteria. | ||
|
||
This will be modified into a form suitable for POSTing to the Controller | ||
to create the requested lab. | ||
""" | ||
|
||
def __init__( | ||
self, session: ClientSession, token: str, username: str | ||
) -> None: | ||
self._session = session | ||
self._token = token | ||
self._username = username | ||
self._url = config.environment_url + "/nublado/spawner/v1/images" | ||
|
||
async def get_latest_weekly(self) -> ControllerImage: | ||
"""Image for the latest weekly version. | ||
|
||
Returns | ||
------- | ||
image : `mobu.models.jupyter.ControllerImage` | ||
The corresponding image. | ||
|
||
Raises | ||
------ | ||
mobu.exceptions.ControllerError | ||
Some error occurred talking to JupyterLab Controller or it does | ||
not have a latest weekly image. | ||
""" | ||
images = await self._get_images() | ||
if images.latest_weekly is None: | ||
raise ControllerError( | ||
self._username, "No latest weekly image found" | ||
) | ||
return images.latest_weekly | ||
|
||
async def get_recommended(self) -> ControllerImage: | ||
"""Path suitable for image pulling for the latest recommended version. | ||
|
||
Returns | ||
------- | ||
path : `mobu.models.jupyter.ControllerImage` | ||
The corresponding image. | ||
|
||
Raises | ||
------ | ||
mobu.exceptions.ControllerError | ||
Some error occurred talking to JupyterLab Controller or it does | ||
not have a recommended image. | ||
""" | ||
images = await self._get_images() | ||
if images.recommended is None: | ||
raise ControllerError(self._username, "No recommended image found") | ||
return images.recommended | ||
|
||
async def _get_images(self) -> ControllerImages: | ||
headers = {"Authorization": f"bearer {self._token}"} | ||
async with self._session.get(self._url, headers=headers) as r: | ||
if r.status != 200: | ||
msg = f"Cannot get image status: {r.status} {r.reason}" | ||
raise ControllerError(self._username, msg) | ||
try: | ||
data = await r.json() | ||
return ControllerImages.parse_obj(data) | ||
except Exception as e: | ||
msg = f"Invalid response: {type(e).__name__}: {str(e)}" | ||
raise ControllerError(self._username, msg) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
Awaitable, | ||
Callable, | ||
Dict, | ||
List, | ||
Optional, | ||
TypeVar, | ||
cast, | ||
|
@@ -38,15 +39,15 @@ | |
from aiohttp.client import _RequestContextManager, _WSRequestContextManager | ||
from structlog import BoundLogger | ||
|
||
from .cachemachine import CachemachineClient | ||
from .config import config | ||
from .controller import ControllerClient | ||
from .exceptions import ( | ||
CodeExecutionError, | ||
JupyterError, | ||
JupyterResponseError, | ||
JupyterWebSocketError, | ||
) | ||
from .models.jupyter import JupyterConfig, JupyterImage, JupyterImageClass | ||
from .models.jupyter import ControllerImage, JupyterConfig, JupyterImageClass | ||
from .models.user import AuthenticatedUser | ||
|
||
__all__ = ["JupyterClient", "JupyterLabSession"] | ||
|
@@ -242,10 +243,10 @@ def __init__( | |
session.cookie_jar.update_cookies(BaseCookie({"_xsrf": xsrftoken})) | ||
self.session = JupyterClientSession(session, user.token) | ||
|
||
# We also send the XSRF token to cachemachine because of how we're | ||
# sharing the session, but that shouldn't matter. | ||
# We also send the XSRF token to the JupyterLabController because | ||
# of how we're sharing the session, but that shouldn't matter. | ||
assert config.gafaelfawr_token | ||
self.cachemachine = CachemachineClient( | ||
self.controller = ControllerClient( | ||
session, config.gafaelfawr_token, self.user.username | ||
) | ||
|
||
|
@@ -295,17 +296,21 @@ async def is_lab_stopped(self, final: bool = False) -> bool: | |
return result | ||
|
||
@_convert_exception | ||
async def spawn_lab(self) -> JupyterImage: | ||
async def spawn_lab(self) -> ControllerImage: | ||
spawn_url = self.jupyter_url + "hub/spawn" | ||
|
||
# Determine what image to spawn. | ||
if self.config.image_class == JupyterImageClass.RECOMMENDED: | ||
image = await self.cachemachine.get_recommended() | ||
image = await self.controller.get_recommended() | ||
elif self.config.image_class == JupyterImageClass.LATEST_WEEKLY: | ||
image = await self.cachemachine.get_latest_weekly() | ||
image = await self.controller.get_latest_weekly() | ||
else: | ||
assert self.config.image_reference | ||
image = JupyterImage.from_reference(self.config.image_reference) | ||
ref = self.config.image_reference | ||
image = ControllerImage( | ||
path=ref, name=ref, digest="unknown", tags={} | ||
) | ||
|
||
msg = f"Spawning lab image {image.name} for {self.user.username}" | ||
self.log.info(msg) | ||
|
||
|
@@ -501,10 +506,12 @@ def _remove_ansi_escapes(string: str) -> str: | |
""" | ||
return _ANSI_REGEX.sub("", string) | ||
|
||
def _build_jupyter_spawn_form(self, image: JupyterImage) -> Dict[str, str]: | ||
def _build_jupyter_spawn_form( | ||
self, image: ControllerImage | ||
) -> Dict[str, List[str]]: | ||
"""Construct the form to submit to the JupyterHub login page.""" | ||
return { | ||
"image_list": str(image), | ||
"image_dropdown": "use_image_from_dropdown", | ||
"size": self.config.image_size, | ||
"image_list": [image.path], | ||
"image_dropdown": [image.path], # Not used | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this have to be sent? It would be nice if it could be omitted since it's not used. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I can modify the models on the receiving end to make those optional. At least one of list or dropdown needs to be sent, but I can certainly send dropdown as null. |
||
"size": [self.config.image_size], | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
digest
andtags
should be optional in the model so that you can just omit them here.