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

Merge host config/create_kwargs #501

Merged
merged 2 commits into from
Nov 14, 2023
Merged
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
80 changes: 54 additions & 26 deletions dockerspawner/dockerspawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
A Spawner for JupyterHub that runs each user's server in a separate docker container
"""
import asyncio
import inspect
import os
import string
import warnings
Expand Down Expand Up @@ -54,6 +55,24 @@ def validate(self, obj, value):
_jupyterhub_xy = "%i.%i" % (jupyterhub.version_info[:2])


def _deep_merge(dest, src):
"""Merge dict `src` into `dest`, recursively

Modifies `dest` in-place, returns dest
"""
for key, value in src.items():
if key in dest:
dest_value = dest[key]
if isinstance(dest_value, dict) and isinstance(value, dict):
dest[key] = _deep_merge(dest_value, value)
else:
dest[key] = value
else:
dest[key] = value

return dest


class DockerSpawner(Spawner):
"""A Spawner for JupyterHub that runs each user's server in a separate docker container"""

Expand Down Expand Up @@ -580,7 +599,8 @@ def will_resume(self):
# so JupyterHub >= 0.7.1 won't cleanup our API token
return not self.remove

extra_create_kwargs = Dict(
extra_create_kwargs = Union(
[Callable(), Dict()],
config=True,
help="""Additional args to pass for container create

Expand All @@ -590,11 +610,29 @@ def will_resume(self):
"user": "root" # Can also be an integer UID
}

The above is equivalent to ``docker run --user root``
The above is equivalent to ``docker run --user root``.

If a callable, will be called with the Spawner as the only argument,
must return the same dictionary structure, and may be async.

.. versionchanged:: 13

Added callable support.
""",
)
extra_host_config = Dict(
config=True, help="Additional args to create_host_config for container create"
extra_host_config = Union(
[Callable(), Dict()],
config=True,
help="""
Additional args to create_host_config for container create.

If a callable, will be called with the Spawner as the only argument,
must return the same dictionary structure, and may be async.

.. versionchanged:: 13

Added callable support.
""",
)

escape = Any(
Expand Down Expand Up @@ -1139,11 +1177,19 @@ async def create_object(self):
name=self.container_name,
command=(await self.get_command()),
)
extra_create_kwargs = self._eval_if_callable(self.extra_create_kwargs)
if inspect.isawaitable(extra_create_kwargs):
extra_create_kwargs = await extra_create_kwargs
extra_create_kwargs = self._render_templates(extra_create_kwargs)
extra_host_config = self._eval_if_callable(self.extra_host_config)
if inspect.isawaitable(extra_host_config):
extra_host_config = await extra_host_config
extra_host_config = self._render_templates(extra_host_config)

# ensure internal port is exposed
create_kwargs["ports"] = {"%i/tcp" % self.port: None}

create_kwargs.update(self._render_templates(self.extra_create_kwargs))
_deep_merge(create_kwargs, extra_create_kwargs)

# build the dictionary of keyword arguments for host_config
host_config = dict(
Expand All @@ -1160,14 +1206,14 @@ async def create_object(self):
# docker cpu units are in microseconds
# cpu_period default is 100ms
# cpu_quota is cpu_period * cpu_limit
cpu_period = host_config["cpu_period"] = self.extra_host_config.get(
cpu_period = host_config["cpu_period"] = extra_host_config.get(
"cpu_period", 100_000
)
host_config["cpu_quota"] = int(self.cpu_limit * cpu_period)

if not self.use_internal_ip:
host_config["port_bindings"] = {self.port: (self.host_ip,)}
host_config.update(self._render_templates(self.extra_host_config))
_deep_merge(host_config, extra_host_config)
host_config.setdefault("network_mode", self.network_name)

self.log.debug("Starting host with config: %s", host_config)
Expand Down Expand Up @@ -1243,31 +1289,13 @@ async def pull_image(self, image):
self.log.info("pulling image %s", image)
await self.docker('pull', repo, tag)

async def start(self, image=None, extra_create_kwargs=None, extra_host_config=None):
async def start(self):
"""Start the single-user server in a docker container.

Additional arguments to create/host config/etc. can be specified
via .extra_create_kwargs and .extra_host_config attributes.

If the container exists and ``c.DockerSpawner.remove`` is ``True``, then
the container is removed first. Otherwise, the existing containers
will be restarted.
"""

if image:
self.log.warning("Specifying image via .start args is deprecated")
self.image = image
if extra_create_kwargs:
self.log.warning(
"Specifying extra_create_kwargs via .start args is deprecated"
)
self.extra_create_kwargs.update(extra_create_kwargs)
if extra_host_config:
self.log.warning(
"Specifying extra_host_config via .start args is deprecated"
)
self.extra_host_config.update(extra_host_config)

# image priority:
# 1. user options (from spawn options form)
# 2. self.image from config
Expand Down
Loading