Skip to content

Commit

Permalink
feat(core): use existing remote image when starting sessions (#2991)
Browse files Browse the repository at this point in the history
  • Loading branch information
m-alisafaee committed Jul 5, 2022
1 parent 6c697a6 commit b09805c
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 87 deletions.
8 changes: 6 additions & 2 deletions design/003-interactive-session/003-interactive-session.md
Expand Up @@ -133,6 +133,10 @@ The implementation of this interface in detail:
```python
class ISessionProvider:

def get_name(self) -> str:
"""Return session provider's name."""
pass

def build_image(self, image_descriptor: Path, image_name: str, config: Optional[Dict[str, Any]]) -> Optional[str]:
"""Builds the container image.
:param image_descriptor: Path to the container image descriptor file.
Expand All @@ -150,9 +154,9 @@ class ISessionProvider:
"""
pass

def session_provider(self) -> Tuple["ISessionProvider", str]:
def session_provider(self) -> "ISessionProvider":
"""Supported session provider.
:returns: a tuple of ``self`` and engine type name.
:returns: a reference to ``self``.
"""
pass

Expand Down
11 changes: 11 additions & 0 deletions renku/core/errors.py
Expand Up @@ -632,6 +632,17 @@ class RenkulabSessionError(RenkuException):
"""Raised when an error occurs trying to start sessions with the notebook service."""


class RenkulabSessionGetUrlError(RenkuException):
"""Raised when Renku deployment's URL cannot be gotten from project's remotes or configured remotes."""

def __init__(self):
message = (
"Cannot determine the Renku deployment's URL. Ensure your current project is a valid Renku project and has "
"a remote URL."
)
super().__init__(message)


class NotebookSessionNotReadyError(RenkuException):
"""Raised when a user attempts to open a session that is not ready."""

Expand Down
2 changes: 1 addition & 1 deletion renku/core/plugin/session.py
Expand Up @@ -35,7 +35,7 @@ def session_provider() -> Tuple[ISessionProvider, str]:
pass


def supported_session_providers() -> List[Tuple[ISessionProvider, str]]:
def get_supported_session_providers() -> List[ISessionProvider]:
"""Returns the currently available interactive session providers."""
from renku.core.plugin.pluginmanager import get_plugin_manager

Expand Down
29 changes: 18 additions & 11 deletions renku/core/session/docker.py
Expand Up @@ -18,14 +18,15 @@
"""Docker based interactive session provider."""

from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple, cast
from typing import Any, Dict, Iterable, List, Optional, cast
from uuid import uuid4

import docker

from renku.core import errors
from renku.core.management.client import LocalClient
from renku.core.plugin import hookimpl
from renku.core.util import communication
from renku.domain_model.session import ISessionProvider, Session


Expand Down Expand Up @@ -65,31 +66,37 @@ def _get_jupyter_urls(ports: Dict[str, Any], auth_token: str, jupyter_port: int
def _get_docker_containers(self, project_name: str) -> List[docker.models.containers.Container]:
return self.docker_client().containers.list(filters={"label": f"renku_project={project_name}"})

def get_name(self) -> str:
"""Return session provider's name."""
return "docker"

def build_image(self, image_descriptor: Path, image_name: str, config: Optional[Dict[str, Any]]):
"""Builds the container image."""
self.docker_client().images.build(path=str(image_descriptor), tag=image_name)

def find_image(self, image_name: str, config: Optional[Dict[str, Any]]) -> bool:
"""Find the given container image."""
try:
_ = self.docker_client().images.get(image_name)
return True
self.docker_client().images.get(image_name)
except docker.errors.ImageNotFound:
try:
_ = self.docker_client().images.pull(image_name)
with communication.busy(msg=f"Pulling image from remote {image_name}"):
self.docker_client().images.pull(image_name)
except docker.errors.NotFound:
return False
else:
return True
except docker.errors.ImageNotFound:
pass
return False
else:
return True

@hookimpl
def session_provider(self) -> Tuple[ISessionProvider, str]:
def session_provider(self) -> ISessionProvider:
"""Supported session provider.
Returns:
a tuple of ``self`` and provider name.
a reference to ``self``.
"""
return (self, "docker")
return self

def session_list(self, project_name: str, config: Optional[Dict[str, Any]]) -> List[Session]:
"""Lists all the sessions currently running by the given session provider.
Expand Down Expand Up @@ -209,7 +216,7 @@ def session_stop(self, project_name: str, session_name: Optional[str], stop_all:
try:
docker_containers = (
self._get_docker_containers(project_name)
if all
if stop_all
else self.docker_client().containers.list(filters={"id": session_name})
)

Expand Down
21 changes: 11 additions & 10 deletions renku/core/session/renkulab.py
Expand Up @@ -100,10 +100,7 @@ def _renku_url(self) -> str:
if not self.__renku_url:
renku_url = get_renku_url()
if not renku_url:
raise errors.UsageError(
"Cannot determine the renku URL to launch a session. "
"Ensure your current project is a valid Renku project."
)
raise errors.RenkulabSessionGetUrlError()
self.__renku_url = renku_url
return self.__renku_url

Expand Down Expand Up @@ -219,6 +216,10 @@ def _send_renku_request(req_type: str, *args, **kwargs):
)
return res

def get_name(self) -> str:
"""Return session provider's name."""
return "renkulab"

def build_image(self, image_descriptor: Path, image_name: str, config: Optional[Dict[str, Any]]):
"""Builds the container image."""
if self.find_image(image_name, config=config):
Expand All @@ -244,13 +245,13 @@ def find_image(self, image_name: str, config: Optional[Dict[str, Any]]) -> bool:
)

@hookimpl
def session_provider(self) -> Tuple[ISessionProvider, str]:
def session_provider(self) -> ISessionProvider:
"""Supported session provider.
Returns:
a tuple of ``self`` and provider name.
a reference to ``self``.
"""
return (self, "renkulab")
return self

def session_list(self, project_name: str, config: Optional[Dict[str, Any]]) -> List[Session]:
"""Lists all the sessions currently running by the given session provider.
Expand Down Expand Up @@ -289,7 +290,7 @@ def session_start(
"""Creates an interactive session.
Returns:
str: a unique id for the created interactive sesssion.
str: a unique id for the created interactive session.
"""
session_commit = client.repository.head.commit.hexsha
if not self._is_user_registered():
Expand Down Expand Up @@ -356,7 +357,7 @@ def session_stop(self, project_name: str, session_name: Optional[str], stop_all:
)
)
self._wait_for_session_status(session_name, "stopping")
return all([response.status_code == 204 for response in responses])
return all([response.status_code == 204 for response in responses]) if responses else False

def session_url(self, session_name: str) -> str:
"""Get the URL of the interactive session."""
Expand All @@ -374,6 +375,6 @@ def session_url(self, session_name: str) -> str:
# NOTE: The sessions/show logic of the UI expects a cookie to already be present
# with the anonymous user ID, but in this case we need to open a new browser window
# and need to pass the token in the URL, that is why anonymous sessions will be shown
# and openeed in the full session view not in the i-frame view like registered sessions
# and opened in the full session view not in the i-frame view like registered sessions
session_url_parts = ["sessions", f"{session_name}?token={self._token()}"]
return urllib.parse.urljoin(self._renku_url(), "/".join(session_url_parts))
89 changes: 47 additions & 42 deletions renku/core/session/session.py
Expand Up @@ -19,40 +19,42 @@

import webbrowser
from itertools import chain
from typing import Optional
from typing import List, Optional

from renku.command.command_builder import inject
from renku.core import errors
from renku.core.interface.client_dispatcher import IClientDispatcher
from renku.core.plugin.session import supported_session_providers
from renku.core.plugin.session import get_supported_session_providers
from renku.core.session.utils import get_image_repository_host, get_renku_project_name
from renku.core.util import communication
from renku.core.util.os import safe_read_yaml
from renku.domain_model.session import ISessionProvider
from renku.domain_model.session import ISessionProvider, Session


def _safe_get_provider(provider: str) -> ISessionProvider:
providers = supported_session_providers()
p = next(filter(lambda x: x[1] == provider, providers), None)

if p is None:
try:
return next(p for p in get_supported_session_providers() if p.get_name() == provider)
except StopIteration:
raise errors.ParameterError(f"Session provider '{provider}' is not available!")
return p[0]


def session_list(config_path: str, provider: Optional[str] = None):
"""List interactive sessions."""

def list_sessions(session_provider: ISessionProvider) -> List[Session]:
try:
return session_provider.session_list(config=config, project_name=project_name)
except errors.RenkulabSessionGetUrlError:
if provider:
raise
return []

project_name = get_renku_project_name()
config = safe_read_yaml(config_path) if config_path else dict()

providers = supported_session_providers()
if provider:
providers = list(filter(lambda x: x[1] == provider, providers))

if len(providers) == 0:
raise errors.ParameterError("No session provider is available!")
providers = [_safe_get_provider(provider)] if provider else get_supported_session_providers()

return list(chain(*map(lambda x: x[0].session_list(config=config, project_name=project_name), providers)))
return list(chain(*map(list_sessions, providers)))


@inject.autoparams("client_dispatcher")
Expand Down Expand Up @@ -121,42 +123,45 @@ def session_start(

def session_stop(session_name: str, stop_all: bool = False, provider: Optional[str] = None):
"""Stop interactive session."""

def stop_sessions(session_provider: ISessionProvider) -> bool:
try:
return session_provider.session_stop(
project_name=project_name, session_name=session_name, stop_all=stop_all
)
except errors.RenkulabSessionGetUrlError:
if provider:
raise
return False

session_detail = "all sessions" if stop_all else f"session {session_name}"
project_name = get_renku_project_name()
if provider:
p = _safe_get_provider(provider)
with communication.busy(msg=f"Waiting for {session_detail} to stop..."):
is_stopped = p.session_stop(project_name=project_name, session_name=session_name, stop_all=stop_all)
else:
providers = supported_session_providers()
with communication.busy(msg=f"Waiting for {session_detail} to stop..."):
is_stopped = any(
map(
lambda x: x[0].session_stop(
project_name=project_name, session_name=session_name, stop_all=stop_all
),
providers,
)
)

providers = [_safe_get_provider(provider)] if provider else get_supported_session_providers()

with communication.busy(msg=f"Waiting for {session_detail} to stop..."):
is_stopped = any(map(stop_sessions, providers))

if not is_stopped:
if not session_name:
raise errors.ParameterError("There are no running sessions.")
raise errors.ParameterError(f"Could not find '{session_name}' among the running sessions.")


def session_open(session_name: str, provider: Optional[str] = None):
"""Open interactive session in the browser."""
if provider:
p = _safe_get_provider(provider)
url = p.session_url(session_name=session_name)
else:
providers = supported_session_providers()
url = next(
filter(
lambda x: x is not None,
map(lambda p: p[0].session_url(session_name=session_name), providers),
),
None,
)

def open_sessions(session_provider: ISessionProvider) -> Optional[str]:
try:
return session_provider.session_url(session_name=session_name)
except errors.RenkulabSessionGetUrlError:
if provider:
raise
return None

providers = [_safe_get_provider(provider)] if provider else get_supported_session_providers()

url = next(filter(lambda u: u is not None, map(open_sessions, providers)), None)

if url is None:
raise errors.ParameterError(f"Could not find '{session_name}' among the running sessions.")
Expand Down
11 changes: 8 additions & 3 deletions renku/domain_model/session.py
Expand Up @@ -21,7 +21,7 @@

from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional

from renku.core.management.client import LocalClient

Expand All @@ -38,6 +38,11 @@ def __init__(self, id: str, status: str, url: str):
class ISessionProvider(metaclass=ABCMeta):
"""Abstract class for a interactive session provider."""

@abstractmethod
def get_name(self) -> str:
"""Return session provider's name."""
pass

@abstractmethod
def build_image(self, image_descriptor: Path, image_name: str, config: Optional[Dict[str, Any]]) -> Optional[str]:
"""Builds the container image.
Expand Down Expand Up @@ -66,11 +71,11 @@ def find_image(self, image_name: str, config: Optional[Dict[str, Any]]) -> bool:
pass

@abstractmethod
def session_provider(self) -> Tuple["ISessionProvider", str]:
def session_provider(self) -> "ISessionProvider":
"""Supported session provider.
Returns:
a tuple of ``self`` and engine type name.
a reference to ``self``.
"""
pass

Expand Down

0 comments on commit b09805c

Please sign in to comment.