Skip to content

Commit

Permalink
Save multiple images into single tar file from ImageCollection
Browse files Browse the repository at this point in the history
The image collection in the Docker API allows you to save multiple
images into a single bundle. This makes it easy to export and transfer
multiple containers together.

This feature is documented within the swagger for the apiserver:
https://github.com/moby/moby/blob/6f8c671d702197a189d162d86a3f4cccfa5a3db2/api/swagger.yaml#L7617-L7645

This feature mirrors the docker cli's `docker image save` command:
https://docs.docker.com/engine/reference/commandline/image_save/

Signed-off-by: Joshua Katz <gravypod@gravypod.com>
  • Loading branch information
gravypod committed Nov 8, 2019
1 parent a0b9c3d commit 6cf2afd
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 4 deletions.
37 changes: 36 additions & 1 deletion docker/api/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class ImageApiMixin(object):
@utils.check_resource('image')
def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
"""
Get a tarball of an image. Similar to the ``docker save`` command.
Get a tarball of an image. Similar to the
``docker image save`` command.
Args:
image (str): Image name to get
Expand All @@ -40,6 +41,40 @@ def get_image(self, image, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
res = self._get(self._url("/images/{0}/get", image), stream=True)
return self._stream_raw_result(res, chunk_size, False)

@utils.check_resource('image')
def get_images(self, images, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
"""
Get a tarball of images. Similar to the
``docker image save IMAGE...`` command.
Args:
images (list): Images name to get
chunk_size (int): The number of bytes returned by each iteration
of the generator. If ``None``, data will be streamed as it is
received. Default: 2 MB
Returns:
(generator): A stream of raw archive data.
Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
Example:
>>> names = ["busybox:latest", "alpine:latest"]
>>> image = cli.get_images(names)
>>> f = open('/tmp/busybox-and-alpine-latest.tar', 'wb')
>>> for chunk in image:
>>> f.write(chunk)
>>> f.close()
"""
params = {
'names': images
}
res = self._get(self._url("/images/get"), params=params, stream=True)
return self._stream_raw_result(res, chunk_size, False)

@utils.check_resource('image')
def history(self, image):
"""
Expand Down
41 changes: 38 additions & 3 deletions docker/models/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def history(self):

def save(self, chunk_size=DEFAULT_DATA_CHUNK_SIZE, named=False):
"""
Get a tarball of an image. Similar to the ``docker save`` command.
Get a tarball of an image. Similar to the
``docker image save IMAGE`` command.
Args:
chunk_size (int): The generator will return up to that much data
Expand All @@ -84,9 +85,9 @@ def save(self, chunk_size=DEFAULT_DATA_CHUNK_SIZE, named=False):
Example:
>>> image = cli.get_image("busybox:latest")
>>> image = client.images.get("busybox:latest")
>>> f = open('/tmp/busybox-latest.tar', 'wb')
>>> for chunk in image:
>>> for chunk in image.save():
>>> f.write(chunk)
>>> f.close()
"""
Expand Down Expand Up @@ -297,6 +298,40 @@ def build(self, **kwargs):
return (self.get(image_id), result_stream)
raise BuildError(last_event or 'Unknown', result_stream)

def save(self, images, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
"""
Get a tarball of multiple images. Similar to the
``docker images save IMAGE [IMAGES...]`` command.
Args:
images (list): Images to get
chunk_size (int): The generator will return up to that much data
per iteration, but may return less. If ``None``, data will be
streamed as it is received. Default: 2 MB
named (str or bool): If ``False`` (default), the tarball will not
retain repository and tag information for this image. If set
to ``True``, the first tag in the :py:attr:`~tags` list will
be used to identify the image. Alternatively, any element of
the :py:attr:`~tags` list can be used as an argument to use
that specific tag as the saved identifier.
Returns:
(generator): A stream of raw archive data.
Raises:
:py:class:`docker.errors.APIError`
If the server returns an error.
Example:
>>> image = client.images.save(["busybox:latest", "alpine:latest"])
>>> f = open('/tmp/busybox-and-alpine-latest.tar', 'wb')
>>> for chunk in image:
>>> f.write(chunk)
>>> f.close()
"""
return self.client.api.get_images(images, chunk_size)

def get(self, name):
"""
Gets an image.
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/models_images_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ def test_save_and_load(self):
assert len(result) == 1
assert result[0].id == image.id

def test_save_and_load_multiple(self):
client = docker.from_env(version=TEST_API_VERSION)
with tempfile.TemporaryFile() as f:
stream = client.images.save([TEST_IMG, 'hello-world'])
for chunk in stream:
f.write(chunk)

f.seek(0)
result = client.images.load(f.read())

assert len(result) == 2

def test_save_and_load_repo_name(self):
client = docker.from_env(version=TEST_API_VERSION)
image = client.images.get(TEST_IMG)
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/models_images_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ def test_remove(self):
client.images.remove('test_image')
client.api.remove_image.assert_called_with('test_image')

def test_save(self):
client = make_fake_client()
client.images.save([FAKE_IMAGE_ID])
client.api.get_images.assert_called_with(
[FAKE_IMAGE_ID], DEFAULT_DATA_CHUNK_SIZE
)

def test_search(self):
client = make_fake_client()
client.images.search('test')
Expand Down

0 comments on commit 6cf2afd

Please sign in to comment.