summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoffrey F <joffrey@docker.com>2015-10-08 14:00:07 -0700
committerJoffrey F <joffrey@docker.com>2015-10-08 14:00:07 -0700
commitf479720d517a7db7f886916190b3032d29d18f10 (patch)
treef17d462f50e1577e8170e367b5b8295c9975a1dd
parent28627a3af867d99e8e77383b9e3d057903872eb4 (diff)
parent4845dae0c02516f98f43f301a654cad7efd04fa4 (diff)
downloaddocker-py-f479720d517a7db7f886916190b3032d29d18f10.tar.gz
Merge branch 'archive_endpoint'
-rw-r--r--docker/api/container.py30
-rw-r--r--docker/client.py3
-rw-r--r--docker/utils/__init__.py2
-rw-r--r--docker/utils/utils.py8
-rw-r--r--docs/api.md37
-rw-r--r--tests/helpers.py21
-rw-r--r--tests/integration_test.py85
-rw-r--r--tests/utils_test.py14
8 files changed, 198 insertions, 2 deletions
diff --git a/docker/api/container.py b/docker/api/container.py
index 94889e9..142bd0f 100644
--- a/docker/api/container.py
+++ b/docker/api/container.py
@@ -75,6 +75,12 @@ class ContainerApiMixin(object):
@utils.check_resource
def copy(self, container, resource):
+ if utils.version_gte(self._version, '1.20'):
+ warnings.warn(
+ 'Client.copy() is deprecated for API version >= 1.20, '
+ 'please use get_archive() instead',
+ DeprecationWarning
+ )
res = self._post_json(
self._url("/containers/{0}/copy".format(container)),
data={"Resource": resource},
@@ -146,6 +152,21 @@ class ContainerApiMixin(object):
return res.raw
@utils.check_resource
+ @utils.minimum_version('1.20')
+ def get_archive(self, container, path):
+ params = {
+ 'path': path
+ }
+ url = self._url('/containers/{0}/archive', container)
+ res = self._get(url, params=params, stream=True)
+ self._raise_for_status(res)
+ encoded_stat = res.headers.get('x-docker-container-path-stat')
+ return (
+ res.raw,
+ utils.decode_json_header(encoded_stat) if encoded_stat else None
+ )
+
+ @utils.check_resource
def inspect_container(self, container):
return self._result(
self._get(self._url("/containers/{0}/json", container)), True
@@ -215,6 +236,15 @@ class ContainerApiMixin(object):
return h_ports
@utils.check_resource
+ @utils.minimum_version('1.20')
+ def put_archive(self, container, path, data):
+ params = {'path': path}
+ url = self._url('/containers/{0}/archive', container)
+ res = self._put(url, params=params, data=data)
+ self._raise_for_status(res)
+ return res.status_code == 200
+
+ @utils.check_resource
def remove_container(self, container, v=False, link=False, force=False):
params = {'v': v, 'link': link, 'force': force}
res = self._delete(
diff --git a/docker/client.py b/docker/client.py
index 79efc9f..d219472 100644
--- a/docker/client.py
+++ b/docker/client.py
@@ -109,6 +109,9 @@ class Client(
def _get(self, url, **kwargs):
return self.get(url, **self._set_request_timeout(kwargs))
+ def _put(self, url, **kwargs):
+ return self.put(url, **self._set_request_timeout(kwargs))
+
def _delete(self, url, **kwargs):
return self.delete(url, **self._set_request_timeout(kwargs))
diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py
index fd0ef5c..92e03e9 100644
--- a/docker/utils/__init__.py
+++ b/docker/utils/__init__.py
@@ -3,7 +3,7 @@ from .utils import (
mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host,
kwargs_from_env, convert_filters, create_host_config,
create_container_config, parse_bytes, ping_registry, parse_env_file,
- version_lt, version_gte
+ version_lt, version_gte, decode_json_header
) # flake8: noqa
from .types import Ulimit, LogConfig # flake8: noqa
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
index 1fce137..89837b7 100644
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import base64
import io
import os
import os.path
@@ -66,6 +67,13 @@ def mkbuildcontext(dockerfile):
return f
+def decode_json_header(header):
+ data = base64.b64decode(header)
+ if six.PY3:
+ data = data.decode('utf-8')
+ return json.loads(data)
+
+
def tar(path, exclude=None, dockerfile=None):
f = tempfile.NamedTemporaryFile()
t = tarfile.open(mode='w', fileobj=f)
diff --git a/docs/api.md b/docs/api.md
index 103b235..6cd69fe 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -165,6 +165,8 @@ non-running ones
## copy
Identical to the `docker cp` command. Get files/folders from the container.
+**Deprecated for API version >= 1.20** &ndash; Consider using
+[`get_archive`](#get_archive) **instead.**
**Params**:
@@ -376,6 +378,27 @@ Export the contents of a filesystem as a tar archive to STDOUT.
**Returns** (str): The filesystem tar archive as a str
+## get_archive
+
+Retrieve a file or folder from a container in the form of a tar archive.
+
+**Params**:
+
+* container (str): The container where the file is located
+* path (str): Path to the file or folder to retrieve
+
+**Returns** (tuple): First element is a raw tar data stream. Second element is
+a dict containing `stat` information on the specified `path`.
+
+```python
+>>> import docker
+>>> c = docker.Client()
+>>> ctnr = c.create_container('busybox', 'true')
+>>> strm, stat = c.get_archive(ctnr, '/bin/sh')
+>>> print(stat)
+{u'linkTarget': u'', u'mode': 493, u'mtime': u'2015-09-16T12:34:23-07:00', u'name': u'sh', u'size': 962860}
+```
+
## get_image
Get an image from the docker daemon. Similar to the `docker save` command.
@@ -712,6 +735,20 @@ command.
yourname/app/tags/latest}"}\\n']
```
+## put_archive
+
+Insert a file or folder in an existing container using a tar archive as source.
+
+**Params**:
+
+* container (str): The container where the file(s) will be extracted
+* path (str): Path inside the container where the file(s) will be extracted.
+ Must exist.
+* data (bytes): tar data to be extracted
+
+**Returns** (bool): True if the call succeeds. `docker.errors.APIError` will
+be raised if an error occurs.
+
## remove_container
Remove a container. Similar to the `docker rm` command.
diff --git a/tests/helpers.py b/tests/helpers.py
index 95692db..392be3b 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -1,5 +1,6 @@
import os
import os.path
+import tarfile
import tempfile
@@ -14,3 +15,23 @@ def make_tree(dirs, files):
f.write("content")
return base
+
+
+def simple_tar(path):
+ f = tempfile.NamedTemporaryFile()
+ t = tarfile.open(mode='w', fileobj=f)
+
+ abs_path = os.path.abspath(path)
+ t.add(abs_path, arcname=os.path.basename(path), recursive=False)
+
+ t.close()
+ f.seek(0)
+ return f
+
+
+def untar_file(tardata, filename):
+ with tarfile.open(mode='r', fileobj=tardata) as t:
+ f = t.extractfile(filename)
+ result = f.read()
+ f.close()
+ return result
diff --git a/tests/integration_test.py b/tests/integration_test.py
index da380c1..8a92708 100644
--- a/tests/integration_test.py
+++ b/tests/integration_test.py
@@ -37,6 +37,7 @@ import docker
from docker.errors import APIError, NotFound
from docker.utils import kwargs_from_env
+from . import helpers
from .base import requires_api_version
from .test import Cleanup
@@ -427,6 +428,90 @@ class CreateContainerWithLogConfigTest(BaseTestCase):
self.assertEqual(container_log_config['Config'], {})
+@requires_api_version('1.20')
+class GetArchiveTest(BaseTestCase):
+ def test_get_file_archive_from_container(self):
+ data = 'The Maid and the Pocket Watch of Blood'
+ ctnr = self.client.create_container(
+ BUSYBOX, 'sh -c "echo {0} > /vol1/data.txt"'.format(data),
+ volumes=['/vol1']
+ )
+ self.tmp_containers.append(ctnr)
+ self.client.start(ctnr)
+ self.client.wait(ctnr)
+ with tempfile.NamedTemporaryFile() as destination:
+ strm, stat = self.client.get_archive(ctnr, '/vol1/data.txt')
+ for d in strm:
+ destination.write(d)
+ destination.seek(0)
+ retrieved_data = helpers.untar_file(destination, 'data.txt')
+ if six.PY3:
+ retrieved_data = retrieved_data.decode('utf-8')
+ self.assertEqual(data, retrieved_data.strip())
+
+ def test_get_file_stat_from_container(self):
+ data = 'The Maid and the Pocket Watch of Blood'
+ ctnr = self.client.create_container(
+ BUSYBOX, 'sh -c "echo -n {0} > /vol1/data.txt"'.format(data),
+ volumes=['/vol1']
+ )
+ self.tmp_containers.append(ctnr)
+ self.client.start(ctnr)
+ self.client.wait(ctnr)
+ strm, stat = self.client.get_archive(ctnr, '/vol1/data.txt')
+ self.assertIn('name', stat)
+ self.assertEqual(stat['name'], 'data.txt')
+ self.assertIn('size', stat)
+ self.assertEqual(stat['size'], len(data))
+
+
+@requires_api_version('1.20')
+class PutArchiveTest(BaseTestCase):
+ def test_copy_file_to_container(self):
+ data = b'Deaf To All But The Song'
+ with tempfile.NamedTemporaryFile() as test_file:
+ test_file.write(data)
+ test_file.seek(0)
+ ctnr = self.client.create_container(
+ BUSYBOX,
+ 'cat {0}'.format(
+ os.path.join('/vol1', os.path.basename(test_file.name))
+ ),
+ volumes=['/vol1']
+ )
+ self.tmp_containers.append(ctnr)
+ with helpers.simple_tar(test_file.name) as test_tar:
+ self.client.put_archive(ctnr, '/vol1', test_tar)
+ self.client.start(ctnr)
+ self.client.wait(ctnr)
+ logs = self.client.logs(ctnr)
+ if six.PY3:
+ logs = logs.decode('utf-8')
+ data = data.decode('utf-8')
+ self.assertEqual(logs.strip(), data)
+
+ def test_copy_directory_to_container(self):
+ files = ['a.py', 'b.py', 'foo/b.py']
+ dirs = ['foo', 'bar']
+ base = helpers.make_tree(dirs, files)
+ ctnr = self.client.create_container(
+ BUSYBOX, 'ls -p /vol1', volumes=['/vol1']
+ )
+ self.tmp_containers.append(ctnr)
+ with docker.utils.tar(base) as test_tar:
+ self.client.put_archive(ctnr, '/vol1', test_tar)
+ self.client.start(ctnr)
+ self.client.wait(ctnr)
+ logs = self.client.logs(ctnr)
+ if six.PY3:
+ logs = logs.decode('utf-8')
+ results = logs.strip().split()
+ self.assertIn('a.py', results)
+ self.assertIn('b.py', results)
+ self.assertIn('foo/', results)
+ self.assertIn('bar/', results)
+
+
class TestCreateContainerReadOnlyFs(BaseTestCase):
def runTest(self):
if not exec_driver_is_native():
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 8ac1dcb..b1adde2 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
+import base64
+import json
import os
import os.path
import shutil
@@ -14,7 +16,7 @@ from docker.errors import DockerException
from docker.utils import (
parse_repository_tag, parse_host, convert_filters, kwargs_from_env,
create_host_config, Ulimit, LogConfig, parse_bytes, parse_env_file,
- exclude_paths, convert_volume_binds,
+ exclude_paths, convert_volume_binds, decode_json_header
)
from docker.utils.ports import build_port_bindings, split_port
from docker.auth import resolve_repository_name, resolve_authconfig
@@ -370,6 +372,16 @@ class UtilsTest(base.BaseTestCase):
for filters, expected in tests:
self.assertEqual(convert_filters(filters), expected)
+ def test_decode_json_header(self):
+ obj = {'a': 'b', 'c': 1}
+ data = None
+ if six.PY3:
+ data = base64.b64encode(bytes(json.dumps(obj), 'utf-8'))
+ else:
+ data = base64.b64encode(json.dumps(obj))
+ decoded_data = decode_json_header(data)
+ self.assertEqual(obj, decoded_data)
+
def test_resolve_repository_name(self):
# docker hub library image
self.assertEqual(