From 63d0e814c6e661773bfb2c4437baeceb57843e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Wed, 6 May 2015 11:31:28 -0300 Subject: [PATCH 01/15] Add support for building contexts objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docker/utils/__init__.py | 5 + docker/utils/context.py | 206 +++++++++++++++++++++++++++++++++++++++ docker/utils/utils.py | 14 +++ requirements.txt | 1 + 4 files changed, 226 insertions(+) create mode 100644 docker/utils/context.py diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index fd0ef5c0c9..33f149960a 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -8,3 +8,8 @@ from .types import Ulimit, LogConfig # flake8: noqa from .decorators import check_resource, minimum_version #flake8: noqa +from .context import ( + create_context_from_path, + is_remote, + ContextError +) # flake8: noqa diff --git a/docker/utils/context.py b/docker/utils/context.py new file mode 100644 index 0000000000..fe159e5162 --- /dev/null +++ b/docker/utils/context.py @@ -0,0 +1,206 @@ +import os +import re +import tarfile +from collections import namedtuple +from .utils import lazy_line_reader + +lzma = None +try: + import lzma +except ImportError as ie: + try: + import backports.lzma as lzma + except ImportError: + pass + + +class ContextError(Exception): + def __init__(self, msg): + self.message = msg + + +INVALID_CONTEXT_FORMAT_LONG_FMT = """ +Build context at %s is not supported by docker-compose\\n +The path must point to either: +\t * A readable directory containing a valid Dockerfile +\t * A tarball (optionally compressed with gzip, xz or bzip2) +\t * A valid Dockerfile +\t * A valid URL for a remote build context. +%s""" + +INVALID_FIRST_INSTRUCTION_FMT = """ +Invalid first instruction in Dockerfile %s: '%s'\\n +The first instruction in a Dockerfile must be "FROM ...""" + +# these prefixes are treated as remote by the docker daemon +# (ref: pkg/urlutil/*) as of v1.6.0 +REMOTE_CONTEXT_PREFIXES = ["http://", + "https://", + "git://", + "git@", + "github.com/"] + +DOCKERFILE_CMD_RG = re.compile(r"[\t\s]*\\n$|([\s\t]*" + r"FROM|MAINTAINER|RUN|CMD|EXPOSE|ENV|ADD|COPY|" + r"ENTRYPOINT|VOLUME|USER|WORKDIR|ONBUILD" + r"[\s\t]+.+)") + + +class BuildContext(namedtuple('BuildContext', + ['format', 'path', 'dockerfile', 'job_params'])): + def __new__(cls, context_format, + path, + dockerfile='Dockerfile', + job_params=None): + ctx_tuple = super(BuildContext, cls) + return ctx_tuple.__new__( + cls, + context_format, + path, + dockerfile, + job_params, + ) + + +def make_context_from_tarball(path, dockerfile='Dockerfile'): + return BuildContext( + 'tarball', + path, + dockerfile=dockerfile, + job_params={ + 'encoding': 'gzip', + 'custom_context': True, + 'fileobj': open(path) + } + ) + + +def make_context_from_dockerfile(path, dockerfile='Dockerfile'): + return BuildContext( + 'dockerfile', + path=path, + dockerfile=dockerfile, + job_params={'fileobj': open(path, 'r')}, + ) + + +def make_context_from_url(path, dockerfile='Dockerfile'): + return BuildContext( + 'remote', + path, + dockerfile=dockerfile, + job_params={}, + ) + + +def make_context_from_directory(path, dockerfile='Dockerfile'): + return BuildContext( + 'directory', + path, + dockerfile=dockerfile, + job_params={} + ) + + +context_builders = { + 'tarball': make_context_from_tarball, + 'dockerfile': make_context_from_dockerfile, + 'remote': make_context_from_url, + 'directory': make_context_from_directory +} + + +def create_context_from_path(path, dockerfile='Dockerfile'): + context_maker = detect_context_format(path, dockerfile) + if context_maker is None: + raise ContextError("Format not supported at " + "%s [dockerfile='%s']." % (path, dockerfile)) + + return context_maker(path, dockerfile) + + +def is_remote(path): + for prefix in REMOTE_CONTEXT_PREFIXES: + if path.startswith(prefix): + return True + return False + + +def detect_context_format(path, dockerfile='Dockerfile'): + if is_remote(path): + return context_builders['remote'] + + try: + os.access(path, os.R_OK) + except IOError as ioe: + raise ContextError("%s: %s" % (path, ioe)) + + if os.path.isdir(path): + if dockerfile in os.listdir(path): + return context_builders['directory'] + else: + raise ContextError("Directory %s does not contain a Dockerfile" + " with name %s" % (path, dockerfile)) + elif is_tarball_context(path): + return context_builders['tarball'] + + elif os.path.isfile(path): + try: + if validate_dockerfile_head(path): + return context_builders['dockerfile'] + except ContextError as e: + raise e + else: + return None + + +# The actual contents of the tarball are not checked; this just makes sure the +# file exists and that this Python installation recognizes the format. +def is_tarball_context(path): + if os.path.isdir(path): + return False + + if tarfile.is_tarfile(path): + return True + + if lzma is not None: + try: + with lzma.LZMAFile(path) as xzfile: + xzfile.peek(0) + return True + except lzma.LZMAError: + pass + return False + + +def is_directory_context(path, dockerfile='Dockerfile'): + return ( + os.path.isdir(path) and + dockerfile in os.listdir(path) and + validate_dockerfile_head(os.path.join(path, dockerfile)) + ) + + +def validate_dockerfile_head(fpath): + with open(fpath, 'r') as candidate: + line_reader = lazy_line_reader(candidate) + first_line = _read_first_instruction_line(line_reader) + try: + if not first_line.startswith("FROM"): + raise ContextError(INVALID_FIRST_INSTRUCTION_FMT % + (fpath, first_line)) + except UnicodeDecodeError: + return False + return True + + +def _read_first_instruction_line(line_reader, marker=r'^[\s\t]*(\#|$)'): + import re + cmt_regex = re.compile(marker) + for line in line_reader: + if cmt_regex.findall(line): + continue + else: + break + + return line # only in python... diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 46b3516043..6d3bd8e0c7 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -760,3 +760,17 @@ def create_container_config( 'Labels': labels, 'VolumeDriver': volume_driver, } + + +def lazy_line_reader(fileobj, line_limit=1024): + read_count = 0 + while read_count < line_limit: + try: + line = fileobj.readline() + if not line: + break + yield line + except IOError: + break + except EOFError: + break diff --git a/requirements.txt b/requirements.txt index 72c255d318..455058a95b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests==2.5.3 six>=1.4.0 websocket-client==0.32.0 +backports.lzma==0.0.3 From 305f28b3f23bc0f33207a60d96b312d854ea5b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Thu, 7 May 2015 18:11:00 -0300 Subject: [PATCH 02/15] Tests for utils/context.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docker/utils/context.py | 14 +++----- tests/test.py | 41 +++++++++++++++++++++++ tests/testdata/context/Dockerfile | 2 ++ tests/testdata/context/ctx.tar.gz | Bin 0 -> 171 bytes tests/testdata/context/custom_dockerfile | 2 ++ 5 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 tests/testdata/context/Dockerfile create mode 100644 tests/testdata/context/ctx.tar.gz create mode 100644 tests/testdata/context/custom_dockerfile diff --git a/docker/utils/context.py b/docker/utils/context.py index fe159e5162..2d21e86fbc 100644 --- a/docker/utils/context.py +++ b/docker/utils/context.py @@ -20,7 +20,7 @@ def __init__(self, msg): INVALID_CONTEXT_FORMAT_LONG_FMT = """ -Build context at %s is not supported by docker-compose\\n +Build context at %s is not supported by Docker\\n The path must point to either: \t * A readable directory containing a valid Dockerfile \t * A tarball (optionally compressed with gzip, xz or bzip2) @@ -40,11 +40,6 @@ def __init__(self, msg): "git@", "github.com/"] -DOCKERFILE_CMD_RG = re.compile(r"[\t\s]*\\n$|([\s\t]*" - r"FROM|MAINTAINER|RUN|CMD|EXPOSE|ENV|ADD|COPY|" - r"ENTRYPOINT|VOLUME|USER|WORKDIR|ONBUILD" - r"[\s\t]+.+)") - class BuildContext(namedtuple('BuildContext', ['format', 'path', 'dockerfile', 'job_params'])): @@ -194,11 +189,10 @@ def validate_dockerfile_head(fpath): return True -def _read_first_instruction_line(line_reader, marker=r'^[\s\t]*(\#|$)'): - import re - cmt_regex = re.compile(marker) +def _read_first_instruction_line(line_reader, to_ignore=r'^[\s\t]*(\#|$)'): + to_ignore_regex = re.compile(to_ignore) for line in line_reader: - if cmt_regex.findall(line): + if to_ignore_regex.findall(line): continue else: break diff --git a/tests/test.py b/tests/test.py index 8ed5c14d00..14be2c2bda 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2103,6 +2103,47 @@ def test_build_container_invalid_container_limits(self): }) ) + def test_build_container_from_context_object_with_tarball(self): + base_path = os.path.join( + os.path.dirname(__file__), + 'testdata/context' + ) + tarball_path = os.path.join(base_path, 'ctx.tar.gz') + context = docker.utils.context.create_context_from_path(tarball_path) + try: + self.client.build(context.path, **context.job_params) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + def test_build_container_from_context_object_with_custom_dockerfile(self): + base_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'testdata/context' + )) + custom_dockerfile = 'custom_dockerfile' + try: + context = docker.utils.context.create_context_from_path( + base_path, + dockerfile=custom_dockerfile + ) + self.client.build(context.path, **context.job_params) + except docker.utils.context.ContextError as ce: + self.fail(ce.message) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + def test_build_container_from_remote_context(self): + ctxurl = 'https://localhost/staging/context.tar.gz' + try: + context = docker.utils.context.create_context_from_path(ctxurl) + self.assertEquals(context.path, ctxurl) + self.assertEquals(context.format, 'remote') + self.client.build(context.path, **context.job_params) + except docker.utils.context.ContextError as ce: + self.fail(ce.message) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + ################### # VOLUMES TESTS # ################### diff --git a/tests/testdata/context/Dockerfile b/tests/testdata/context/Dockerfile new file mode 100644 index 0000000000..d1ceac6b74 --- /dev/null +++ b/tests/testdata/context/Dockerfile @@ -0,0 +1,2 @@ +FROM busybox:latest +CMD echo "success" diff --git a/tests/testdata/context/ctx.tar.gz b/tests/testdata/context/ctx.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c14e5b971f1e3bcd55e0f4c02f76681d9f846810 GIT binary patch literal 171 zcmb2|=HLj~?H$U%oLo|&SCUx7@b;V|*C7LumWOMqyDkVXnWXmM+M{(G_ZIL99`#zV zp0&DKS5L$-MpW=$zOl_S-DO@6Wqu!C*_~zh?3|@_`a|!}v-q#AP70Mu@4jTc{7g;7 z(z8XA=ebGFipwtbob2-R&-b Date: Fri, 8 May 2015 10:12:10 -0300 Subject: [PATCH 03/15] Simplified the validation logic for build contexts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docker/utils/context.py | 67 +++-------------------------------------- docker/utils/utils.py | 14 --------- requirements.txt | 1 - 3 files changed, 5 insertions(+), 77 deletions(-) diff --git a/docker/utils/context.py b/docker/utils/context.py index 2d21e86fbc..324949a98e 100644 --- a/docker/utils/context.py +++ b/docker/utils/context.py @@ -1,17 +1,6 @@ import os -import re import tarfile from collections import namedtuple -from .utils import lazy_line_reader - -lzma = None -try: - import lzma -except ImportError as ie: - try: - import backports.lzma as lzma - except ImportError: - pass class ContextError(Exception): @@ -28,9 +17,6 @@ def __init__(self, msg): \t * A valid URL for a remote build context. %s""" -INVALID_FIRST_INSTRUCTION_FMT = """ -Invalid first instruction in Dockerfile %s: '%s'\\n -The first instruction in a Dockerfile must be "FROM ...""" # these prefixes are treated as remote by the docker daemon # (ref: pkg/urlutil/*) as of v1.6.0 @@ -140,11 +126,7 @@ def detect_context_format(path, dockerfile='Dockerfile'): return context_builders['tarball'] elif os.path.isfile(path): - try: - if validate_dockerfile_head(path): - return context_builders['dockerfile'] - except ContextError as e: - raise e + return context_builders['dockerfile'] else: return None @@ -152,49 +134,10 @@ def detect_context_format(path, dockerfile='Dockerfile'): # The actual contents of the tarball are not checked; this just makes sure the # file exists and that this Python installation recognizes the format. def is_tarball_context(path): - if os.path.isdir(path): - return False - - if tarfile.is_tarfile(path): - return True - - if lzma is not None: - try: - with lzma.LZMAFile(path) as xzfile: - xzfile.peek(0) - return True - except lzma.LZMAError: - pass - return False + return (not os.path.isdir(path) and (path.endswith('.xz') or + tarfile.is_tarfile(path))) def is_directory_context(path, dockerfile='Dockerfile'): - return ( - os.path.isdir(path) and - dockerfile in os.listdir(path) and - validate_dockerfile_head(os.path.join(path, dockerfile)) - ) - - -def validate_dockerfile_head(fpath): - with open(fpath, 'r') as candidate: - line_reader = lazy_line_reader(candidate) - first_line = _read_first_instruction_line(line_reader) - try: - if not first_line.startswith("FROM"): - raise ContextError(INVALID_FIRST_INSTRUCTION_FMT % - (fpath, first_line)) - except UnicodeDecodeError: - return False - return True - - -def _read_first_instruction_line(line_reader, to_ignore=r'^[\s\t]*(\#|$)'): - to_ignore_regex = re.compile(to_ignore) - for line in line_reader: - if to_ignore_regex.findall(line): - continue - else: - break - - return line # only in python... + dockerfile_path = os.path.abspath(os.path.join(path, dockerfile)) + return os.path.isdir(path) and os.path.isfile(dockerfile_path) diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 6d3bd8e0c7..46b3516043 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -760,17 +760,3 @@ def create_container_config( 'Labels': labels, 'VolumeDriver': volume_driver, } - - -def lazy_line_reader(fileobj, line_limit=1024): - read_count = 0 - while read_count < line_limit: - try: - line = fileobj.readline() - if not line: - break - yield line - except IOError: - break - except EOFError: - break diff --git a/requirements.txt b/requirements.txt index 455058a95b..72c255d318 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ requests==2.5.3 six>=1.4.0 websocket-client==0.32.0 -backports.lzma==0.0.3 From b2809a02d8f6d6889c2c0b0ec2cc999daef73b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Fri, 8 May 2015 12:59:24 -0300 Subject: [PATCH 04/15] Docs for create_context_from_path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docs/context.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/context.md diff --git a/docs/context.md b/docs/context.md new file mode 100644 index 0000000000..47a3b6388a --- /dev/null +++ b/docs/context.md @@ -0,0 +1,68 @@ +# BuildContext object +An immutable representation (named tuple) of a Docker build context. This object +has the following fields: +* path (str): the absolute filesystem path to the build context +* format: a string tag for the context type; one of 'tarball', 'dockerfile', +'remote', 'directory'. +* dockerfile: the name of the Dockerfile for this context. +* job_params: a helper dictionary holding the parameters for a `Client.build` +invocation. The `create_context_from_path` function populates this dictionary +with the specific combination of values that is valid for the kind of build +context represented by the tuple. For example, if the `BuildContext` represents +a tarball context, its `job_params` field will contain a dict with the mappings: +```python +job_params = { + 'encoding': 'gzip' + 'custom_context': True + 'fileobj': open(path) +} +``` +When the `BuildContext` represents a single Dockerfile context, `job_params` +will contain: +```python +job_params = { + 'fileobj': open(path) +} +``` +## create_context_from_path + +This is an intermediary call that you can use to create a `BuildContext`. Using +the returned object you can perform custom validation and filtering steps before +invoking `Client.build`. In a call to `create_context_from_path`, the parameter +`path` can point to any kind of resource supported by the docker daemon, namely: +* A local path to a directory containing a Dockerfile. +* An URL pointing to a git repository or a remote Dockerfile. +* A local path to a tarball containing a pre-packaged build context. + +The returned `BuildContext` object can be used in an invocation of +`Client.build` as such: +```python +from docker import Client +from docker.utils.context import ( + create_context_from_path, + ContextError +) +cli = Client(base_url='tcp://127.0.0.1:2375') +try: + ctxpath = '/context/path' # or '/context/Dockerfile', + # or '/context/ctx.tar' + # or 'https://github.com/user/repo.git' + ctx = create_context_from_path(ctxpath) +except ContextError as e + print(e.message) + +# here you can perform custom validation, filtering, inserting, etc. on 'ctx' + +cli.build(ctx.path, **ctx.job_params) +``` + +**Params**: + +* path (str): Path to the build context. +* dockerfile (str): path within the build context to the Dockerfile + +**Returns** (namedtuple): A `BuildContext` object. + +**Raises** ContextError: when the contents at `path` are either inaccessible or +inconsistent with the parameters (e.g. a custom 'Dockerfile' name was specified +but the file does not exist at `path`. From 087f0e572d934b832bded8b03d410117ffe24328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Tue, 19 May 2015 13:38:14 -0300 Subject: [PATCH 05/15] Normalize paths as byte arrays in context.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docker/utils/context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/utils/context.py b/docker/utils/context.py index 324949a98e..3af9d2e9ca 100644 --- a/docker/utils/context.py +++ b/docker/utils/context.py @@ -1,5 +1,6 @@ import os import tarfile +import six from collections import namedtuple @@ -92,7 +93,9 @@ def make_context_from_directory(path, dockerfile='Dockerfile'): def create_context_from_path(path, dockerfile='Dockerfile'): - context_maker = detect_context_format(path, dockerfile) + _dockerfile = six.binary_type(dockerfile) + _path = six.binary_type(path) + context_maker = detect_context_format(_path, _dockerfile) if context_maker is None: raise ContextError("Format not supported at " "%s [dockerfile='%s']." % (path, dockerfile)) From 0d6bb531c736d2097797d0c2f6c15f0f322de1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Tue, 19 May 2015 14:49:44 -0300 Subject: [PATCH 06/15] Better handling of non-ascii filenames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docker/utils/context.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/docker/utils/context.py b/docker/utils/context.py index 3af9d2e9ca..d7371a23c8 100644 --- a/docker/utils/context.py +++ b/docker/utils/context.py @@ -93,8 +93,13 @@ def make_context_from_directory(path, dockerfile='Dockerfile'): def create_context_from_path(path, dockerfile='Dockerfile'): - _dockerfile = six.binary_type(dockerfile) - _path = six.binary_type(path) + if path is None: + raise ContextError("'path' parameter cannot be None") + if dockerfile is None: + raise ContextError("'dockerfile' parameter cannot be None") + + _dockerfile = dockerfile.encode('utf-8') + _path = path.encode('utf-8') context_maker = detect_context_format(_path, _dockerfile) if context_maker is None: raise ContextError("Format not supported at " @@ -104,8 +109,14 @@ def create_context_from_path(path, dockerfile='Dockerfile'): def is_remote(path): + if path is None: + return False + + _path = path + if isinstance(_path, six.binary_type): + _path = _path.decode('utf-8') for prefix in REMOTE_CONTEXT_PREFIXES: - if path.startswith(prefix): + if _path.startswith(prefix): return True return False @@ -137,8 +148,13 @@ def detect_context_format(path, dockerfile='Dockerfile'): # The actual contents of the tarball are not checked; this just makes sure the # file exists and that this Python installation recognizes the format. def is_tarball_context(path): - return (not os.path.isdir(path) and (path.endswith('.xz') or - tarfile.is_tarfile(path))) + if path is None: + return False + _path = path + if isinstance(_path, six.binary_type): + _path = _path.decode('utf-8') + return (not os.path.isdir(_path) and (_path.endswith('.xz') or + tarfile.is_tarfile(_path))) def is_directory_context(path, dockerfile='Dockerfile'): From 450dff44f26868912bbb76de7fb404c10be2e079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Tue, 19 May 2015 14:50:21 -0300 Subject: [PATCH 07/15] Updated tests for context.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges Signed-off-by: Moysés Borges --- tests/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test.py b/tests/test.py index 14be2c2bda..f5f1eedd54 100644 --- a/tests/test.py +++ b/tests/test.py @@ -2112,6 +2112,8 @@ def test_build_container_from_context_object_with_tarball(self): context = docker.utils.context.create_context_from_path(tarball_path) try: self.client.build(context.path, **context.job_params) + if context.job_params['fileobj'] is not None: + context.job_params['fileobj'].close() except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -2136,8 +2138,8 @@ def test_build_container_from_remote_context(self): ctxurl = 'https://localhost/staging/context.tar.gz' try: context = docker.utils.context.create_context_from_path(ctxurl) - self.assertEquals(context.path, ctxurl) - self.assertEquals(context.format, 'remote') + self.assertEqual(context.path, ctxurl) + self.assertEqual(context.format, 'remote') self.client.build(context.path, **context.job_params) except docker.utils.context.ContextError as ce: self.fail(ce.message) From ff1bb29f1ef189795adef71e32b86f527a5fdcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moys=C3=A9s=20Borges?= Date: Tue, 19 May 2015 15:21:04 -0300 Subject: [PATCH 08/15] Use bytes() for file paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Moysés Borges --- docker/utils/context.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/utils/context.py b/docker/utils/context.py index d7371a23c8..964a1440d8 100644 --- a/docker/utils/context.py +++ b/docker/utils/context.py @@ -98,8 +98,13 @@ def create_context_from_path(path, dockerfile='Dockerfile'): if dockerfile is None: raise ContextError("'dockerfile' parameter cannot be None") - _dockerfile = dockerfile.encode('utf-8') - _path = path.encode('utf-8') + _dockerfile = dockerfile + _path = path + if isinstance(_dockerfile, six.string_types): + _dockerfile = dockerfile.encode('utf-8') + if isinstance(_path, six.string_types): + _path = path.encode('utf-8') + context_maker = detect_context_format(_path, _dockerfile) if context_maker is None: raise ContextError("Format not supported at " From e08c3095c682a448bdba96919a4affeb51631f10 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 12 Jan 2015 17:47:21 -0800 Subject: [PATCH 09/15] Kickstart efficiency module with some utility functions Signed-off-by: Joffrey F --- docker/efficiency/__init__.py | 0 docker/efficiency/builds.py | 34 +++++++++++++++++++++++++++++++++ docker/efficiency/containers.py | 34 +++++++++++++++++++++++++++++++++ docker/efficiency/images.py | 0 setup.py | 5 +++-- 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 docker/efficiency/__init__.py create mode 100644 docker/efficiency/builds.py create mode 100644 docker/efficiency/containers.py create mode 100644 docker/efficiency/images.py diff --git a/docker/efficiency/__init__.py b/docker/efficiency/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker/efficiency/builds.py b/docker/efficiency/builds.py new file mode 100644 index 0000000000..a9849abd9f --- /dev/null +++ b/docker/efficiency/builds.py @@ -0,0 +1,34 @@ +import json +import re + + +build_success_re = r'^Successfully built ([a-f0-9]+)\n$' + + +def get_build_id(build_result, discard_logs=False): + """ **Params:** + * `build_result` is a python generator returned by `Client.build` + * `discard_logs` (bool, default=False). If True, log lines will + be discarded after they're processed. Limits memory footprint. + **Returns** tuple: + 1. Image ID if found, None otherwise + 2. List of log lines + """ + parsed_lines = [] + image_id = None + for line in build_result: + try: + parsed_line = json.loads(line).get('stream', '') + if not discard_logs: + parsed_lines.append(parsed_line) + match = re.match(build_success_re, line) + if match: + image_id = match.group(1) + except ValueError: + # sometimes all the data is sent on a single line + # This ONLY works because every line is formatted as + # {"stream": STRING} + lines = re.findall('{\s*"stream"\s*:\s*"[^"]*"\s*}', line) + return get_build_id(lines, discard_logs) + + return image_id, parsed_lines diff --git a/docker/efficiency/containers.py b/docker/efficiency/containers.py new file mode 100644 index 0000000000..bf4aa1d2f7 --- /dev/null +++ b/docker/efficiency/containers.py @@ -0,0 +1,34 @@ +import io +import tarfile + + +def copy_to_fs(client, container_id, path, target="."): + """ + Copy file from container to filesystem + + **Params:** + client: a docker `Client` object + container_id: ID of the container to copy from + path: path to the file in the container + target: folder where file will be copied (default ".") + """ + response = client.copy(container_id, path) + buffer = io.BytesIO() + buffer.write(response.data) + buffer.seek(0) + tar = tarfile.open(fileobj=buffer, mode='r|') + tar.extractall(path=target) + + +def start_auto_remove(client, container, *args, **kwargs): + """ + Start a container and try to remove it when it's finished running, + similar to using --autorm in the docker CLI. + **Params:** + client: a docker `Client` object + container: ID of the container to be started + args, kwargs: `Client.start()` arguments + """ + client.start(container, *args, **kwargs) + if client.wait(container) == 0: + return client.remove_container(container) diff --git a/docker/efficiency/images.py b/docker/efficiency/images.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setup.py b/setup.py index 1d0a842238..b3b4a5eb10 100644 --- a/setup.py +++ b/setup.py @@ -24,8 +24,9 @@ description="Python client for Docker.", url='https://github.com/docker/docker-py/', packages=[ - 'docker', 'docker.api', 'docker.auth', 'docker.unixconn', - 'docker.utils', 'docker.utils.ports', 'docker.ssladapter' + 'docker', 'docker.api', 'docker.auth', 'docker.efficiency', + 'docker.unixconn', 'docker.utils', 'docker.utils.ports', + 'docker.ssladapter' ], install_requires=requirements, tests_require=test_requirements, From 390fdb5212cc0f808645bcb7bc6f9dbca0d5c988 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 25 Jun 2015 21:27:51 +0200 Subject: [PATCH 10/15] Efficiency module hub, added smart pull and push methods Signed-off-by: Joffrey F --- docker/efficiency/__init__.py | 1 + docker/efficiency/containers.py | 4 +- docker/efficiency/efficiency.py | 82 +++++++++++++++++++++++++++++++++ docker/efficiency/images.py | 51 ++++++++++++++++++++ 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 docker/efficiency/efficiency.py diff --git a/docker/efficiency/__init__.py b/docker/efficiency/__init__.py index e69de29bb2..e6fc3d8968 100644 --- a/docker/efficiency/__init__.py +++ b/docker/efficiency/__init__.py @@ -0,0 +1 @@ +from .efficiency import * # flake8: noqa \ No newline at end of file diff --git a/docker/efficiency/containers.py b/docker/efficiency/containers.py index bf4aa1d2f7..9aa6b79575 100644 --- a/docker/efficiency/containers.py +++ b/docker/efficiency/containers.py @@ -2,7 +2,7 @@ import tarfile -def copy_to_fs(client, container_id, path, target="."): +def copy_to_fs(client, container, path, target="."): """ Copy file from container to filesystem @@ -12,7 +12,7 @@ def copy_to_fs(client, container_id, path, target="."): path: path to the file in the container target: folder where file will be copied (default ".") """ - response = client.copy(container_id, path) + response = client.copy(container, path) buffer = io.BytesIO() buffer.write(response.data) buffer.seek(0) diff --git a/docker/efficiency/efficiency.py b/docker/efficiency/efficiency.py new file mode 100644 index 0000000000..90289e0451 --- /dev/null +++ b/docker/efficiency/efficiency.py @@ -0,0 +1,82 @@ +from . import builds +from . import containers +from . import images + +_client = None + + +def init(client): + """ + Set a client object to be used by the efficiency module. + + **Params:** + client: a `docker.Client` object that will be used globally by the + module + """ + global _client + _client = client + + +def copy_to_fs(container, path, target='.'): + """ + Copy file from container to filesystem + + **Params:** + container_id: ID of the container to copy from + path: path to the file in the container + target: folder where file will be copied (default ".") + """ + return containers.copy_to_fs( + _client, container, path, target + ) + + +def start_auto_remove(container, *args, **kwargs): + """ + Start a container and try to remove it when it's finished running, + similar to using --autorm in the docker CLI. + + **Params:** + container: ID of the container to be started + args, kwargs: `Client.start()` arguments + """ + return containers.start_auto_remove( + _client, container + ) + + +def pull(repo, tag=None, insecure_registry=False, auth_config=None): + """ + Pull an image and stream the response chunks as JSON objects. + If an error is encountered during streaming, a DockerException will be + raised. + + **Params:** + repo: Name of the repository to pull + tag: Optional tag name to pull (default: latest) + insecure_registry: Set to true if pulling from an insecure registry + auth_config: Optional transient auth config object + """ + return images.pull( + _client, repo, tag, insecure_registry, auth_config + ) + + +def push(repo, tag=None, insecure_registry=False): + """ + Push an image and stream the response chunks as JSON objects. + If an error is encountered during streaming, a DockerException will be + raised. + + **Params:** + repo: Name of the repository to push + tag: Optional tag name to push (default: all) + insecure_registry: Set to true if pulling from an insecure registry + auth_config: Optional transient auth config object + """ + return images.push( + _client, repo, tag, insecure_registry + ) + + +get_build_id = builds.get_build_id diff --git a/docker/efficiency/images.py b/docker/efficiency/images.py index e69de29bb2..6d64b7d206 100644 --- a/docker/efficiency/images.py +++ b/docker/efficiency/images.py @@ -0,0 +1,51 @@ +import json + +from .. import errors + + +def _generator_parser(gen): + for line in gen: + status = json.loads(line) + if status.get('error'): + raise errors.DockerException(status.get('error')) + yield status + + +def pull(client, repo, tag=None, insecure_registry=False, auth_config=None): + """ + Pull an image and stream the response chunks as JSON objects. + If an error is encountered during streaming, a DockerException will be + raised. + + **Params:** + client: a docker `Client` object + repo: Name of the repository to pull + tag: Optional tag name to pull (default: latest) + insecure_registry: Set to true if pulling from an insecure registry + auth_config: Optional transient auth config object + """ + gen = client.pull( + repo, tag=tag, stream=True, insecure_registry=insecure_registry, + auth_config=auth_config + ) + + return _generator_parser(gen) + + +def push(client, repo, tag=None, insecure_registry=False): + """ + Push an image and stream the response chunks as JSON objects. + If an error is encountered during streaming, a DockerException will be + raised. + + **Params:** + client: a docker `Client` object + repo: Name of the repository to push + tag: Optional tag name to push (default: all) + insecure_registry: Set to true if pulling from an insecure registry + auth_config: Optional transient auth config object + """ + gen = client.push( + repo, tag=tag, stream=True, insecure_registry=insecure_registry + ) + return _generator_parser(gen) From 1a8c749ed8e292b4f0b94eb2eed7afadc9a3e9da Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 25 Jun 2015 23:12:11 +0200 Subject: [PATCH 11/15] Moved build context to efficiency module Signed-off-by: Joffrey F --- docker/efficiency/builds.py | 166 +++++++++++++++++++++++++++++++++++ docker/errors.py | 5 ++ docker/utils/__init__.py | 5 -- docker/utils/context.py | 167 ------------------------------------ 4 files changed, 171 insertions(+), 172 deletions(-) delete mode 100644 docker/utils/context.py diff --git a/docker/efficiency/builds.py b/docker/efficiency/builds.py index a9849abd9f..368152b055 100644 --- a/docker/efficiency/builds.py +++ b/docker/efficiency/builds.py @@ -1,10 +1,35 @@ import json +import os import re +import six +import tarfile +from collections import namedtuple + +from .. import errors build_success_re = r'^Successfully built ([a-f0-9]+)\n$' +INVALID_CONTEXT_FORMAT_LONG_FMT = """ +Build context at %s is not supported by Docker\\n +The path must point to either: +\t * A readable directory containing a valid Dockerfile +\t * A tarball (optionally compressed with gzip, xz or bzip2) +\t * A valid Dockerfile +\t * A valid URL for a remote build context. +%s""" + + +# these prefixes are treated as remote by the docker daemon +# (ref: pkg/urlutil/*) as of v1.6.0 +REMOTE_CONTEXT_PREFIXES = ["http://", + "https://", + "git://", + "git@", + "github.com/"] + + def get_build_id(build_result, discard_logs=False): """ **Params:** * `build_result` is a python generator returned by `Client.build` @@ -32,3 +57,144 @@ def get_build_id(build_result, discard_logs=False): return get_build_id(lines, discard_logs) return image_id, parsed_lines + + +class BuildContext(namedtuple('BuildContext', + ['format', 'path', 'dockerfile', 'job_params'])): + def __new__(cls, context_format, + path, + dockerfile='Dockerfile', + job_params=None): + ctx_tuple = super(BuildContext, cls) + return ctx_tuple.__new__( + cls, + context_format, + path, + dockerfile, + job_params, + ) + + +def make_context_from_tarball(path, dockerfile='Dockerfile'): + return BuildContext( + 'tarball', + path, + dockerfile=dockerfile, + job_params={ + 'encoding': 'gzip', + 'custom_context': True, + 'fileobj': open(path) + } + ) + + +def make_context_from_dockerfile(path, dockerfile='Dockerfile'): + return BuildContext( + 'dockerfile', + path=path, + dockerfile=dockerfile, + job_params={'fileobj': open(path, 'r')}, + ) + + +def make_context_from_url(path, dockerfile='Dockerfile'): + return BuildContext( + 'remote', + path, + dockerfile=dockerfile, + job_params={}, + ) + + +def make_context_from_directory(path, dockerfile='Dockerfile'): + return BuildContext( + 'directory', + path, + dockerfile=dockerfile, + job_params={} + ) + + +context_builders = { + 'tarball': make_context_from_tarball, + 'dockerfile': make_context_from_dockerfile, + 'remote': make_context_from_url, + 'directory': make_context_from_directory +} + + +def create_context_from_path(path, dockerfile='Dockerfile'): + if path is None: + raise errors.ContextError("'path' parameter cannot be None") + if dockerfile is None: + raise errors.ContextError("'dockerfile' parameter cannot be None") + + _dockerfile = dockerfile + _path = path + if isinstance(_dockerfile, six.string_types): + _dockerfile = dockerfile.encode('utf-8') + if isinstance(_path, six.string_types): + _path = path.encode('utf-8') + + context_maker = detect_context_format(_path, _dockerfile) + if context_maker is None: + raise errors.ContextError( + "Format not supported at %s [dockerfile='%s']." % (path, dockerfile) + ) + + return context_maker(path, dockerfile) + + +def is_remote(path): + if path is None: + return False + + _path = path + if isinstance(_path, six.binary_type): + _path = _path.decode('utf-8') + for prefix in REMOTE_CONTEXT_PREFIXES: + if _path.startswith(prefix): + return True + return False + + +def detect_context_format(path, dockerfile='Dockerfile'): + if is_remote(path): + return context_builders['remote'] + + try: + os.access(path, os.R_OK) + except IOError as ioe: + raise errors.ContextError("%s: %s" % (path, ioe)) + + if os.path.isdir(path): + if dockerfile in os.listdir(path): + return context_builders['directory'] + else: + raise errors.ContextError( + "Directory %s does not contain a Dockerfile with name %s" % (path, dockerfile) + ) + elif is_tarball_context(path): + return context_builders['tarball'] + + elif os.path.isfile(path): + return context_builders['dockerfile'] + else: + return None + + +# The actual contents of the tarball are not checked; this just makes sure the +# file exists and that this Python installation recognizes the format. +def is_tarball_context(path): + if path is None: + return False + _path = path + if isinstance(_path, six.binary_type): + _path = _path.decode('utf-8') + return (not os.path.isdir(_path) and (_path.endswith('.xz') or + tarfile.is_tarfile(_path))) + + +def is_directory_context(path, dockerfile='Dockerfile'): + dockerfile_path = os.path.abspath(os.path.join(path, dockerfile)) + return os.path.isdir(path) and os.path.isfile(dockerfile_path) diff --git a/docker/errors.py b/docker/errors.py index 066406ae25..7a7f0ea09d 100644 --- a/docker/errors.py +++ b/docker/errors.py @@ -86,3 +86,8 @@ def __str__(self): class NullResource(DockerException, ValueError): pass + + +class ContextError(DockerException): + def __init__(self, msg): + self.message = msg diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 33f149960a..fd0ef5c0c9 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -8,8 +8,3 @@ from .types import Ulimit, LogConfig # flake8: noqa from .decorators import check_resource, minimum_version #flake8: noqa -from .context import ( - create_context_from_path, - is_remote, - ContextError -) # flake8: noqa diff --git a/docker/utils/context.py b/docker/utils/context.py deleted file mode 100644 index 964a1440d8..0000000000 --- a/docker/utils/context.py +++ /dev/null @@ -1,167 +0,0 @@ -import os -import tarfile -import six -from collections import namedtuple - - -class ContextError(Exception): - def __init__(self, msg): - self.message = msg - - -INVALID_CONTEXT_FORMAT_LONG_FMT = """ -Build context at %s is not supported by Docker\\n -The path must point to either: -\t * A readable directory containing a valid Dockerfile -\t * A tarball (optionally compressed with gzip, xz or bzip2) -\t * A valid Dockerfile -\t * A valid URL for a remote build context. -%s""" - - -# these prefixes are treated as remote by the docker daemon -# (ref: pkg/urlutil/*) as of v1.6.0 -REMOTE_CONTEXT_PREFIXES = ["http://", - "https://", - "git://", - "git@", - "github.com/"] - - -class BuildContext(namedtuple('BuildContext', - ['format', 'path', 'dockerfile', 'job_params'])): - def __new__(cls, context_format, - path, - dockerfile='Dockerfile', - job_params=None): - ctx_tuple = super(BuildContext, cls) - return ctx_tuple.__new__( - cls, - context_format, - path, - dockerfile, - job_params, - ) - - -def make_context_from_tarball(path, dockerfile='Dockerfile'): - return BuildContext( - 'tarball', - path, - dockerfile=dockerfile, - job_params={ - 'encoding': 'gzip', - 'custom_context': True, - 'fileobj': open(path) - } - ) - - -def make_context_from_dockerfile(path, dockerfile='Dockerfile'): - return BuildContext( - 'dockerfile', - path=path, - dockerfile=dockerfile, - job_params={'fileobj': open(path, 'r')}, - ) - - -def make_context_from_url(path, dockerfile='Dockerfile'): - return BuildContext( - 'remote', - path, - dockerfile=dockerfile, - job_params={}, - ) - - -def make_context_from_directory(path, dockerfile='Dockerfile'): - return BuildContext( - 'directory', - path, - dockerfile=dockerfile, - job_params={} - ) - - -context_builders = { - 'tarball': make_context_from_tarball, - 'dockerfile': make_context_from_dockerfile, - 'remote': make_context_from_url, - 'directory': make_context_from_directory -} - - -def create_context_from_path(path, dockerfile='Dockerfile'): - if path is None: - raise ContextError("'path' parameter cannot be None") - if dockerfile is None: - raise ContextError("'dockerfile' parameter cannot be None") - - _dockerfile = dockerfile - _path = path - if isinstance(_dockerfile, six.string_types): - _dockerfile = dockerfile.encode('utf-8') - if isinstance(_path, six.string_types): - _path = path.encode('utf-8') - - context_maker = detect_context_format(_path, _dockerfile) - if context_maker is None: - raise ContextError("Format not supported at " - "%s [dockerfile='%s']." % (path, dockerfile)) - - return context_maker(path, dockerfile) - - -def is_remote(path): - if path is None: - return False - - _path = path - if isinstance(_path, six.binary_type): - _path = _path.decode('utf-8') - for prefix in REMOTE_CONTEXT_PREFIXES: - if _path.startswith(prefix): - return True - return False - - -def detect_context_format(path, dockerfile='Dockerfile'): - if is_remote(path): - return context_builders['remote'] - - try: - os.access(path, os.R_OK) - except IOError as ioe: - raise ContextError("%s: %s" % (path, ioe)) - - if os.path.isdir(path): - if dockerfile in os.listdir(path): - return context_builders['directory'] - else: - raise ContextError("Directory %s does not contain a Dockerfile" - " with name %s" % (path, dockerfile)) - elif is_tarball_context(path): - return context_builders['tarball'] - - elif os.path.isfile(path): - return context_builders['dockerfile'] - else: - return None - - -# The actual contents of the tarball are not checked; this just makes sure the -# file exists and that this Python installation recognizes the format. -def is_tarball_context(path): - if path is None: - return False - _path = path - if isinstance(_path, six.binary_type): - _path = _path.decode('utf-8') - return (not os.path.isdir(_path) and (_path.endswith('.xz') or - tarfile.is_tarfile(_path))) - - -def is_directory_context(path, dockerfile='Dockerfile'): - dockerfile_path = os.path.abspath(os.path.join(path, dockerfile)) - return os.path.isdir(path) and os.path.isfile(dockerfile_path) From 797a242347e0d23c9acec57246eae44b5d61c001 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 26 Jun 2015 00:10:29 +0200 Subject: [PATCH 12/15] BuildContext cleanup. Added smart build function in efficiency. Signed-off-by: Joffrey F --- docker/efficiency/builds.py | 55 +++++++++++++++++++++++---------- docker/efficiency/efficiency.py | 20 ++++++++++++ docker/efficiency/images.py | 16 ++-------- docker/efficiency/tools.py | 11 +++++++ tests/test.py | 11 ++++--- 5 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 docker/efficiency/tools.py diff --git a/docker/efficiency/builds.py b/docker/efficiency/builds.py index 368152b055..9f13b3bef9 100644 --- a/docker/efficiency/builds.py +++ b/docker/efficiency/builds.py @@ -3,24 +3,14 @@ import re import six import tarfile -from collections import namedtuple +import collections +from . import tools from .. import errors build_success_re = r'^Successfully built ([a-f0-9]+)\n$' - -INVALID_CONTEXT_FORMAT_LONG_FMT = """ -Build context at %s is not supported by Docker\\n -The path must point to either: -\t * A readable directory containing a valid Dockerfile -\t * A tarball (optionally compressed with gzip, xz or bzip2) -\t * A valid Dockerfile -\t * A valid URL for a remote build context. -%s""" - - # these prefixes are treated as remote by the docker daemon # (ref: pkg/urlutil/*) as of v1.6.0 REMOTE_CONTEXT_PREFIXES = ["http://", @@ -59,8 +49,12 @@ def get_build_id(build_result, discard_logs=False): return image_id, parsed_lines -class BuildContext(namedtuple('BuildContext', - ['format', 'path', 'dockerfile', 'job_params'])): +BuildCtxTuple = collections.namedtuple( + 'BuildContext', ['format', 'path', 'dockerfile', 'job_params'] +) + + +class BuildContext(BuildCtxTuple): def __new__(cls, context_format, path, dockerfile='Dockerfile', @@ -139,7 +133,9 @@ def create_context_from_path(path, dockerfile='Dockerfile'): context_maker = detect_context_format(_path, _dockerfile) if context_maker is None: raise errors.ContextError( - "Format not supported at %s [dockerfile='%s']." % (path, dockerfile) + "Format not supported at {0} [dockerfile='{1}']".format( + path, dockerfile + ) ) return context_maker(path, dockerfile) @@ -165,14 +161,16 @@ def detect_context_format(path, dockerfile='Dockerfile'): try: os.access(path, os.R_OK) except IOError as ioe: - raise errors.ContextError("%s: %s" % (path, ioe)) + raise errors.ContextError("{0}: {1}".format(path, ioe)) if os.path.isdir(path): if dockerfile in os.listdir(path): return context_builders['directory'] else: raise errors.ContextError( - "Directory %s does not contain a Dockerfile with name %s" % (path, dockerfile) + "Directory {0} does not contain a Dockerfile named {1}".format( + path, dockerfile + ) ) elif is_tarball_context(path): return context_builders['tarball'] @@ -198,3 +196,26 @@ def is_tarball_context(path): def is_directory_context(path, dockerfile='Dockerfile'): dockerfile_path = os.path.abspath(os.path.join(path, dockerfile)) return os.path.isdir(path) and os.path.isfile(dockerfile_path) + + +def build(client, path, dockerfile='Dockerfile', **kwargs): + """ + Build an image from the specified Dockerfile found in context indicated by + `path`. If an error is encountered during streaming, a DockerException + will be raised. + + **Params:** + client: a docker `Client` object + path: string pointing to the build context. Can be any of: + * A readable directory containing a valid Dockerfile + * A tarball (optionally compressed with gzip, xz or bzip2) + * A valid Dockerfile + * A valid URL for a remote build context. + dockerfile: Name of the Dockerfile inside the context path. + Default: "Dockerfile" + kwargs: Additional `docker.Client.build` arguments + """ + ctx = create_context_from_path(path, dockerfile) + kwargs.update(ctx.job_params) + gen = client.build(ctx.path, **kwargs) + return tools.generator_parser(gen) diff --git a/docker/efficiency/efficiency.py b/docker/efficiency/efficiency.py index 90289e0451..65b2c419aa 100644 --- a/docker/efficiency/efficiency.py +++ b/docker/efficiency/efficiency.py @@ -79,4 +79,24 @@ def push(repo, tag=None, insecure_registry=False): ) +def build(path, dockerfile='Dockerfile', **kwargs): + """ + Build an image from the specified Dockerfile found in context indicated by + `path`. If an error is encountered during streaming, a DockerException + will be raised. + + **Params:** + path: string pointing to the build context. Can be any of: + * A readable directory containing a valid Dockerfile + * A tarball (optionally compressed with gzip, xz or bzip2) + * A valid Dockerfile + * A valid URL for a remote build context. + dockerfile: Name of the Dockerfile inside the context path. + Default: "Dockerfile" + kwargs: Additional `docker.Client.build` arguments + """ + return builds.build(_client, path, dockerfile, **kwargs) + + get_build_id = builds.get_build_id +create_context_from_path = builds.create_context_from_path diff --git a/docker/efficiency/images.py b/docker/efficiency/images.py index 6d64b7d206..917bfbef29 100644 --- a/docker/efficiency/images.py +++ b/docker/efficiency/images.py @@ -1,14 +1,4 @@ -import json - -from .. import errors - - -def _generator_parser(gen): - for line in gen: - status = json.loads(line) - if status.get('error'): - raise errors.DockerException(status.get('error')) - yield status +from . import tools def pull(client, repo, tag=None, insecure_registry=False, auth_config=None): @@ -29,7 +19,7 @@ def pull(client, repo, tag=None, insecure_registry=False, auth_config=None): auth_config=auth_config ) - return _generator_parser(gen) + return tools.generator_parser(gen) def push(client, repo, tag=None, insecure_registry=False): @@ -48,4 +38,4 @@ def push(client, repo, tag=None, insecure_registry=False): gen = client.push( repo, tag=tag, stream=True, insecure_registry=insecure_registry ) - return _generator_parser(gen) + return tools.generator_parser(gen) diff --git a/docker/efficiency/tools.py b/docker/efficiency/tools.py new file mode 100644 index 0000000000..3eac337f72 --- /dev/null +++ b/docker/efficiency/tools.py @@ -0,0 +1,11 @@ +import json + +from .. import errors + + +def generator_parser(gen): + for line in gen: + status = json.loads(line) + if status.get('error'): + raise errors.DockerException(status.get('error')) + yield status diff --git a/tests/test.py b/tests/test.py index f5f1eedd54..9993484a56 100644 --- a/tests/test.py +++ b/tests/test.py @@ -30,6 +30,7 @@ import random import docker +import docker.efficiency import requests import six @@ -2109,7 +2110,7 @@ def test_build_container_from_context_object_with_tarball(self): 'testdata/context' ) tarball_path = os.path.join(base_path, 'ctx.tar.gz') - context = docker.utils.context.create_context_from_path(tarball_path) + context = docker.efficiency.create_context_from_path(tarball_path) try: self.client.build(context.path, **context.job_params) if context.job_params['fileobj'] is not None: @@ -2124,12 +2125,12 @@ def test_build_container_from_context_object_with_custom_dockerfile(self): )) custom_dockerfile = 'custom_dockerfile' try: - context = docker.utils.context.create_context_from_path( + context = docker.efficiency.create_context_from_path( base_path, dockerfile=custom_dockerfile ) self.client.build(context.path, **context.job_params) - except docker.utils.context.ContextError as ce: + except docker.errors.ContextError as ce: self.fail(ce.message) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -2137,11 +2138,11 @@ def test_build_container_from_context_object_with_custom_dockerfile(self): def test_build_container_from_remote_context(self): ctxurl = 'https://localhost/staging/context.tar.gz' try: - context = docker.utils.context.create_context_from_path(ctxurl) + context = docker.efficiency.create_context_from_path(ctxurl) self.assertEqual(context.path, ctxurl) self.assertEqual(context.format, 'remote') self.client.build(context.path, **context.job_params) - except docker.utils.context.ContextError as ce: + except docker.errors.ContextError as ce: self.fail(ce.message) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) From b23105dd7f1a6c6e1902efb7206a0909403d37f6 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 26 Jun 2015 23:41:29 +0200 Subject: [PATCH 13/15] smart objects Signed-off-by: Joffrey F --- docker/efficiency/builds.py | 4 +- docker/efficiency/commons.py | 36 ++++++++ docker/efficiency/containers.py | 143 ++++++++++++++++++++++++++++++++ docker/efficiency/efficiency.py | 11 +++ docker/efficiency/images.py | 49 ++++++++++- docker/efficiency/tools.py | 11 --- 6 files changed, 238 insertions(+), 16 deletions(-) create mode 100644 docker/efficiency/commons.py delete mode 100644 docker/efficiency/tools.py diff --git a/docker/efficiency/builds.py b/docker/efficiency/builds.py index 9f13b3bef9..250be2fb22 100644 --- a/docker/efficiency/builds.py +++ b/docker/efficiency/builds.py @@ -5,7 +5,7 @@ import tarfile import collections -from . import tools +from . import commons from .. import errors @@ -218,4 +218,4 @@ def build(client, path, dockerfile='Dockerfile', **kwargs): ctx = create_context_from_path(path, dockerfile) kwargs.update(ctx.job_params) gen = client.build(ctx.path, **kwargs) - return tools.generator_parser(gen) + return commons.generator_parser(gen) diff --git a/docker/efficiency/commons.py b/docker/efficiency/commons.py new file mode 100644 index 0000000000..98765635c0 --- /dev/null +++ b/docker/efficiency/commons.py @@ -0,0 +1,36 @@ +import json +import re + +from .. import errors + + +first_cap_re = re.compile('(.)([A-Z][a-z]+)') +all_cap_re = re.compile('([a-z0-9])([A-Z])') + + +def to_snakecase(name): + s1 = first_cap_re.sub(r'\1_\2', name) + return all_cap_re.sub(r'\1_\2', s1).lower() + + +def generator_parser(gen): + for line in gen: + status = json.loads(line) + if status.get('error'): + raise errors.DockerException(status.get('error')) + yield status + + +class Identifiable(object): + def __init__(self, id): + self._id = id + + @property + def id(self): + return self._id + + +class Interactive(Identifiable): + def __init__(self, client, id): + super(Interactive, self).__init__(id) + self._client = client diff --git a/docker/efficiency/containers.py b/docker/efficiency/containers.py index 9aa6b79575..0d1e9170b0 100644 --- a/docker/efficiency/containers.py +++ b/docker/efficiency/containers.py @@ -1,6 +1,11 @@ import io import tarfile +import six + +from . import commons +from . import images + def copy_to_fs(client, container, path, target="."): """ @@ -32,3 +37,141 @@ def start_auto_remove(client, container, *args, **kwargs): client.start(container, *args, **kwargs) if client.wait(container) == 0: return client.remove_container(container) + + +class Exec(commons.Interactive): + def __init__(self, client, id): + super(Exec, self).__init__(client, id) + data = self._client.exec_inspect(self.id) + for k, v in six.iteritems(data): + if k == 'Id': + continue + setattr(self, commons.to_snakecase(k), v) + + def start(self, detach=False, tty=False, stream=False): + return self._client.exec_start(self.id, detach, tty, stream) + + def resize(self, height=None, width=None): + self._client.exec_resize(self.id, height, width) + + def __str__(self): + return ''.format(self.id[:16]) + + def __repr__(self): + return str(self) + + +class Container(commons.Interactive): + + @classmethod + def list(cls, all=False, latest=False, + since=None, before=None, limit=-1, size=False, filters=None): + from . import efficiency + lst = efficiency._client.containers( + quiet=True, all=all, trunc=False, latest=latest, since=since, + before=before, limit=limit, size=size, filters=filters + ) + return [cls(efficiency._client, x.get('Id')) for x in lst] + + def __init__(self, *args, **kwargs): + super(Container, self).__init__(*args, **kwargs) + self._update() + + def _update(self): + data = self._client.inspect_container(self.id) + for k, v in six.iteritems(data): + if k == 'Id': + self._id = v + continue + setattr(self, commons.to_snakecase(k), v) + + def commit(self, repository=None, tag=None, message=None, author=None, + conf=None): + return images.Image( + self._client, self._client.commit( + self.id, repository, tag, message, author, conf + ).get('Id') + ) + + def diff(self): + return self._client.diff(self.id) + + def exec_create(self, cmd, stdout=True, stderr=True, tty=False, + privileged=False): + return Exec(self._client, self._client.exec_create( + self.id, cmd, stdout, stderr, tty, privileged + ).get('Id')) + + def pause(self): + return self._client.pause(self.id) + + def unpause(self): + return self._client.unpause(self.id) + + def kill(self, signal=None): + self._client.kill(self.id, signal) + self._update() + + def start(self): + self._client.start(self.id) + self._update() + + def stop(self, timeout=10): + self._client.stop(self.id, timeout) + self._update() + + def restart(self, timeout=10): + self.stop(timeout) + self.start() + self._update() + + def wait(self, timeout=None): + res = self._client.wait(self.id, timeout) + self._update() + return res + + def top(self): + return self._client.top(self.id) + + def stats(self): + return self._client.stats(self.id, decode=True) + + def resize(self, height=None, width=None): + self._client.resize(self.id, height, width) + self._update() + + def rename(self, name): + self._client.rename(self.id, name) + self._update() + + def remove(self, volumes=False, link=False, force=False): + self._client.remove_container(self.id, volumes, link, force) + + def port(self, private_port): + return self._client.port(self.id, private_port) + + def logs(self, stdout=True, stderr=True, stream=True, timestamps=False, + tail='all'): + return self._client.logs( + self.id, stdout, stderr, stream, timestamps, tail + ) + + @property + def status(self): + if self.state['Restarting']: + return 'restarting' + if self.state['Paused']: + return 'paused' + if self.state['Running']: + return 'running' + if self.state['OOMKilled']: + return 'oom-killed' + if self.state['Dead']: + return 'dead' + return 'stopped' + + def __str__(self): + return ''.format(self.id[:16]) + + def __repr__(self): + return str(self) diff --git a/docker/efficiency/efficiency.py b/docker/efficiency/efficiency.py index 65b2c419aa..1af87d0fd3 100644 --- a/docker/efficiency/efficiency.py +++ b/docker/efficiency/efficiency.py @@ -100,3 +100,14 @@ def build(path, dockerfile='Dockerfile', **kwargs): get_build_id = builds.get_build_id create_context_from_path = builds.create_context_from_path + +Image = images.Image +Container = containers.Container + + +def get_image(id): + return Image(_client, id) + + +def get_container(id): + return Container(_client, id) diff --git a/docker/efficiency/images.py b/docker/efficiency/images.py index 917bfbef29..e7de98ea79 100644 --- a/docker/efficiency/images.py +++ b/docker/efficiency/images.py @@ -1,4 +1,6 @@ -from . import tools +import six + +from . import commons def pull(client, repo, tag=None, insecure_registry=False, auth_config=None): @@ -19,7 +21,7 @@ def pull(client, repo, tag=None, insecure_registry=False, auth_config=None): auth_config=auth_config ) - return tools.generator_parser(gen) + return commons.generator_parser(gen) def push(client, repo, tag=None, insecure_registry=False): @@ -38,4 +40,45 @@ def push(client, repo, tag=None, insecure_registry=False): gen = client.push( repo, tag=tag, stream=True, insecure_registry=insecure_registry ) - return tools.generator_parser(gen) + return commons.generator_parser(gen) + + +class Image(commons.Interactive): + + @classmethod + def list(cls, name=None, all=False, filters=None): + from . import efficiency + lst = efficiency._client.images(name, True, all, False, filters) + return [cls(efficiency._client, x) for x in lst] + + def __init__(self, *args, **kwargs): + super(Image, self).__init__(*args, **kwargs) + data = self._client.inspect_image(self.id) + for k, v in six.iteritems(data): + if k == 'Id': + self._id = v + continue + setattr(self, commons.to_snakecase(k), v) + + def remove(self, force=False): + self._client.remove_image(self.id, force) + + def tag(self, repository, tag=None, force=False): + return self._client.tag(self.id, repository, tag, force) + + def history(self): + hist = self._client.history(self.id) + return [Image(self._client, img['Id']) for img in hist] + + def create_container(self, *args, **kwargs): + from . import containers + return containers.Container( + self._client, + self._client.create_container(self.id, *args, **kwargs).get('Id') + ) + + def __str__(self): + return ''.format(self.id[:16]) + + def __repr__(self): + return str(self) diff --git a/docker/efficiency/tools.py b/docker/efficiency/tools.py deleted file mode 100644 index 3eac337f72..0000000000 --- a/docker/efficiency/tools.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -from .. import errors - - -def generator_parser(gen): - for line in gen: - status = json.loads(line) - if status.get('error'): - raise errors.DockerException(status.get('error')) - yield status From 36264349912c3192a7712167b901115a123fbada Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 30 Jun 2015 01:46:06 +0200 Subject: [PATCH 14/15] Update before getting status info Signed-off-by: Joffrey F --- docker/efficiency/containers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/efficiency/containers.py b/docker/efficiency/containers.py index 0d1e9170b0..165c57593c 100644 --- a/docker/efficiency/containers.py +++ b/docker/efficiency/containers.py @@ -158,6 +158,7 @@ def logs(self, stdout=True, stderr=True, stream=True, timestamps=False, @property def status(self): + self._update() if self.state['Restarting']: return 'restarting' if self.state['Paused']: From 15385685323b18671bbfed38ba744ac2b6b01bb9 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 23 Sep 2015 17:42:29 -0700 Subject: [PATCH 15/15] Reorganize test directories More clearly separate unit and integration tests Allow splitting into multiple files Cleaner Signed-off-by: Joffrey F --- Makefile | 14 +- tests/base.py | 25 + tests/integration/__init__.py | 2 + tests/integration/api_test.py | 1719 ++++++++++++++++ tests/integration_test.py | 1724 +---------------- .../certs/ca.pem => unit/__init__.py} | 0 tests/{test.py => unit/api_test.py} | 33 +- tests/{ => unit}/fake_api.py | 0 tests/{ => unit}/fake_stat.py | 0 .../cert.pem => unit/testdata/certs/ca.pem} | 0 .../key.pem => unit/testdata/certs/cert.pem} | 0 tests/unit/testdata/certs/key.pem | 0 tests/{ => unit}/testdata/context/Dockerfile | 0 tests/{ => unit}/testdata/context/ctx.tar.gz | Bin .../testdata/context/custom_dockerfile | 0 tests/{ => unit}/utils_test.py | 4 +- tox.ini | 2 +- 17 files changed, 1764 insertions(+), 1759 deletions(-) create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/api_test.py rename tests/{testdata/certs/ca.pem => unit/__init__.py} (100%) rename tests/{test.py => unit/api_test.py} (98%) rename tests/{ => unit}/fake_api.py (100%) rename tests/{ => unit}/fake_stat.py (100%) rename tests/{testdata/certs/cert.pem => unit/testdata/certs/ca.pem} (100%) rename tests/{testdata/certs/key.pem => unit/testdata/certs/cert.pem} (100%) create mode 100644 tests/unit/testdata/certs/key.pem rename tests/{ => unit}/testdata/context/Dockerfile (100%) rename tests/{ => unit}/testdata/context/ctx.tar.gz (100%) rename tests/{ => unit}/testdata/context/custom_dockerfile (100%) rename tests/{ => unit}/utils_test.py (99%) diff --git a/Makefile b/Makefile index f98abe74a2..772c2e2065 100644 --- a/Makefile +++ b/Makefile @@ -14,22 +14,22 @@ build-py3: test: flake8 unit-test unit-test-py3 integration-dind unit-test: build - docker run docker-py py.test tests/test.py tests/utils_test.py + docker run docker-py py.test tests/unit unit-test-py3: build-py3 - docker run docker-py3 py.test tests/test.py tests/utils_test.py + docker run docker-py3 py.test tests/unit integration-test: build - docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py py.test -rxs tests/integration_test.py + docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py py.test -rxs tests/integration integration-test-py3: build-py3 - docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py3 py.test -rxs tests/integration_test.py + docker run -v /var/run/docker.sock:/var/run/docker.sock docker-py3 py.test -rxs tests/integration integration-dind: build build-py3 docker run -d --name dpy-dind --privileged dockerswarm/dind:1.8.1 docker -d -H tcp://0.0.0.0:2375 - docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py py.test -rxs tests/integration_test.py - docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py3 py.test -rxs tests/integration_test.py + docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py py.test -rxs tests/integration + docker run --env="DOCKER_HOST=tcp://docker:2375" --link=dpy-dind:docker docker-py3 py.test -rxs tests/integration docker rm -vf dpy-dind flake8: build - docker run docker-py flake8 docker tests \ No newline at end of file + docker run docker-py flake8 docker tests diff --git a/tests/base.py b/tests/base.py index 51b2300336..a2c01fc2d8 100644 --- a/tests/base.py +++ b/tests/base.py @@ -21,3 +21,28 @@ def requires_api_version(version): ), reason="API version is too low (< {0})".format(version) ) + + +class Cleanup(object): + if sys.version_info < (2, 7): + # Provide a basic implementation of addCleanup for Python < 2.7 + def __init__(self, *args, **kwargs): + super(Cleanup, self).__init__(*args, **kwargs) + self._cleanups = [] + + def tearDown(self): + super(Cleanup, self).tearDown() + ok = True + while self._cleanups: + fn, args, kwargs = self._cleanups.pop(-1) + try: + fn(*args, **kwargs) + except KeyboardInterrupt: + raise + except: + ok = False + if not ok: + raise + + def addCleanup(self, function, *args, **kwargs): + self._cleanups.append((function, args, kwargs)) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000000..07feaa108e --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa +from .api_test import * diff --git a/tests/integration/api_test.py b/tests/integration/api_test.py new file mode 100644 index 0000000000..9080891b53 --- /dev/null +++ b/tests/integration/api_test.py @@ -0,0 +1,1719 @@ +# Copyright 2013 dotCloud inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import contextlib +import json +import io +import os +import shutil +import signal +import socket +import tarfile +import tempfile +import threading +import time +import unittest +import warnings + +import pytest +import six +from six.moves import BaseHTTPServer +from six.moves import socketserver + +import docker +from docker.errors import APIError, NotFound +from docker.utils import kwargs_from_env + +from ..base import requires_api_version, Cleanup + + +# FIXME: missing tests for +# export; history; insert; port; push; tag; get; load; stats + +warnings.simplefilter('error') +compare_version = docker.utils.compare_version + +EXEC_DRIVER = [] +BUSYBOX = 'busybox:buildroot-2014.02' + + +def exec_driver_is_native(): + global EXEC_DRIVER + if not EXEC_DRIVER: + c = docker_client() + EXEC_DRIVER = c.info()['ExecutionDriver'] + c.close() + return EXEC_DRIVER.startswith('native') + + +def docker_client(**kwargs): + return docker.Client(**docker_client_kwargs(**kwargs)) + + +def docker_client_kwargs(**kwargs): + client_kwargs = kwargs_from_env(assert_hostname=False) + client_kwargs.update(kwargs) + return client_kwargs + + +def setup_module(): + c = docker_client() + try: + c.inspect_image(BUSYBOX) + except NotFound: + c.pull(BUSYBOX) + c.inspect_image(BUSYBOX) + c.close() + + +class BaseTestCase(unittest.TestCase): + tmp_imgs = [] + tmp_containers = [] + tmp_folders = [] + tmp_volumes = [] + + def setUp(self): + if six.PY2: + self.assertRegex = self.assertRegexpMatches + self.assertCountEqual = self.assertItemsEqual + self.client = docker_client(timeout=60) + self.tmp_imgs = [] + self.tmp_containers = [] + self.tmp_folders = [] + self.tmp_volumes = [] + + def tearDown(self): + for img in self.tmp_imgs: + try: + self.client.remove_image(img) + except docker.errors.APIError: + pass + for container in self.tmp_containers: + try: + self.client.stop(container, timeout=1) + self.client.remove_container(container) + except docker.errors.APIError: + pass + for folder in self.tmp_folders: + shutil.rmtree(folder) + + for volume in self.tmp_volumes: + try: + self.client.remove_volume(volume) + except docker.errors.APIError: + pass + + self.client.close() + + def run_container(self, *args, **kwargs): + container = self.client.create_container(*args, **kwargs) + self.tmp_containers.append(container) + self.client.start(container) + exitcode = self.client.wait(container) + + if exitcode != 0: + output = self.client.logs(container) + raise Exception( + "Container exited with code {}:\n{}" + .format(exitcode, output)) + + return container + + +######################### +# INFORMATION TESTS # +######################### + + +class TestVersion(BaseTestCase): + def runTest(self): + res = self.client.version() + self.assertIn('GoVersion', res) + self.assertIn('Version', res) + self.assertEqual(len(res['Version'].split('.')), 3) + + +class TestInfo(BaseTestCase): + def runTest(self): + res = self.client.info() + self.assertIn('Containers', res) + self.assertIn('Images', res) + self.assertIn('Debug', res) + + +class TestSearch(BaseTestCase): + def runTest(self): + self.client = docker_client(timeout=10) + res = self.client.search('busybox') + self.assertTrue(len(res) >= 1) + base_img = [x for x in res if x['name'] == 'busybox'] + self.assertEqual(len(base_img), 1) + self.assertIn('description', base_img[0]) + +################### +# LISTING TESTS # +################### + + +class TestImages(BaseTestCase): + def runTest(self): + res1 = self.client.images(all=True) + self.assertIn('Id', res1[0]) + res10 = res1[0] + self.assertIn('Created', res10) + self.assertIn('RepoTags', res10) + distinct = [] + for img in res1: + if img['Id'] not in distinct: + distinct.append(img['Id']) + self.assertEqual(len(distinct), self.client.info()['Images']) + + +class TestImageIds(BaseTestCase): + def runTest(self): + res1 = self.client.images(quiet=True) + self.assertEqual(type(res1[0]), six.text_type) + + +class TestListContainers(BaseTestCase): + def runTest(self): + res0 = self.client.containers(all=True) + size = len(res0) + res1 = self.client.create_container(BUSYBOX, 'true') + self.assertIn('Id', res1) + self.client.start(res1['Id']) + self.tmp_containers.append(res1['Id']) + res2 = self.client.containers(all=True) + self.assertEqual(size + 1, len(res2)) + retrieved = [x for x in res2 if x['Id'].startswith(res1['Id'])] + self.assertEqual(len(retrieved), 1) + retrieved = retrieved[0] + self.assertIn('Command', retrieved) + self.assertEqual(retrieved['Command'], six.text_type('true')) + self.assertIn('Image', retrieved) + self.assertRegex(retrieved['Image'], r'busybox:.*') + self.assertIn('Status', retrieved) + +##################### +# CONTAINER TESTS # +##################### + + +class TestCreateContainer(BaseTestCase): + def runTest(self): + res = self.client.create_container(BUSYBOX, 'true') + self.assertIn('Id', res) + self.tmp_containers.append(res['Id']) + + +class TestCreateContainerWithBinds(BaseTestCase): + def setUp(self): + super(TestCreateContainerWithBinds, self).setUp() + + self.mount_dest = '/mnt' + + # Get a random pathname - we don't need it to exist locally + self.mount_origin = tempfile.mkdtemp() + shutil.rmtree(self.mount_origin) + + self.filename = 'shared.txt' + + self.run_with_volume( + False, + BUSYBOX, + ['touch', os.path.join(self.mount_dest, self.filename)], + ) + + def run_with_volume(self, ro, *args, **kwargs): + return self.run_container( + *args, + volumes={self.mount_dest: {}}, + host_config=self.client.create_host_config( + binds={ + self.mount_origin: { + 'bind': self.mount_dest, + 'ro': ro, + }, + }, + network_mode='none' + ), + **kwargs + ) + + def test_rw(self): + container = self.run_with_volume( + False, + BUSYBOX, + ['ls', self.mount_dest], + ) + logs = self.client.logs(container) + + if six.PY3: + logs = logs.decode('utf-8') + self.assertIn(self.filename, logs) + inspect_data = self.client.inspect_container(container) + self.check_container_data(inspect_data, True) + + def test_ro(self): + container = self.run_with_volume( + True, + BUSYBOX, + ['ls', self.mount_dest], + ) + logs = self.client.logs(container) + + if six.PY3: + logs = logs.decode('utf-8') + self.assertIn(self.filename, logs) + + inspect_data = self.client.inspect_container(container) + self.check_container_data(inspect_data, False) + + def check_container_data(self, inspect_data, rw): + if docker.utils.compare_version('1.20', self.client._version) < 0: + self.assertIn('Volumes', inspect_data) + self.assertIn(self.mount_dest, inspect_data['Volumes']) + self.assertEqual( + self.mount_origin, inspect_data['Volumes'][self.mount_dest] + ) + self.assertIn(self.mount_dest, inspect_data['VolumesRW']) + self.assertFalse(inspect_data['VolumesRW'][self.mount_dest]) + else: + self.assertIn('Mounts', inspect_data) + filtered = list(filter( + lambda x: x['Destination'] == self.mount_dest, + inspect_data['Mounts'] + )) + self.assertEqual(len(filtered), 1) + mount_data = filtered[0] + self.assertEqual(mount_data['Source'], self.mount_origin) + self.assertEqual(mount_data['RW'], rw) + + +@requires_api_version('1.20') +class CreateContainerWithGroupAddTest(BaseTestCase): + def test_group_id_ints(self): + container = self.client.create_container( + BUSYBOX, 'id -G', + host_config=self.client.create_host_config(group_add=[1000, 1001]) + ) + self.tmp_containers.append(container) + self.client.start(container) + self.client.wait(container) + + logs = self.client.logs(container) + if six.PY3: + logs = logs.decode('utf-8') + groups = logs.strip().split(' ') + self.assertIn('1000', groups) + self.assertIn('1001', groups) + + def test_group_id_strings(self): + container = self.client.create_container( + BUSYBOX, 'id -G', host_config=self.client.create_host_config( + group_add=['1000', '1001'] + ) + ) + self.tmp_containers.append(container) + self.client.start(container) + self.client.wait(container) + + logs = self.client.logs(container) + if six.PY3: + logs = logs.decode('utf-8') + + groups = logs.strip().split(' ') + self.assertIn('1000', groups) + self.assertIn('1001', groups) + + +class CreateContainerWithLogConfigTest(BaseTestCase): + def test_valid_log_driver_and_log_opt(self): + log_config = docker.utils.LogConfig( + type='json-file', + config={'max-file': '100'} + ) + + container = self.client.create_container( + BUSYBOX, ['true'], + host_config=self.client.create_host_config(log_config=log_config) + ) + self.tmp_containers.append(container['Id']) + self.client.start(container) + + info = self.client.inspect_container(container) + container_log_config = info['HostConfig']['LogConfig'] + + self.assertEqual(container_log_config['Type'], log_config.type) + self.assertEqual(container_log_config['Config'], log_config.config) + + def test_invalid_log_driver_raises_exception(self): + log_config = docker.utils.LogConfig( + type='asdf-nope', + config={} + ) + + container = self.client.create_container( + BUSYBOX, ['true'], + host_config=self.client.create_host_config(log_config=log_config) + ) + + expected_msg = "logger: no log driver named 'asdf-nope' is registered" + + with pytest.raises(APIError) as excinfo: + # raises an internal server error 500 + self.client.start(container) + + assert expected_msg in str(excinfo.value) + + @pytest.mark.skipif(True, + reason="https://github.com/docker/docker/issues/15633") + def test_valid_no_log_driver_specified(self): + log_config = docker.utils.LogConfig( + type="", + config={'max-file': '100'} + ) + + container = self.client.create_container( + BUSYBOX, ['true'], + host_config=self.client.create_host_config(log_config=log_config) + ) + self.tmp_containers.append(container['Id']) + self.client.start(container) + + info = self.client.inspect_container(container) + container_log_config = info['HostConfig']['LogConfig'] + + self.assertEqual(container_log_config['Type'], "json-file") + self.assertEqual(container_log_config['Config'], log_config.config) + + def test_valid_no_config_specified(self): + log_config = docker.utils.LogConfig( + type="json-file", + config=None + ) + + container = self.client.create_container( + BUSYBOX, ['true'], + host_config=self.client.create_host_config(log_config=log_config) + ) + self.tmp_containers.append(container['Id']) + self.client.start(container) + + info = self.client.inspect_container(container) + container_log_config = info['HostConfig']['LogConfig'] + + self.assertEqual(container_log_config['Type'], "json-file") + self.assertEqual(container_log_config['Config'], {}) + + +class TestCreateContainerReadOnlyFs(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + ctnr = self.client.create_container( + BUSYBOX, ['mkdir', '/shrine'], + host_config=self.client.create_host_config( + read_only=True, network_mode='none' + ) + ) + self.assertIn('Id', ctnr) + self.tmp_containers.append(ctnr['Id']) + self.client.start(ctnr) + res = self.client.wait(ctnr) + self.assertNotEqual(res, 0) + + +class TestCreateContainerWithName(BaseTestCase): + def runTest(self): + res = self.client.create_container(BUSYBOX, 'true', name='foobar') + self.assertIn('Id', res) + self.tmp_containers.append(res['Id']) + inspect = self.client.inspect_container(res['Id']) + self.assertIn('Name', inspect) + self.assertEqual('/foobar', inspect['Name']) + + +class TestRenameContainer(BaseTestCase): + def runTest(self): + version = self.client.version()['Version'] + name = 'hong_meiling' + res = self.client.create_container(BUSYBOX, 'true') + self.assertIn('Id', res) + self.tmp_containers.append(res['Id']) + self.client.rename(res, name) + inspect = self.client.inspect_container(res['Id']) + self.assertIn('Name', inspect) + if version == '1.5.0': + self.assertEqual(name, inspect['Name']) + else: + self.assertEqual('/{0}'.format(name), inspect['Name']) + + +class TestStartContainer(BaseTestCase): + def runTest(self): + res = self.client.create_container(BUSYBOX, 'true') + self.assertIn('Id', res) + self.tmp_containers.append(res['Id']) + self.client.start(res['Id']) + inspect = self.client.inspect_container(res['Id']) + self.assertIn('Config', inspect) + self.assertIn('Id', inspect) + self.assertTrue(inspect['Id'].startswith(res['Id'])) + self.assertIn('Image', inspect) + self.assertIn('State', inspect) + self.assertIn('Running', inspect['State']) + if not inspect['State']['Running']: + self.assertIn('ExitCode', inspect['State']) + self.assertEqual(inspect['State']['ExitCode'], 0) + + +class TestStartContainerWithDictInsteadOfId(BaseTestCase): + def runTest(self): + res = self.client.create_container(BUSYBOX, 'true') + self.assertIn('Id', res) + self.tmp_containers.append(res['Id']) + self.client.start(res) + inspect = self.client.inspect_container(res['Id']) + self.assertIn('Config', inspect) + self.assertIn('Id', inspect) + self.assertTrue(inspect['Id'].startswith(res['Id'])) + self.assertIn('Image', inspect) + self.assertIn('State', inspect) + self.assertIn('Running', inspect['State']) + if not inspect['State']['Running']: + self.assertIn('ExitCode', inspect['State']) + self.assertEqual(inspect['State']['ExitCode'], 0) + + +class TestCreateContainerPrivileged(BaseTestCase): + def runTest(self): + res = self.client.create_container( + BUSYBOX, 'true', host_config=self.client.create_host_config( + privileged=True, network_mode='none' + ) + ) + self.assertIn('Id', res) + self.tmp_containers.append(res['Id']) + self.client.start(res['Id']) + inspect = self.client.inspect_container(res['Id']) + self.assertIn('Config', inspect) + self.assertIn('Id', inspect) + self.assertTrue(inspect['Id'].startswith(res['Id'])) + self.assertIn('Image', inspect) + self.assertIn('State', inspect) + self.assertIn('Running', inspect['State']) + if not inspect['State']['Running']: + self.assertIn('ExitCode', inspect['State']) + self.assertEqual(inspect['State']['ExitCode'], 0) + # Since Nov 2013, the Privileged flag is no longer part of the + # container's config exposed via the API (safety concerns?). + # + if 'Privileged' in inspect['Config']: + self.assertEqual(inspect['Config']['Privileged'], True) + + +class TestWait(BaseTestCase): + def runTest(self): + res = self.client.create_container(BUSYBOX, ['sleep', '3']) + id = res['Id'] + self.tmp_containers.append(id) + self.client.start(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0) + inspect = self.client.inspect_container(id) + self.assertIn('Running', inspect['State']) + self.assertEqual(inspect['State']['Running'], False) + self.assertIn('ExitCode', inspect['State']) + self.assertEqual(inspect['State']['ExitCode'], exitcode) + + +class TestWaitWithDictInsteadOfId(BaseTestCase): + def runTest(self): + res = self.client.create_container(BUSYBOX, ['sleep', '3']) + id = res['Id'] + self.tmp_containers.append(id) + self.client.start(res) + exitcode = self.client.wait(res) + self.assertEqual(exitcode, 0) + inspect = self.client.inspect_container(res) + self.assertIn('Running', inspect['State']) + self.assertEqual(inspect['State']['Running'], False) + self.assertIn('ExitCode', inspect['State']) + self.assertEqual(inspect['State']['ExitCode'], exitcode) + + +class TestLogs(BaseTestCase): + def runTest(self): + snippet = 'Flowering Nights (Sakuya Iyazoi)' + container = self.client.create_container( + BUSYBOX, 'echo {0}'.format(snippet) + ) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0) + logs = self.client.logs(id) + self.assertEqual(logs, (snippet + '\n').encode(encoding='ascii')) + + +class TestLogsWithTailOption(BaseTestCase): + def runTest(self): + snippet = '''Line1 +Line2''' + container = self.client.create_container( + BUSYBOX, 'echo "{0}"'.format(snippet) + ) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0) + logs = self.client.logs(id, tail=1) + self.assertEqual(logs, ('Line2\n').encode(encoding='ascii')) + + +# class TestLogsStreaming(BaseTestCase): +# def runTest(self): +# snippet = 'Flowering Nights (Sakuya Iyazoi)' +# container = self.client.create_container( +# BUSYBOX, 'echo {0}'.format(snippet) +# ) +# id = container['Id'] +# self.client.start(id) +# self.tmp_containers.append(id) +# logs = bytes() if six.PY3 else str() +# for chunk in self.client.logs(id, stream=True): +# logs += chunk + +# exitcode = self.client.wait(id) +# self.assertEqual(exitcode, 0) + +# self.assertEqual(logs, (snippet + '\n').encode(encoding='ascii')) + + +class TestLogsWithDictInsteadOfId(BaseTestCase): + def runTest(self): + snippet = 'Flowering Nights (Sakuya Iyazoi)' + container = self.client.create_container( + BUSYBOX, 'echo {0}'.format(snippet) + ) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0) + logs = self.client.logs(container) + self.assertEqual(logs, (snippet + '\n').encode(encoding='ascii')) + + +class TestDiff(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['touch', '/test']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0) + diff = self.client.diff(id) + test_diff = [x for x in diff if x.get('Path', None) == '/test'] + self.assertEqual(len(test_diff), 1) + self.assertIn('Kind', test_diff[0]) + self.assertEqual(test_diff[0]['Kind'], 1) + + +class TestDiffWithDictInsteadOfId(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['touch', '/test']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0) + diff = self.client.diff(container) + test_diff = [x for x in diff if x.get('Path', None) == '/test'] + self.assertEqual(len(test_diff), 1) + self.assertIn('Kind', test_diff[0]) + self.assertEqual(test_diff[0]['Kind'], 1) + + +class TestStop(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + self.client.stop(id, timeout=2) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + if exec_driver_is_native(): + self.assertNotEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], False) + + +class TestStopWithDictInsteadOfId(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + self.assertIn('Id', container) + id = container['Id'] + self.client.start(container) + self.tmp_containers.append(id) + self.client.stop(container, timeout=2) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + if exec_driver_is_native(): + self.assertNotEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], False) + + +class TestKill(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + self.client.kill(id) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + if exec_driver_is_native(): + self.assertNotEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], False) + + +class TestKillWithDictInsteadOfId(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + self.client.kill(container) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + if exec_driver_is_native(): + self.assertNotEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], False) + + +class TestKillWithSignal(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '60']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + self.client.kill(id, signal=signal.SIGKILL) + exitcode = self.client.wait(id) + self.assertNotEqual(exitcode, 0) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + self.assertNotEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], False, state) + + +class TestPort(BaseTestCase): + def runTest(self): + + port_bindings = { + '1111': ('127.0.0.1', '4567'), + '2222': ('127.0.0.1', '4568') + } + + container = self.client.create_container( + BUSYBOX, ['sleep', '60'], ports=list(port_bindings.keys()), + host_config=self.client.create_host_config( + port_bindings=port_bindings, network_mode='bridge' + ) + ) + id = container['Id'] + + self.client.start(container) + + # Call the port function on each biding and compare expected vs actual + for port in port_bindings: + actual_bindings = self.client.port(container, port) + port_binding = actual_bindings.pop() + + ip, host_port = port_binding['HostIp'], port_binding['HostPort'] + + self.assertEqual(ip, port_bindings[port][0]) + self.assertEqual(host_port, port_bindings[port][1]) + + self.client.kill(id) + + +class TestMacAddress(BaseTestCase): + def runTest(self): + mac_address_expected = "02:42:ac:11:00:0a" + container = self.client.create_container( + BUSYBOX, ['sleep', '60'], mac_address=mac_address_expected) + + id = container['Id'] + + self.client.start(container) + res = self.client.inspect_container(container['Id']) + self.assertEqual(mac_address_expected, + res['NetworkSettings']['MacAddress']) + + self.client.kill(id) + + +class TestRestart(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + info = self.client.inspect_container(id) + self.assertIn('State', info) + self.assertIn('StartedAt', info['State']) + start_time1 = info['State']['StartedAt'] + self.client.restart(id, timeout=2) + info2 = self.client.inspect_container(id) + self.assertIn('State', info2) + self.assertIn('StartedAt', info2['State']) + start_time2 = info2['State']['StartedAt'] + self.assertNotEqual(start_time1, start_time2) + self.assertIn('Running', info2['State']) + self.assertEqual(info2['State']['Running'], True) + self.client.kill(id) + + +class TestRestartWithDictInsteadOfId(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + self.assertIn('Id', container) + id = container['Id'] + self.client.start(container) + self.tmp_containers.append(id) + info = self.client.inspect_container(id) + self.assertIn('State', info) + self.assertIn('StartedAt', info['State']) + start_time1 = info['State']['StartedAt'] + self.client.restart(container, timeout=2) + info2 = self.client.inspect_container(id) + self.assertIn('State', info2) + self.assertIn('StartedAt', info2['State']) + start_time2 = info2['State']['StartedAt'] + self.assertNotEqual(start_time1, start_time2) + self.assertIn('Running', info2['State']) + self.assertEqual(info2['State']['Running'], True) + self.client.kill(id) + + +class TestRemoveContainer(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['true']) + id = container['Id'] + self.client.start(id) + self.client.wait(id) + self.client.remove_container(id) + containers = self.client.containers(all=True) + res = [x for x in containers if 'Id' in x and x['Id'].startswith(id)] + self.assertEqual(len(res), 0) + + +class TestRemoveContainerWithDictInsteadOfId(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['true']) + id = container['Id'] + self.client.start(id) + self.client.wait(id) + self.client.remove_container(container) + containers = self.client.containers(all=True) + res = [x for x in containers if 'Id' in x and x['Id'].startswith(id)] + self.assertEqual(len(res), 0) + + +class TestCreateContainerWithVolumesFrom(BaseTestCase): + def runTest(self): + vol_names = ['foobar_vol0', 'foobar_vol1'] + + res0 = self.client.create_container( + BUSYBOX, 'true', name=vol_names[0] + ) + container1_id = res0['Id'] + self.tmp_containers.append(container1_id) + self.client.start(container1_id) + + res1 = self.client.create_container( + BUSYBOX, 'true', name=vol_names[1] + ) + container2_id = res1['Id'] + self.tmp_containers.append(container2_id) + self.client.start(container2_id) + with self.assertRaises(docker.errors.DockerException): + self.client.create_container( + BUSYBOX, 'cat', detach=True, stdin_open=True, + volumes_from=vol_names + ) + res2 = self.client.create_container( + BUSYBOX, 'cat', detach=True, stdin_open=True, + host_config=self.client.create_host_config( + volumes_from=vol_names, network_mode='none' + ) + ) + container3_id = res2['Id'] + self.tmp_containers.append(container3_id) + self.client.start(container3_id) + + info = self.client.inspect_container(res2['Id']) + self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names) + + +class TestCreateContainerWithLinks(BaseTestCase): + def runTest(self): + res0 = self.client.create_container( + BUSYBOX, 'cat', + detach=True, stdin_open=True, + environment={'FOO': '1'}) + + container1_id = res0['Id'] + self.tmp_containers.append(container1_id) + + self.client.start(container1_id) + + res1 = self.client.create_container( + BUSYBOX, 'cat', + detach=True, stdin_open=True, + environment={'FOO': '1'}) + + container2_id = res1['Id'] + self.tmp_containers.append(container2_id) + + self.client.start(container2_id) + + # we don't want the first / + link_path1 = self.client.inspect_container(container1_id)['Name'][1:] + link_alias1 = 'mylink1' + link_env_prefix1 = link_alias1.upper() + + link_path2 = self.client.inspect_container(container2_id)['Name'][1:] + link_alias2 = 'mylink2' + link_env_prefix2 = link_alias2.upper() + + res2 = self.client.create_container( + BUSYBOX, 'env', host_config=self.client.create_host_config( + links={link_path1: link_alias1, link_path2: link_alias2}, + network_mode='none' + ) + ) + container3_id = res2['Id'] + self.tmp_containers.append(container3_id) + self.client.start(container3_id) + self.assertEqual(self.client.wait(container3_id), 0) + + logs = self.client.logs(container3_id) + if six.PY3: + logs = logs.decode('utf-8') + self.assertIn('{0}_NAME='.format(link_env_prefix1), logs) + self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs) + self.assertIn('{0}_NAME='.format(link_env_prefix2), logs) + self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix2), logs) + + +class TestRestartingContainer(BaseTestCase): + def runTest(self): + container = self.client.create_container( + BUSYBOX, ['sleep', '2'], + host_config=self.client.create_host_config( + restart_policy={"Name": "always", "MaximumRetryCount": 0}, + network_mode='none' + ) + ) + id = container['Id'] + self.client.start(id) + self.client.wait(id) + with self.assertRaises(docker.errors.APIError) as exc: + self.client.remove_container(id) + err = exc.exception.response.text + self.assertIn( + 'You cannot remove a running container', err + ) + self.client.remove_container(id, force=True) + + +class TestExecuteCommand(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + container = self.client.create_container(BUSYBOX, 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + res = self.client.exec_create(id, ['echo', 'hello']) + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) + self.assertEqual(exec_log, b'hello\n') + + +class TestExecuteCommandString(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + container = self.client.create_container(BUSYBOX, 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + res = self.client.exec_create(id, 'echo hello world') + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) + self.assertEqual(exec_log, b'hello world\n') + + +class TestExecuteCommandStringAsUser(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + container = self.client.create_container(BUSYBOX, 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + res = self.client.exec_create(id, 'whoami', user='default') + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) + self.assertEqual(exec_log, b'default\n') + + +class TestExecuteCommandStringAsRoot(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + container = self.client.create_container(BUSYBOX, 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + res = self.client.exec_create(id, 'whoami') + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) + self.assertEqual(exec_log, b'root\n') + + +class TestExecuteCommandStreaming(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + container = self.client.create_container(BUSYBOX, 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + exec_id = self.client.exec_create(id, ['echo', 'hello\nworld']) + self.assertIn('Id', exec_id) + + res = b'' + for chunk in self.client.exec_start(exec_id, stream=True): + res += chunk + self.assertEqual(res, b'hello\nworld\n') + + +class TestExecInspect(BaseTestCase): + def runTest(self): + if not exec_driver_is_native(): + pytest.skip('Exec driver not native') + + container = self.client.create_container(BUSYBOX, 'cat', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + + exec_id = self.client.exec_create(id, ['mkdir', '/does/not/exist']) + self.assertIn('Id', exec_id) + self.client.exec_start(exec_id) + exec_info = self.client.exec_inspect(exec_id) + self.assertIn('ExitCode', exec_info) + self.assertNotEqual(exec_info['ExitCode'], 0) + + +class TestRunContainerStreaming(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, '/bin/sh', + detach=True, stdin_open=True) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + sock = self.client.attach_socket(container, ws=False) + self.assertTrue(sock.fileno() > -1) + + +class TestPauseUnpauseContainer(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['sleep', '9999']) + id = container['Id'] + self.tmp_containers.append(id) + self.client.start(container) + self.client.pause(id) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + self.assertEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], True) + self.assertIn('Paused', state) + self.assertEqual(state['Paused'], True) + + self.client.unpause(id) + container_info = self.client.inspect_container(id) + self.assertIn('State', container_info) + state = container_info['State'] + self.assertIn('ExitCode', state) + self.assertEqual(state['ExitCode'], 0) + self.assertIn('Running', state) + self.assertEqual(state['Running'], True) + self.assertIn('Paused', state) + self.assertEqual(state['Paused'], False) + + +class TestCreateContainerWithHostPidMode(BaseTestCase): + def runTest(self): + ctnr = self.client.create_container( + BUSYBOX, 'true', host_config=self.client.create_host_config( + pid_mode='host', network_mode='none' + ) + ) + self.assertIn('Id', ctnr) + self.tmp_containers.append(ctnr['Id']) + self.client.start(ctnr) + inspect = self.client.inspect_container(ctnr) + self.assertIn('HostConfig', inspect) + host_config = inspect['HostConfig'] + self.assertIn('PidMode', host_config) + self.assertEqual(host_config['PidMode'], 'host') + + +################# +# LINKS TESTS # +################# + + +class TestRemoveLink(BaseTestCase): + def runTest(self): + # Create containers + container1 = self.client.create_container( + BUSYBOX, 'cat', detach=True, stdin_open=True + ) + container1_id = container1['Id'] + self.tmp_containers.append(container1_id) + self.client.start(container1_id) + + # Create Link + # we don't want the first / + link_path = self.client.inspect_container(container1_id)['Name'][1:] + link_alias = 'mylink' + + container2 = self.client.create_container( + BUSYBOX, 'cat', host_config=self.client.create_host_config( + links={link_path: link_alias}, network_mode='none' + ) + ) + container2_id = container2['Id'] + self.tmp_containers.append(container2_id) + self.client.start(container2_id) + + # Remove link + linked_name = self.client.inspect_container(container2_id)['Name'][1:] + link_name = '%s/%s' % (linked_name, link_alias) + self.client.remove_container(link_name, link=True) + + # Link is gone + containers = self.client.containers(all=True) + retrieved = [x for x in containers if link_name in x['Names']] + self.assertEqual(len(retrieved), 0) + + # Containers are still there + retrieved = [ + x for x in containers if x['Id'].startswith(container1_id) or + x['Id'].startswith(container2_id) + ] + self.assertEqual(len(retrieved), 2) + +################## +# IMAGES TESTS # +################## + + +class TestPull(BaseTestCase): + def runTest(self): + try: + self.client.remove_image('hello-world') + except docker.errors.APIError: + pass + res = self.client.pull('hello-world') + self.tmp_imgs.append('hello-world') + self.assertEqual(type(res), six.text_type) + self.assertGreaterEqual( + len(self.client.images('hello-world')), 1 + ) + img_info = self.client.inspect_image('hello-world') + self.assertIn('Id', img_info) + + +class TestPullStream(BaseTestCase): + def runTest(self): + try: + self.client.remove_image('hello-world') + except docker.errors.APIError: + pass + stream = self.client.pull('hello-world', stream=True) + self.tmp_imgs.append('hello-world') + for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') + json.loads(chunk) # ensure chunk is a single, valid JSON blob + self.assertGreaterEqual( + len(self.client.images('hello-world')), 1 + ) + img_info = self.client.inspect_image('hello-world') + self.assertIn('Id', img_info) + + +class TestCommit(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['touch', '/test']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + res = self.client.commit(id) + self.assertIn('Id', res) + img_id = res['Id'] + self.tmp_imgs.append(img_id) + img = self.client.inspect_image(img_id) + self.assertIn('Container', img) + self.assertTrue(img['Container'].startswith(id)) + self.assertIn('ContainerConfig', img) + self.assertIn('Image', img['ContainerConfig']) + self.assertEqual(BUSYBOX, img['ContainerConfig']['Image']) + busybox_id = self.client.inspect_image(BUSYBOX)['Id'] + self.assertIn('Parent', img) + self.assertEqual(img['Parent'], busybox_id) + + +class TestRemoveImage(BaseTestCase): + def runTest(self): + container = self.client.create_container(BUSYBOX, ['touch', '/test']) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + res = self.client.commit(id) + self.assertIn('Id', res) + img_id = res['Id'] + self.tmp_imgs.append(img_id) + self.client.remove_image(img_id, force=True) + images = self.client.images(all=True) + res = [x for x in images if x['Id'].startswith(img_id)] + self.assertEqual(len(res), 0) + + +################## +# IMPORT TESTS # +################## + + +class ImportTestCase(BaseTestCase): + '''Base class for `docker import` test cases.''' + + TAR_SIZE = 512 * 1024 + + def write_dummy_tar_content(self, n_bytes, tar_fd): + def extend_file(f, n_bytes): + f.seek(n_bytes - 1) + f.write(bytearray([65])) + f.seek(0) + + tar = tarfile.TarFile(fileobj=tar_fd, mode='w') + + with tempfile.NamedTemporaryFile() as f: + extend_file(f, n_bytes) + tarinfo = tar.gettarinfo(name=f.name, arcname='testdata') + tar.addfile(tarinfo, fileobj=f) + + tar.close() + + @contextlib.contextmanager + def dummy_tar_stream(self, n_bytes): + '''Yields a stream that is valid tar data of size n_bytes.''' + with tempfile.NamedTemporaryFile() as tar_file: + self.write_dummy_tar_content(n_bytes, tar_file) + tar_file.seek(0) + yield tar_file + + @contextlib.contextmanager + def dummy_tar_file(self, n_bytes): + '''Yields the name of a valid tar file of size n_bytes.''' + with tempfile.NamedTemporaryFile() as tar_file: + self.write_dummy_tar_content(n_bytes, tar_file) + tar_file.seek(0) + yield tar_file.name + + +class TestImportFromBytes(ImportTestCase): + '''Tests importing an image from in-memory byte data.''' + + def runTest(self): + with self.dummy_tar_stream(n_bytes=500) as f: + content = f.read() + + # The generic import_image() function cannot import in-memory bytes + # data that happens to be represented as a string type, because + # import_image() will try to use it as a filename and usually then + # trigger an exception. So we test the import_image_from_data() + # function instead. + statuses = self.client.import_image_from_data( + content, repository='test/import-from-bytes') + + result_text = statuses.splitlines()[-1] + result = json.loads(result_text) + + self.assertNotIn('error', result) + + img_id = result['status'] + self.tmp_imgs.append(img_id) + + +class TestImportFromFile(ImportTestCase): + '''Tests importing an image from a tar file on disk.''' + + def runTest(self): + with self.dummy_tar_file(n_bytes=self.TAR_SIZE) as tar_filename: + # statuses = self.client.import_image( + # src=tar_filename, repository='test/import-from-file') + statuses = self.client.import_image_from_file( + tar_filename, repository='test/import-from-file') + + result_text = statuses.splitlines()[-1] + result = json.loads(result_text) + + self.assertNotIn('error', result) + + self.assertIn('status', result) + img_id = result['status'] + self.tmp_imgs.append(img_id) + + +class TestImportFromStream(ImportTestCase): + '''Tests importing an image from a stream containing tar data.''' + + def runTest(self): + with self.dummy_tar_stream(n_bytes=self.TAR_SIZE) as tar_stream: + statuses = self.client.import_image( + src=tar_stream, repository='test/import-from-stream') + # statuses = self.client.import_image_from_stream( + # tar_stream, repository='test/import-from-stream') + result_text = statuses.splitlines()[-1] + result = json.loads(result_text) + + self.assertNotIn('error', result) + + self.assertIn('status', result) + img_id = result['status'] + self.tmp_imgs.append(img_id) + + +class TestImportFromURL(ImportTestCase): + '''Tests downloading an image over HTTP.''' + + @contextlib.contextmanager + def temporary_http_file_server(self, stream): + '''Serve data from an IO stream over HTTP.''' + + class Handler(BaseHTTPServer.BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'application/x-tar') + self.end_headers() + shutil.copyfileobj(stream, self.wfile) + + server = socketserver.TCPServer(('', 0), Handler) + thread = threading.Thread(target=server.serve_forever) + thread.setDaemon(True) + thread.start() + + yield 'http://%s:%s' % (socket.gethostname(), server.server_address[1]) + + server.shutdown() + + @pytest.mark.skipif(True, reason="Doesn't work inside a container - FIXME") + def runTest(self): + # The crappy test HTTP server doesn't handle large files well, so use + # a small file. + TAR_SIZE = 10240 + + with self.dummy_tar_stream(n_bytes=TAR_SIZE) as tar_data: + with self.temporary_http_file_server(tar_data) as url: + statuses = self.client.import_image( + src=url, repository='test/import-from-url') + + result_text = statuses.splitlines()[-1] + result = json.loads(result_text) + + self.assertNotIn('error', result) + + self.assertIn('status', result) + img_id = result['status'] + self.tmp_imgs.append(img_id) + + +################# +# VOLUMES TESTS # +################# + +@requires_api_version('1.21') +class TestVolumes(BaseTestCase): + def test_create_volume(self): + name = 'perfectcherryblossom' + self.tmp_volumes.append(name) + result = self.client.create_volume(name) + self.assertIn('Name', result) + self.assertEqual(result['Name'], name) + self.assertIn('Driver', result) + self.assertEqual(result['Driver'], 'local') + + def test_create_volume_invalid_driver(self): + driver_name = 'invalid.driver' + + with pytest.raises(docker.errors.NotFound): + self.client.create_volume('perfectcherryblossom', driver_name) + + def test_list_volumes(self): + name = 'imperishablenight' + self.tmp_volumes.append(name) + volume_info = self.client.create_volume(name) + result = self.client.volumes() + self.assertIn('Volumes', result) + volumes = result['Volumes'] + self.assertIn(volume_info, volumes) + + def test_inspect_volume(self): + name = 'embodimentofscarletdevil' + self.tmp_volumes.append(name) + volume_info = self.client.create_volume(name) + result = self.client.inspect_volume(name) + self.assertEqual(volume_info, result) + + def test_inspect_nonexistent_volume(self): + name = 'embodimentofscarletdevil' + with pytest.raises(docker.errors.NotFound): + self.client.inspect_volume(name) + + def test_remove_volume(self): + name = 'shootthebullet' + self.tmp_volumes.append(name) + self.client.create_volume(name) + result = self.client.remove_volume(name) + self.assertTrue(result) + + def test_remove_nonexistent_volume(self): + name = 'shootthebullet' + with pytest.raises(docker.errors.NotFound): + self.client.remove_volume(name) + + +################# +# BUILDER TESTS # +################# + +class TestBuildStream(BaseTestCase): + def runTest(self): + script = io.BytesIO('\n'.join([ + 'FROM busybox', + 'MAINTAINER docker-py', + 'RUN mkdir -p /tmp/test', + 'EXPOSE 8080', + 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' + ' /tmp/silence.tar.gz' + ]).encode('ascii')) + stream = self.client.build(fileobj=script, stream=True) + logs = '' + for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') + json.loads(chunk) # ensure chunk is a single, valid JSON blob + logs += chunk + self.assertNotEqual(logs, '') + + +class TestBuildFromStringIO(BaseTestCase): + def runTest(self): + if six.PY3: + return + script = io.StringIO(six.text_type('\n').join([ + 'FROM busybox', + 'MAINTAINER docker-py', + 'RUN mkdir -p /tmp/test', + 'EXPOSE 8080', + 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' + ' /tmp/silence.tar.gz' + ])) + stream = self.client.build(fileobj=script, stream=True) + logs = '' + for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') + logs += chunk + self.assertNotEqual(logs, '') + + +@requires_api_version('1.8') +class TestBuildWithDockerignore(Cleanup, BaseTestCase): + def runTest(self): + base_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, base_dir) + + with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: + f.write("\n".join([ + 'FROM busybox', + 'MAINTAINER docker-py', + 'ADD . /test', + ])) + + with open(os.path.join(base_dir, '.dockerignore'), 'w') as f: + f.write("\n".join([ + 'ignored', + 'Dockerfile', + '.dockerignore', + '', # empty line + ])) + + with open(os.path.join(base_dir, 'not-ignored'), 'w') as f: + f.write("this file should not be ignored") + + subdir = os.path.join(base_dir, 'ignored', 'subdir') + os.makedirs(subdir) + with open(os.path.join(subdir, 'file'), 'w') as f: + f.write("this file should be ignored") + + tag = 'docker-py-test-build-with-dockerignore' + stream = self.client.build( + path=base_dir, + tag=tag, + ) + for chunk in stream: + pass + + c = self.client.create_container(tag, ['ls', '-1A', '/test']) + self.client.start(c) + self.client.wait(c) + logs = self.client.logs(c) + + if six.PY3: + logs = logs.decode('utf-8') + + self.assertEqual( + list(filter(None, logs.split('\n'))), + ['not-ignored'], + ) + +####################### +# PY SPECIFIC TESTS # +####################### + + +class TestRunShlex(BaseTestCase): + def runTest(self): + commands = [ + 'true', + 'echo "The Young Descendant of Tepes & Septette for the ' + 'Dead Princess"', + 'echo -n "The Young Descendant of Tepes & Septette for the ' + 'Dead Princess"', + '/bin/sh -c "echo Hello World"', + '/bin/sh -c \'echo "Hello World"\'', + 'echo "\"Night of Nights\""', + 'true && echo "Night of Nights"' + ] + for cmd in commands: + container = self.client.create_container(BUSYBOX, cmd) + id = container['Id'] + self.client.start(id) + self.tmp_containers.append(id) + exitcode = self.client.wait(id) + self.assertEqual(exitcode, 0, msg=cmd) + + +class TestLoadConfig(BaseTestCase): + def runTest(self): + folder = tempfile.mkdtemp() + self.tmp_folders.append(folder) + cfg_path = os.path.join(folder, '.dockercfg') + f = open(cfg_path, 'w') + auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') + f.write('auth = {0}\n'.format(auth_)) + f.write('email = sakuya@scarlet.net') + f.close() + cfg = docker.auth.load_config(cfg_path) + self.assertNotEqual(cfg[docker.auth.INDEX_NAME], None) + cfg = cfg[docker.auth.INDEX_NAME] + self.assertEqual(cfg['username'], 'sakuya') + self.assertEqual(cfg['password'], 'izayoi') + self.assertEqual(cfg['email'], 'sakuya@scarlet.net') + self.assertEqual(cfg.get('Auth'), None) + + +class TestLoadJSONConfig(BaseTestCase): + def runTest(self): + folder = tempfile.mkdtemp() + self.tmp_folders.append(folder) + cfg_path = os.path.join(folder, '.dockercfg') + f = open(os.path.join(folder, '.dockercfg'), 'w') + auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') + email_ = 'sakuya@scarlet.net' + f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format( + docker.auth.INDEX_URL, auth_, email_)) + f.close() + cfg = docker.auth.load_config(cfg_path) + self.assertNotEqual(cfg[docker.auth.INDEX_URL], None) + cfg = cfg[docker.auth.INDEX_URL] + self.assertEqual(cfg['username'], 'sakuya') + self.assertEqual(cfg['password'], 'izayoi') + self.assertEqual(cfg['email'], 'sakuya@scarlet.net') + self.assertEqual(cfg.get('Auth'), None) + + +class TestAutoDetectVersion(unittest.TestCase): + def test_client_init(self): + client = docker_client(version='auto') + client_version = client._version + api_version = client.version(api_version=False)['ApiVersion'] + self.assertEqual(client_version, api_version) + api_version_2 = client.version()['ApiVersion'] + self.assertEqual(client_version, api_version_2) + client.close() + + def test_auto_client(self): + client = docker.AutoVersionClient(**docker_client_kwargs()) + client_version = client._version + api_version = client.version(api_version=False)['ApiVersion'] + self.assertEqual(client_version, api_version) + api_version_2 = client.version()['ApiVersion'] + self.assertEqual(client_version, api_version_2) + client.close() + with self.assertRaises(docker.errors.DockerException): + docker.AutoVersionClient(**docker_client_kwargs(version='1.11')) + + +class TestConnectionTimeout(unittest.TestCase): + def setUp(self): + self.timeout = 0.5 + self.client = docker.client.Client(base_url='http://192.168.10.2:4243', + timeout=self.timeout) + + def runTest(self): + start = time.time() + res = None + # This call isn't supposed to complete, and it should fail fast. + try: + res = self.client.inspect_container('id') + except: + pass + end = time.time() + self.assertTrue(res is None) + self.assertTrue(end - start < 2 * self.timeout) + + +class UnixconnTestCase(unittest.TestCase): + """ + Test UNIX socket connection adapter. + """ + + def test_resource_warnings(self): + """ + Test no warnings are produced when using the client. + """ + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + + client = docker_client() + client.images() + client.close() + del client + + assert len(w) == 0, \ + "No warnings produced: {0}".format(w[0].message) + + +#################### +# REGRESSION TESTS # +#################### + +class TestRegressions(BaseTestCase): + def test_443(self): + dfile = io.BytesIO() + with self.assertRaises(docker.errors.APIError) as exc: + for line in self.client.build(fileobj=dfile, tag="a/b/c"): + pass + self.assertEqual(exc.exception.response.status_code, 500) + dfile.close() + + def test_542(self): + self.client.start( + self.client.create_container(BUSYBOX, ['true']) + ) + result = self.client.containers(all=True, trunc=True) + self.assertEqual(len(result[0]['Id']), 12) + + def test_647(self): + with self.assertRaises(docker.errors.APIError): + self.client.inspect_image('gensokyo.jp//kirisame') + + def test_649(self): + self.client.timeout = None + ctnr = self.client.create_container(BUSYBOX, ['sleep', '2']) + self.client.start(ctnr) + self.client.stop(ctnr) + + def test_715(self): + ctnr = self.client.create_container(BUSYBOX, ['id', '-u'], user=1000) + self.client.start(ctnr) + self.client.wait(ctnr) + logs = self.client.logs(ctnr) + if six.PY3: + logs = logs.decode('utf-8') + assert logs == '1000\n' diff --git a/tests/integration_test.py b/tests/integration_test.py index 763c8637bf..01987e74f1 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -1,1720 +1,4 @@ -# Copyright 2013 dotCloud inc. - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import contextlib -import json -import io -import os -import shutil -import signal -import socket -import tarfile -import tempfile -import threading -import time -import unittest -import warnings - -import pytest -import six -from six.moves import BaseHTTPServer -from six.moves import socketserver - -import docker -from docker.errors import APIError, NotFound -from docker.utils import kwargs_from_env - -from .base import requires_api_version -from .test import Cleanup - - -# FIXME: missing tests for -# export; history; insert; port; push; tag; get; load; stats - -warnings.simplefilter('error') -compare_version = docker.utils.compare_version - -EXEC_DRIVER = [] -BUSYBOX = 'busybox:buildroot-2014.02' - - -def exec_driver_is_native(): - global EXEC_DRIVER - if not EXEC_DRIVER: - c = docker_client() - EXEC_DRIVER = c.info()['ExecutionDriver'] - c.close() - return EXEC_DRIVER.startswith('native') - - -def docker_client(**kwargs): - return docker.Client(**docker_client_kwargs(**kwargs)) - - -def docker_client_kwargs(**kwargs): - client_kwargs = kwargs_from_env(assert_hostname=False) - client_kwargs.update(kwargs) - return client_kwargs - - -def setup_module(): - c = docker_client() - try: - c.inspect_image(BUSYBOX) - except NotFound: - c.pull(BUSYBOX) - c.inspect_image(BUSYBOX) - c.close() - - -class BaseTestCase(unittest.TestCase): - tmp_imgs = [] - tmp_containers = [] - tmp_folders = [] - tmp_volumes = [] - - def setUp(self): - if six.PY2: - self.assertRegex = self.assertRegexpMatches - self.assertCountEqual = self.assertItemsEqual - self.client = docker_client(timeout=60) - self.tmp_imgs = [] - self.tmp_containers = [] - self.tmp_folders = [] - self.tmp_volumes = [] - - def tearDown(self): - for img in self.tmp_imgs: - try: - self.client.remove_image(img) - except docker.errors.APIError: - pass - for container in self.tmp_containers: - try: - self.client.stop(container, timeout=1) - self.client.remove_container(container) - except docker.errors.APIError: - pass - for folder in self.tmp_folders: - shutil.rmtree(folder) - - for volume in self.tmp_volumes: - try: - self.client.remove_volume(volume) - except docker.errors.APIError: - pass - - self.client.close() - - def run_container(self, *args, **kwargs): - container = self.client.create_container(*args, **kwargs) - self.tmp_containers.append(container) - self.client.start(container) - exitcode = self.client.wait(container) - - if exitcode != 0: - output = self.client.logs(container) - raise Exception( - "Container exited with code {}:\n{}" - .format(exitcode, output)) - - return container - - -######################### -# INFORMATION TESTS # -######################### - - -class TestVersion(BaseTestCase): - def runTest(self): - res = self.client.version() - self.assertIn('GoVersion', res) - self.assertIn('Version', res) - self.assertEqual(len(res['Version'].split('.')), 3) - - -class TestInfo(BaseTestCase): - def runTest(self): - res = self.client.info() - self.assertIn('Containers', res) - self.assertIn('Images', res) - self.assertIn('Debug', res) - - -class TestSearch(BaseTestCase): - def runTest(self): - self.client = docker_client(timeout=10) - res = self.client.search('busybox') - self.assertTrue(len(res) >= 1) - base_img = [x for x in res if x['name'] == 'busybox'] - self.assertEqual(len(base_img), 1) - self.assertIn('description', base_img[0]) - -################### -# LISTING TESTS # -################### - - -class TestImages(BaseTestCase): - def runTest(self): - res1 = self.client.images(all=True) - self.assertIn('Id', res1[0]) - res10 = res1[0] - self.assertIn('Created', res10) - self.assertIn('RepoTags', res10) - distinct = [] - for img in res1: - if img['Id'] not in distinct: - distinct.append(img['Id']) - self.assertEqual(len(distinct), self.client.info()['Images']) - - -class TestImageIds(BaseTestCase): - def runTest(self): - res1 = self.client.images(quiet=True) - self.assertEqual(type(res1[0]), six.text_type) - - -class TestListContainers(BaseTestCase): - def runTest(self): - res0 = self.client.containers(all=True) - size = len(res0) - res1 = self.client.create_container(BUSYBOX, 'true') - self.assertIn('Id', res1) - self.client.start(res1['Id']) - self.tmp_containers.append(res1['Id']) - res2 = self.client.containers(all=True) - self.assertEqual(size + 1, len(res2)) - retrieved = [x for x in res2 if x['Id'].startswith(res1['Id'])] - self.assertEqual(len(retrieved), 1) - retrieved = retrieved[0] - self.assertIn('Command', retrieved) - self.assertEqual(retrieved['Command'], six.text_type('true')) - self.assertIn('Image', retrieved) - self.assertRegex(retrieved['Image'], r'busybox:.*') - self.assertIn('Status', retrieved) - -##################### -# CONTAINER TESTS # -##################### - - -class TestCreateContainer(BaseTestCase): - def runTest(self): - res = self.client.create_container(BUSYBOX, 'true') - self.assertIn('Id', res) - self.tmp_containers.append(res['Id']) - - -class TestCreateContainerWithBinds(BaseTestCase): - def setUp(self): - super(TestCreateContainerWithBinds, self).setUp() - - self.mount_dest = '/mnt' - - # Get a random pathname - we don't need it to exist locally - self.mount_origin = tempfile.mkdtemp() - shutil.rmtree(self.mount_origin) - - self.filename = 'shared.txt' - - self.run_with_volume( - False, - BUSYBOX, - ['touch', os.path.join(self.mount_dest, self.filename)], - ) - - def run_with_volume(self, ro, *args, **kwargs): - return self.run_container( - *args, - volumes={self.mount_dest: {}}, - host_config=self.client.create_host_config( - binds={ - self.mount_origin: { - 'bind': self.mount_dest, - 'ro': ro, - }, - }, - network_mode='none' - ), - **kwargs - ) - - def test_rw(self): - container = self.run_with_volume( - False, - BUSYBOX, - ['ls', self.mount_dest], - ) - logs = self.client.logs(container) - - if six.PY3: - logs = logs.decode('utf-8') - self.assertIn(self.filename, logs) - inspect_data = self.client.inspect_container(container) - self.check_container_data(inspect_data, True) - - def test_ro(self): - container = self.run_with_volume( - True, - BUSYBOX, - ['ls', self.mount_dest], - ) - logs = self.client.logs(container) - - if six.PY3: - logs = logs.decode('utf-8') - self.assertIn(self.filename, logs) - - inspect_data = self.client.inspect_container(container) - self.check_container_data(inspect_data, False) - - def check_container_data(self, inspect_data, rw): - if docker.utils.compare_version('1.20', self.client._version) < 0: - self.assertIn('Volumes', inspect_data) - self.assertIn(self.mount_dest, inspect_data['Volumes']) - self.assertEqual( - self.mount_origin, inspect_data['Volumes'][self.mount_dest] - ) - self.assertIn(self.mount_dest, inspect_data['VolumesRW']) - self.assertFalse(inspect_data['VolumesRW'][self.mount_dest]) - else: - self.assertIn('Mounts', inspect_data) - filtered = list(filter( - lambda x: x['Destination'] == self.mount_dest, - inspect_data['Mounts'] - )) - self.assertEqual(len(filtered), 1) - mount_data = filtered[0] - self.assertEqual(mount_data['Source'], self.mount_origin) - self.assertEqual(mount_data['RW'], rw) - - -@requires_api_version('1.20') -class CreateContainerWithGroupAddTest(BaseTestCase): - def test_group_id_ints(self): - container = self.client.create_container( - BUSYBOX, 'id -G', - host_config=self.client.create_host_config(group_add=[1000, 1001]) - ) - self.tmp_containers.append(container) - self.client.start(container) - self.client.wait(container) - - logs = self.client.logs(container) - if six.PY3: - logs = logs.decode('utf-8') - groups = logs.strip().split(' ') - self.assertIn('1000', groups) - self.assertIn('1001', groups) - - def test_group_id_strings(self): - container = self.client.create_container( - BUSYBOX, 'id -G', host_config=self.client.create_host_config( - group_add=['1000', '1001'] - ) - ) - self.tmp_containers.append(container) - self.client.start(container) - self.client.wait(container) - - logs = self.client.logs(container) - if six.PY3: - logs = logs.decode('utf-8') - - groups = logs.strip().split(' ') - self.assertIn('1000', groups) - self.assertIn('1001', groups) - - -class CreateContainerWithLogConfigTest(BaseTestCase): - def test_valid_log_driver_and_log_opt(self): - log_config = docker.utils.LogConfig( - type='json-file', - config={'max-file': '100'} - ) - - container = self.client.create_container( - BUSYBOX, ['true'], - host_config=self.client.create_host_config(log_config=log_config) - ) - self.tmp_containers.append(container['Id']) - self.client.start(container) - - info = self.client.inspect_container(container) - container_log_config = info['HostConfig']['LogConfig'] - - self.assertEqual(container_log_config['Type'], log_config.type) - self.assertEqual(container_log_config['Config'], log_config.config) - - def test_invalid_log_driver_raises_exception(self): - log_config = docker.utils.LogConfig( - type='asdf-nope', - config={} - ) - - container = self.client.create_container( - BUSYBOX, ['true'], - host_config=self.client.create_host_config(log_config=log_config) - ) - - expected_msg = "logger: no log driver named 'asdf-nope' is registered" - - with pytest.raises(APIError) as excinfo: - # raises an internal server error 500 - self.client.start(container) - - assert expected_msg in str(excinfo.value) - - @pytest.mark.skipif(True, - reason="https://github.com/docker/docker/issues/15633") - def test_valid_no_log_driver_specified(self): - log_config = docker.utils.LogConfig( - type="", - config={'max-file': '100'} - ) - - container = self.client.create_container( - BUSYBOX, ['true'], - host_config=self.client.create_host_config(log_config=log_config) - ) - self.tmp_containers.append(container['Id']) - self.client.start(container) - - info = self.client.inspect_container(container) - container_log_config = info['HostConfig']['LogConfig'] - - self.assertEqual(container_log_config['Type'], "json-file") - self.assertEqual(container_log_config['Config'], log_config.config) - - def test_valid_no_config_specified(self): - log_config = docker.utils.LogConfig( - type="json-file", - config=None - ) - - container = self.client.create_container( - BUSYBOX, ['true'], - host_config=self.client.create_host_config(log_config=log_config) - ) - self.tmp_containers.append(container['Id']) - self.client.start(container) - - info = self.client.inspect_container(container) - container_log_config = info['HostConfig']['LogConfig'] - - self.assertEqual(container_log_config['Type'], "json-file") - self.assertEqual(container_log_config['Config'], {}) - - -class TestCreateContainerReadOnlyFs(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - ctnr = self.client.create_container( - BUSYBOX, ['mkdir', '/shrine'], - host_config=self.client.create_host_config( - read_only=True, network_mode='none' - ) - ) - self.assertIn('Id', ctnr) - self.tmp_containers.append(ctnr['Id']) - self.client.start(ctnr) - res = self.client.wait(ctnr) - self.assertNotEqual(res, 0) - - -class TestCreateContainerWithName(BaseTestCase): - def runTest(self): - res = self.client.create_container(BUSYBOX, 'true', name='foobar') - self.assertIn('Id', res) - self.tmp_containers.append(res['Id']) - inspect = self.client.inspect_container(res['Id']) - self.assertIn('Name', inspect) - self.assertEqual('/foobar', inspect['Name']) - - -class TestRenameContainer(BaseTestCase): - def runTest(self): - version = self.client.version()['Version'] - name = 'hong_meiling' - res = self.client.create_container(BUSYBOX, 'true') - self.assertIn('Id', res) - self.tmp_containers.append(res['Id']) - self.client.rename(res, name) - inspect = self.client.inspect_container(res['Id']) - self.assertIn('Name', inspect) - if version == '1.5.0': - self.assertEqual(name, inspect['Name']) - else: - self.assertEqual('/{0}'.format(name), inspect['Name']) - - -class TestStartContainer(BaseTestCase): - def runTest(self): - res = self.client.create_container(BUSYBOX, 'true') - self.assertIn('Id', res) - self.tmp_containers.append(res['Id']) - self.client.start(res['Id']) - inspect = self.client.inspect_container(res['Id']) - self.assertIn('Config', inspect) - self.assertIn('Id', inspect) - self.assertTrue(inspect['Id'].startswith(res['Id'])) - self.assertIn('Image', inspect) - self.assertIn('State', inspect) - self.assertIn('Running', inspect['State']) - if not inspect['State']['Running']: - self.assertIn('ExitCode', inspect['State']) - self.assertEqual(inspect['State']['ExitCode'], 0) - - -class TestStartContainerWithDictInsteadOfId(BaseTestCase): - def runTest(self): - res = self.client.create_container(BUSYBOX, 'true') - self.assertIn('Id', res) - self.tmp_containers.append(res['Id']) - self.client.start(res) - inspect = self.client.inspect_container(res['Id']) - self.assertIn('Config', inspect) - self.assertIn('Id', inspect) - self.assertTrue(inspect['Id'].startswith(res['Id'])) - self.assertIn('Image', inspect) - self.assertIn('State', inspect) - self.assertIn('Running', inspect['State']) - if not inspect['State']['Running']: - self.assertIn('ExitCode', inspect['State']) - self.assertEqual(inspect['State']['ExitCode'], 0) - - -class TestCreateContainerPrivileged(BaseTestCase): - def runTest(self): - res = self.client.create_container( - BUSYBOX, 'true', host_config=self.client.create_host_config( - privileged=True, network_mode='none' - ) - ) - self.assertIn('Id', res) - self.tmp_containers.append(res['Id']) - self.client.start(res['Id']) - inspect = self.client.inspect_container(res['Id']) - self.assertIn('Config', inspect) - self.assertIn('Id', inspect) - self.assertTrue(inspect['Id'].startswith(res['Id'])) - self.assertIn('Image', inspect) - self.assertIn('State', inspect) - self.assertIn('Running', inspect['State']) - if not inspect['State']['Running']: - self.assertIn('ExitCode', inspect['State']) - self.assertEqual(inspect['State']['ExitCode'], 0) - # Since Nov 2013, the Privileged flag is no longer part of the - # container's config exposed via the API (safety concerns?). - # - if 'Privileged' in inspect['Config']: - self.assertEqual(inspect['Config']['Privileged'], True) - - -class TestWait(BaseTestCase): - def runTest(self): - res = self.client.create_container(BUSYBOX, ['sleep', '3']) - id = res['Id'] - self.tmp_containers.append(id) - self.client.start(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0) - inspect = self.client.inspect_container(id) - self.assertIn('Running', inspect['State']) - self.assertEqual(inspect['State']['Running'], False) - self.assertIn('ExitCode', inspect['State']) - self.assertEqual(inspect['State']['ExitCode'], exitcode) - - -class TestWaitWithDictInsteadOfId(BaseTestCase): - def runTest(self): - res = self.client.create_container(BUSYBOX, ['sleep', '3']) - id = res['Id'] - self.tmp_containers.append(id) - self.client.start(res) - exitcode = self.client.wait(res) - self.assertEqual(exitcode, 0) - inspect = self.client.inspect_container(res) - self.assertIn('Running', inspect['State']) - self.assertEqual(inspect['State']['Running'], False) - self.assertIn('ExitCode', inspect['State']) - self.assertEqual(inspect['State']['ExitCode'], exitcode) - - -class TestLogs(BaseTestCase): - def runTest(self): - snippet = 'Flowering Nights (Sakuya Iyazoi)' - container = self.client.create_container( - BUSYBOX, 'echo {0}'.format(snippet) - ) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0) - logs = self.client.logs(id) - self.assertEqual(logs, (snippet + '\n').encode(encoding='ascii')) - - -class TestLogsWithTailOption(BaseTestCase): - def runTest(self): - snippet = '''Line1 -Line2''' - container = self.client.create_container( - BUSYBOX, 'echo "{0}"'.format(snippet) - ) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0) - logs = self.client.logs(id, tail=1) - self.assertEqual(logs, ('Line2\n').encode(encoding='ascii')) - - -# class TestLogsStreaming(BaseTestCase): -# def runTest(self): -# snippet = 'Flowering Nights (Sakuya Iyazoi)' -# container = self.client.create_container( -# BUSYBOX, 'echo {0}'.format(snippet) -# ) -# id = container['Id'] -# self.client.start(id) -# self.tmp_containers.append(id) -# logs = bytes() if six.PY3 else str() -# for chunk in self.client.logs(id, stream=True): -# logs += chunk - -# exitcode = self.client.wait(id) -# self.assertEqual(exitcode, 0) - -# self.assertEqual(logs, (snippet + '\n').encode(encoding='ascii')) - - -class TestLogsWithDictInsteadOfId(BaseTestCase): - def runTest(self): - snippet = 'Flowering Nights (Sakuya Iyazoi)' - container = self.client.create_container( - BUSYBOX, 'echo {0}'.format(snippet) - ) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0) - logs = self.client.logs(container) - self.assertEqual(logs, (snippet + '\n').encode(encoding='ascii')) - - -class TestDiff(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['touch', '/test']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0) - diff = self.client.diff(id) - test_diff = [x for x in diff if x.get('Path', None) == '/test'] - self.assertEqual(len(test_diff), 1) - self.assertIn('Kind', test_diff[0]) - self.assertEqual(test_diff[0]['Kind'], 1) - - -class TestDiffWithDictInsteadOfId(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['touch', '/test']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0) - diff = self.client.diff(container) - test_diff = [x for x in diff if x.get('Path', None) == '/test'] - self.assertEqual(len(test_diff), 1) - self.assertIn('Kind', test_diff[0]) - self.assertEqual(test_diff[0]['Kind'], 1) - - -class TestStop(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - self.client.stop(id, timeout=2) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - if exec_driver_is_native(): - self.assertNotEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], False) - - -class TestStopWithDictInsteadOfId(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - self.assertIn('Id', container) - id = container['Id'] - self.client.start(container) - self.tmp_containers.append(id) - self.client.stop(container, timeout=2) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - if exec_driver_is_native(): - self.assertNotEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], False) - - -class TestKill(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - self.client.kill(id) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - if exec_driver_is_native(): - self.assertNotEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], False) - - -class TestKillWithDictInsteadOfId(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - self.client.kill(container) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - if exec_driver_is_native(): - self.assertNotEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], False) - - -class TestKillWithSignal(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '60']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - self.client.kill(id, signal=signal.SIGKILL) - exitcode = self.client.wait(id) - self.assertNotEqual(exitcode, 0) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - self.assertNotEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], False, state) - - -class TestPort(BaseTestCase): - def runTest(self): - - port_bindings = { - '1111': ('127.0.0.1', '4567'), - '2222': ('127.0.0.1', '4568') - } - - container = self.client.create_container( - BUSYBOX, ['sleep', '60'], ports=list(port_bindings.keys()), - host_config=self.client.create_host_config( - port_bindings=port_bindings, network_mode='bridge' - ) - ) - id = container['Id'] - - self.client.start(container) - - # Call the port function on each biding and compare expected vs actual - for port in port_bindings: - actual_bindings = self.client.port(container, port) - port_binding = actual_bindings.pop() - - ip, host_port = port_binding['HostIp'], port_binding['HostPort'] - - self.assertEqual(ip, port_bindings[port][0]) - self.assertEqual(host_port, port_bindings[port][1]) - - self.client.kill(id) - - -class TestMacAddress(BaseTestCase): - def runTest(self): - mac_address_expected = "02:42:ac:11:00:0a" - container = self.client.create_container( - BUSYBOX, ['sleep', '60'], mac_address=mac_address_expected) - - id = container['Id'] - - self.client.start(container) - res = self.client.inspect_container(container['Id']) - self.assertEqual(mac_address_expected, - res['NetworkSettings']['MacAddress']) - - self.client.kill(id) - - -class TestRestart(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - info = self.client.inspect_container(id) - self.assertIn('State', info) - self.assertIn('StartedAt', info['State']) - start_time1 = info['State']['StartedAt'] - self.client.restart(id, timeout=2) - info2 = self.client.inspect_container(id) - self.assertIn('State', info2) - self.assertIn('StartedAt', info2['State']) - start_time2 = info2['State']['StartedAt'] - self.assertNotEqual(start_time1, start_time2) - self.assertIn('Running', info2['State']) - self.assertEqual(info2['State']['Running'], True) - self.client.kill(id) - - -class TestRestartWithDictInsteadOfId(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - self.assertIn('Id', container) - id = container['Id'] - self.client.start(container) - self.tmp_containers.append(id) - info = self.client.inspect_container(id) - self.assertIn('State', info) - self.assertIn('StartedAt', info['State']) - start_time1 = info['State']['StartedAt'] - self.client.restart(container, timeout=2) - info2 = self.client.inspect_container(id) - self.assertIn('State', info2) - self.assertIn('StartedAt', info2['State']) - start_time2 = info2['State']['StartedAt'] - self.assertNotEqual(start_time1, start_time2) - self.assertIn('Running', info2['State']) - self.assertEqual(info2['State']['Running'], True) - self.client.kill(id) - - -class TestRemoveContainer(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['true']) - id = container['Id'] - self.client.start(id) - self.client.wait(id) - self.client.remove_container(id) - containers = self.client.containers(all=True) - res = [x for x in containers if 'Id' in x and x['Id'].startswith(id)] - self.assertEqual(len(res), 0) - - -class TestRemoveContainerWithDictInsteadOfId(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['true']) - id = container['Id'] - self.client.start(id) - self.client.wait(id) - self.client.remove_container(container) - containers = self.client.containers(all=True) - res = [x for x in containers if 'Id' in x and x['Id'].startswith(id)] - self.assertEqual(len(res), 0) - - -class TestCreateContainerWithVolumesFrom(BaseTestCase): - def runTest(self): - vol_names = ['foobar_vol0', 'foobar_vol1'] - - res0 = self.client.create_container( - BUSYBOX, 'true', name=vol_names[0] - ) - container1_id = res0['Id'] - self.tmp_containers.append(container1_id) - self.client.start(container1_id) - - res1 = self.client.create_container( - BUSYBOX, 'true', name=vol_names[1] - ) - container2_id = res1['Id'] - self.tmp_containers.append(container2_id) - self.client.start(container2_id) - with self.assertRaises(docker.errors.DockerException): - self.client.create_container( - BUSYBOX, 'cat', detach=True, stdin_open=True, - volumes_from=vol_names - ) - res2 = self.client.create_container( - BUSYBOX, 'cat', detach=True, stdin_open=True, - host_config=self.client.create_host_config( - volumes_from=vol_names, network_mode='none' - ) - ) - container3_id = res2['Id'] - self.tmp_containers.append(container3_id) - self.client.start(container3_id) - - info = self.client.inspect_container(res2['Id']) - self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names) - - -class TestCreateContainerWithLinks(BaseTestCase): - def runTest(self): - res0 = self.client.create_container( - BUSYBOX, 'cat', - detach=True, stdin_open=True, - environment={'FOO': '1'}) - - container1_id = res0['Id'] - self.tmp_containers.append(container1_id) - - self.client.start(container1_id) - - res1 = self.client.create_container( - BUSYBOX, 'cat', - detach=True, stdin_open=True, - environment={'FOO': '1'}) - - container2_id = res1['Id'] - self.tmp_containers.append(container2_id) - - self.client.start(container2_id) - - # we don't want the first / - link_path1 = self.client.inspect_container(container1_id)['Name'][1:] - link_alias1 = 'mylink1' - link_env_prefix1 = link_alias1.upper() - - link_path2 = self.client.inspect_container(container2_id)['Name'][1:] - link_alias2 = 'mylink2' - link_env_prefix2 = link_alias2.upper() - - res2 = self.client.create_container( - BUSYBOX, 'env', host_config=self.client.create_host_config( - links={link_path1: link_alias1, link_path2: link_alias2}, - network_mode='none' - ) - ) - container3_id = res2['Id'] - self.tmp_containers.append(container3_id) - self.client.start(container3_id) - self.assertEqual(self.client.wait(container3_id), 0) - - logs = self.client.logs(container3_id) - if six.PY3: - logs = logs.decode('utf-8') - self.assertIn('{0}_NAME='.format(link_env_prefix1), logs) - self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs) - self.assertIn('{0}_NAME='.format(link_env_prefix2), logs) - self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix2), logs) - - -class TestRestartingContainer(BaseTestCase): - def runTest(self): - container = self.client.create_container( - BUSYBOX, ['sleep', '2'], - host_config=self.client.create_host_config( - restart_policy={"Name": "always", "MaximumRetryCount": 0}, - network_mode='none' - ) - ) - id = container['Id'] - self.client.start(id) - self.client.wait(id) - with self.assertRaises(docker.errors.APIError) as exc: - self.client.remove_container(id) - err = exc.exception.response.text - self.assertIn( - 'You cannot remove a running container', err - ) - self.client.remove_container(id, force=True) - - -class TestExecuteCommand(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - container = self.client.create_container(BUSYBOX, 'cat', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - - res = self.client.exec_create(id, ['echo', 'hello']) - self.assertIn('Id', res) - - exec_log = self.client.exec_start(res) - self.assertEqual(exec_log, b'hello\n') - - -class TestExecuteCommandString(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - container = self.client.create_container(BUSYBOX, 'cat', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - - res = self.client.exec_create(id, 'echo hello world') - self.assertIn('Id', res) - - exec_log = self.client.exec_start(res) - self.assertEqual(exec_log, b'hello world\n') - - -class TestExecuteCommandStringAsUser(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - container = self.client.create_container(BUSYBOX, 'cat', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - - res = self.client.exec_create(id, 'whoami', user='default') - self.assertIn('Id', res) - - exec_log = self.client.exec_start(res) - self.assertEqual(exec_log, b'default\n') - - -class TestExecuteCommandStringAsRoot(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - container = self.client.create_container(BUSYBOX, 'cat', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - - res = self.client.exec_create(id, 'whoami') - self.assertIn('Id', res) - - exec_log = self.client.exec_start(res) - self.assertEqual(exec_log, b'root\n') - - -class TestExecuteCommandStreaming(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - container = self.client.create_container(BUSYBOX, 'cat', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - - exec_id = self.client.exec_create(id, ['echo', 'hello\nworld']) - self.assertIn('Id', exec_id) - - res = b'' - for chunk in self.client.exec_start(exec_id, stream=True): - res += chunk - self.assertEqual(res, b'hello\nworld\n') - - -class TestExecInspect(BaseTestCase): - def runTest(self): - if not exec_driver_is_native(): - pytest.skip('Exec driver not native') - - container = self.client.create_container(BUSYBOX, 'cat', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - - exec_id = self.client.exec_create(id, ['mkdir', '/does/not/exist']) - self.assertIn('Id', exec_id) - self.client.exec_start(exec_id) - exec_info = self.client.exec_inspect(exec_id) - self.assertIn('ExitCode', exec_info) - self.assertNotEqual(exec_info['ExitCode'], 0) - - -class TestRunContainerStreaming(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, '/bin/sh', - detach=True, stdin_open=True) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - sock = self.client.attach_socket(container, ws=False) - self.assertTrue(sock.fileno() > -1) - - -class TestPauseUnpauseContainer(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['sleep', '9999']) - id = container['Id'] - self.tmp_containers.append(id) - self.client.start(container) - self.client.pause(id) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - self.assertEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], True) - self.assertIn('Paused', state) - self.assertEqual(state['Paused'], True) - - self.client.unpause(id) - container_info = self.client.inspect_container(id) - self.assertIn('State', container_info) - state = container_info['State'] - self.assertIn('ExitCode', state) - self.assertEqual(state['ExitCode'], 0) - self.assertIn('Running', state) - self.assertEqual(state['Running'], True) - self.assertIn('Paused', state) - self.assertEqual(state['Paused'], False) - - -class TestCreateContainerWithHostPidMode(BaseTestCase): - def runTest(self): - ctnr = self.client.create_container( - BUSYBOX, 'true', host_config=self.client.create_host_config( - pid_mode='host', network_mode='none' - ) - ) - self.assertIn('Id', ctnr) - self.tmp_containers.append(ctnr['Id']) - self.client.start(ctnr) - inspect = self.client.inspect_container(ctnr) - self.assertIn('HostConfig', inspect) - host_config = inspect['HostConfig'] - self.assertIn('PidMode', host_config) - self.assertEqual(host_config['PidMode'], 'host') - - -################# -# LINKS TESTS # -################# - - -class TestRemoveLink(BaseTestCase): - def runTest(self): - # Create containers - container1 = self.client.create_container( - BUSYBOX, 'cat', detach=True, stdin_open=True - ) - container1_id = container1['Id'] - self.tmp_containers.append(container1_id) - self.client.start(container1_id) - - # Create Link - # we don't want the first / - link_path = self.client.inspect_container(container1_id)['Name'][1:] - link_alias = 'mylink' - - container2 = self.client.create_container( - BUSYBOX, 'cat', host_config=self.client.create_host_config( - links={link_path: link_alias}, network_mode='none' - ) - ) - container2_id = container2['Id'] - self.tmp_containers.append(container2_id) - self.client.start(container2_id) - - # Remove link - linked_name = self.client.inspect_container(container2_id)['Name'][1:] - link_name = '%s/%s' % (linked_name, link_alias) - self.client.remove_container(link_name, link=True) - - # Link is gone - containers = self.client.containers(all=True) - retrieved = [x for x in containers if link_name in x['Names']] - self.assertEqual(len(retrieved), 0) - - # Containers are still there - retrieved = [ - x for x in containers if x['Id'].startswith(container1_id) or - x['Id'].startswith(container2_id) - ] - self.assertEqual(len(retrieved), 2) - -################## -# IMAGES TESTS # -################## - - -class TestPull(BaseTestCase): - def runTest(self): - try: - self.client.remove_image('hello-world') - except docker.errors.APIError: - pass - res = self.client.pull('hello-world') - self.tmp_imgs.append('hello-world') - self.assertEqual(type(res), six.text_type) - self.assertGreaterEqual( - len(self.client.images('hello-world')), 1 - ) - img_info = self.client.inspect_image('hello-world') - self.assertIn('Id', img_info) - - -class TestPullStream(BaseTestCase): - def runTest(self): - try: - self.client.remove_image('hello-world') - except docker.errors.APIError: - pass - stream = self.client.pull('hello-world', stream=True) - self.tmp_imgs.append('hello-world') - for chunk in stream: - if six.PY3: - chunk = chunk.decode('utf-8') - json.loads(chunk) # ensure chunk is a single, valid JSON blob - self.assertGreaterEqual( - len(self.client.images('hello-world')), 1 - ) - img_info = self.client.inspect_image('hello-world') - self.assertIn('Id', img_info) - - -class TestCommit(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['touch', '/test']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - res = self.client.commit(id) - self.assertIn('Id', res) - img_id = res['Id'] - self.tmp_imgs.append(img_id) - img = self.client.inspect_image(img_id) - self.assertIn('Container', img) - self.assertTrue(img['Container'].startswith(id)) - self.assertIn('ContainerConfig', img) - self.assertIn('Image', img['ContainerConfig']) - self.assertEqual(BUSYBOX, img['ContainerConfig']['Image']) - busybox_id = self.client.inspect_image(BUSYBOX)['Id'] - self.assertIn('Parent', img) - self.assertEqual(img['Parent'], busybox_id) - - -class TestRemoveImage(BaseTestCase): - def runTest(self): - container = self.client.create_container(BUSYBOX, ['touch', '/test']) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - res = self.client.commit(id) - self.assertIn('Id', res) - img_id = res['Id'] - self.tmp_imgs.append(img_id) - self.client.remove_image(img_id, force=True) - images = self.client.images(all=True) - res = [x for x in images if x['Id'].startswith(img_id)] - self.assertEqual(len(res), 0) - - -################## -# IMPORT TESTS # -################## - - -class ImportTestCase(BaseTestCase): - '''Base class for `docker import` test cases.''' - - TAR_SIZE = 512 * 1024 - - def write_dummy_tar_content(self, n_bytes, tar_fd): - def extend_file(f, n_bytes): - f.seek(n_bytes - 1) - f.write(bytearray([65])) - f.seek(0) - - tar = tarfile.TarFile(fileobj=tar_fd, mode='w') - - with tempfile.NamedTemporaryFile() as f: - extend_file(f, n_bytes) - tarinfo = tar.gettarinfo(name=f.name, arcname='testdata') - tar.addfile(tarinfo, fileobj=f) - - tar.close() - - @contextlib.contextmanager - def dummy_tar_stream(self, n_bytes): - '''Yields a stream that is valid tar data of size n_bytes.''' - with tempfile.NamedTemporaryFile() as tar_file: - self.write_dummy_tar_content(n_bytes, tar_file) - tar_file.seek(0) - yield tar_file - - @contextlib.contextmanager - def dummy_tar_file(self, n_bytes): - '''Yields the name of a valid tar file of size n_bytes.''' - with tempfile.NamedTemporaryFile() as tar_file: - self.write_dummy_tar_content(n_bytes, tar_file) - tar_file.seek(0) - yield tar_file.name - - -class TestImportFromBytes(ImportTestCase): - '''Tests importing an image from in-memory byte data.''' - - def runTest(self): - with self.dummy_tar_stream(n_bytes=500) as f: - content = f.read() - - # The generic import_image() function cannot import in-memory bytes - # data that happens to be represented as a string type, because - # import_image() will try to use it as a filename and usually then - # trigger an exception. So we test the import_image_from_data() - # function instead. - statuses = self.client.import_image_from_data( - content, repository='test/import-from-bytes') - - result_text = statuses.splitlines()[-1] - result = json.loads(result_text) - - self.assertNotIn('error', result) - - img_id = result['status'] - self.tmp_imgs.append(img_id) - - -class TestImportFromFile(ImportTestCase): - '''Tests importing an image from a tar file on disk.''' - - def runTest(self): - with self.dummy_tar_file(n_bytes=self.TAR_SIZE) as tar_filename: - # statuses = self.client.import_image( - # src=tar_filename, repository='test/import-from-file') - statuses = self.client.import_image_from_file( - tar_filename, repository='test/import-from-file') - - result_text = statuses.splitlines()[-1] - result = json.loads(result_text) - - self.assertNotIn('error', result) - - self.assertIn('status', result) - img_id = result['status'] - self.tmp_imgs.append(img_id) - - -class TestImportFromStream(ImportTestCase): - '''Tests importing an image from a stream containing tar data.''' - - def runTest(self): - with self.dummy_tar_stream(n_bytes=self.TAR_SIZE) as tar_stream: - statuses = self.client.import_image( - src=tar_stream, repository='test/import-from-stream') - # statuses = self.client.import_image_from_stream( - # tar_stream, repository='test/import-from-stream') - result_text = statuses.splitlines()[-1] - result = json.loads(result_text) - - self.assertNotIn('error', result) - - self.assertIn('status', result) - img_id = result['status'] - self.tmp_imgs.append(img_id) - - -class TestImportFromURL(ImportTestCase): - '''Tests downloading an image over HTTP.''' - - @contextlib.contextmanager - def temporary_http_file_server(self, stream): - '''Serve data from an IO stream over HTTP.''' - - class Handler(BaseHTTPServer.BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header('Content-Type', 'application/x-tar') - self.end_headers() - shutil.copyfileobj(stream, self.wfile) - - server = socketserver.TCPServer(('', 0), Handler) - thread = threading.Thread(target=server.serve_forever) - thread.setDaemon(True) - thread.start() - - yield 'http://%s:%s' % (socket.gethostname(), server.server_address[1]) - - server.shutdown() - - @pytest.mark.skipif(True, reason="Doesn't work inside a container - FIXME") - def runTest(self): - # The crappy test HTTP server doesn't handle large files well, so use - # a small file. - TAR_SIZE = 10240 - - with self.dummy_tar_stream(n_bytes=TAR_SIZE) as tar_data: - with self.temporary_http_file_server(tar_data) as url: - statuses = self.client.import_image( - src=url, repository='test/import-from-url') - - result_text = statuses.splitlines()[-1] - result = json.loads(result_text) - - self.assertNotIn('error', result) - - self.assertIn('status', result) - img_id = result['status'] - self.tmp_imgs.append(img_id) - - -################# -# VOLUMES TESTS # -################# - -@requires_api_version('1.21') -class TestVolumes(BaseTestCase): - def test_create_volume(self): - name = 'perfectcherryblossom' - self.tmp_volumes.append(name) - result = self.client.create_volume(name) - self.assertIn('Name', result) - self.assertEqual(result['Name'], name) - self.assertIn('Driver', result) - self.assertEqual(result['Driver'], 'local') - - def test_create_volume_invalid_driver(self): - driver_name = 'invalid.driver' - - with pytest.raises(docker.errors.NotFound): - self.client.create_volume('perfectcherryblossom', driver_name) - - def test_list_volumes(self): - name = 'imperishablenight' - self.tmp_volumes.append(name) - volume_info = self.client.create_volume(name) - result = self.client.volumes() - self.assertIn('Volumes', result) - volumes = result['Volumes'] - self.assertIn(volume_info, volumes) - - def test_inspect_volume(self): - name = 'embodimentofscarletdevil' - self.tmp_volumes.append(name) - volume_info = self.client.create_volume(name) - result = self.client.inspect_volume(name) - self.assertEqual(volume_info, result) - - def test_inspect_nonexistent_volume(self): - name = 'embodimentofscarletdevil' - with pytest.raises(docker.errors.NotFound): - self.client.inspect_volume(name) - - def test_remove_volume(self): - name = 'shootthebullet' - self.tmp_volumes.append(name) - self.client.create_volume(name) - result = self.client.remove_volume(name) - self.assertTrue(result) - - def test_remove_nonexistent_volume(self): - name = 'shootthebullet' - with pytest.raises(docker.errors.NotFound): - self.client.remove_volume(name) - - -################# -# BUILDER TESTS # -################# - -class TestBuildStream(BaseTestCase): - def runTest(self): - script = io.BytesIO('\n'.join([ - 'FROM busybox', - 'MAINTAINER docker-py', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ]).encode('ascii')) - stream = self.client.build(fileobj=script, stream=True) - logs = '' - for chunk in stream: - if six.PY3: - chunk = chunk.decode('utf-8') - json.loads(chunk) # ensure chunk is a single, valid JSON blob - logs += chunk - self.assertNotEqual(logs, '') - - -class TestBuildFromStringIO(BaseTestCase): - def runTest(self): - if six.PY3: - return - script = io.StringIO(six.text_type('\n').join([ - 'FROM busybox', - 'MAINTAINER docker-py', - 'RUN mkdir -p /tmp/test', - 'EXPOSE 8080', - 'ADD https://dl.dropboxusercontent.com/u/20637798/silence.tar.gz' - ' /tmp/silence.tar.gz' - ])) - stream = self.client.build(fileobj=script, stream=True) - logs = '' - for chunk in stream: - if six.PY3: - chunk = chunk.decode('utf-8') - logs += chunk - self.assertNotEqual(logs, '') - - -@requires_api_version('1.8') -class TestBuildWithDockerignore(Cleanup, BaseTestCase): - def runTest(self): - base_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, base_dir) - - with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f: - f.write("\n".join([ - 'FROM busybox', - 'MAINTAINER docker-py', - 'ADD . /test', - ])) - - with open(os.path.join(base_dir, '.dockerignore'), 'w') as f: - f.write("\n".join([ - 'ignored', - 'Dockerfile', - '.dockerignore', - '', # empty line - ])) - - with open(os.path.join(base_dir, 'not-ignored'), 'w') as f: - f.write("this file should not be ignored") - - subdir = os.path.join(base_dir, 'ignored', 'subdir') - os.makedirs(subdir) - with open(os.path.join(subdir, 'file'), 'w') as f: - f.write("this file should be ignored") - - tag = 'docker-py-test-build-with-dockerignore' - stream = self.client.build( - path=base_dir, - tag=tag, - ) - for chunk in stream: - pass - - c = self.client.create_container(tag, ['ls', '-1A', '/test']) - self.client.start(c) - self.client.wait(c) - logs = self.client.logs(c) - - if six.PY3: - logs = logs.decode('utf-8') - - self.assertEqual( - list(filter(None, logs.split('\n'))), - ['not-ignored'], - ) - -####################### -# PY SPECIFIC TESTS # -####################### - - -class TestRunShlex(BaseTestCase): - def runTest(self): - commands = [ - 'true', - 'echo "The Young Descendant of Tepes & Septette for the ' - 'Dead Princess"', - 'echo -n "The Young Descendant of Tepes & Septette for the ' - 'Dead Princess"', - '/bin/sh -c "echo Hello World"', - '/bin/sh -c \'echo "Hello World"\'', - 'echo "\"Night of Nights\""', - 'true && echo "Night of Nights"' - ] - for cmd in commands: - container = self.client.create_container(BUSYBOX, cmd) - id = container['Id'] - self.client.start(id) - self.tmp_containers.append(id) - exitcode = self.client.wait(id) - self.assertEqual(exitcode, 0, msg=cmd) - - -class TestLoadConfig(BaseTestCase): - def runTest(self): - folder = tempfile.mkdtemp() - self.tmp_folders.append(folder) - cfg_path = os.path.join(folder, '.dockercfg') - f = open(cfg_path, 'w') - auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') - f.write('auth = {0}\n'.format(auth_)) - f.write('email = sakuya@scarlet.net') - f.close() - cfg = docker.auth.load_config(cfg_path) - self.assertNotEqual(cfg[docker.auth.INDEX_NAME], None) - cfg = cfg[docker.auth.INDEX_NAME] - self.assertEqual(cfg['username'], 'sakuya') - self.assertEqual(cfg['password'], 'izayoi') - self.assertEqual(cfg['email'], 'sakuya@scarlet.net') - self.assertEqual(cfg.get('Auth'), None) - - -class TestLoadJSONConfig(BaseTestCase): - def runTest(self): - folder = tempfile.mkdtemp() - self.tmp_folders.append(folder) - cfg_path = os.path.join(folder, '.dockercfg') - f = open(os.path.join(folder, '.dockercfg'), 'w') - auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') - email_ = 'sakuya@scarlet.net' - f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format( - docker.auth.INDEX_URL, auth_, email_)) - f.close() - cfg = docker.auth.load_config(cfg_path) - self.assertNotEqual(cfg[docker.auth.INDEX_URL], None) - cfg = cfg[docker.auth.INDEX_URL] - self.assertEqual(cfg['username'], 'sakuya') - self.assertEqual(cfg['password'], 'izayoi') - self.assertEqual(cfg['email'], 'sakuya@scarlet.net') - self.assertEqual(cfg.get('Auth'), None) - - -class TestAutoDetectVersion(unittest.TestCase): - def test_client_init(self): - client = docker_client(version='auto') - client_version = client._version - api_version = client.version(api_version=False)['ApiVersion'] - self.assertEqual(client_version, api_version) - api_version_2 = client.version()['ApiVersion'] - self.assertEqual(client_version, api_version_2) - client.close() - - def test_auto_client(self): - client = docker.AutoVersionClient(**docker_client_kwargs()) - client_version = client._version - api_version = client.version(api_version=False)['ApiVersion'] - self.assertEqual(client_version, api_version) - api_version_2 = client.version()['ApiVersion'] - self.assertEqual(client_version, api_version_2) - client.close() - with self.assertRaises(docker.errors.DockerException): - docker.AutoVersionClient(**docker_client_kwargs(version='1.11')) - - -class TestConnectionTimeout(unittest.TestCase): - def setUp(self): - self.timeout = 0.5 - self.client = docker.client.Client(base_url='http://192.168.10.2:4243', - timeout=self.timeout) - - def runTest(self): - start = time.time() - res = None - # This call isn't supposed to complete, and it should fail fast. - try: - res = self.client.inspect_container('id') - except: - pass - end = time.time() - self.assertTrue(res is None) - self.assertTrue(end - start < 2 * self.timeout) - - -class UnixconnTestCase(unittest.TestCase): - """ - Test UNIX socket connection adapter. - """ - - def test_resource_warnings(self): - """ - Test no warnings are produced when using the client. - """ - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - - client = docker_client() - client.images() - client.close() - del client - - assert len(w) == 0, \ - "No warnings produced: {0}".format(w[0].message) - - -#################### -# REGRESSION TESTS # -#################### - -class TestRegressions(BaseTestCase): - def test_443(self): - dfile = io.BytesIO() - with self.assertRaises(docker.errors.APIError) as exc: - for line in self.client.build(fileobj=dfile, tag="a/b/c"): - pass - self.assertEqual(exc.exception.response.status_code, 500) - dfile.close() - - def test_542(self): - self.client.start( - self.client.create_container(BUSYBOX, ['true']) - ) - result = self.client.containers(all=True, trunc=True) - self.assertEqual(len(result[0]['Id']), 12) - - def test_647(self): - with self.assertRaises(docker.errors.APIError): - self.client.inspect_image('gensokyo.jp//kirisame') - - def test_649(self): - self.client.timeout = None - ctnr = self.client.create_container(BUSYBOX, ['sleep', '2']) - self.client.start(ctnr) - self.client.stop(ctnr) - - def test_715(self): - ctnr = self.client.create_container(BUSYBOX, ['id', '-u'], user=1000) - self.client.start(ctnr) - self.client.wait(ctnr) - logs = self.client.logs(ctnr) - if six.PY3: - logs = logs.decode('utf-8') - assert logs == '1000\n' +# FIXME: placeholder while we transition to the new folder architecture +# Remove when merged in master and Jenkins is updated to find the tests +# in the new location. +from integration import * # flake8: noqa diff --git a/tests/testdata/certs/ca.pem b/tests/unit/__init__.py similarity index 100% rename from tests/testdata/certs/ca.pem rename to tests/unit/__init__.py diff --git a/tests/test.py b/tests/unit/api_test.py similarity index 98% rename from tests/test.py rename to tests/unit/api_test.py index 9993484a56..e44e562243 100644 --- a/tests/test.py +++ b/tests/unit/api_test.py @@ -34,9 +34,9 @@ import requests import six -from . import base +from .. import base from . import fake_api -from .helpers import make_tree +from ..helpers import make_tree import pytest @@ -109,34 +109,9 @@ def fake_delete(self, url, *args, **kwargs): docker.constants.DEFAULT_DOCKER_API_VERSION) -class Cleanup(object): - if sys.version_info < (2, 7): - # Provide a basic implementation of addCleanup for Python < 2.7 - def __init__(self, *args, **kwargs): - super(Cleanup, self).__init__(*args, **kwargs) - self._cleanups = [] - - def tearDown(self): - super(Cleanup, self).tearDown() - ok = True - while self._cleanups: - fn, args, kwargs = self._cleanups.pop(-1) - try: - fn(*args, **kwargs) - except KeyboardInterrupt: - raise - except: - ok = False - if not ok: - raise - - def addCleanup(self, function, *args, **kwargs): - self._cleanups.append((function, args, kwargs)) - - @mock.patch.multiple('docker.Client', get=fake_get, post=fake_post, put=fake_put, delete=fake_delete) -class DockerClientTest(Cleanup, base.BaseTestCase): +class DockerClientTest(base.Cleanup, base.BaseTestCase): def setUp(self): self.client = docker.Client() # Force-clear authconfig to avoid tampering with the tests @@ -2376,7 +2351,7 @@ def test_create_host_config_secopt(self): ) -class StreamTest(Cleanup, base.BaseTestCase): +class StreamTest(base.Cleanup, base.BaseTestCase): def setUp(self): socket_dir = tempfile.mkdtemp() diff --git a/tests/fake_api.py b/tests/unit/fake_api.py similarity index 100% rename from tests/fake_api.py rename to tests/unit/fake_api.py diff --git a/tests/fake_stat.py b/tests/unit/fake_stat.py similarity index 100% rename from tests/fake_stat.py rename to tests/unit/fake_stat.py diff --git a/tests/testdata/certs/cert.pem b/tests/unit/testdata/certs/ca.pem similarity index 100% rename from tests/testdata/certs/cert.pem rename to tests/unit/testdata/certs/ca.pem diff --git a/tests/testdata/certs/key.pem b/tests/unit/testdata/certs/cert.pem similarity index 100% rename from tests/testdata/certs/key.pem rename to tests/unit/testdata/certs/cert.pem diff --git a/tests/unit/testdata/certs/key.pem b/tests/unit/testdata/certs/key.pem new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/testdata/context/Dockerfile b/tests/unit/testdata/context/Dockerfile similarity index 100% rename from tests/testdata/context/Dockerfile rename to tests/unit/testdata/context/Dockerfile diff --git a/tests/testdata/context/ctx.tar.gz b/tests/unit/testdata/context/ctx.tar.gz similarity index 100% rename from tests/testdata/context/ctx.tar.gz rename to tests/unit/testdata/context/ctx.tar.gz diff --git a/tests/testdata/context/custom_dockerfile b/tests/unit/testdata/context/custom_dockerfile similarity index 100% rename from tests/testdata/context/custom_dockerfile rename to tests/unit/testdata/context/custom_dockerfile diff --git a/tests/utils_test.py b/tests/unit/utils_test.py similarity index 99% rename from tests/utils_test.py rename to tests/unit/utils_test.py index b67ac4ec69..71a382bf8f 100644 --- a/tests/utils_test.py +++ b/tests/unit/utils_test.py @@ -14,8 +14,8 @@ from docker.utils.ports import build_port_bindings, split_port from docker.auth import resolve_repository_name, resolve_authconfig -from . import base -from .helpers import make_tree +from .. import base +from ..helpers import make_tree import pytest diff --git a/tox.ini b/tox.ini index eb31bee51f..96b91773bb 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skipsdist=True [testenv] usedevelop=True commands = - py.test --cov=docker tests/test.py tests/utils_test.py + py.test --cov=docker tests/unit/ deps = -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt