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

Support checkpoint restore #3094

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
111 changes: 108 additions & 3 deletions docker/api/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .. import errors
from .. import utils
from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..models.checkpoints import Checkpoint
from ..types import CancellableStream
from ..types import ContainerConfig
from ..types import EndpointConfig
Expand Down Expand Up @@ -669,6 +670,93 @@ def create_endpoint_config(self, *args, **kwargs):
"""
return EndpointConfig(self._version, *args, **kwargs)

@utils.check_resource('container')
def container_checkpoints(self, container, checkpoint_dir=None):
"""
(Experimental) List all container checkpoints.

Args:
container (str): The container to find checkpoints for
checkpoint_dir (str): Custom directory in which to search for
checkpoints. Default: None (use default checkpoint dir)
Returns:
List of dicts, one for each checkpoint. In the form of:
[{"Name": "<checkpoint_name>"}]

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
params = {}
if checkpoint_dir:
params["dir"] = checkpoint_dir

return self._result(
self._get(self._url("/containers/{0}/checkpoints", container),
params=params),
True
)

@utils.check_resource('container')
def container_remove_checkpoint(self, container, checkpoint,
checkpoint_dir=None):
"""
(Experimental) Remove container checkpoint.

Args:
container (str): The container the checkpoint belongs to
checkpoint (str): The checkpoint ID to remove
checkpoint_dir (str): Custom directory in which to search for
checkpoints. Default: None (use default checkpoint dir)

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
params = {}
if checkpoint_dir:
params["dir"] = checkpoint_dir

res = self._delete(
self._url("/containers/{0}/checkpoints/{1}",
container,
checkpoint),
params=params
)
self._raise_for_status(res)

@utils.check_resource('container')
def container_create_checkpoint(self, container, checkpoint,
checkpoint_dir=None,
leave_running=False):
"""
(Experimental) Create new container checkpoint.

Args:
container (str): The container to checkpoint
checkpoint (str): The checkpoint ID
checkpoint_dir (str): Custom directory in which to place the
checkpoint. Default: None (use default checkpoint dir)
leave_running (bool): Determines if the container should be left
running after the checkpoint is created

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
data = {
"CheckpointID": checkpoint,
"Exit": not leave_running,
}
if checkpoint_dir:
data["CheckpointDir"] = checkpoint_dir

res = self._post_json(
self._url("/containers/{0}/checkpoints", container),
data=data
)
self._raise_for_status(res)

@utils.check_resource('container')
def diff(self, container):
"""
Expand Down Expand Up @@ -1088,7 +1176,8 @@ def restart(self, container, timeout=10):
self._raise_for_status(res)

@utils.check_resource('container')
def start(self, container, *args, **kwargs):
def start(self, container, checkpoint=None, checkpoint_dir=None,
*args, **kwargs):
"""
Start a container. Similar to the ``docker start`` command, but
doesn't support attach options.
Expand All @@ -1101,12 +1190,20 @@ def start(self, container, *args, **kwargs):

Args:
container (str): The container to start
checkpoint (:py:class:`docker.models.checkpoints.Checkpoint` or
str):
(Experimental) The checkpoint ID from which to start.
Default: None (do not start from a checkpoint)
checkpoint_dir (str): (Experimental) Custom directory in which to
search for checkpoints. Default: None (use default
checkpoint dir)

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
:py:class:`docker.errors.DeprecatedMethod`
If any argument besides ``container`` are provided.
If any argument besides ``container``, ``checkpoint``
or ``checkpoint_dir`` are provided.

Example:

