This commit is contained in:
sdimovv 2023-09-14 18:49:38 +08:00 committed by GitHub
commit 0d5654ae6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 417 additions and 7 deletions

View File

@ -3,6 +3,7 @@ from datetime import datetime
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
@ -677,6 +678,93 @@ class ContainerApiMixin:
"""
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):
"""
@ -1097,7 +1185,8 @@ class ContainerApiMixin:
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.
@ -1110,12 +1199,20 @@ class ContainerApiMixin:
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:
@ -1124,6 +1221,14 @@ class ContainerApiMixin:
... 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 '
@ -1131,7 +1236,7 @@ class ContainerApiMixin:
'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')

View File

@ -97,6 +97,10 @@ class ImageNotFound(NotFound):
pass
class CheckpointNotFound(NotFound):
pass
class InvalidVersion(DockerException):
pass

View File

@ -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()

View File

@ -10,6 +10,7 @@ from ..errors import (
)
from ..types import HostConfig
from ..utils import version_gte
from .checkpoints import CheckpointCollection
from .images import Image
from .resource import Collection, Model
@ -263,6 +264,27 @@ class Container(Model):
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.

View File

@ -9,10 +9,12 @@ import pytest
from . import fake_api
from ..helpers import requires_api_version
from .api_test import (
BaseAPIClientTest, url_prefix, fake_request, DEFAULT_TIMEOUT_SECONDS,
fake_inspect_container, url_base
)
from .api_test import BaseAPIClientTest
from .api_test import url_prefix
from .api_test import fake_request
from .api_test import DEFAULT_TIMEOUT_SECONDS
from .api_test import fake_inspect_container
from .api_test import url_base
def fake_inspect_container_tty(self, container):
@ -29,6 +31,19 @@ class StartContainerTest(BaseAPIClientTest):
assert 'data' not in args[1]
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_start_container_from_checkpoint(self):
self.client.start(fake_api.FAKE_CONTAINER_ID,
checkpoint=fake_api.FAKE_CHECKPOINT_ID,
checkpoint_dir="/path/to/checkpoint/dir")
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID + '/start')
assert 'data' not in args[1]
assert args[1]["params"]["checkpoint"] == fake_api.FAKE_CHECKPOINT_ID
assert args[1]["params"]["checkpoint-dir"] == "/path/to/checkpoint/dir"
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_start_container_none(self):
with pytest.raises(ValueError) as excinfo:
self.client.start(container=None)
@ -124,6 +139,110 @@ class StartContainerTest(BaseAPIClientTest):
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
class CheckpointContainerTest(BaseAPIClientTest):
def test_create_container_checkpoint(self):
self.client.container_create_checkpoint(
fake_api.FAKE_CONTAINER_ID,
fake_api.FAKE_CHECKPOINT_ID,
)
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID + '/checkpoints')
data = json.loads(args[1]["data"])
assert data["CheckpointID"] == fake_api.FAKE_CHECKPOINT_ID
assert data["Exit"] is True
assert "CheckpointDir" not in data
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_create_container_checkpoint_custom_opts(self):
self.client.container_create_checkpoint(
fake_api.FAKE_CONTAINER_ID,
fake_api.FAKE_CHECKPOINT_ID,
fake_api.FAKE_CHECKPOINT_DIR,
leave_running=True
)
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID + '/checkpoints')
data = json.loads(args[1]["data"])
assert data["CheckpointID"] == fake_api.FAKE_CHECKPOINT_ID
assert data["Exit"] is False
assert data["CheckpointDir"] == fake_api.FAKE_CHECKPOINT_DIR
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_remove_container_checkpoint(self):
self.client.container_remove_checkpoint(
fake_api.FAKE_CONTAINER_ID,
fake_api.FAKE_CHECKPOINT_ID,
)
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID +
'/checkpoints/' +
fake_api.FAKE_CHECKPOINT_ID)
assert "data" not in args[1]
assert "dir" not in args[1]["params"]
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_remove_container_checkpoint_custom_opts(self):
self.client.container_remove_checkpoint(
fake_api.FAKE_CONTAINER_ID,
fake_api.FAKE_CHECKPOINT_ID,
fake_api.FAKE_CHECKPOINT_DIR,
)
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID +
'/checkpoints/' +
fake_api.FAKE_CHECKPOINT_ID)
assert "data" not in args[1]
assert args[1]["params"]["dir"] == fake_api.FAKE_CHECKPOINT_DIR
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_container_checkpoints(self):
self.client.container_checkpoints(
fake_api.FAKE_CONTAINER_ID,
)
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID +
'/checkpoints')
assert "data" not in args[1]
assert "dir" not in args[1]["params"]
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
def test_container_checkpoints_custom_opts(self):
self.client.container_checkpoints(
fake_api.FAKE_CONTAINER_ID,
fake_api.FAKE_CHECKPOINT_DIR,
)
args = fake_request.call_args
assert args[0][1] == (url_prefix + 'containers/' +
fake_api.FAKE_CONTAINER_ID +
'/checkpoints')
assert "data" not in args[1]
assert args[1]["params"]["dir"] == fake_api.FAKE_CHECKPOINT_DIR
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
class CreateContainerTest(BaseAPIClientTest):
def test_create_container(self):
self.client.create_container('busybox', 'true')

View File

@ -19,6 +19,8 @@ FAKE_VOLUME_NAME = 'perfectcherryblossom'
FAKE_NODE_ID = '24ifsmvkjbyhk'
FAKE_SECRET_ID = 'epdyrw4tsi03xy3deu8g8ly6o'
FAKE_SECRET_NAME = 'super_secret'
FAKE_CHECKPOINT_ID = "my-checkpoint"
FAKE_CHECKPOINT_DIR = "/my-dir"
# Each method is prefixed with HTTP method (get, post...)
# for clarity and readability
@ -141,6 +143,24 @@ def post_fake_create_container():
return status_code, response
def post_fake_container_create_checkpoint():
status_code = 201
response = ""
return status_code, response
def get_fake_container_checkpoints():
status_code = 200
response = [{"Name": FAKE_CHECKPOINT_ID}]
return status_code, response
def delete_fake_container_remove_checkpoint():
status_code = 204
response = ""
return status_code, response
def get_fake_inspect_container(tty=False):
status_code = 200
response = {
@ -559,6 +579,12 @@ fake_responses = {
post_fake_update_container,
f'{prefix}/{CURRENT_VERSION}/containers/{FAKE_CONTAINER_ID}/exec':
post_fake_exec_create,
(f'{prefix}/{CURRENT_VERSION}/containers/{FAKE_CONTAINER_ID}/checkpoints', "POST"): # noqa: E501
post_fake_container_create_checkpoint,
(f'{prefix}/{CURRENT_VERSION}/containers/{FAKE_CONTAINER_ID}/checkpoints', "GET"): # noqa: E501
get_fake_container_checkpoints,
f'{prefix}/{CURRENT_VERSION}/containers/{FAKE_CONTAINER_ID}/checkpoints/{FAKE_CHECKPOINT_ID}': # noqa: E501
delete_fake_container_remove_checkpoint,
f'{prefix}/{CURRENT_VERSION}/exec/{FAKE_EXEC_ID}/start':
post_fake_exec_start,
f'{prefix}/{CURRENT_VERSION}/exec/{FAKE_EXEC_ID}/json':