From 6dab87460beebb97e70df9b6369887dcfb0679ca Mon Sep 17 00:00:00 2001 From: Alkim Gozen Date: Tue, 17 Apr 2018 16:21:59 +0900 Subject: [PATCH] Save and load for docker images (#220) * added docker save command and test for alpine package * added chunked post method for docker client * added import method to load image tarball to docker * changed return element of export method * fixes of flake8 errors and warnings * import image tested over saved hello-world image file * support python 3.5 for load file by using async generators * test with asyncio.streamer * removed unused async generator functions * removed unused dependencies from test requirements * Contributers as alphabetical order * added 219 news to CHANGES folder --- CHANGES/219.feature | 1 + CONTRIBUTORS.txt | 3 ++- aiodocker/docker.py | 22 ++++++++++++++++-- aiodocker/images.py | 37 +++++++++++++++++++++++++++++++ tests/docker/hello-world.img.tar | Bin 0 -> 12800 bytes tests/test_images.py | 29 ++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 CHANGES/219.feature create mode 100644 tests/docker/hello-world.img.tar diff --git a/CHANGES/219.feature b/CHANGES/219.feature new file mode 100644 index 00000000..72370191 --- /dev/null +++ b/CHANGES/219.feature @@ -0,0 +1 @@ +Feature: Add support for docker save and load api methods diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e6531451..e624068a 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,3 +1,4 @@ +Alkim Gozen Andrew Svetlov Anton Patrushev Byeongjun Park @@ -12,4 +13,4 @@ Joshua Chung Martin Koppehel Nikolay Novik Paul Tagliamonte -Tianon Gravi +Tianon Gravi \ No newline at end of file diff --git a/aiodocker/docker.py b/aiodocker/docker.py index 7fe5741c..09ee7113 100644 --- a/aiodocker/docker.py +++ b/aiodocker/docker.py @@ -136,7 +136,7 @@ def _canonicalize_url(self, path): async def _query(self, path, method='GET', *, params=None, data=None, headers=None, - timeout=None): + timeout=None, chunked=None): ''' Get the response object by performing the HTTP request. The caller is responsible to finalize the response object. @@ -150,7 +150,8 @@ async def _query(self, path, method='GET', *, params=httpize(params), headers=headers, data=data, - timeout=timeout) + timeout=timeout, + chunked=chunked) except asyncio.TimeoutError: raise if (response.status // 100) in [4, 5]: @@ -183,6 +184,23 @@ async def _query_json(self, path, method='GET', *, data = await parse_result(response) return data + async def _query_chunked_post(self, path, method='POST', *, + params=None, data=None, headers=None, + timeout=None): + ''' + A shorthand for uploading data by chunks + ''' + if headers is None: + headers = {} + if headers and 'content-type' not in headers: + headers['content-type'] = 'application/octet-stream' + response = await self._query( + path, method, + params=params, data=data, headers=headers, + timeout=timeout, chunked=1024) + data = await parse_result(response) + return data + async def _websocket(self, path, **params): if not params: params = { diff --git a/aiodocker/images.py b/aiodocker/images.py index 763dd60e..588bd4bb 100644 --- a/aiodocker/images.py +++ b/aiodocker/images.py @@ -229,3 +229,40 @@ async def build(self, *, ) return (await json_stream_result(response, stream=stream)) + + async def export_image(self, name: str): + """ + Get a tarball of an image by name or id. + + Args: + name: name/id of the image to be exported + + Returns: + Streamreader of tarball image + """ + response = await self.docker._query( + "images/{name}/get".format(name=name), + "GET", + ) + return response.content + + async def import_image(self, data): + """ + Import tarball of image to docker. + + Args: + data: tarball data of image to be imported + + Returns: + Tarball of the image + """ + headers = { + "Content-Type": "application/x-tar", + } + response = await self.docker._query_chunked_post( + "images/load", + "POST", + data=data, + headers=headers + ) + return response diff --git a/tests/docker/hello-world.img.tar b/tests/docker/hello-world.img.tar new file mode 100644 index 0000000000000000000000000000000000000000..814ce8426985eb44e9f67d091242cea330c66a5c GIT binary patch literal 12800 zcmeGiO^+N$wbyZCYXGeeZ~{TmaZn=7M^}IMh#(v9hOxBH8hdR-9BZkru9|7w(>?00 z_U^J;aWDr~2#Fg9Zg2yMJ3=BBxkUK^h(CY?5(l3M2c+dyf6UIVXLpm##E^JO>aMC+ z@4c#eujO=2GQ)$Xp^OEM9Yp)3UWN-`v2e3-wJv9v6? zDg>c}+ad^KwaZDAv8FnPCAoMS$WkD!FgL``IK4h%@lifSZat>Zhzfr-eyDcusW-1~ z^j`1eS=xybN}^8YhjDizKh7Q&6reH-N%ekHy*3Oaug-u)Q6d3X(|)#^Q8HpsAkrLq za(zhpQUuw6=$74uq3;<6Hf-v8x@{4z`NXEC$9(Q%9Qdwn7}T*X>=KP~)A5LgEyu-{ z=TetxK1lLz66Hgdsogk_qNe)2Bz+LZ{gtp1=V~m{olz3TAh;4^J^pqcM$n=%yGDE# zRpfhx!USlh&<6H2PHY{!m^miZEgE>-G&DxA=LRszh|g`!)EG=mlfdk@G51a4S`-ti zxw>o}=)YvQIdvpV*693=DsOE({~Gz$}$<0^LAn%&l0 zFfE(vAk0LP!X&=8dj?bN+Ky{Fh9#L+^>6w>sdh?PqZUP#oUg6l{66Ag#JUDGC~@_G z2fnEVhQ>WiJj`^)Jj>t|RwN^!rUh+QRG9Cp=E?)_#R28SCt zV0Pw4a(uP`GPkC&BgxzM0w7$pt$m!C&MYcr2ZY}#Ph#lIuqxAIQRp; zvDp7j5#%QT`tj=88|Rjm#+=V7-%@1m=F*h{R{j-b`^6$#!Cd9Maz(kIyj0*$6hV1S zQO0gnSjUQj07n7(@c@zzFOF;7PJ)nASjeRnX>P)1r@Vi z?gJ+XuNJUM2gjK}ZKE&0{G=RxoDUjOK#)k(3D_ zj?yH^DJu{Vc)CV;A#6lahG-7Lj6s+PF51C0)`zy4)7p~>Ko}PN1eO&SP|QTnN)a|u z*`!R0GZaOO0I$%lVCgW7NmObM97rfjh71%3Y8fWb95VDnP<@^?B>|!&mR&;--SP8S z0HpT&psi30K#P}AP>ibruN4ZEmxw;e293t;f$U@}xw%gTD$1X_{)ASlj!X5(r^bM&tjHz5`V9m#B69kC(1M zPEj6L^ewP>lL1X;Al-y-lK#p0D+R`rs-e9iclf(_m@HB@{3Yy#x?`37uh)edT$3Ho z%es#>u|TcqrHP&lRktQVi}XSQ3kfVF@Tr!-ki;QpnfNg8xIS)2&p7`Vhdt>8t^%C@ zu!FV5{r|_6_eY_F#~RnFmtHEH$6G+SalOSx$p+~cj~FKQ>)i-;hMADhWY*;4h@yOc z_fPQ7;eN5j`zYOi?r~Gl6>OxFG-UJHm`;)CS@R#f{q@@rX#(W@x9!FJpCZZmgLaRc hsNqFYHA&~-65QKcOvm{vY{79MfrSJX5}1<&{tFt50|Ed5 literal 0 HcmV?d00001 diff --git a/tests/test_images.py b/tests/test_images.py index 4aa843c9..761f68ab 100644 --- a/tests/test_images.py +++ b/tests/test_images.py @@ -1,8 +1,10 @@ +import os from io import BytesIO import pytest from aiodocker import utils from aiodocker.exceptions import DockerError +import aiohttp @pytest.mark.asyncio @@ -112,6 +114,33 @@ async def test_build_from_tar(docker, random_name): assert image +@pytest.mark.asyncio +async def test_export_image(docker): + name = "alpine:latest" + exported_image = await docker.images.export_image(name=name) + assert exported_image + + +@pytest.mark.asyncio +async def test_import_image(docker): + + @aiohttp.streamer + async def file_sender(writer, file_name=None): + with open(file_name, 'rb') as f: + chunk = f.read(2**16) + while chunk: + await writer.write(chunk) + chunk = f.read(2**16) + + dir = os.path.dirname(__file__) + hello_world = os.path.join(dir, 'docker/hello-world.img.tar') + response = await docker.images.import_image( + data=file_sender(file_name=hello_world)) + assert 'error' not in response + image = await docker.images.get(name='hello-world:latest') + assert image + + @pytest.mark.asyncio async def test_pups_image_auth(docker): name = "alpine:latest"