Expand All @@ -1115,14 +1212,22 @@ def start(self, container, *args, **kwargs):
... command='/bin/sleep 30')
>>> client.api.start(container=container.get('Id'))
"""
params = {}
if checkpoint:
if isinstance(checkpoint, Checkpoint):
checkpoint = checkpoint.id
params["checkpoint"] = checkpoint
if checkpoint_dir:
params['checkpoint-dir'] = checkpoint_dir

if args or kwargs:
raise errors.DeprecatedMethod(
'Providing configuration in the start() method is no longer '
'supported. Use the host_config param in create_container '
'instead.'
)
url = self._url("/containers/{0}/start", container)
res = self._post(url)
res = self._post(url, params=params)
self._raise_for_status(res)

@utils.check_resource('container')
Expand Down
4 changes: 4 additions & 0 deletions docker/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ class ImageNotFound(NotFound):
pass


class CheckpointNotFound(NotFound):
pass


class InvalidVersion(DockerException):
pass

Expand Down
134 changes: 134 additions & 0 deletions docker/models/checkpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
from ..errors import CheckpointNotFound
from .resource import Collection
from .resource import Model


class Checkpoint(Model):
""" (Experimental) Local representation of a checkpoint object. Detailed
configuration may be accessed through the :py:attr:`attrs` attribute.
Note that local attributes are cached; users may call :py:meth:`reload`
to query the Docker daemon for the current properties, causing
:py:attr:`attrs` to be refreshed.
"""
id_attribute = 'Name'

@property
def short_id(self):
"""
The ID of the object.
"""
return self.id

def remove(self):
"""
Remove this checkpoint. Similar to the
``docker checkpoint rm`` command.

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
return self.client.api.container_remove_checkpoint(
self.collection.container_id,
checkpoint=self.id,
checkpoint_dir=self.collection.checkpoint_dir,
)

def __eq__(self, other):
if isinstance(other, Checkpoint):
return self.id == other.id
return self.id == other

class CheckpointCollection(Collection):
"""(Experimental)."""
model = Checkpoint

def __init__(self, container_id, checkpoint_dir=None, **kwargs):
#: The client pointing at the server that this collection of objects
#: is on.
super().__init__(**kwargs)
self.container_id = container_id
self.checkpoint_dir = checkpoint_dir

def create(self, checkpoint_id, **kwargs):
"""
Create a new container checkpoint. Similar to
``docker checkpoint create``.

Args:
checkpoint_id (str): The id (name) of the checkpoint
leave_running (bool): Determines if the container should be left
running after the checkpoint is created

Returns:
A :py:class:`Checkpoint` object.

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
self.client.api.container_create_checkpoint(
self.container_id,
checkpoint=checkpoint_id,
checkpoint_dir=self.checkpoint_dir,
**kwargs,
)
return Checkpoint(
attrs={"Name": checkpoint_id},
client=self.client,
collection=self
)

def get(self, id):
"""
Get a container checkpoint by id (name).

Args:
id (str): The checkpoint id (name)

Returns:
A :py:class:`Checkpoint` object.

Raises:
:py:class:`docker.errors.NotFound`
If the checkpoint does not exist.
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
checkpoints = self.list()

for checkpoint in checkpoints:
if checkpoint == id:
return checkpoint

raise CheckpointNotFound(
f"Checkpoint with id={id} does not exist"
f" in checkpoint_dir={self.checkpoint_dir}"
)

def list(self):
"""
List checkpoints. Similar to the ``docker checkpoint ls`` command.

Returns:
(list of :py:class:`Checkpoint`)

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
resp = self.client.api.container_checkpoints(
self.container_id, checkpoint_dir=self.checkpoint_dir
)
return [self.prepare_model(checkpoint) for checkpoint in resp or []]

def prune(self):
"""
Remove all checkpoints in this collection.

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
for checkpoint in self.list():
checkpoint.remove()
22 changes: 22 additions & 0 deletions docker/models/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from ..types import HostConfig
from ..utils import version_gte
from .checkpoints import CheckpointCollection
from .images import Image
from .resource import Collection, Model

Expand Down Expand Up @@ -261,6 +262,27 @@ def get_archive(self, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE,
return self.client.api.get_archive(self.id, path,
chunk_size, encode_stream)

def get_checkpoints(self, checkpoint_dir=None):
"""
Get a collection of all container checkpoints in a given directory.
Similar to the ``docker checkpoint ls`` command.

Args:
checkpoint_dir (str): Custom directory in which to search for
checkpoints. Default: None (use default checkpoint dir)
Returns:
:py:class:`CheckpointCollection`

Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
return CheckpointCollection(
container_id=self.id,
checkpoint_dir=checkpoint_dir,
client=self.client,
)

def kill(self, signal=None):
"""
Kill or send a signal to the container.
Expand Down
Loading