diff -Nru docker-py-0.6.0/debian/changelog docker-py-1.3.0/debian/changelog --- docker-py-0.6.0/debian/changelog 2014-12-16 22:15:53.000000000 +0000 +++ docker-py-1.3.0/debian/changelog 2016-05-05 22:14:24.000000000 +0000 @@ -1,6 +1,5 @@ -docker-py (0.6.0-1contrail1) trusty; urgency=medium +docker-py (1.3.0-1contrail1) trusty; urgency=medium - * Source package for python bindings of docker - - -- Ted Ghose Wed, 15 Dec 2014 15:07:08 -0800 + * Initial build of python-docker-py from surce version 1.3.0, without websocket + -- Ignatious Johnson Thu, 05 May 2016 12:34:44 -0700 diff -Nru docker-py-0.6.0/debian/control docker-py-1.3.0/debian/control --- docker-py-0.6.0/debian/control 2014-12-16 22:15:53.000000000 +0000 +++ docker-py-1.3.0/debian/control 2016-05-05 22:14:24.000000000 +0000 @@ -1,5 +1,5 @@ Source: docker-py -Maintainer: unknown +Maintainer: Ignatious Johnson Section: python Priority: optional Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 7) diff -Nru docker-py-0.6.0/debian/patches/0001-docker-py.patch docker-py-1.3.0/debian/patches/0001-docker-py.patch --- docker-py-0.6.0/debian/patches/0001-docker-py.patch 2014-12-16 22:15:53.000000000 +0000 +++ docker-py-1.3.0/debian/patches/0001-docker-py.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,72 +0,0 @@ -From bbfed0e0fcaa4bab5e195cb15bdc33dcbace2a39 Mon Sep 17 00:00:00 2001 -From: Bartlomiej Biernacki -Date: Tue, 9 Dec 2014 13:24:47 -0800 -Subject: [PATCH] Removed websocket support for deb/rpm packaging - ---- - docker/client.py | 20 -------------------- - setup.py | 3 --- - 2 files changed, 23 deletions(-) - -diff --git a/docker/client.py b/docker/client.py -index 936ab52..abd413d 100644 ---- a/docker/client.py -+++ b/docker/client.py -@@ -31,9 +31,6 @@ from .utils import utils - from . import errors - from .tls import TLSConfig - --if not six.PY3: -- import websocket -- - DEFAULT_DOCKER_API_VERSION = '1.15' - DEFAULT_TIMEOUT_SECONDS = 60 - STREAM_HEADER_SIZE_BYTES = 8 -@@ -249,20 +246,6 @@ class Client(requests.Session): - 'stream': 1 - } - -- def _attach_websocket(self, container, params=None): -- if six.PY3: -- raise NotImplementedError("This method is not currently supported " -- "under python 3") -- url = self._url("/containers/{0}/attach/ws".format(container)) -- req = requests.Request("POST", url, params=self._attach_params(params)) -- full_url = req.prepare().url -- full_url = full_url.replace("http://", "ws://", 1) -- full_url = full_url.replace("https://", "wss://", 1) -- return self._create_websocket_connection(full_url) -- -- def _create_websocket_connection(self, url): -- return websocket.create_connection(url) -- - def _get_raw_response_socket(self, response): - self._raise_for_status(response) - if six.PY3: -@@ -396,9 +379,6 @@ class Client(requests.Session): - 'stream': 1 - } - -- if ws: -- return self._attach_websocket(container, params) -- - if isinstance(container, dict): - container = container.get('Id') - -diff --git a/setup.py b/setup.py -index b63cc92..0a7ba3e 100644 ---- a/setup.py -+++ b/setup.py -@@ -11,9 +11,6 @@ requirements = [ - 'six >= 1.3.0', - ] - --if sys.version_info[0] < 3: -- requirements.append('websocket-client >= 0.11.0') -- - exec(open('docker/version.py').read()) - - with open('./test-requirements.txt') as test_reqs_txt: --- -1.9.1 - diff -Nru docker-py-0.6.0/debian/patches/0001-remove-websocket.patch docker-py-1.3.0/debian/patches/0001-remove-websocket.patch --- docker-py-0.6.0/debian/patches/0001-remove-websocket.patch 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/debian/patches/0001-remove-websocket.patch 2016-05-05 22:14:24.000000000 +0000 @@ -0,0 +1,57 @@ +--- a/docker/client.py ++++ b/docker/client.py +@@ -51,9 +51,6 @@ + 'stream': 1 + } + +- if ws: +- return self._attach_websocket(container, params) +- + u = self._url("/containers/{0}/attach".format(container)) + return self._get_raw_response_socket(self.post( + u, None, params=self._attach_params(params), stream=True)) +--- a/docker/clientbase.py ++++ b/docker/clientbase.py +@@ -4,7 +4,6 @@ + import requests + import requests.exceptions + import six +-import websocket + + + from . import constants +@@ -132,18 +131,6 @@ + 'stream': 1 + } + +- @check_resource +- def _attach_websocket(self, container, params=None): +- url = self._url("/containers/{0}/attach/ws".format(container)) +- req = requests.Request("POST", url, params=self._attach_params(params)) +- full_url = req.prepare().url +- full_url = full_url.replace("http://", "ws://", 1) +- full_url = full_url.replace("https://", "wss://", 1) +- return self._create_websocket_connection(full_url) +- +- def _create_websocket_connection(self, url): +- return websocket.create_connection(url) +- + def _get_raw_response_socket(self, response): + self._raise_for_status(response) + if six.PY3: +--- a/requirements.txt ++++ b/requirements.txt +@@ -1,3 +1,2 @@ + requests==2.5.3 + six>=1.3.0 +-websocket-client==0.32.0 +--- a/setup.py ++++ b/setup.py +@@ -9,7 +9,6 @@ + requirements = [ + 'requests >= 2.5.2', + 'six >= 1.3.0', +- 'websocket-client >= 0.32.0', + ] + + exec(open('docker/version.py').read()) diff -Nru docker-py-0.6.0/debian/patches/series docker-py-1.3.0/debian/patches/series --- docker-py-0.6.0/debian/patches/series 2014-12-16 22:15:53.000000000 +0000 +++ docker-py-1.3.0/debian/patches/series 2016-05-05 22:14:24.000000000 +0000 @@ -1 +1 @@ -0001-docker-py.patch +0001-remove-websocket.patch diff -Nru docker-py-0.6.0/docker/auth/auth.py docker-py-1.3.0/docker/auth/auth.py --- docker-py-0.6.0/docker/auth/auth.py 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/docker/auth/auth.py 2015-07-09 17:59:07.000000000 +0000 @@ -23,26 +23,17 @@ from .. import errors INDEX_URL = 'https://index.docker.io/v1/' -DOCKER_CONFIG_FILENAME = '.dockercfg' - - -def swap_protocol(url): - if url.startswith('http://'): - return url.replace('http://', 'https://', 1) - if url.startswith('https://'): - return url.replace('https://', 'http://', 1) - return url +DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json') +LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg' def expand_registry_url(hostname, insecure=False): if hostname.startswith('http:') or hostname.startswith('https:'): - if '/' not in hostname[9:]: - hostname = hostname + '/v1/' return hostname - if utils.ping('https://' + hostname + '/v1/_ping'): - return 'https://' + hostname + '/v1/' + if utils.ping_registry('https://' + hostname): + return 'https://' + hostname elif insecure: - return 'http://' + hostname + '/v1/' + return 'http://' + hostname else: raise errors.DockerException( "HTTPS endpoint unresponsive and insecure mode isn't enabled." @@ -61,7 +52,7 @@ raise errors.InvalidRepository( 'Invalid repository name ({0})'.format(repo_name)) - if 'index.docker.io' in parts[0] or 'registry.hub.docker.com' in parts[0]: + if 'index.docker.io' in parts[0]: raise errors.InvalidRepository( 'Invalid repository name, try "{0}" instead'.format(parts[1]) ) @@ -70,29 +61,27 @@ def resolve_authconfig(authconfig, registry=None): - """Return the authentication data from the given auth configuration for a - specific registry. We'll do our best to infer the correct URL for the - registry, trying both http and https schemes. Returns an empty dictionnary - if no data exists.""" + """ + Returns the authentication data from the given auth configuration for a + specific registry. As with the Docker client, legacy entries in the config + with full URLs are stripped down to hostnames before checking for a match. + Returns None if no match was found. + """ # Default to the public index server - registry = registry or INDEX_URL - - # Ff its not the index server there are three cases: - # - # 1. this is a full config url -> it should be used as is - # 2. it could be a full url, but with the wrong protocol - # 3. it can be the hostname optionally with a port - # - # as there is only one auth entry which is fully qualified we need to start - # parsing and matching - if '/' not in registry: - registry = registry + '/v1/' - if not registry.startswith('http:') and not registry.startswith('https:'): - registry = 'https://' + registry + registry = convert_to_hostname(registry) if registry else INDEX_URL if registry in authconfig: return authconfig[registry] - return authconfig.get(swap_protocol(registry), None) + + for key, config in six.iteritems(authconfig): + if convert_to_hostname(key) == registry: + return config + + return None + + +def convert_to_hostname(url): + return url.replace('http://', '').replace('https://', '').split('/', 1)[0] def encode_auth(auth_info): @@ -104,7 +93,7 @@ if isinstance(auth, six.string_types): auth = auth.encode('ascii') s = base64.b64decode(auth) - login, pwd = s.split(b':') + login, pwd = s.split(b':', 1) return login.decode('ascii'), pwd.decode('ascii') @@ -119,28 +108,65 @@ return encode_header({'configs': auth}) -def load_config(root=None): - """Loads authentication data from a Docker configuration file in the given - root directory.""" +def parse_auth(entries): + """ + Parses authentication entries + + Args: + entries: Dict of authentication entries. + + Returns: + Authentication registry. + """ + + conf = {} + for registry, entry in six.iteritems(entries): + username, password = decode_auth(entry['auth']) + conf[registry] = { + 'username': username, + 'password': password, + 'email': entry['email'], + 'serveraddress': registry, + } + return conf + + +def load_config(config_path=None): + """ + Loads authentication data from a Docker configuration file in the given + root directory or if config_path is passed use given path. + """ conf = {} data = None - config_file = os.path.join(root or os.environ.get('HOME', '.'), - DOCKER_CONFIG_FILENAME) + # Prefer ~/.docker/config.json. + config_file = config_path or os.path.join(os.path.expanduser('~'), + DOCKER_CONFIG_FILENAME) + + if os.path.exists(config_file): + try: + with open(config_file) as f: + for section, data in six.iteritems(json.load(f)): + if section != 'auths': + continue + return parse_auth(data) + except (IOError, KeyError, ValueError): + # Likely missing new Docker config file or it's in an + # unknown format, continue to attempt to read old location + # and format. + pass + + config_file = config_path or os.path.join(os.path.expanduser('~'), + LEGACY_DOCKER_CONFIG_FILENAME) + + # if config path doesn't exist return empty config + if not os.path.exists(config_file): + return {} - # First try as JSON + # Try reading legacy location as JSON. try: with open(config_file) as f: - conf = {} - for registry, entry in six.iteritems(json.load(f)): - username, password = decode_auth(entry['auth']) - conf[registry] = { - 'username': username, - 'password': password, - 'email': entry['email'], - 'serveraddress': registry, - } - return conf + return parse_auth(json.load(f)) except: pass diff -Nru docker-py-0.6.0/docker/auth/__init__.py docker-py-1.3.0/docker/auth/__init__.py --- docker-py-0.6.0/docker/auth/__init__.py 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/docker/auth/__init__.py 2015-02-02 18:10:46.000000000 +0000 @@ -3,5 +3,5 @@ encode_header, load_config, resolve_authconfig, - resolve_repository_name + resolve_repository_name, ) # flake8: noqa \ No newline at end of file diff -Nru docker-py-0.6.0/docker/clientbase.py docker-py-1.3.0/docker/clientbase.py --- docker-py-0.6.0/docker/clientbase.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/docker/clientbase.py 2015-07-09 17:59:07.000000000 +0000 @@ -0,0 +1,275 @@ +import json +import struct + +import requests +import requests.exceptions +import six +import websocket + + +from . import constants +from . import errors +from .auth import auth +from .unixconn import unixconn +from .ssladapter import ssladapter +from .utils import utils, check_resource +from .tls import TLSConfig + + +class ClientBase(requests.Session): + def __init__(self, base_url=None, version=None, + timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False): + super(ClientBase, self).__init__() + + if tls and not base_url.startswith('https://'): + raise errors.TLSParameterError( + 'If using TLS, the base_url argument must begin with ' + '"https://".') + + self.base_url = base_url + self.timeout = timeout + + self._auth_configs = auth.load_config() + + base_url = utils.parse_host(base_url) + if base_url.startswith('http+unix://'): + self._custom_adapter = unixconn.UnixAdapter(base_url, timeout) + self.mount('http+docker://', self._custom_adapter) + self.base_url = 'http+docker://localunixsocket' + else: + # Use SSLAdapter for the ability to specify SSL version + if isinstance(tls, TLSConfig): + tls.configure_client(self) + elif tls: + self._custom_adapter = ssladapter.SSLAdapter() + self.mount('https://', self._custom_adapter) + self.base_url = base_url + + # version detection needs to be after unix adapter mounting + if version is None: + self._version = constants.DEFAULT_DOCKER_API_VERSION + elif isinstance(version, six.string_types): + if version.lower() == 'auto': + self._version = self._retrieve_server_version() + else: + self._version = version + else: + raise errors.DockerException( + 'Version parameter must be a string or None. Found {0}'.format( + type(version).__name__ + ) + ) + + def _retrieve_server_version(self): + try: + return self.version(api_version=False)["ApiVersion"] + except KeyError: + raise errors.DockerException( + 'Invalid response from docker daemon: key "ApiVersion"' + ' is missing.' + ) + except Exception as e: + raise errors.DockerException( + 'Error while fetching server API version: {0}'.format(e) + ) + + def _set_request_timeout(self, kwargs): + """Prepare the kwargs for an HTTP request by inserting the timeout + parameter, if not already present.""" + kwargs.setdefault('timeout', self.timeout) + return kwargs + + def _post(self, url, **kwargs): + return self.post(url, **self._set_request_timeout(kwargs)) + + def _get(self, url, **kwargs): + return self.get(url, **self._set_request_timeout(kwargs)) + + def _delete(self, url, **kwargs): + return self.delete(url, **self._set_request_timeout(kwargs)) + + def _url(self, path, versioned_api=True): + if versioned_api: + return '{0}/v{1}{2}'.format(self.base_url, self._version, path) + else: + return '{0}{1}'.format(self.base_url, path) + + def _raise_for_status(self, response, explanation=None): + """Raises stored :class:`APIError`, if one occurred.""" + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise errors.APIError(e, response, explanation=explanation) + + def _result(self, response, json=False, binary=False): + assert not (json and binary) + self._raise_for_status(response) + + if json: + return response.json() + if binary: + return response.content + return response.text + + def _post_json(self, url, data, **kwargs): + # Go <1.1 can't unserialize null to a string + # so we do this disgusting thing here. + data2 = {} + if data is not None: + for k, v in six.iteritems(data): + if v is not None: + data2[k] = v + + if 'headers' not in kwargs: + kwargs['headers'] = {} + kwargs['headers']['Content-Type'] = 'application/json' + return self._post(url, data=json.dumps(data2), **kwargs) + + def _attach_params(self, override=None): + return override or { + 'stdout': 1, + 'stderr': 1, + 'stream': 1 + } + + @check_resource + def _attach_websocket(self, container, params=None): + url = self._url("/containers/{0}/attach/ws".format(container)) + req = requests.Request("POST", url, params=self._attach_params(params)) + full_url = req.prepare().url + full_url = full_url.replace("http://", "ws://", 1) + full_url = full_url.replace("https://", "wss://", 1) + return self._create_websocket_connection(full_url) + + def _create_websocket_connection(self, url): + return websocket.create_connection(url) + + def _get_raw_response_socket(self, response): + self._raise_for_status(response) + if six.PY3: + sock = response.raw._fp.fp.raw + else: + sock = response.raw._fp.fp._sock + try: + # Keep a reference to the response to stop it being garbage + # collected. If the response is garbage collected, it will + # close TLS sockets. + sock._response = response + except AttributeError: + # UNIX sockets can't have attributes set on them, but that's + # fine because we won't be doing TLS over them + pass + + return sock + + def _stream_helper(self, response, decode=False): + """Generator for data coming from a chunked-encoded HTTP response.""" + if response.raw._fp.chunked: + reader = response.raw + while not reader.closed: + # this read call will block until we get a chunk + data = reader.read(1) + if not data: + break + if reader._fp.chunk_left: + data += reader.read(reader._fp.chunk_left) + if decode: + if six.PY3: + data = data.decode('utf-8') + data = json.loads(data) + yield data + else: + # Response isn't chunked, meaning we probably + # encountered an error immediately + yield self._result(response) + + def _multiplexed_buffer_helper(self, response): + """A generator of multiplexed data blocks read from a buffered + response.""" + buf = self._result(response, binary=True) + walker = 0 + while True: + if len(buf[walker:]) < 8: + break + _, length = struct.unpack_from('>BxxxL', buf[walker:]) + start = walker + constants.STREAM_HEADER_SIZE_BYTES + end = start + length + walker = end + yield buf[start:end] + + def _multiplexed_response_stream_helper(self, response): + """A generator of multiplexed data blocks coming from a response + stream.""" + + # Disable timeout on the underlying socket to prevent + # Read timed out(s) for long running processes + socket = self._get_raw_response_socket(response) + if six.PY3: + socket._sock.settimeout(None) + else: + socket.settimeout(None) + + while True: + header = response.raw.read(constants.STREAM_HEADER_SIZE_BYTES) + if not header: + break + _, length = struct.unpack('>BxxxL', header) + if not length: + break + data = response.raw.read(length) + if not data: + break + yield data + + def _stream_raw_result_old(self, response): + ''' Stream raw output for API versions below 1.6 ''' + self._raise_for_status(response) + for line in response.iter_lines(chunk_size=1, + decode_unicode=True): + # filter out keep-alive new lines + if line: + yield line + + def _stream_raw_result(self, response): + ''' Stream result for TTY-enabled container above API 1.6 ''' + self._raise_for_status(response) + for out in response.iter_content(chunk_size=1, decode_unicode=True): + yield out + + def _get_result(self, container, stream, res): + cont = self.inspect_container(container) + return self._get_result_tty(stream, res, cont['Config']['Tty']) + + def _get_result_tty(self, stream, res, is_tty): + # Stream multi-plexing was only introduced in API v1.6. Anything + # before that needs old-style streaming. + if utils.compare_version('1.6', self._version) < 0: + return self._stream_raw_result_old(res) + + # We should also use raw streaming (without keep-alives) + # if we're dealing with a tty-enabled container. + if is_tty: + return self._stream_raw_result(res) if stream else \ + self._result(res, binary=True) + + self._raise_for_status(res) + sep = six.binary_type() + if stream: + return self._multiplexed_response_stream_helper(res) + else: + return sep.join( + [x for x in self._multiplexed_buffer_helper(res)] + ) + + def get_adapter(self, url): + try: + return super(ClientBase, self).get_adapter(url) + except requests.exceptions.InvalidSchema as e: + if self._custom_adapter: + return self._custom_adapter + else: + raise e + + @property + def api_version(self): + return self._version diff -Nru docker-py-0.6.0/docker/client.py docker-py-1.3.0/docker/client.py --- docker-py-0.6.0/docker/client.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/docker/client.py 2015-07-09 17:59:07.000000000 +0000 @@ -12,348 +12,25 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import os import re import shlex -import struct -from socket import socket as socket_obj import warnings +from datetime import datetime -import requests -import requests.exceptions import six -from .auth import auth -from .unixconn import unixconn -from .ssladapter import ssladapter -from .utils import utils +from . import clientbase +from . import constants from . import errors -from .tls import TLSConfig - -if not six.PY3: - import websocket - -DEFAULT_DOCKER_API_VERSION = '1.15' -DEFAULT_TIMEOUT_SECONDS = 60 -STREAM_HEADER_SIZE_BYTES = 8 - - -class Client(requests.Session): - def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, - timeout=DEFAULT_TIMEOUT_SECONDS, tls=False): - super(Client, self).__init__() - base_url = utils.parse_host(base_url) - if 'http+unix:///' in base_url: - base_url = base_url.replace('unix:/', 'unix:') - if tls and not base_url.startswith('https://'): - raise errors.TLSParameterError( - 'If using TLS, the base_url argument must begin with ' - '"https://".') - self.base_url = base_url - self._version = version - self._timeout = timeout - self._auth_configs = auth.load_config() - - # Use SSLAdapter for the ability to specify SSL version - if isinstance(tls, TLSConfig): - tls.configure_client(self) - elif tls: - self.mount('https://', ssladapter.SSLAdapter()) - else: - self.mount('http+unix://', unixconn.UnixAdapter(base_url, timeout)) - - def _set_request_timeout(self, kwargs): - """Prepare the kwargs for an HTTP request by inserting the timeout - parameter, if not already present.""" - kwargs.setdefault('timeout', self._timeout) - return kwargs - - def _post(self, url, **kwargs): - return self.post(url, **self._set_request_timeout(kwargs)) - - def _get(self, url, **kwargs): - return self.get(url, **self._set_request_timeout(kwargs)) - - def _delete(self, url, **kwargs): - return self.delete(url, **self._set_request_timeout(kwargs)) - - def _url(self, path): - return '{0}/v{1}{2}'.format(self.base_url, self._version, path) - - def _raise_for_status(self, response, explanation=None): - """Raises stored :class:`APIError`, if one occurred.""" - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - raise errors.APIError(e, response, explanation=explanation) - - def _result(self, response, json=False, binary=False): - assert not (json and binary) - self._raise_for_status(response) - - if json: - return response.json() - if binary: - return response.content - return response.text - - def _container_config(self, image, command, hostname=None, user=None, - detach=False, stdin_open=False, tty=False, - mem_limit=0, ports=None, environment=None, dns=None, - volumes=None, volumes_from=None, - network_disabled=False, entrypoint=None, - cpu_shares=None, working_dir=None, - domainname=None, memswap_limit=0, cpuset=None): - if isinstance(command, six.string_types): - command = shlex.split(str(command)) - if isinstance(environment, dict): - environment = [ - '{0}={1}'.format(k, v) for k, v in environment.items() - ] - - if isinstance(mem_limit, six.string_types): - if len(mem_limit) == 0: - mem_limit = 0 - else: - units = {'b': 1, - 'k': 1024, - 'm': 1024 * 1024, - 'g': 1024 * 1024 * 1024} - suffix = mem_limit[-1].lower() - - # Check if the variable is a string representation of an int - # without a units part. Assuming that the units are bytes. - if suffix.isdigit(): - digits_part = mem_limit - suffix = 'b' - else: - digits_part = mem_limit[:-1] - - if suffix in units.keys() or suffix.isdigit(): - try: - digits = int(digits_part) - except ValueError: - message = ('Failed converting the string value for' - ' mem_limit ({0}) to a number.') - formatted_message = message.format(digits_part) - raise errors.DockerException(formatted_message) - - mem_limit = digits * units[suffix] - else: - message = ('The specified value for mem_limit parameter' - ' ({0}) should specify the units. The postfix' - ' should be one of the `b` `k` `m` `g`' - ' characters') - raise errors.DockerException(message.format(mem_limit)) - - if isinstance(ports, list): - exposed_ports = {} - for port_definition in ports: - port = port_definition - proto = 'tcp' - if isinstance(port_definition, tuple): - if len(port_definition) == 2: - proto = port_definition[1] - port = port_definition[0] - exposed_ports['{0}/{1}'.format(port, proto)] = {} - ports = exposed_ports - - if isinstance(volumes, six.string_types): - volumes = [volumes, ] - - if isinstance(volumes, list): - volumes_dict = {} - for vol in volumes: - volumes_dict[vol] = {} - volumes = volumes_dict - - if volumes_from: - if not isinstance(volumes_from, six.string_types): - volumes_from = ','.join(volumes_from) - else: - # Force None, an empty list or dict causes client.start to fail - volumes_from = None - - attach_stdin = False - attach_stdout = False - attach_stderr = False - stdin_once = False - - if not detach: - attach_stdout = True - attach_stderr = True - - if stdin_open: - attach_stdin = True - stdin_once = True - - if utils.compare_version('1.10', self._version) >= 0: - message = ('{0!r} parameter has no effect on create_container().' - ' It has been moved to start()') - if dns is not None: - raise errors.DockerException(message.format('dns')) - if volumes_from is not None: - raise errors.DockerException(message.format('volumes_from')) - - return { - 'Hostname': hostname, - 'Domainname': domainname, - 'ExposedPorts': ports, - 'User': user, - 'Tty': tty, - 'OpenStdin': stdin_open, - 'StdinOnce': stdin_once, - 'Memory': mem_limit, - 'AttachStdin': attach_stdin, - 'AttachStdout': attach_stdout, - 'AttachStderr': attach_stderr, - 'Env': environment, - 'Cmd': command, - 'Dns': dns, - 'Image': image, - 'Volumes': volumes, - 'VolumesFrom': volumes_from, - 'NetworkDisabled': network_disabled, - 'Entrypoint': entrypoint, - 'CpuShares': cpu_shares, - 'Cpuset': cpuset, - 'WorkingDir': working_dir, - 'MemorySwap': memswap_limit - } - - def _post_json(self, url, data, **kwargs): - # Go <1.1 can't unserialize null to a string - # so we do this disgusting thing here. - data2 = {} - if data is not None: - for k, v in six.iteritems(data): - if v is not None: - data2[k] = v - - if 'headers' not in kwargs: - kwargs['headers'] = {} - kwargs['headers']['Content-Type'] = 'application/json' - return self._post(url, data=json.dumps(data2), **kwargs) - - def _attach_params(self, override=None): - return override or { - 'stdout': 1, - 'stderr': 1, - 'stream': 1 - } - - def _attach_websocket(self, container, params=None): - if six.PY3: - raise NotImplementedError("This method is not currently supported " - "under python 3") - url = self._url("/containers/{0}/attach/ws".format(container)) - req = requests.Request("POST", url, params=self._attach_params(params)) - full_url = req.prepare().url - full_url = full_url.replace("http://", "ws://", 1) - full_url = full_url.replace("https://", "wss://", 1) - return self._create_websocket_connection(full_url) - - def _create_websocket_connection(self, url): - return websocket.create_connection(url) - - def _get_raw_response_socket(self, response): - self._raise_for_status(response) - if six.PY3: - sock = response.raw._fp.fp.raw._sock - else: - sock = response.raw._fp.fp._sock - try: - # Keep a reference to the response to stop it being garbage - # collected. If the response is garbage collected, it will close - # TLS sockets. - sock._response = response - except AttributeError: - # UNIX sockets can't have attributes set on them, but that's fine - # because we won't be doing TLS over them - pass - - return sock - - def _stream_helper(self, response): - """Generator for data coming from a chunked-encoded HTTP response.""" - if six.PY3: - socket_fp = self._get_raw_response_socket(response) - else: - socket_fp = socket_obj( - _sock=self._get_raw_response_socket(response) - ) - socket_fp.setblocking(1) - socket = socket_fp.makefile() - while True: - # Because Docker introduced newlines at the end of chunks in v0.9, - # and only on some API endpoints, we have to cater for both cases. - size_line = socket.readline() - if size_line == '\r\n' or size_line == '\n': - size_line = socket.readline() - - if len(size_line.strip()) > 0: - size = int(size_line, 16) - else: - break +from .auth import auth +from .utils import utils, check_resource - if size <= 0: - break - data = socket.readline() - if not data: - break - yield data - - def _multiplexed_buffer_helper(self, response): - """A generator of multiplexed data blocks read from a buffered - response.""" - buf = self._result(response, binary=True) - walker = 0 - while True: - if len(buf[walker:]) < 8: - break - _, length = struct.unpack_from('>BxxxL', buf[walker:]) - start = walker + STREAM_HEADER_SIZE_BYTES - end = start + length - walker = end - yield buf[start:end] - - def _multiplexed_socket_stream_helper(self, response): - """A generator of multiplexed data blocks coming from a response - socket.""" - socket = self._get_raw_response_socket(response) - - def recvall(socket, size): - blocks = [] - while size > 0: - block = socket.recv(size) - if not block: - return None - - blocks.append(block) - size -= len(block) - - sep = bytes() if six.PY3 else str() - data = sep.join(blocks) - return data - - while True: - socket.settimeout(None) - header = recvall(socket, STREAM_HEADER_SIZE_BYTES) - if not header: - break - _, length = struct.unpack('>BxxxL', header) - if not length: - break - data = recvall(socket, length) - if not data: - break - yield data +class Client(clientbase.ClientBase): + @check_resource def attach(self, container, stdout=True, stderr=True, stream=False, logs=False): - if isinstance(container, dict): - container = container.get('Id') params = { 'logs': logs and 1 or 0, 'stdout': stdout and 1 or 0, @@ -363,25 +40,9 @@ u = self._url("/containers/{0}/attach".format(container)) response = self._post(u, params=params, stream=stream) - # Stream multi-plexing was only introduced in API v1.6. Anything before - # that needs old-style streaming. - if utils.compare_version('1.6', self._version) < 0: - def stream_result(): - self._raise_for_status(response) - for line in response.iter_lines(chunk_size=1, - decode_unicode=True): - # filter out keep-alive new lines - if line: - yield line - - return stream_result() if stream else \ - self._result(response, binary=True) - - sep = bytes() if six.PY3 else str() - - return stream and self._multiplexed_socket_stream_helper(response) or \ - sep.join([x for x in self._multiplexed_buffer_helper(response)]) + return self._get_result(container, stream, response) + @check_resource def attach_socket(self, container, params=None, ws=False): if params is None: params = { @@ -393,20 +54,26 @@ if ws: return self._attach_websocket(container, params) - if isinstance(container, dict): - container = container.get('Id') - u = self._url("/containers/{0}/attach".format(container)) return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) def build(self, path=None, tag=None, quiet=False, fileobj=None, nocache=False, rm=False, stream=False, timeout=None, - custom_context=False, encoding=None): + custom_context=False, encoding=None, pull=False, + forcerm=False, dockerfile=None, container_limits=None, + decode=False): remote = context = headers = None + container_limits = container_limits or {} if path is None and fileobj is None: raise TypeError("Either path or fileobj needs to be provided.") + for key in container_limits.keys(): + if key not in constants.CONTAINER_LIMITS_KEYS: + raise errors.DockerException( + 'Invalid container_limits key {0}'.format(key) + ) + if custom_context: if not fileobj: raise TypeError("You must specify fileobj with custom_context") @@ -414,7 +81,7 @@ elif fileobj is not None: context = utils.mkbuildcontext(fileobj) elif path.startswith(('http://', 'https://', - 'git://', 'github.com/')): + 'git://', 'github.com/', 'git@')): remote = path elif not os.path.isdir(path): raise TypeError("You must specify a directory to build in path") @@ -423,20 +90,38 @@ exclude = None if os.path.exists(dockerignore): with open(dockerignore, 'r') as f: - exclude = list(filter(bool, f.read().split('\n'))) + exclude = list(filter(bool, f.read().splitlines())) + # These are handled by the docker daemon and should not be + # excluded on the client + if 'Dockerfile' in exclude: + exclude.remove('Dockerfile') + if '.dockerignore' in exclude: + exclude.remove(".dockerignore") context = utils.tar(path, exclude=exclude) if utils.compare_version('1.8', self._version) >= 0: stream = True + if dockerfile and utils.compare_version('1.17', self._version) < 0: + raise errors.InvalidVersion( + 'dockerfile was only introduced in API version 1.17' + ) + + if utils.compare_version('1.19', self._version) < 0: + pull = 1 if pull else 0 + u = self._url('/build') params = { 't': tag, 'remote': remote, 'q': quiet, 'nocache': nocache, - 'rm': rm + 'rm': rm, + 'forcerm': forcerm, + 'pull': pull, + 'dockerfile': dockerfile, } + params.update(container_limits) if context is not None: headers = {'Content-Type': 'application/tar'} @@ -452,6 +137,8 @@ # Send the full auth configuration (if any exists), since the build # could use any (or all) of the registries. if self._auth_configs: + if headers is None: + headers = {} headers['X-Registry-Config'] = auth.encode_full_header( self._auth_configs ) @@ -465,11 +152,11 @@ timeout=timeout, ) - if context is not None: + if context is not None and not custom_context: context.close() if stream: - return self._stream_helper(response) + return self._stream_helper(response, decode=decode) else: output = self._result(response) srch = r'Successfully built ([0-9a-f]+)' @@ -478,6 +165,7 @@ return None, output return match.group(1), output + @check_resource def commit(self, container, repository=None, tag=None, message=None, author=None, conf=None): params = { @@ -491,7 +179,7 @@ return self._result(self._post_json(u, data=conf, params=params), json=True) - def containers(self, quiet=False, all=False, trunc=True, latest=False, + def containers(self, quiet=False, all=False, trunc=False, latest=False, since=None, before=None, limit=-1, size=False, filters=None): params = { @@ -509,11 +197,13 @@ if quiet: return [{'Id': x['Id']} for x in res] + if trunc: + for x in res: + x['Id'] = x['Id'][:12] return res + @check_resource def copy(self, container, resource): - if isinstance(container, dict): - container = container.get('Id') res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, @@ -524,20 +214,27 @@ def create_container(self, image, command=None, hostname=None, user=None, detach=False, stdin_open=False, tty=False, - mem_limit=0, ports=None, environment=None, dns=None, - volumes=None, volumes_from=None, + mem_limit=None, ports=None, environment=None, + dns=None, volumes=None, volumes_from=None, network_disabled=False, name=None, entrypoint=None, - cpu_shares=None, working_dir=None, - domainname=None, memswap_limit=0, cpuset=None): + cpu_shares=None, working_dir=None, domainname=None, + memswap_limit=None, cpuset=None, host_config=None, + mac_address=None, labels=None, volume_driver=None): if isinstance(volumes, six.string_types): volumes = [volumes, ] - config = self._container_config( - image, command, hostname, user, detach, stdin_open, tty, mem_limit, - ports, environment, dns, volumes, volumes_from, network_disabled, - entrypoint, cpu_shares, working_dir, domainname, - memswap_limit, cpuset + if host_config and utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion( + 'host_config is not supported in API < 1.15' + ) + + config = utils.create_container_config( + self._version, image, command, hostname, user, detach, stdin_open, + tty, mem_limit, ports, environment, dns, volumes, volumes_from, + network_disabled, entrypoint, cpu_shares, working_dir, domainname, + memswap_limit, cpuset, host_config, mac_address, labels, + volume_driver ) return self.create_container_from_config(config, name) @@ -549,71 +246,106 @@ res = self._post_json(u, data=config, params=params) return self._result(res, True) + @check_resource def diff(self, container): - if isinstance(container, dict): - container = container.get('Id') return self._result(self._get(self._url("/containers/{0}/changes". format(container))), True) - def events(self): - return self._stream_helper(self.get(self._url('/events'), stream=True)) + def events(self, since=None, until=None, filters=None, decode=None): + if isinstance(since, datetime): + since = utils.datetime_to_timestamp(since) + + if isinstance(until, datetime): + until = utils.datetime_to_timestamp(until) + + if filters: + filters = utils.convert_filters(filters) - def execute(self, container, cmd, detach=False, stdout=True, stderr=True, - stream=False, tty=False): + params = { + 'since': since, + 'until': until, + 'filters': filters + } + + return self._stream_helper(self.get(self._url('/events'), + params=params, stream=True), + decode=decode) + + def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False, + privileged=False): if utils.compare_version('1.15', self._version) < 0: - raise Exception('Exec is not supported in API < 1.15!') - if isinstance(container, dict): - container = container.get('Id') + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if privileged and utils.compare_version('1.19', self._version) < 0: + raise errors.InvalidVersion( + 'Privileged exec is not supported in API < 1.19' + ) if isinstance(cmd, six.string_types): cmd = shlex.split(str(cmd)) data = { 'Container': container, 'User': '', - 'Privileged': False, + 'Privileged': privileged, 'Tty': tty, 'AttachStdin': False, 'AttachStdout': stdout, 'AttachStderr': stderr, - 'Detach': detach, 'Cmd': cmd } - # create the command url = self._url('/containers/{0}/exec'.format(container)) res = self._post_json(url, data=data) + return self._result(res, True) + + def exec_inspect(self, exec_id): + if utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if isinstance(exec_id, dict): + exec_id = exec_id.get('Id') + res = self._get(self._url("/exec/{0}/json".format(exec_id))) + return self._result(res, True) + + def exec_resize(self, exec_id, height=None, width=None): + if utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if isinstance(exec_id, dict): + exec_id = exec_id.get('Id') + + params = {'h': height, 'w': width} + url = self._url("/exec/{0}/resize".format(exec_id)) + res = self._post(url, params=params) self._raise_for_status(res) - # start the command - cmd_id = res.json().get('Id') - res = self._post_json(self._url('/exec/{0}/start'.format(cmd_id)), + def exec_start(self, exec_id, detach=False, tty=False, stream=False): + if utils.compare_version('1.15', self._version) < 0: + raise errors.InvalidVersion('Exec is not supported in API < 1.15') + if isinstance(exec_id, dict): + exec_id = exec_id.get('Id') + + data = { + 'Tty': tty, + 'Detach': detach + } + + res = self._post_json(self._url('/exec/{0}/start'.format(exec_id)), data=data, stream=stream) - self._raise_for_status(res) - if stream: - return self._multiplexed_socket_stream_helper(res) - elif six.PY3: - return bytes().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) - else: - return str().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) + return self._get_result_tty(stream, res, tty) + @check_resource def export(self, container): - if isinstance(container, dict): - container = container.get('Id') res = self._get(self._url("/containers/{0}/export".format(container)), stream=True) self._raise_for_status(res) return res.raw + @check_resource def get_image(self, image): res = self._get(self._url("/images/{0}/get".format(image)), stream=True) self._raise_for_status(res) return res.raw + @check_resource def history(self, image): res = self._get(self._url("/images/{0}/history".format(image))) return self._result(res, True) @@ -638,66 +370,121 @@ return res def import_image(self, src=None, repository=None, tag=None, image=None): + if src: + if isinstance(src, six.string_types): + try: + result = self.import_image_from_file( + src, repository=repository, tag=tag) + except IOError: + result = self.import_image_from_url( + src, repository=repository, tag=tag) + else: + result = self.import_image_from_data( + src, repository=repository, tag=tag) + elif image: + result = self.import_image_from_image( + image, repository=repository, tag=tag) + else: + raise Exception("Must specify a src or image") + + return result + + def import_image_from_data(self, data, repository=None, tag=None): u = self._url("/images/create") params = { + 'fromSrc': '-', 'repo': repository, 'tag': tag } + headers = { + 'Content-Type': 'application/tar', + } + return self._result( + self._post(u, data=data, params=params, headers=headers)) - if src: - try: - # XXX: this is ways not optimal but the only way - # for now to import tarballs through the API - fic = open(src) - data = fic.read() - fic.close() - src = "-" - except IOError: - # file does not exists or not a file (URL) - data = None - if isinstance(src, six.string_types): - params['fromSrc'] = src - return self._result(self._post(u, data=data, params=params)) - return self._result(self._post(u, data=src, params=params)) - - if image: - params['fromImage'] = image - return self._result(self._post(u, data=None, params=params)) + def import_image_from_file(self, filename, repository=None, tag=None): + u = self._url("/images/create") + params = { + 'fromSrc': '-', + 'repo': repository, + 'tag': tag + } + headers = { + 'Content-Type': 'application/tar', + } + with open(filename, 'rb') as f: + return self._result( + self._post(u, data=f, params=params, headers=headers, + timeout=None)) - raise Exception("Must specify a src or image") + def import_image_from_stream(self, stream, repository=None, tag=None): + u = self._url("/images/create") + params = { + 'fromSrc': '-', + 'repo': repository, + 'tag': tag + } + headers = { + 'Content-Type': 'application/tar', + 'Transfer-Encoding': 'chunked', + } + return self._result( + self._post(u, data=stream, params=params, headers=headers)) + + def import_image_from_url(self, url, repository=None, tag=None): + u = self._url("/images/create") + params = { + 'fromSrc': url, + 'repo': repository, + 'tag': tag + } + return self._result( + self._post(u, data=None, params=params)) + + def import_image_from_image(self, image, repository=None, tag=None): + u = self._url("/images/create") + params = { + 'fromImage': image, + 'repo': repository, + 'tag': tag + } + return self._result( + self._post(u, data=None, params=params)) def info(self): return self._result(self._get(self._url("/info")), True) + @check_resource def insert(self, image, url, path): if utils.compare_version('1.12', self._version) >= 0: raise errors.DeprecatedMethod( 'insert is not available for API version >=1.12' ) - api_url = self._url("/images/" + image + "/insert") + api_url = self._url("/images/{0}/insert".format(image)) params = { 'url': url, 'path': path } return self._result(self._post(api_url, params=params)) + @check_resource def inspect_container(self, container): - if isinstance(container, dict): - container = container.get('Id') return self._result( self._get(self._url("/containers/{0}/json".format(container))), True) - def inspect_image(self, image_id): + @check_resource + def inspect_image(self, image): return self._result( - self._get(self._url("/images/{0}/json".format(image_id))), + self._get( + self._url("/images/{0}/json".format(image.replace('/', '%2F'))) + ), True ) + @check_resource def kill(self, container, signal=None): - if isinstance(container, dict): - container = container.get('Id') url = self._url("/containers/{0}/kill".format(container)) params = {} if signal is not None: @@ -711,10 +498,14 @@ self._raise_for_status(res) def login(self, username, password=None, email=None, registry=None, - reauth=False): + reauth=False, insecure_registry=False, dockercfg_path=None): # If we don't have any auth data so far, try reloading the config file # one more time in case anything showed up in there. - if not self._auth_configs: + # If dockercfg_path is passed check to see if the config file exists, + # if so load that config. + if dockercfg_path and os.path.exists(dockercfg_path): + self._auth_configs = auth.load_config(dockercfg_path) + elif not self._auth_configs: self._auth_configs = auth.load_config() registry = registry or auth.INDEX_URL @@ -738,10 +529,9 @@ self._auth_configs[registry] = req_data return self._result(response, json=True) + @check_resource def logs(self, container, stdout=True, stderr=True, stream=False, timestamps=False, tail='all'): - if isinstance(container, dict): - container = container.get('Id') if utils.compare_version('1.11', self._version) >= 0: params = {'stderr': stderr and 1 or 0, 'stdout': stdout and 1 or 0, @@ -754,16 +544,7 @@ params['tail'] = tail url = self._url("/containers/{0}/logs".format(container)) res = self._get(url, params=params, stream=stream) - if stream: - return self._multiplexed_socket_stream_helper(res) - elif six.PY3: - return bytes().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) - else: - return str().join( - [x for x in self._multiplexed_buffer_helper(res)] - ) + return self._get_result(container, stream, res) return self.attach( container, stdout=stdout, @@ -772,9 +553,8 @@ logs=True ) + @check_resource def pause(self, container): - if isinstance(container, dict): - container = container.get('Id') url = self._url('/containers/{0}/pause'.format(container)) res = self._post(url) self._raise_for_status(res) @@ -782,23 +562,28 @@ def ping(self): return self._result(self._get(self._url('/_ping'))) + @check_resource def port(self, container, private_port): - if isinstance(container, dict): - container = container.get('Id') res = self._get(self._url("/containers/{0}/json".format(container))) self._raise_for_status(res) json_ = res.json() s_port = str(private_port) h_ports = None - h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp') + # Port settings is None when the container is running with + # network_mode=host. + port_settings = json_.get('NetworkSettings', {}).get('Ports') + if port_settings is None: + return None + + h_ports = port_settings.get(s_port + '/udp') if h_ports is None: - h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp') + h_ports = port_settings.get(s_port + '/tcp') return h_ports def pull(self, repository, tag=None, stream=False, - insecure_registry=False): + insecure_registry=False, auth_config=None): if not tag: repository, tag = utils.parse_repository_tag(repository) registry, repo_name = auth.resolve_repository_name( @@ -816,23 +601,33 @@ if utils.compare_version('1.5', self._version) >= 0: # If we don't have any auth data so far, try reloading the config # file one more time in case anything showed up in there. - if not self._auth_configs: - self._auth_configs = auth.load_config() - authcfg = auth.resolve_authconfig(self._auth_configs, registry) + if auth_config is None: + if not self._auth_configs: + self._auth_configs = auth.load_config() + authcfg = auth.resolve_authconfig(self._auth_configs, registry) + # Do not fail here if no authentication exists for this + # specific registry as we can have a readonly pull. Just + # put the header if we can. + if authcfg: + # auth_config needs to be a dict in the format used by + # auth.py username , password, serveraddress, email + headers['X-Registry-Auth'] = auth.encode_header( + authcfg + ) + else: + headers['X-Registry-Auth'] = auth.encode_header(auth_config) - # Do not fail here if no authentication exists for this specific - # registry as we can have a readonly pull. Just put the header if - # we can. - if authcfg: - headers['X-Registry-Auth'] = auth.encode_header(authcfg) + response = self._post( + self._url('/images/create'), params=params, headers=headers, + stream=stream, timeout=None + ) - response = self._post(self._url('/images/create'), params=params, - headers=headers, stream=stream, timeout=None) + self._raise_for_status(response) if stream: return self._stream_helper(response) - else: - return self._result(response) + + return self._result(response) def push(self, repository, tag=None, stream=False, insecure_registry=False): @@ -860,32 +655,50 @@ if authcfg: headers['X-Registry-Auth'] = auth.encode_header(authcfg) - response = self._post_json(u, None, headers=headers, - stream=stream, params=params) - else: - response = self._post_json(u, None, stream=stream, params=params) + response = self._post_json( + u, None, headers=headers, stream=stream, params=params + ) + + self._raise_for_status(response) - return stream and self._stream_helper(response) \ - or self._result(response) + if stream: + return self._stream_helper(response) + + return self._result(response) + @check_resource def remove_container(self, container, v=False, link=False, force=False): - if isinstance(container, dict): - container = container.get('Id') params = {'v': v, 'link': link, 'force': force} res = self._delete(self._url("/containers/" + container), params=params) self._raise_for_status(res) + @check_resource def remove_image(self, image, force=False, noprune=False): - if isinstance(image, dict): - image = image.get('Id') params = {'force': force, 'noprune': noprune} res = self._delete(self._url("/images/" + image), params=params) self._raise_for_status(res) + @check_resource + def rename(self, container, name): + if utils.compare_version('1.17', self._version) < 0: + raise errors.InvalidVersion( + 'rename was only introduced in API version 1.17' + ) + url = self._url("/containers/{0}/rename".format(container)) + params = {'name': name} + res = self._post(url, params=params) + self._raise_for_status(res) + + @check_resource + def resize(self, container, height, width): + params = {'h': height, 'w': width} + url = self._url("/containers/{0}/resize".format(container)) + res = self._post(url, params=params) + self._raise_for_status(res) + + @check_resource def restart(self, container, timeout=10): - if isinstance(container, dict): - container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/restart".format(container)) res = self._post(url, params=params) @@ -896,102 +709,91 @@ params={'term': term}), True) + @check_resource def start(self, container, binds=None, port_bindings=None, lxc_conf=None, publish_all_ports=False, links=None, privileged=False, dns=None, dns_search=None, volumes_from=None, network_mode=None, - restart_policy=None, cap_add=None, cap_drop=None, devices=None): - if isinstance(container, dict): - container = container.get('Id') - - if isinstance(lxc_conf, dict): - formatted = [] - for k, v in six.iteritems(lxc_conf): - formatted.append({'Key': k, 'Value': str(v)}) - lxc_conf = formatted - - start_config = { - 'LxcConf': lxc_conf - } - if binds: - start_config['Binds'] = utils.convert_volume_binds(binds) - - if port_bindings: - start_config['PortBindings'] = utils.convert_port_bindings( - port_bindings - ) - - start_config['PublishAllPorts'] = publish_all_ports - - if links: - if isinstance(links, dict): - links = six.iteritems(links) - - formatted_links = [ - '{0}:{1}'.format(k, v) for k, v in sorted(links) - ] + restart_policy=None, cap_add=None, cap_drop=None, devices=None, + extra_hosts=None, read_only=None, pid_mode=None, ipc_mode=None, + security_opt=None, ulimits=None): - start_config['Links'] = formatted_links - - start_config['Privileged'] = privileged - - if utils.compare_version('1.10', self._version) >= 0: + if utils.compare_version('1.10', self._version) < 0: if dns is not None: - start_config['Dns'] = dns - if volumes_from is not None: - if isinstance(volumes_from, six.string_types): - volumes_from = volumes_from.split(',') - start_config['VolumesFrom'] = volumes_from - else: - warning_message = ('{0!r} parameter is discarded. It is only' - ' available for API version greater or equal' - ' than 1.10') - - if dns is not None: - warnings.warn(warning_message.format('dns'), - DeprecationWarning) + raise errors.InvalidVersion( + 'dns is only supported for API version >= 1.10' + ) if volumes_from is not None: - warnings.warn(warning_message.format('volumes_from'), - DeprecationWarning) - if dns_search: - start_config['DnsSearch'] = dns_search - - if network_mode: - start_config['NetworkMode'] = network_mode + raise errors.InvalidVersion( + 'volumes_from is only supported for API version >= 1.10' + ) - if restart_policy: - start_config['RestartPolicy'] = restart_policy + if utils.compare_version('1.15', self._version) < 0: + if security_opt is not None: + raise errors.InvalidVersion( + 'security_opt is only supported for API version >= 1.15' + ) + if ipc_mode: + raise errors.InvalidVersion( + 'ipc_mode is only supported for API version >= 1.15' + ) - if cap_add: - start_config['CapAdd'] = cap_add + if utils.compare_version('1.17', self._version) < 0: + if read_only is not None: + raise errors.InvalidVersion( + 'read_only is only supported for API version >= 1.17' + ) + if pid_mode is not None: + raise errors.InvalidVersion( + 'pid_mode is only supported for API version >= 1.17' + ) - if cap_drop: - start_config['CapDrop'] = cap_drop + if utils.compare_version('1.18', self._version) < 0: + if ulimits is not None: + raise errors.InvalidVersion( + 'ulimits is only supported for API version >= 1.18' + ) - if devices: - start_config['Devices'] = utils.parse_devices(devices) + start_config = utils.create_host_config( + binds=binds, port_bindings=port_bindings, lxc_conf=lxc_conf, + publish_all_ports=publish_all_ports, links=links, dns=dns, + privileged=privileged, dns_search=dns_search, cap_add=cap_add, + cap_drop=cap_drop, volumes_from=volumes_from, devices=devices, + network_mode=network_mode, restart_policy=restart_policy, + extra_hosts=extra_hosts, read_only=read_only, pid_mode=pid_mode, + ipc_mode=ipc_mode, security_opt=security_opt, ulimits=ulimits + ) url = self._url("/containers/{0}/start".format(container)) + if not start_config: + start_config = None + elif utils.compare_version('1.15', self._version) > 0: + warnings.warn( + 'Passing host config parameters in start() is deprecated. ' + 'Please use host_config in create_container instead!', + DeprecationWarning + ) res = self._post_json(url, data=start_config) self._raise_for_status(res) - def resize(self, container, height, width): - if isinstance(container, dict): - container = container.get('Id') + @check_resource + def stats(self, container, decode=None): + if utils.compare_version('1.17', self._version) < 0: + raise errors.InvalidVersion( + 'Stats retrieval is not supported in API < 1.17!') - params = {'h': height, 'w': width} - url = self._url("/containers/{0}/resize".format(container)) - res = self._post(url, params=params) - self._raise_for_status(res) + url = self._url("/containers/{0}/stats".format(container)) + return self._stream_helper(self._get(url, stream=True), decode=decode) + @check_resource def stop(self, container, timeout=10): - if isinstance(container, dict): - container = container.get('Id') params = {'t': timeout} url = self._url("/containers/{0}/stop".format(container)) + res = self._post(url, params=params, - timeout=(timeout + self._timeout)) + timeout=(timeout + (self.timeout or 0))) self._raise_for_status(res) + @check_resource def tag(self, image, repository, tag=None, force=False): params = { 'tag': tag, @@ -1003,27 +805,37 @@ self._raise_for_status(res) return res.status_code == 201 + @check_resource def top(self, container): u = self._url("/containers/{0}/top".format(container)) return self._result(self._get(u), True) - def version(self): - return self._result(self._get(self._url("/version")), True) + def version(self, api_version=True): + url = self._url("/version", versioned_api=api_version) + return self._result(self._get(url), json=True) + @check_resource def unpause(self, container): - if isinstance(container, dict): - container = container.get('Id') url = self._url('/containers/{0}/unpause'.format(container)) res = self._post(url) self._raise_for_status(res) - def wait(self, container): - if isinstance(container, dict): - container = container.get('Id') + @check_resource + def wait(self, container, timeout=None): url = self._url("/containers/{0}/wait".format(container)) - res = self._post(url, timeout=None) + res = self._post(url, timeout=timeout) self._raise_for_status(res) json_ = res.json() if 'StatusCode' in json_: return json_['StatusCode'] return -1 + + +class AutoVersionClient(Client): + def __init__(self, *args, **kwargs): + if 'version' in kwargs and kwargs['version']: + raise errors.DockerException( + 'Can not specify version for AutoVersionClient' + ) + kwargs['version'] = 'auto' + super(AutoVersionClient, self).__init__(*args, **kwargs) diff -Nru docker-py-0.6.0/docker/constants.py docker-py-1.3.0/docker/constants.py --- docker-py-0.6.0/docker/constants.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/docker/constants.py 2015-07-09 17:59:07.000000000 +0000 @@ -0,0 +1,6 @@ +DEFAULT_DOCKER_API_VERSION = '1.19' +DEFAULT_TIMEOUT_SECONDS = 60 +STREAM_HEADER_SIZE_BYTES = 8 +CONTAINER_LIMITS_KEYS = [ + 'memory', 'memswap', 'cpushares', 'cpusetcpus' +] diff -Nru docker-py-0.6.0/docker/errors.py docker-py-1.3.0/docker/errors.py --- docker-py-0.6.0/docker/errors.py 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/docker/errors.py 2015-04-28 18:59:29.000000000 +0000 @@ -30,15 +30,15 @@ message = super(APIError, self).__str__() if self.is_client_error(): - message = '%s Client Error: %s' % ( + message = '{0} Client Error: {1}'.format( self.response.status_code, self.response.reason) elif self.is_server_error(): - message = '%s Server Error: %s' % ( + message = '{0} Server Error: {1}'.format( self.response.status_code, self.response.reason) if self.explanation: - message = '%s ("%s")' % (message, self.explanation) + message = '{0} ("{1}")'.format(message, self.explanation) return message @@ -53,6 +53,10 @@ pass +class InvalidVersion(DockerException): + pass + + class InvalidRepository(DockerException): pass @@ -74,3 +78,7 @@ "client configurations. See " "http://docs.docker.com/examples/https/ for " "API details.") + + +class NullResource(DockerException, ValueError): + pass diff -Nru docker-py-0.6.0/docker/__init__.py docker-py-1.3.0/docker/__init__.py --- docker-py-0.6.0/docker/__init__.py 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/docker/__init__.py 2015-04-28 18:59:29.000000000 +0000 @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .version import version +from .version import version, version_info __version__ = version __title__ = 'docker-py' -from .client import Client # flake8: noqa +from .client import Client, AutoVersionClient # flake8: noqa diff -Nru docker-py-0.6.0/docker/ssladapter/ssladapter.py docker-py-1.3.0/docker/ssladapter/ssladapter.py --- docker-py-0.6.0/docker/ssladapter/ssladapter.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/docker/ssladapter/ssladapter.py 2015-04-28 18:59:29.000000000 +0000 @@ -5,12 +5,12 @@ from distutils.version import StrictVersion from requests.adapters import HTTPAdapter import ssl + try: import requests.packages.urllib3 as urllib3 except ImportError: import urllib3 - PoolManager = urllib3.poolmanager.PoolManager @@ -25,10 +25,12 @@ class SSLAdapter(HTTPAdapter): '''An HTTPS Transport Adapter that uses an arbitrary SSL version.''' - def __init__(self, ssl_version=None, assert_hostname=None, **kwargs): + def __init__(self, ssl_version=None, assert_hostname=None, + assert_fingerprint=None, **kwargs): ssl_version = ssl_version or get_max_tls_protocol() self.ssl_version = ssl_version self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint super(SSLAdapter, self).__init__(**kwargs) def init_poolmanager(self, connections, maxsize, block=False): @@ -37,6 +39,7 @@ 'maxsize': maxsize, 'block': block, 'assert_hostname': self.assert_hostname, + 'assert_fingerprint': self.assert_fingerprint, } if self.can_override_ssl_version(): kwargs['ssl_version'] = self.ssl_version diff -Nru docker-py-0.6.0/docker/tls.py docker-py-1.3.0/docker/tls.py --- docker-py-0.6.0/docker/tls.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/docker/tls.py 2015-04-28 18:59:29.000000000 +0000 @@ -10,7 +10,8 @@ ssl_version = None def __init__(self, client_cert=None, ca_cert=None, verify=None, - ssl_version=None, assert_hostname=None): + ssl_version=None, assert_hostname=None, + assert_fingerprint=None): # Argument compatibility/mapping with # http://docs.docker.com/examples/https/ # This diverges from the Docker CLI in that users can specify 'tls' @@ -24,6 +25,7 @@ ssl_version = ssl_version or ssladapter.get_max_tls_protocol() self.ssl_version = ssl_version self.assert_hostname = assert_hostname + self.assert_fingerprint = assert_fingerprint # "tls" and "tls_verify" must have both or neither cert/key files # In either case, Alert the user when both are expected, but any are @@ -72,4 +74,5 @@ client.mount('https://', ssladapter.SSLAdapter( ssl_version=self.ssl_version, assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, )) diff -Nru docker-py-0.6.0/docker/unixconn/unixconn.py docker-py-1.3.0/docker/unixconn/unixconn.py --- docker-py-0.6.0/docker/unixconn/unixconn.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/docker/unixconn/unixconn.py 2015-04-28 18:59:29.000000000 +0000 @@ -12,23 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. import six +import requests.adapters +import socket if six.PY3: import http.client as httplib else: import httplib -import requests.adapters -import socket try: - import requests.packages.urllib3.connectionpool as connectionpool + import requests.packages.urllib3 as urllib3 except ImportError: - import urllib3.connectionpool as connectionpool + import urllib3 -try: - from requests.packages.urllib3._collections import RecentlyUsedContainer -except ImportError: - from urllib3._collections import RecentlyUsedContainer +RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer class UnixHTTPConnection(httplib.HTTPConnection, object): @@ -41,22 +38,15 @@ def connect(self): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(self.timeout) - sock.connect(self.base_url.replace("http+unix:/", "")) + sock.connect(self.unix_socket) self.sock = sock - def _extract_path(self, url): - # remove the base_url entirely.. - return url.replace(self.base_url, "") - - def request(self, method, url, **kwargs): - url = self._extract_path(self.unix_socket) - super(UnixHTTPConnection, self).request(method, url, **kwargs) - -class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool): +class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool): def __init__(self, base_url, socket_path, timeout=60): - connectionpool.HTTPConnectionPool.__init__(self, 'localhost', - timeout=timeout) + urllib3.connectionpool.HTTPConnectionPool.__init__( + self, 'localhost', timeout=timeout + ) self.base_url = base_url self.socket_path = socket_path self.timeout = timeout @@ -67,23 +57,26 @@ class UnixAdapter(requests.adapters.HTTPAdapter): - def __init__(self, base_url, timeout=60): - self.base_url = base_url + def __init__(self, socket_url, timeout=60): + socket_path = socket_url.replace('http+unix://', '') + if not socket_path.startswith('/'): + socket_path = '/' + socket_path + self.socket_path = socket_path self.timeout = timeout self.pools = RecentlyUsedContainer(10, dispose_func=lambda p: p.close()) super(UnixAdapter, self).__init__() - def get_connection(self, socket_path, proxies=None): + def get_connection(self, url, proxies=None): with self.pools.lock: - pool = self.pools.get(socket_path) + pool = self.pools.get(url) if pool: return pool - pool = UnixHTTPConnectionPool(self.base_url, - socket_path, + pool = UnixHTTPConnectionPool(url, + self.socket_path, self.timeout) - self.pools[socket_path] = pool + self.pools[url] = pool return pool diff -Nru docker-py-0.6.0/docker/utils/decorators.py docker-py-1.3.0/docker/utils/decorators.py --- docker-py-0.6.0/docker/utils/decorators.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/docker/utils/decorators.py 2015-07-09 17:59:07.000000000 +0000 @@ -0,0 +1,21 @@ +import functools + +from .. import errors + + +def check_resource(f): + @functools.wraps(f) + def wrapped(self, resource_id=None, *args, **kwargs): + if resource_id is None: + if kwargs.get('container'): + resource_id = kwargs.pop('container') + elif kwargs.get('image'): + resource_id = kwargs.pop('image') + if isinstance(resource_id, dict): + resource_id = resource_id.get('Id') + if not resource_id: + raise errors.NullResource( + 'image or container param is undefined' + ) + return f(self, resource_id, *args, **kwargs) + return wrapped diff -Nru docker-py-0.6.0/docker/utils/__init__.py docker-py-1.3.0/docker/utils/__init__.py --- docker-py-0.6.0/docker/utils/__init__.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/docker/utils/__init__.py 2015-04-28 18:59:29.000000000 +0000 @@ -1,5 +1,9 @@ from .utils import ( compare_version, convert_port_bindings, convert_volume_binds, - mkbuildcontext, ping, tar, parse_repository_tag, parse_host, - kwargs_from_env, convert_filters + mkbuildcontext, tar, parse_repository_tag, parse_host, + kwargs_from_env, convert_filters, create_host_config, + create_container_config, parse_bytes, ping_registry ) # flake8: noqa + +from .types import Ulimit, LogConfig # flake8: noqa +from .decorators import check_resource #flake8: noqa diff -Nru docker-py-0.6.0/docker/utils/ports/__init__.py docker-py-1.3.0/docker/utils/ports/__init__.py --- docker-py-0.6.0/docker/utils/ports/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/docker/utils/ports/__init__.py 2015-04-28 18:59:29.000000000 +0000 @@ -0,0 +1,4 @@ +from .ports import ( + split_port, + build_port_bindings +) # flake8: noqa diff -Nru docker-py-0.6.0/docker/utils/ports/ports.py docker-py-1.3.0/docker/utils/ports/ports.py --- docker-py-0.6.0/docker/utils/ports/ports.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/docker/utils/ports/ports.py 2015-04-28 18:59:29.000000000 +0000 @@ -0,0 +1,84 @@ + + +def add_port_mapping(port_bindings, internal_port, external): + if internal_port in port_bindings: + port_bindings[internal_port].append(external) + else: + port_bindings[internal_port] = [external] + + +def add_port(port_bindings, internal_port_range, external_range): + if external_range is None: + for internal_port in internal_port_range: + add_port_mapping(port_bindings, internal_port, None) + else: + ports = zip(internal_port_range, external_range) + for internal_port, external_port in ports: + add_port_mapping(port_bindings, internal_port, external_port) + + +def build_port_bindings(ports): + port_bindings = {} + for port in ports: + internal_port_range, external_range = split_port(port) + add_port(port_bindings, internal_port_range, external_range) + return port_bindings + + +def to_port_range(port): + if not port: + return None + + protocol = "" + if "/" in port: + parts = port.split("/") + if len(parts) != 2: + raise ValueError('Invalid port "%s", should be ' + '[[remote_ip:]remote_port[-remote_port]:]' + 'port[/protocol]' % port) + port, protocol = parts + protocol = "/" + protocol + + parts = str(port).split('-') + + if len(parts) == 1: + return ["%s%s" % (port, protocol)] + + if len(parts) == 2: + full_port_range = range(int(parts[0]), int(parts[1]) + 1) + return ["%s%s" % (p, protocol) for p in full_port_range] + + raise ValueError('Invalid port range "%s", should be ' + 'port or startport-endport' % port) + + +def split_port(port): + parts = str(port).split(':') + if not 1 <= len(parts) <= 3: + raise ValueError('Invalid port "%s", should be ' + '[[remote_ip:]remote_port:]port[/protocol]' % port) + + if len(parts) == 1: + internal_port, = parts + return to_port_range(internal_port), None + if len(parts) == 2: + external_port, internal_port = parts + + internal_range = to_port_range(internal_port) + external_range = to_port_range(external_port) + if len(internal_range) != len(external_range): + raise ValueError('Port ranges don\'t match in length') + + return internal_range, external_range + + external_ip, external_port, internal_port = parts + internal_range = to_port_range(internal_port) + external_range = to_port_range(external_port) + if not external_range: + external_range = [None] * len(internal_range) + + if len(internal_range) != len(external_range): + raise ValueError('Port ranges don\'t match in length') + + return internal_range, [(external_ip, ex_port or None) + for ex_port in external_range] diff -Nru docker-py-0.6.0/docker/utils/types.py docker-py-1.3.0/docker/utils/types.py --- docker-py-0.6.0/docker/utils/types.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/docker/utils/types.py 2015-04-28 18:59:29.000000000 +0000 @@ -0,0 +1,100 @@ +import six + + +class LogConfigTypesEnum(object): + _values = ( + 'json-file', + 'syslog', + 'none' + ) + JSON, SYSLOG, NONE = _values + + +class DictType(dict): + def __init__(self, init): + for k, v in six.iteritems(init): + self[k] = v + + +class LogConfig(DictType): + types = LogConfigTypesEnum + + def __init__(self, **kwargs): + type_ = kwargs.get('type', kwargs.get('Type')) + config = kwargs.get('config', kwargs.get('Config')) + if type_ not in self.types._values: + raise ValueError("LogConfig.type must be one of ({0})".format( + ', '.join(self.types._values) + )) + if config and not isinstance(config, dict): + raise ValueError("LogConfig.config must be a dictionary") + + super(LogConfig, self).__init__({ + 'Type': type_, + 'Config': config or {} + }) + + @property + def type(self): + return self['Type'] + + @type.setter + def type(self, value): + if value not in self.types._values: + raise ValueError("LogConfig.type must be one of {0}".format( + ', '.join(self.types._values) + )) + self['Type'] = value + + @property + def config(self): + return self['Config'] + + def set_config_value(self, key, value): + self.config[key] = value + + def unset_config(self, key): + if key in self.config: + del self.config[key] + + +class Ulimit(DictType): + def __init__(self, **kwargs): + name = kwargs.get('name', kwargs.get('Name')) + soft = kwargs.get('soft', kwargs.get('Soft')) + hard = kwargs.get('hard', kwargs.get('Hard')) + if not isinstance(name, six.string_types): + raise ValueError("Ulimit.name must be a string") + if soft and not isinstance(soft, int): + raise ValueError("Ulimit.soft must be an integer") + if hard and not isinstance(hard, int): + raise ValueError("Ulimit.hard must be an integer") + super(Ulimit, self).__init__({ + 'Name': name, + 'Soft': soft, + 'Hard': hard + }) + + @property + def name(self): + return self['Name'] + + @name.setter + def name(self, value): + self['Name'] = value + + @property + def soft(self): + return self.get('Soft') + + @soft.setter + def soft(self, value): + self['Soft'] = value + + @property + def hard(self): + return self.get('Hard') + + @hard.setter + def hard(self, value): + self['Hard'] = value diff -Nru docker-py-0.6.0/docker/utils/utils.py docker-py-1.3.0/docker/utils/utils.py --- docker-py-0.6.0/docker/utils/utils.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/docker/utils/utils.py 2015-07-09 17:59:07.000000000 +0000 @@ -16,19 +16,29 @@ import os import os.path import json +import shlex import tarfile import tempfile from distutils.version import StrictVersion from fnmatch import fnmatch +from datetime import datetime import requests import six from .. import errors from .. import tls +from .types import Ulimit, LogConfig + DEFAULT_HTTP_HOST = "127.0.0.1" DEFAULT_UNIX_SOCKET = "http+unix://var/run/docker.sock" +BYTE_UNITS = { + 'b': 1, + 'k': 1024, + 'm': 1024 * 1024, + 'g': 1024 * 1024 * 1024 +} def mkbuildcontext(dockerfile): @@ -41,9 +51,11 @@ 'Dockerfiles with Python 3') else: dfinfo.size = len(dockerfile.getvalue()) + dockerfile.seek(0) elif isinstance(dockerfile, io.BytesIO): dfinfo = tarfile.TarInfo('Dockerfile') dfinfo.size = len(dockerfile.getvalue()) + dockerfile.seek(0) else: dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile') t.addfile(dfinfo, dockerfile) @@ -72,7 +84,8 @@ fnames = [name for name in filenames if not fnmatch_any(os.path.join(relpath, name), exclude)] - for name in fnames: + dirnames.sort() + for name in sorted(fnames): arcname = os.path.join(relpath, name) t.add(os.path.join(path, arcname), arcname=arcname) for name in dirnames: @@ -106,13 +119,22 @@ return 1 -def ping(url): +def ping_registry(url): + return ping(url + '/v2/', [401]) or ping(url + '/v1/_ping') + + +def ping(url, valid_4xx_statuses=None): try: res = requests.get(url, timeout=3) except Exception: return False else: - return res.status_code < 400 + # We don't send yet auth headers + # and a v2 registry will respond with status 401 + return ( + res.status_code < 400 or + (valid_4xx_statuses and res.status_code in valid_4xx_statuses) + ) def _convert_port_binding(binding): @@ -157,14 +179,30 @@ def convert_volume_binds(binds): + if isinstance(binds, list): + return binds + result = [] for k, v in binds.items(): if isinstance(v, dict): - result.append('%s:%s:%s' % ( - k, v['bind'], 'ro' if v.get('ro', False) else 'rw' + if 'ro' in v and 'mode' in v: + raise ValueError( + 'Binding cannot contain both "ro" and "mode": {}' + .format(repr(v)) + ) + + if 'ro' in v: + mode = 'ro' if v['ro'] else 'rw' + elif 'mode' in v: + mode = v['mode'] + else: + mode = 'rw' + + result.append('{0}:{1}:{2}'.format( + k, v['bind'], mode )) else: - result.append('%s:%s:rw' % (k, v)) + result.append('{0}:{1}:rw'.format(k, v)) return result @@ -198,7 +236,8 @@ addr = addr.replace('http+unix://', 'unix://') if addr == 'tcp://': - raise errors.DockerException("Invalid bind address format: %s" % addr) + raise errors.DockerException( + "Invalid bind address format: {0}".format(addr)) elif addr.startswith('unix://'): addr = addr[7:] elif addr.startswith('tcp://'): @@ -212,7 +251,7 @@ else: if "://" in addr: raise errors.DockerException( - "Invalid bind address protocol: %s" % addr + "Invalid bind address protocol: {0}".format(addr) ) proto = "http" @@ -220,7 +259,7 @@ host_parts = addr.split(':') if len(host_parts) != 2: raise errors.DockerException( - "Invalid bind address format: %s" % addr + "Invalid bind address format: {0}".format(addr) ) if host_parts[0]: host = host_parts[0] @@ -233,19 +272,20 @@ ) elif proto in ("http", "https") and ':' not in addr: - raise errors.DockerException("Bind address needs a port: %s" % addr) + raise errors.DockerException( + "Bind address needs a port: {0}".format(addr)) else: host = addr if proto == "http+unix": - return "%s://%s" % (proto, host) - return "%s://%s:%d" % (proto, host, port) + return "{0}://{1}".format(proto, host) + return "{0}://{1}:{2}".format(proto, host, port) def parse_devices(devices): device_list = [] for device in devices: - device_mapping = device.split(",") + device_mapping = device.split(":") if device_mapping: path_on_host = device_mapping[0] if len(device_mapping) > 1: @@ -291,3 +331,311 @@ v = [v, ] result[k] = v return json.dumps(result) + + +def datetime_to_timestamp(dt=datetime.now()): + """Convert a datetime in local timezone to a unix timestamp""" + delta = dt - datetime.fromtimestamp(0) + return delta.seconds + delta.days * 24 * 3600 + + +def parse_bytes(s): + if len(s) == 0: + s = 0 + else: + if s[-2:-1].isalpha() and s[-1].isalpha(): + if (s[-1] == "b" or s[-1] == "B"): + s = s[:-1] + units = BYTE_UNITS + suffix = s[-1].lower() + + # Check if the variable is a string representation of an int + # without a units part. Assuming that the units are bytes. + if suffix.isdigit(): + digits_part = s + suffix = 'b' + else: + digits_part = s[:-1] + + if suffix in units.keys() or suffix.isdigit(): + try: + digits = int(digits_part) + except ValueError: + message = ('Failed converting the string value for' + 'memory ({0}) to a number.') + formatted_message = message.format(digits_part) + raise errors.DockerException(formatted_message) + + s = digits * units[suffix] + else: + message = ('The specified value for memory' + ' ({0}) should specify the units. The postfix' + ' should be one of the `b` `k` `m` `g`' + ' characters') + raise errors.DockerException(message.format(s)) + + return s + + +def create_host_config( + binds=None, port_bindings=None, lxc_conf=None, + publish_all_ports=False, links=None, privileged=False, + dns=None, dns_search=None, volumes_from=None, network_mode=None, + restart_policy=None, cap_add=None, cap_drop=None, devices=None, + extra_hosts=None, read_only=None, pid_mode=None, ipc_mode=None, + security_opt=None, ulimits=None, log_config=None, mem_limit=None, + memswap_limit=None +): + host_config = {} + + if mem_limit is not None: + if isinstance(mem_limit, six.string_types): + mem_limit = parse_bytes(mem_limit) + host_config['Memory'] = mem_limit + + if memswap_limit is not None: + if isinstance(memswap_limit, six.string_types): + memswap_limit = parse_bytes(memswap_limit) + host_config['MemorySwap'] = memswap_limit + + if pid_mode not in (None, 'host'): + raise errors.DockerException( + 'Invalid value for pid param: {0}'.format(pid_mode) + ) + elif pid_mode: + host_config['PidMode'] = pid_mode + + if ipc_mode: + host_config['IpcMode'] = ipc_mode + + if privileged: + host_config['Privileged'] = privileged + + if publish_all_ports: + host_config['PublishAllPorts'] = publish_all_ports + + if read_only is not None: + host_config['ReadonlyRootfs'] = read_only + + if dns_search: + host_config['DnsSearch'] = dns_search + + if network_mode: + host_config['NetworkMode'] = network_mode + + if restart_policy: + host_config['RestartPolicy'] = restart_policy + + if cap_add: + host_config['CapAdd'] = cap_add + + if cap_drop: + host_config['CapDrop'] = cap_drop + + if devices: + host_config['Devices'] = parse_devices(devices) + + if dns is not None: + host_config['Dns'] = dns + + if security_opt is not None: + if not isinstance(security_opt, list): + raise errors.DockerException( + 'Invalid type for security_opt param: expected list but found' + ' {0}'.format(type(security_opt)) + ) + host_config['SecurityOpt'] = security_opt + + if volumes_from is not None: + if isinstance(volumes_from, six.string_types): + volumes_from = volumes_from.split(',') + host_config['VolumesFrom'] = volumes_from + + if binds is not None: + host_config['Binds'] = convert_volume_binds(binds) + + if port_bindings is not None: + host_config['PortBindings'] = convert_port_bindings( + port_bindings + ) + + if extra_hosts is not None: + if isinstance(extra_hosts, dict): + extra_hosts = [ + '{0}:{1}'.format(k, v) + for k, v in sorted(six.iteritems(extra_hosts)) + ] + + host_config['ExtraHosts'] = extra_hosts + + if links is not None: + if isinstance(links, dict): + links = six.iteritems(links) + + formatted_links = [ + '{0}:{1}'.format(k, v) for k, v in sorted(links) + ] + + host_config['Links'] = formatted_links + + if isinstance(lxc_conf, dict): + formatted = [] + for k, v in six.iteritems(lxc_conf): + formatted.append({'Key': k, 'Value': str(v)}) + lxc_conf = formatted + + if lxc_conf is not None: + host_config['LxcConf'] = lxc_conf + + if ulimits is not None: + if not isinstance(ulimits, list): + raise errors.DockerException( + 'Invalid type for ulimits param: expected list but found' + ' {0}'.format(type(ulimits)) + ) + host_config['Ulimits'] = [] + for l in ulimits: + if not isinstance(l, Ulimit): + l = Ulimit(**l) + host_config['Ulimits'].append(l) + + if log_config is not None: + if not isinstance(log_config, LogConfig): + if not isinstance(log_config, dict): + raise errors.DockerException( + 'Invalid type for log_config param: expected LogConfig but' + ' found {0}'.format(type(log_config)) + ) + log_config = LogConfig(**log_config) + host_config['LogConfig'] = log_config + + return host_config + + +def create_container_config( + version, image, command, hostname=None, user=None, detach=False, + stdin_open=False, tty=False, mem_limit=None, ports=None, environment=None, + dns=None, volumes=None, volumes_from=None, network_disabled=False, + entrypoint=None, cpu_shares=None, working_dir=None, domainname=None, + memswap_limit=None, cpuset=None, host_config=None, mac_address=None, + labels=None, volume_driver=None +): + if isinstance(command, six.string_types): + command = shlex.split(str(command)) + if isinstance(environment, dict): + environment = [ + six.text_type('{0}={1}').format(k, v) + for k, v in six.iteritems(environment) + ] + + if labels is not None and compare_version('1.18', version) < 0: + raise errors.InvalidVersion( + 'labels were only introduced in API version 1.18' + ) + + if compare_version('1.19', version) < 0: + if volume_driver is not None: + raise errors.InvalidVersion( + 'Volume drivers were only introduced in API version 1.19' + ) + mem_limit = mem_limit if mem_limit is not None else 0 + memswap_limit = memswap_limit if memswap_limit is not None else 0 + else: + if mem_limit is not None: + raise errors.InvalidVersion( + 'mem_limit has been moved to host_config in API version 1.19' + ) + + if memswap_limit is not None: + raise errors.InvalidVersion( + 'memswap_limit has been moved to host_config in API ' + 'version 1.19' + ) + + if isinstance(labels, list): + labels = dict((lbl, six.text_type('')) for lbl in labels) + + if isinstance(mem_limit, six.string_types): + mem_limit = parse_bytes(mem_limit) + if isinstance(memswap_limit, six.string_types): + memswap_limit = parse_bytes(memswap_limit) + + if isinstance(ports, list): + exposed_ports = {} + for port_definition in ports: + port = port_definition + proto = 'tcp' + if isinstance(port_definition, tuple): + if len(port_definition) == 2: + proto = port_definition[1] + port = port_definition[0] + exposed_ports['{0}/{1}'.format(port, proto)] = {} + ports = exposed_ports + + if isinstance(volumes, six.string_types): + volumes = [volumes, ] + + if isinstance(volumes, list): + volumes_dict = {} + for vol in volumes: + volumes_dict[vol] = {} + volumes = volumes_dict + + if volumes_from: + if not isinstance(volumes_from, six.string_types): + volumes_from = ','.join(volumes_from) + else: + # Force None, an empty list or dict causes client.start to fail + volumes_from = None + + attach_stdin = False + attach_stdout = False + attach_stderr = False + stdin_once = False + + if not detach: + attach_stdout = True + attach_stderr = True + + if stdin_open: + attach_stdin = True + stdin_once = True + + if compare_version('1.10', version) >= 0: + message = ('{0!r} parameter has no effect on create_container().' + ' It has been moved to start()') + if dns is not None: + raise errors.InvalidVersion(message.format('dns')) + if volumes_from is not None: + raise errors.InvalidVersion(message.format('volumes_from')) + + return { + 'Hostname': hostname, + 'Domainname': domainname, + 'ExposedPorts': ports, + 'User': user, + 'Tty': tty, + 'OpenStdin': stdin_open, + 'StdinOnce': stdin_once, + 'Memory': mem_limit, + 'AttachStdin': attach_stdin, + 'AttachStdout': attach_stdout, + 'AttachStderr': attach_stderr, + 'Env': environment, + 'Cmd': command, + 'Dns': dns, + 'Image': image, + 'Volumes': volumes, + 'VolumesFrom': volumes_from, + 'NetworkDisabled': network_disabled, + 'Entrypoint': entrypoint, + 'CpuShares': cpu_shares, + 'Cpuset': cpuset, + 'CpusetCpus': cpuset, + 'WorkingDir': working_dir, + 'MemorySwap': memswap_limit, + 'HostConfig': host_config, + 'MacAddress': mac_address, + 'Labels': labels, + 'VolumeDriver': volume_driver, + } diff -Nru docker-py-0.6.0/docker/version.py docker-py-1.3.0/docker/version.py --- docker-py-0.6.0/docker/version.py 2014-11-03 23:00:42.000000000 +0000 +++ docker-py-1.3.0/docker/version.py 2015-07-09 17:59:07.000000000 +0000 @@ -1 +1,2 @@ -version = "0.6.0" +version = "1.3.0" +version_info = tuple([int(d) for d in version.split("-")[0].split(".")]) diff -Nru docker-py-0.6.0/docker_py.egg-info/PKG-INFO docker-py-1.3.0/docker_py.egg-info/PKG-INFO --- docker-py-0.6.0/docker_py.egg-info/PKG-INFO 2014-11-03 23:01:14.000000000 +0000 +++ docker-py-1.3.0/docker_py.egg-info/PKG-INFO 2015-07-09 17:59:55.000000000 +0000 @@ -1,8 +1,8 @@ Metadata-Version: 1.1 Name: docker-py -Version: 0.6.0 +Version: 1.3.0 Summary: Python client for Docker. -Home-page: UNKNOWN +Home-page: https://github.com/docker/docker-py/ Author: UNKNOWN Author-email: UNKNOWN License: UNKNOWN diff -Nru docker-py-0.6.0/docker_py.egg-info/requires.txt docker-py-1.3.0/docker_py.egg-info/requires.txt --- docker-py-0.6.0/docker_py.egg-info/requires.txt 2014-11-03 23:01:14.000000000 +0000 +++ docker-py-1.3.0/docker_py.egg-info/requires.txt 2015-07-09 17:59:55.000000000 +0000 @@ -1,3 +1,3 @@ -requests >= 2.2.1 +requests >= 2.5.2 six >= 1.3.0 -websocket-client >= 0.11.0 \ No newline at end of file +websocket-client >= 0.32.0 \ No newline at end of file diff -Nru docker-py-0.6.0/docker_py.egg-info/SOURCES.txt docker-py-1.3.0/docker_py.egg-info/SOURCES.txt --- docker-py-0.6.0/docker_py.egg-info/SOURCES.txt 2014-11-03 23:01:14.000000000 +0000 +++ docker-py-1.3.0/docker_py.egg-info/SOURCES.txt 2015-07-09 17:59:55.000000000 +0000 @@ -2,11 +2,12 @@ MANIFEST.in README.md requirements.txt -requirements3.txt setup.py test-requirements.txt docker/__init__.py docker/client.py +docker/clientbase.py +docker/constants.py docker/errors.py docker/tls.py docker/version.py @@ -17,7 +18,11 @@ docker/unixconn/__init__.py docker/unixconn/unixconn.py docker/utils/__init__.py +docker/utils/decorators.py +docker/utils/types.py docker/utils/utils.py +docker/utils/ports/__init__.py +docker/utils/ports/ports.py docker_py.egg-info/PKG-INFO docker_py.egg-info/SOURCES.txt docker_py.egg-info/dependency_links.txt @@ -25,7 +30,9 @@ docker_py.egg-info/requires.txt docker_py.egg-info/top_level.txt tests/__init__.py +tests/base.py tests/fake_api.py +tests/fake_stat.py tests/integration_test.py tests/test.py tests/utils_test.py \ No newline at end of file diff -Nru docker-py-0.6.0/PKG-INFO docker-py-1.3.0/PKG-INFO --- docker-py-0.6.0/PKG-INFO 2014-11-03 23:01:17.000000000 +0000 +++ docker-py-1.3.0/PKG-INFO 2015-07-09 17:59:59.000000000 +0000 @@ -1,8 +1,8 @@ Metadata-Version: 1.1 Name: docker-py -Version: 0.6.0 +Version: 1.3.0 Summary: Python client for Docker. -Home-page: UNKNOWN +Home-page: https://github.com/docker/docker-py/ Author: UNKNOWN Author-email: UNKNOWN License: UNKNOWN diff -Nru docker-py-0.6.0/requirements3.txt docker-py-1.3.0/requirements3.txt --- docker-py-0.6.0/requirements3.txt 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/requirements3.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -requests==2.2.1 -six>=1.3.0 diff -Nru docker-py-0.6.0/requirements.txt docker-py-1.3.0/requirements.txt --- docker-py-0.6.0/requirements.txt 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/requirements.txt 2015-07-09 17:59:07.000000000 +0000 @@ -1,3 +1,3 @@ -requests==2.2.1 +requests==2.5.3 six>=1.3.0 -websocket-client==0.11.0 +websocket-client==0.32.0 diff -Nru docker-py-0.6.0/setup.py docker-py-1.3.0/setup.py --- docker-py-0.6.0/setup.py 2014-09-08 15:49:15.000000000 +0000 +++ docker-py-1.3.0/setup.py 2015-07-09 17:59:07.000000000 +0000 @@ -7,13 +7,11 @@ SOURCE_DIR = os.path.join(ROOT_DIR) requirements = [ - 'requests >= 2.2.1', + 'requests >= 2.5.2', 'six >= 1.3.0', + 'websocket-client >= 0.32.0', ] -if sys.version_info[0] < 3: - requirements.append('websocket-client >= 0.11.0') - exec(open('docker/version.py').read()) with open('./test-requirements.txt') as test_reqs_txt: @@ -24,8 +22,11 @@ name="docker-py", version=version, description="Python client for Docker.", - packages=['docker', 'docker.auth', 'docker.unixconn', 'docker.utils', - 'docker.ssladapter'], + url='https://github.com/docker/docker-py/', + packages=[ + 'docker', 'docker.auth', 'docker.unixconn', 'docker.utils', + 'docker.utils.ports', 'docker.ssladapter' + ], install_requires=requirements, tests_require=test_requirements, zip_safe=False, diff -Nru docker-py-0.6.0/tests/base.py docker-py-1.3.0/tests/base.py --- docker-py-0.6.0/tests/base.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/tests/base.py 2015-04-28 18:59:29.000000000 +0000 @@ -0,0 +1,11 @@ +import sys +import unittest + +import six + + +class BaseTestCase(unittest.TestCase): + def assertIn(self, object, collection): + if six.PY2 and sys.version_info[1] <= 6: + return self.assertTrue(object in collection) + return super(BaseTestCase, self).assertIn(object, collection) diff -Nru docker-py-0.6.0/tests/fake_api.py docker-py-1.3.0/tests/fake_api.py --- docker-py-0.6.0/tests/fake_api.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/tests/fake_api.py 2015-07-09 17:59:07.000000000 +0000 @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -CURRENT_VERSION = 'v1.15' +import fake_stat + +CURRENT_VERSION = 'v1.19' FAKE_CONTAINER_ID = '3cc2351ab11b' FAKE_IMAGE_ID = 'e9aa60c60128' +FAKE_EXEC_ID = 'd5d177f121dc' FAKE_IMAGE_NAME = 'test_image' FAKE_TARBALL_PATH = '/path/to/tarball' FAKE_REPO_NAME = 'repo' @@ -28,6 +31,17 @@ # for clarity and readability +def get_fake_raw_version(): + status_code = 200 + response = { + "ApiVersion": "1.18", + "GitCommit": "fake-commit", + "GoVersion": "go1.3.3", + "Version": "1.5.0" + } + return status_code, response + + def get_fake_version(): status_code = 200 response = {'GoVersion': '1', 'Version': '1.1.1', @@ -115,11 +129,11 @@ return status_code, response -def get_fake_inspect_container(): +def get_fake_inspect_container(tty=False): status_code = 200 response = { 'Id': FAKE_CONTAINER_ID, - 'Config': {'Privileged': True}, + 'Config': {'Privileged': True, 'Tty': tty}, 'ID': FAKE_CONTAINER_ID, 'Image': 'busybox:latest', "State": { @@ -129,6 +143,7 @@ "StartedAt": "2013-09-25T14:01:18.869545111+02:00", "Ghost": False }, + "MacAddress": "02:42:ac:11:00:0a" } return status_code, response @@ -188,7 +203,8 @@ 'Ports': { '1111': None, '1111/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '4567'}], - '2222': None} + '2222': None}, + 'MacAddress': '02:42:ac:11:00:0a' } } return status_code, response @@ -219,19 +235,26 @@ return status_code, response +def get_fake_events(): + status_code = 200 + response = [{'status': 'stop', 'id': FAKE_CONTAINER_ID, + 'from': FAKE_IMAGE_ID, 'time': 1423247867}] + return status_code, response + + def get_fake_export(): status_code = 200 response = 'Byte Stream....' return status_code, response -def post_fake_execute(): +def post_fake_exec_create(): status_code = 200 - response = {'Id': FAKE_CONTAINER_ID} + response = {'Id': FAKE_EXEC_ID} return status_code, response -def post_fake_execute_start(): +def post_fake_exec_start(): status_code = 200 response = (b'\x01\x00\x00\x00\x00\x00\x00\x11bin\nboot\ndev\netc\n' b'\x01\x00\x00\x00\x00\x00\x00\x12lib\nmnt\nproc\nroot\n' @@ -239,6 +262,30 @@ return status_code, response +def post_fake_exec_resize(): + status_code = 201 + return status_code, '' + + +def get_fake_exec_inspect(): + return 200, { + 'OpenStderr': True, + 'OpenStdout': True, + 'Container': get_fake_inspect_container()[1], + 'Running': False, + 'ProcessConfig': { + 'arguments': ['hello world'], + 'tty': False, + 'entrypoint': 'echo', + 'privileged': False, + 'user': '' + }, + 'ExitCode': 0, + 'ID': FAKE_EXEC_ID, + 'OpenStdin': False + } + + def post_fake_stop_container(): status_code = 200 response = {'Id': FAKE_CONTAINER_ID} @@ -269,6 +316,11 @@ return status_code, response +def post_fake_rename_container(): + status_code = 204 + return status_code, None + + def delete_fake_remove_container(): status_code = 200 response = {'Id': FAKE_CONTAINER_ID} @@ -323,9 +375,16 @@ return status_code, response +def get_fake_stats(): + status_code = 200 + response = fake_stat.OBJ + return status_code, response + # Maps real api url to fake response callback -prefix = 'http+unix://var/run/docker.sock' +prefix = 'http+docker://localunixsocket' fake_responses = { + '{0}/version'.format(prefix): + get_fake_raw_version, '{1}/{0}/version'.format(CURRENT_VERSION, prefix): get_fake_version, '{1}/{0}/info'.format(CURRENT_VERSION, prefix): @@ -346,6 +405,8 @@ post_fake_resize_container, '{1}/{0}/containers/3cc2351ab11b/json'.format(CURRENT_VERSION, prefix): get_fake_inspect_container, + '{1}/{0}/containers/3cc2351ab11b/rename'.format(CURRENT_VERSION, prefix): + post_fake_rename_container, '{1}/{0}/images/e9aa60c60128/tag'.format(CURRENT_VERSION, prefix): post_fake_tag_image, '{1}/{0}/containers/3cc2351ab11b/wait'.format(CURRENT_VERSION, prefix): @@ -357,9 +418,16 @@ '{1}/{0}/containers/3cc2351ab11b/export'.format(CURRENT_VERSION, prefix): get_fake_export, '{1}/{0}/containers/3cc2351ab11b/exec'.format(CURRENT_VERSION, prefix): - post_fake_execute, - '{1}/{0}/exec/3cc2351ab11b/start'.format(CURRENT_VERSION, prefix): - post_fake_execute_start, + post_fake_exec_create, + '{1}/{0}/exec/d5d177f121dc/start'.format(CURRENT_VERSION, prefix): + post_fake_exec_start, + '{1}/{0}/exec/d5d177f121dc/json'.format(CURRENT_VERSION, prefix): + get_fake_exec_inspect, + '{1}/{0}/exec/d5d177f121dc/resize'.format(CURRENT_VERSION, prefix): + post_fake_exec_resize, + + '{1}/{0}/containers/3cc2351ab11b/stats'.format(CURRENT_VERSION, prefix): + get_fake_stats, '{1}/{0}/containers/3cc2351ab11b/stop'.format(CURRENT_VERSION, prefix): post_fake_stop_container, '{1}/{0}/containers/3cc2351ab11b/kill'.format(CURRENT_VERSION, prefix): @@ -393,5 +461,7 @@ '{1}/{0}/containers/create'.format(CURRENT_VERSION, prefix): post_fake_create_container, '{1}/{0}/build'.format(CURRENT_VERSION, prefix): - post_fake_build_container + post_fake_build_container, + '{1}/{0}/events'.format(CURRENT_VERSION, prefix): + get_fake_events } diff -Nru docker-py-0.6.0/tests/fake_stat.py docker-py-1.3.0/tests/fake_stat.py --- docker-py-0.6.0/tests/fake_stat.py 1970-01-01 00:00:00.000000000 +0000 +++ docker-py-1.3.0/tests/fake_stat.py 2015-02-16 19:03:50.000000000 +0000 @@ -0,0 +1,133 @@ +OBJ = { + "read": "2015-02-11T19:20:46.667237763+02:00", + "network": { + "rx_bytes": 567224, + "rx_packets": 3773, + "rx_errors": 0, + "rx_dropped": 0, + "tx_bytes": 1176, + "tx_packets": 13, + "tx_errors": 0, + "tx_dropped": 0 + }, + "cpu_stats": { + "cpu_usage": { + "total_usage": 157260874053, + "percpu_usage": [ + 52196306950, + 24118413549, + 53292684398, + 27653469156 + ], + "usage_in_kernelmode": 37140000000, + "usage_in_usermode": 62140000000 + }, + "system_cpu_usage": 3.0881377e+14, + "throttling_data": { + "periods": 0, + "throttled_periods": 0, + "throttled_time": 0 + } + }, + "memory_stats": { + "usage": 179314688, + "max_usage": 258166784, + "stats": { + "active_anon": 90804224, + "active_file": 2195456, + "cache": 3096576, + "hierarchical_memory_limit": 1.844674407371e+19, + "inactive_anon": 85516288, + "inactive_file": 798720, + "mapped_file": 2646016, + "pgfault": 101034, + "pgmajfault": 1207, + "pgpgin": 115814, + "pgpgout": 75613, + "rss": 176218112, + "rss_huge": 12582912, + "total_active_anon": 90804224, + "total_active_file": 2195456, + "total_cache": 3096576, + "total_inactive_anon": 85516288, + "total_inactive_file": 798720, + "total_mapped_file": 2646016, + "total_pgfault": 101034, + "total_pgmajfault": 1207, + "total_pgpgin": 115814, + "total_pgpgout": 75613, + "total_rss": 176218112, + "total_rss_huge": 12582912, + "total_unevictable": 0, + "total_writeback": 0, + "unevictable": 0, + "writeback": 0 + }, + "failcnt": 0, + "limit": 8039038976 + }, + "blkio_stats": { + "io_service_bytes_recursive": [ + { + "major": 8, + "minor": 0, + "op": "Read", + "value": 72843264 + }, { + "major": 8, + "minor": 0, + "op": "Write", + "value": 4096 + }, { + "major": 8, + "minor": 0, + "op": "Sync", + "value": 4096 + }, { + "major": 8, + "minor": 0, + "op": "Async", + "value": 72843264 + }, { + "major": 8, + "minor": 0, + "op": "Total", + "value": 72847360 + } + ], + "io_serviced_recursive": [ + { + "major": 8, + "minor": 0, + "op": "Read", + "value": 10581 + }, { + "major": 8, + "minor": 0, + "op": "Write", + "value": 1 + }, { + "major": 8, + "minor": 0, + "op": "Sync", + "value": 1 + }, { + "major": 8, + "minor": 0, + "op": "Async", + "value": 10581 + }, { + "major": 8, + "minor": 0, + "op": "Total", + "value": 10582 + } + ], + "io_queue_recursive": [], + "io_service_time_recursive": [], + "io_wait_time_recursive": [], + "io_merged_recursive": [], + "io_time_recursive": [], + "sectors_recursive": [] + } +} diff -Nru docker-py-0.6.0/tests/integration_test.py docker-py-1.3.0/tests/integration_test.py --- docker-py-0.6.0/tests/integration_test.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/tests/integration_test.py 2015-07-09 17:59:07.000000000 +0000 @@ -12,26 +12,38 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time 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 docker import six +from six.moves import BaseHTTPServer +from six.moves import socketserver + from test import Cleanup # FIXME: missing tests for -# export; history; import_image; insert; port; push; tag; get; load - +# export; history; insert; port; push; tag; get; load; stats DEFAULT_BASE_URL = os.environ.get('DOCKER_HOST') +EXEC_DRIVER_IS_NATIVE = True +NOT_ON_HOST = os.environ.get('NOT_ON_HOST', False) + +warnings.simplefilter('error') +create_host_config = docker.utils.create_host_config +compare_version = docker.utils.compare_version class BaseTestCase(unittest.TestCase): @@ -40,6 +52,9 @@ tmp_folders = [] def setUp(self): + if six.PY2: + self.assertRegex = self.assertRegexpMatches + self.assertCountEqual = self.assertItemsEqual self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=5) self.tmp_imgs = [] self.tmp_containers = [] @@ -59,6 +74,7 @@ pass for folder in self.tmp_folders: shutil.rmtree(folder) + self.client.close() ######################### # INFORMATION TESTS # @@ -83,6 +99,7 @@ class TestSearch(BaseTestCase): def runTest(self): + self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10) res = self.client.search('busybox') self.assertTrue(len(res) >= 1) base_img = [x for x in res if x['name'] == 'busybox'] @@ -128,9 +145,9 @@ self.assertEqual(len(retrieved), 1) retrieved = retrieved[0] self.assertIn('Command', retrieved) - self.assertEqual(retrieved['Command'], u'true') + self.assertEqual(retrieved['Command'], six.text_type('true')) self.assertIn('Image', retrieved) - self.assertRegexpMatches(retrieved['Image'], r'busybox:.*') + self.assertRegex(retrieved['Image'], r'busybox:.*') self.assertIn('Status', retrieved) ##################### @@ -153,29 +170,115 @@ filename = 'shared.txt' shared_file = os.path.join(mount_origin, filename) + binds = { + mount_origin: { + 'bind': mount_dest, + 'ro': False, + }, + } with open(shared_file, 'w'): container = self.client.create_container( 'busybox', - ['ls', mount_dest], volumes={mount_dest: {}} + ['ls', mount_dest], volumes={mount_dest: {}}, + host_config=create_host_config(binds=binds) ) container_id = container['Id'] - self.client.start( - container_id, - binds={ - mount_origin: { - 'bind': mount_dest, - 'ro': False, - }, - }, + self.client.start(container_id) + self.tmp_containers.append(container_id) + exitcode = self.client.wait(container_id) + self.assertEqual(exitcode, 0) + logs = self.client.logs(container_id) + + os.unlink(shared_file) + if six.PY3: + logs = logs.decode('utf-8') + self.assertIn(filename, logs) + inspect_data = self.client.inspect_container(container_id) + self.assertIn('Volumes', inspect_data) + self.assertIn(mount_dest, inspect_data['Volumes']) + self.assertEqual(mount_origin, inspect_data['Volumes'][mount_dest]) + self.assertIn(mount_dest, inspect_data['VolumesRW']) + self.assertTrue(inspect_data['VolumesRW'][mount_dest]) + + +class TestCreateContainerWithRoBinds(BaseTestCase): + def runTest(self): + mount_dest = '/mnt' + mount_origin = tempfile.mkdtemp() + self.tmp_folders.append(mount_origin) + + filename = 'shared.txt' + shared_file = os.path.join(mount_origin, filename) + binds = { + mount_origin: { + 'bind': mount_dest, + 'ro': True, + }, + } + + with open(shared_file, 'w'): + container = self.client.create_container( + 'busybox', + ['ls', mount_dest], volumes={mount_dest: {}}, + host_config=create_host_config(binds=binds) ) + container_id = container['Id'] + self.client.start(container_id) self.tmp_containers.append(container_id) exitcode = self.client.wait(container_id) self.assertEqual(exitcode, 0) logs = self.client.logs(container_id) os.unlink(shared_file) + if six.PY3: + logs = logs.decode('utf-8') self.assertIn(filename, logs) + inspect_data = self.client.inspect_container(container_id) + self.assertIn('Volumes', inspect_data) + self.assertIn(mount_dest, inspect_data['Volumes']) + self.assertEqual(mount_origin, inspect_data['Volumes'][mount_dest]) + self.assertIn(mount_dest, inspect_data['VolumesRW']) + self.assertFalse(inspect_data['VolumesRW'][mount_dest]) + + +class TestCreateContainerWithLogConfig(BaseTestCase): + def runTest(self): + config = docker.utils.LogConfig( + type=docker.utils.LogConfig.types.SYSLOG, + config={'key1': 'val1'} + ) + ctnr = self.client.create_container( + 'busybox', ['true'], + host_config=create_host_config(log_config=config) + ) + self.assertIn('Id', ctnr) + self.tmp_containers.append(ctnr['Id']) + self.client.start(ctnr) + info = self.client.inspect_container(ctnr) + self.assertIn('HostConfig', info) + host_config = info['HostConfig'] + self.assertIn('LogConfig', host_config) + log_config = host_config['LogConfig'] + self.assertIn('Type', log_config) + self.assertEqual(log_config['Type'], config.type) + self.assertIn('Config', log_config) + self.assertEqual(type(log_config['Config']), dict) + self.assertEqual(log_config['Config'], config.config) + + +@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') +class TestCreateContainerReadOnlyFs(BaseTestCase): + def runTest(self): + ctnr = self.client.create_container( + 'busybox', ['mkdir', '/shrine'], + host_config=create_host_config(read_only=True) + ) + 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): @@ -188,6 +291,22 @@ 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') @@ -224,12 +343,14 @@ self.assertEqual(inspect['State']['ExitCode'], 0) -class TestStartContainerPrivileged(BaseTestCase): +class TestCreateContainerPrivileged(BaseTestCase): def runTest(self): - res = self.client.create_container('busybox', 'true') + res = self.client.create_container( + 'busybox', 'true', host_config=create_host_config(privileged=True) + ) self.assertIn('Id', res) self.tmp_containers.append(res['Id']) - self.client.start(res['Id'], privileged=True) + self.client.start(res['Id']) inspect = self.client.inspect_container(res['Id']) self.assertIn('Config', inspect) self.assertIn('Id', inspect) @@ -383,7 +504,8 @@ self.assertIn('State', container_info) state = container_info['State'] self.assertIn('ExitCode', state) - self.assertNotEqual(state['ExitCode'], 0) + if EXEC_DRIVER_IS_NATIVE: + self.assertNotEqual(state['ExitCode'], 0) self.assertIn('Running', state) self.assertEqual(state['Running'], False) @@ -400,7 +522,8 @@ self.assertIn('State', container_info) state = container_info['State'] self.assertIn('ExitCode', state) - self.assertNotEqual(state['ExitCode'], 0) + if EXEC_DRIVER_IS_NATIVE: + self.assertNotEqual(state['ExitCode'], 0) self.assertIn('Running', state) self.assertEqual(state['Running'], False) @@ -416,7 +539,8 @@ self.assertIn('State', container_info) state = container_info['State'] self.assertIn('ExitCode', state) - self.assertNotEqual(state['ExitCode'], 0) + if EXEC_DRIVER_IS_NATIVE: + self.assertNotEqual(state['ExitCode'], 0) self.assertIn('Running', state) self.assertEqual(state['Running'], False) @@ -432,7 +556,8 @@ self.assertIn('State', container_info) state = container_info['State'] self.assertIn('ExitCode', state) - self.assertNotEqual(state['ExitCode'], 0) + if EXEC_DRIVER_IS_NATIVE: + self.assertNotEqual(state['ExitCode'], 0) self.assertIn('Running', state) self.assertEqual(state['Running'], False) @@ -459,16 +584,17 @@ def runTest(self): port_bindings = { - 1111: ('127.0.0.1', '4567'), - 2222: ('127.0.0.1', '4568') + '1111': ('127.0.0.1', '4567'), + '2222': ('127.0.0.1', '4568') } container = self.client.create_container( - 'busybox', ['sleep', '60'], ports=port_bindings.keys() + 'busybox', ['sleep', '60'], ports=list(port_bindings.keys()), + host_config=create_host_config(port_bindings=port_bindings) ) id = container['Id'] - self.client.start(container, port_bindings=port_bindings) + self.client.start(container) # Call the port function on each biding and compare expected vs actual for port in port_bindings: @@ -483,6 +609,22 @@ 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']) @@ -550,40 +692,41 @@ self.assertEqual(len(res), 0) -class TestStartContainerWithVolumesFrom(BaseTestCase): +class TestCreateContainerWithVolumesFrom(BaseTestCase): def runTest(self): vol_names = ['foobar_vol0', 'foobar_vol1'] res0 = self.client.create_container( - 'busybox', 'true', - name=vol_names[0]) + '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]) + '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): - res2 = self.client.create_container( - 'busybox', 'cat', - detach=True, stdin_open=True, - volumes_from=vol_names) + 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) + 'busybox', 'cat', detach=True, stdin_open=True, + host_config=create_host_config(volumes_from=vol_names) + ) container3_id = res2['Id'] self.tmp_containers.append(container3_id) - self.client.start(container3_id, volumes_from=vol_names) + self.client.start(container3_id) info = self.client.inspect_container(res2['Id']) - self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names) + self.assertCountEqual(info['HostConfig']['VolumesFrom'], vol_names) -class TestStartContainerWithLinks(BaseTestCase): +class TestCreateContainerWithLinks(BaseTestCase): def runTest(self): res0 = self.client.create_container( 'busybox', 'cat', @@ -614,16 +757,19 @@ link_alias2 = 'mylink2' link_env_prefix2 = link_alias2.upper() - res2 = self.client.create_container('busybox', 'env') + res2 = self.client.create_container( + 'busybox', 'env', host_config=create_host_config( + links={link_path1: link_alias1, link_path2: link_alias2} + ) + ) container3_id = res2['Id'] self.tmp_containers.append(container3_id) - self.client.start( - container3_id, - links={link_path1: link_alias1, link_path2: link_alias2} - ) + 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) @@ -632,19 +778,24 @@ class TestRestartingContainer(BaseTestCase): def runTest(self): - container = self.client.create_container('busybox', ['false']) + container = self.client.create_container( + 'busybox', ['sleep', '2'], host_config=create_host_config( + restart_policy={"Name": "always", "MaximumRetryCount": 0} + ) + ) id = container['Id'] - self.client.start(id, restart_policy={ - "Name": "on-failure", - "MaximumRetryCount": 1 - }) + 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) + 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) +@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') class TestExecuteCommand(BaseTestCase): def runTest(self): container = self.client.create_container('busybox', 'cat', @@ -653,11 +804,15 @@ self.client.start(id) self.tmp_containers.append(id) - res = self.client.execute(id, ['echo', 'hello']) + res = self.client.exec_create(id, ['echo', 'hello']) + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) expected = b'hello\n' if six.PY3 else 'hello\n' - self.assertEqual(res, expected) + self.assertEqual(exec_log, expected) +@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') class TestExecuteCommandString(BaseTestCase): def runTest(self): container = self.client.create_container('busybox', 'cat', @@ -666,11 +821,15 @@ self.client.start(id) self.tmp_containers.append(id) - res = self.client.execute(id, 'echo hello world', stdout=True) + res = self.client.exec_create(id, 'echo hello world') + self.assertIn('Id', res) + + exec_log = self.client.exec_start(res) expected = b'hello world\n' if six.PY3 else 'hello world\n' - self.assertEqual(res, expected) + self.assertEqual(exec_log, expected) +@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') class TestExecuteCommandStreaming(BaseTestCase): def runTest(self): container = self.client.create_container('busybox', 'cat', @@ -679,14 +838,44 @@ self.client.start(id) self.tmp_containers.append(id) - chunks = self.client.execute(id, ['echo', 'hello\nworld'], stream=True) + exec_id = self.client.exec_create(id, ['echo', 'hello\nworld']) + self.assertIn('Id', exec_id) + res = b'' if six.PY3 else '' - for chunk in chunks: + for chunk in self.client.exec_start(exec_id, stream=True): res += chunk expected = b'hello\nworld\n' if six.PY3 else 'hello\nworld\n' self.assertEqual(res, expected) +@unittest.skipIf(not EXEC_DRIVER_IS_NATIVE, 'Exec driver not native') +class TestExecInspect(BaseTestCase): + def runTest(self): + 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']) @@ -716,6 +905,23 @@ self.assertEqual(state['Paused'], False) +class TestCreateContainerWithHostPidMode(BaseTestCase): + def runTest(self): + ctnr = self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + pid_mode='host' + ) + ) + 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 # ################# @@ -725,7 +931,8 @@ def runTest(self): # Create containers container1 = self.client.create_container( - 'busybox', 'cat', detach=True, stdin_open=True) + 'busybox', 'cat', detach=True, stdin_open=True + ) container1_id = container1['Id'] self.tmp_containers.append(container1_id) self.client.start(container1_id) @@ -735,10 +942,14 @@ link_path = self.client.inspect_container(container1_id)['Name'][1:] link_alias = 'mylink' - container2 = self.client.create_container('busybox', 'cat') + container2 = self.client.create_container( + 'busybox', 'cat', host_config=create_host_config( + links={link_path: link_alias} + ) + ) container2_id = container2['Id'] self.tmp_containers.append(container2_id) - self.client.start(container2_id, links={link_path: link_alias}) + self.client.start(container2_id) # Remove link linked_name = self.client.inspect_container(container2_id)['Name'][1:] @@ -752,8 +963,8 @@ # Containers are still there retrieved = [ - x for x in containers if x['Id'].startswith(container1_id) - or x['Id'].startswith(container2_id) + x for x in containers if x['Id'].startswith(container1_id) or + x['Id'].startswith(container2_id) ] self.assertEqual(len(retrieved), 2) @@ -764,41 +975,39 @@ class TestPull(BaseTestCase): def runTest(self): + self.client.close() + self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10) try: - self.client.remove_image('joffrey/test001') - self.client.remove_image('376968a23351') + self.client.remove_image('busybox') except docker.errors.APIError: pass - info = self.client.info() - self.assertIn('Images', info) - img_count = info['Images'] - res = self.client.pull('joffrey/test001') + res = self.client.pull('busybox') self.assertEqual(type(res), six.text_type) - self.assertEqual(img_count + 3, self.client.info()['Images']) - img_info = self.client.inspect_image('joffrey/test001') + self.assertGreaterEqual( + len(self.client.images('busybox')), 1 + ) + img_info = self.client.inspect_image('busybox') self.assertIn('Id', img_info) - self.tmp_imgs.append('joffrey/test001') - self.tmp_imgs.append('376968a23351') class TestPullStream(BaseTestCase): def runTest(self): + self.client.close() + self.client = docker.Client(base_url=DEFAULT_BASE_URL, timeout=10) try: - self.client.remove_image('joffrey/test001') - self.client.remove_image('376968a23351') + self.client.remove_image('busybox') except docker.errors.APIError: pass - info = self.client.info() - self.assertIn('Images', info) - img_count = info['Images'] - stream = self.client.pull('joffrey/test001', stream=True) + stream = self.client.pull('busybox', stream=True) for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') json.loads(chunk) # ensure chunk is a single, valid JSON blob - self.assertEqual(img_count + 3, self.client.info()['Images']) - img_info = self.client.inspect_image('joffrey/test001') + self.assertGreaterEqual( + len(self.client.images('busybox')), 1 + ) + img_info = self.client.inspect_image('busybox') self.assertIn('Id', img_info) - self.tmp_imgs.append('joffrey/test001') - self.tmp_imgs.append('376968a23351') class TestCommit(BaseTestCase): @@ -832,11 +1041,163 @@ self.assertIn('Id', res) img_id = res['Id'] self.tmp_imgs.append(img_id) - self.client.remove_image(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.''' + + # Use a large file size to increase the chance of triggering any + # MemoryError exceptions we might hit. + TAR_SIZE = 512 * 1024 * 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) + + +@unittest.skipIf(NOT_ON_HOST, 'Tests running inside a container') +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() + + 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) + + ################# # BUILDER TESTS # ################# @@ -844,7 +1205,7 @@ class TestBuild(BaseTestCase): def runTest(self): - if self.client._version >= 1.8: + if compare_version(self.client._version, '1.8') < 0: return script = io.BytesIO('\n'.join([ 'FROM busybox', @@ -886,6 +1247,8 @@ 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, '') @@ -895,7 +1258,7 @@ def runTest(self): if six.PY3: return - script = io.StringIO(u'\n'.join([ + script = io.StringIO(six.text_type('\n').join([ 'FROM busybox', 'MAINTAINER docker-py', 'RUN mkdir -p /tmp/test', @@ -906,37 +1269,15 @@ 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, '') -class TestBuildWithAuth(BaseTestCase): - def runTest(self): - if self.client._version < 1.9: - return - - k = 'K4104GON3P4Q6ZUJFZRRC2ZQTBJ5YT0UMZD7TGT7ZVIR8Y05FAH2TJQI6Y90SMIB' - self.client.login('quay+fortesting', k, registry='https://quay.io/v1/', - email='') - - script = io.BytesIO('\n'.join([ - 'FROM quay.io/quay/teststuff', - 'MAINTAINER docker-py', - 'RUN mkdir -p /tmp/test', - ]).encode('ascii')) - - stream = self.client.build(fileobj=script, stream=True) - logs = '' - for chunk in stream: - logs += chunk - - self.assertNotEqual(logs, '') - self.assertEqual(logs.find('HTTP code: 403'), -1) - - class TestBuildWithDockerignore(Cleanup, BaseTestCase): def runTest(self): - if self.client._version < 1.8: + if compare_version(self.client._version, '1.8') >= 0: return base_dir = tempfile.mkdtemp() @@ -953,6 +1294,8 @@ with open(os.path.join(base_dir, '.dockerignore'), 'w') as f: f.write("\n".join([ 'node_modules', + 'Dockerfile', + '.dockerginore', '', # empty line ])) @@ -967,8 +1310,12 @@ stream = self.client.build(path=base_dir, stream=True) logs = '' for chunk in stream: + if six.PY3: + chunk = chunk.decode('utf-8') logs += chunk self.assertFalse('node_modules' in logs) + self.assertFalse('Dockerfile' in logs) + self.assertFalse('.dockerginore' in logs) self.assertTrue('not-ignored' in logs) ####################### @@ -1002,16 +1349,17 @@ def runTest(self): folder = tempfile.mkdtemp() self.tmp_folders.append(folder) - f = open(os.path.join(folder, '.dockercfg'), 'w') + 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(folder) + 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'], b'sakuya') - self.assertEqual(cfg['password'], b'izayoi') + self.assertEqual(cfg['username'], 'sakuya') + self.assertEqual(cfg['password'], 'izayoi') self.assertEqual(cfg['email'], 'sakuya@scarlet.net') self.assertEqual(cfg.get('Auth'), None) @@ -1020,21 +1368,44 @@ 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('{{"{}": {{"auth": "{}", "email": "{}"}}}}\n'.format( + f.write('{{"{0}": {{"auth": "{1}", "email": "{2}"}}}}\n'.format( docker.auth.INDEX_URL, auth_, email_)) f.close() - cfg = docker.auth.load_config(folder) + 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'], b'sakuya') - self.assertEqual(cfg['password'], b'izayoi') + 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() + 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(version='1.11') + + class TestConnectionTimeout(unittest.TestCase): def setUp(self): self.timeout = 0.5 @@ -1067,7 +1438,7 @@ with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - client = docker.Client() + client = docker.Client(base_url=DEFAULT_BASE_URL) client.images() client.close() del client @@ -1076,7 +1447,41 @@ "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) + + if __name__ == '__main__': c = docker.Client(base_url=DEFAULT_BASE_URL) c.pull('busybox') + exec_driver = c.info()['ExecutionDriver'] + EXEC_DRIVER_IS_NATIVE = exec_driver.startswith('native') + c.close() unittest.main() diff -Nru docker-py-0.6.0/tests/test.py docker-py-1.3.0/tests/test.py --- docker-py-0.6.0/tests/test.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/tests/test.py 2015-07-09 17:59:07.000000000 +0000 @@ -14,30 +14,43 @@ import base64 import datetime +import gzip import io import json import os +import re import shutil import signal +import socket import sys import tarfile import tempfile +import threading +import time import unittest -import gzip +import warnings +import random import docker import requests import six +import base import fake_api - try: from unittest import mock except ImportError: import mock +DEFAULT_TIMEOUT_SECONDS = docker.client.constants.DEFAULT_TIMEOUT_SECONDS + +warnings.simplefilter('error') +warnings.filterwarnings('error') +create_host_config = docker.utils.create_host_config + + def response(status_code=200, content='', headers=None, reason=None, elapsed=0, request=None): res = requests.Response() @@ -56,14 +69,22 @@ return None +def fake_inspect_container(self, container, tty=False): + return fake_api.get_fake_inspect_container(tty=tty)[1] + + +def fake_inspect_container_tty(self, container): + return fake_inspect_container(self, container, tty=True) + + def fake_resp(url, data=None, **kwargs): status_code, content = fake_api.fake_responses[url]() return response(status_code=status_code, content=content) fake_request = mock.Mock(side_effect=fake_resp) -url_prefix = 'http+unix://var/run/docker.sock/v{0}/'.format( - docker.client.DEFAULT_DOCKER_API_VERSION) +url_prefix = 'http+docker://localunixsocket/v{0}/'.format( + docker.client.constants.DEFAULT_DOCKER_API_VERSION) class Cleanup(object): @@ -93,7 +114,7 @@ @mock.patch.multiple('docker.Client', get=fake_request, post=fake_request, put=fake_request, delete=fake_request) -class DockerClientTest(Cleanup, unittest.TestCase): +class DockerClientTest(Cleanup, base.BaseTestCase): def setUp(self): self.client = docker.Client() # Force-clear authconfig to avoid tampering with the tests @@ -102,6 +123,32 @@ def tearDown(self): self.client.close() + def assertIn(self, object, collection): + if six.PY2 and sys.version_info[1] <= 6: + return self.assertTrue(object in collection) + return super(DockerClientTest, self).assertIn(object, collection) + + def base_create_payload(self, img='busybox', cmd=None): + if not cmd: + cmd = ['true'] + return {"Tty": False, "Image": img, "Cmd": cmd, + "AttachStdin": False, + "AttachStderr": True, "AttachStdout": True, + "StdinOnce": False, + "OpenStdin": False, "NetworkDisabled": False, + } + + def test_ctor(self): + try: + docker.Client(version=1.12) + except Exception as e: + self.assertTrue(isinstance(e, docker.errors.DockerException)) + if not six.PY3: + self.assertEqual( + str(e), + 'Version parameter must be a string or None. Found float' + ) + ######################### # INFORMATION TESTS # ######################### @@ -113,9 +160,23 @@ fake_request.assert_called_with( url_prefix + 'version', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) + def test_retrieve_server_version(self): + client = docker.Client(version="auto") + self.assertTrue(isinstance(client._version, six.string_types)) + self.assertFalse(client._version == "auto") + client.close() + + def test_auto_retrieve_server_version(self): + try: + version = self.client._retrieve_server_version() + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.assertTrue(isinstance(version, six.string_types)) + def test_info(self): try: self.client.info() @@ -124,7 +185,7 @@ fake_request.assert_called_with( url_prefix + 'info', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_search(self): @@ -136,7 +197,7 @@ fake_request.assert_called_with( url_prefix + 'images/search', params={'term': 'busybox'}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_image_viz(self): @@ -146,6 +207,57 @@ except Exception: pass + def test_events(self): + try: + self.client.events() + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'events', + params={'since': None, 'until': None, 'filters': None}, + stream=True + ) + + def test_events_with_since_until(self): + ts = 1356048000 + now = datetime.datetime.fromtimestamp(ts) + since = now - datetime.timedelta(seconds=10) + until = now + datetime.timedelta(seconds=10) + try: + self.client.events(since=since, until=until) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'events', + params={ + 'since': ts - 10, + 'until': ts + 10, + 'filters': None + }, + stream=True + ) + + def test_events_with_filters(self): + filters = {'event': ['die', 'stop'], + 'container': fake_api.FAKE_CONTAINER_ID} + try: + self.client.events(filters=filters) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + expected_filters = docker.utils.convert_filters(filters) + fake_request.assert_called_with( + url_prefix + 'events', + params={ + 'since': None, + 'until': None, + 'filters': expected_filters + }, + stream=True + ) + ################### # LISTING TESTS # ################### @@ -158,7 +270,7 @@ fake_request.assert_called_with( url_prefix + 'images/json', params={'filter': None, 'only_ids': 0, 'all': 1}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_images_quiet(self): @@ -169,7 +281,7 @@ fake_request.assert_called_with( url_prefix + 'images/json', params={'filter': None, 'only_ids': 1, 'all': 1}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_image_ids(self): @@ -181,7 +293,7 @@ fake_request.assert_called_with( url_prefix + 'images/json', params={'filter': None, 'only_ids': 1, 'all': 0}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_images_filters(self): @@ -194,7 +306,7 @@ url_prefix + 'images/json', params={'filter': None, 'only_ids': 0, 'all': 0, 'filters': '{"dangling": ["true"]}'}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_list_containers(self): @@ -210,10 +322,10 @@ 'since': None, 'size': 0, 'limit': -1, - 'trunc_cmd': 1, + 'trunc_cmd': 0, 'before': None }, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) ##################### @@ -232,11 +344,10 @@ self.assertEqual(json.loads(args[1]['data']), json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["true"], - "AttachStdin": false, "Memory": 0, + "AttachStdin": false, "AttachStderr": true, "AttachStdout": true, "StdinOnce": false, - "OpenStdin": false, "NetworkDisabled": false, - "MemorySwap": 0}''')) + "OpenStdin": false, "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -256,12 +367,11 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls", "/mnt"], "AttachStdin": false, - "Volumes": {"/mnt": {}}, "Memory": 0, + "Volumes": {"/mnt": {}}, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, - "NetworkDisabled": false, - "MemorySwap": 0}''')) + "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -281,12 +391,11 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls", "/mnt"], "AttachStdin": false, - "Volumes": {"/mnt": {}}, "Memory": 0, + "Volumes": {"/mnt": {}}, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, - "NetworkDisabled": false, - "MemorySwap": 0}''')) + "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -304,7 +413,7 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "ExposedPorts": { + "ExposedPorts": { "1111/tcp": {}, "2222/udp": {}, "3333/tcp": {} @@ -312,8 +421,7 @@ "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, - "NetworkDisabled": false, - "MemorySwap": 0}''')) + "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -331,13 +439,11 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["hello"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, - "Entrypoint": "cowsay", - "MemorySwap": 0}''')) + "Entrypoint": "cowsay"}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -355,13 +461,11 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, - "CpuShares": 5, - "MemorySwap": 0}''')) + "CpuShares": 5}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -379,13 +483,12 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, "Cpuset": "0,1", - "MemorySwap": 0}''')) + "CpusetCpus": "0,1"}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -403,13 +506,11 @@ json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["ls"], "AttachStdin": false, - "Memory": 0, "AttachStderr": true, "AttachStdout": true, "OpenStdin": false, "StdinOnce": false, "NetworkDisabled": false, - "WorkingDir": "/root", - "MemorySwap": 0}''')) + "WorkingDir": "/root"}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -425,11 +526,10 @@ self.assertEqual(json.loads(args[1]['data']), json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["true"], - "AttachStdin": true, "Memory": 0, + "AttachStdin": true, "AttachStderr": true, "AttachStdout": true, "StdinOnce": true, - "OpenStdin": true, "NetworkDisabled": false, - "MemorySwap": 0}''')) + "OpenStdin": true, "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) @@ -475,78 +575,95 @@ self.assertEqual(json.loads(args[1]['data']), json.loads(''' {"Tty": false, "Image": "busybox", "Cmd": ["true"], - "AttachStdin": false, "Memory": 0, + "AttachStdin": false, "AttachStderr": true, "AttachStdout": true, "StdinOnce": false, - "OpenStdin": false, "NetworkDisabled": false, - "MemorySwap": 0}''')) + "OpenStdin": false, "NetworkDisabled": false}''')) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual(args[1]['params'], {'name': 'marisa-kirisame'}) def test_create_container_with_mem_limit_as_int(self): try: - self.client.create_container('busybox', 'true', - mem_limit=128.0) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit=128.0 + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0) + self.assertEqual(data['HostConfig']['Memory'], 128.0) def test_create_container_with_mem_limit_as_string(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128' + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0) + self.assertEqual(data['HostConfig']['Memory'], 128.0) def test_create_container_with_mem_limit_as_string_with_k_unit(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128k') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128k' + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0 * 1024) + self.assertEqual(data['HostConfig']['Memory'], 128.0 * 1024) def test_create_container_with_mem_limit_as_string_with_m_unit(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128m') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128m' + ) + ) + except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0 * 1024 * 1024) + self.assertEqual(data['HostConfig']['Memory'], 128.0 * 1024 * 1024) def test_create_container_with_mem_limit_as_string_with_g_unit(self): try: - self.client.create_container('busybox', 'true', - mem_limit='128g') + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + mem_limit='128g' + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args data = json.loads(args[1]['data']) - self.assertEqual(data['Memory'], 128.0 * 1024 * 1024 * 1024) + self.assertEqual( + data['HostConfig']['Memory'], 128.0 * 1024 * 1024 * 1024 + ) def test_create_container_with_mem_limit_as_string_with_wrong_value(self): - self.assertRaises(docker.errors.DockerException, - self.client.create_container, - 'busybox', 'true', mem_limit='128p') - - self.assertRaises(docker.errors.DockerException, - self.client.create_container, - 'busybox', 'true', mem_limit='1f28') + self.assertRaises( + docker.errors.DockerException, create_host_config, mem_limit='128p' + ) + + self.assertRaises( + docker.errors.DockerException, create_host_config, mem_limit='1f28' + ) def test_start_container(self): try: @@ -559,277 +676,597 @@ args[0][0], url_prefix + 'containers/3cc2351ab11b/start' ) + self.assertEqual(json.loads(args[1]['data']), {}) self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False} - ) - self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_lxc_conf(self): + def test_start_container_none(self): + try: + self.client.start(container=None) + except ValueError as e: + self.assertEqual(str(e), 'image or container param is undefined') + else: + self.fail('Command should raise ValueError') + + try: + self.client.start(None) + except ValueError as e: + self.assertEqual(str(e), 'image or container param is undefined') + else: + self.fail('Command should raise ValueError') + + def test_start_container_regression_573(self): try: - self.client.start( - fake_api.FAKE_CONTAINER_ID, - lxc_conf={'lxc.conf.k': 'lxc.conf.value'} + self.client.start(**{'container': fake_api.FAKE_CONTAINER_ID}) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + def test_create_container_with_lxc_conf(self): + try: + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + lxc_conf={'lxc.conf.k': 'lxc.conf.value'} + ) ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args self.assertEqual( args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - {"LxcConf": [{"Value": "lxc.conf.value", "Key": "lxc.conf.k"}], - "PublishAllPorts": False, "Privileged": False} + url_prefix + 'containers/create' ) + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['LxcConf'] = [ + {"Value": "lxc.conf.value", "Key": "lxc.conf.k"} + ] + + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual( args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_lxc_conf_compat(self): + def test_create_container_with_lxc_conf_compat(self): try: - self.client.start( - fake_api.FAKE_CONTAINER_ID, - lxc_conf=[{'Key': 'lxc.conf.k', 'Value': 'lxc.conf.value'}] + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + lxc_conf=[{'Key': 'lxc.conf.k', 'Value': 'lxc.conf.value'}] + ) + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['LxcConf'] = [ + {"Value": "lxc.conf.value", "Key": "lxc.conf.k"} + ] + self.assertEqual( + json.loads(args[1]['data']), expected_payload) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + self.assertEqual( + args[1]['timeout'], + DEFAULT_TIMEOUT_SECONDS + ) + + def test_create_container_with_binds_ro(self): + try: + mount_dest = '/mnt' + mount_origin = '/tmp' + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds={mount_origin: { + "bind": mount_dest, + "ro": True + }} + ) ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args self.assertEqual(args[0][0], url_prefix + - 'containers/3cc2351ab11b/start') + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = ["/tmp:/mnt:ro"] + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) self.assertEqual( - json.loads(args[1]['data']), - { - "LxcConf": [{"Key": "lxc.conf.k", "Value": "lxc.conf.value"}], - "PublishAllPorts": False, - "Privileged": False, - } + args[1]['timeout'], + DEFAULT_TIMEOUT_SECONDS ) + + def test_create_container_with_binds_rw(self): + try: + mount_dest = '/mnt' + mount_origin = '/tmp' + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds={mount_origin: { + "bind": mount_dest, + "ro": False + }} + ) + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = ["/tmp:/mnt:rw"] + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_binds_ro(self): + def test_create_container_with_binds_mode(self): try: mount_dest = '/mnt' mount_origin = '/tmp' - self.client.start(fake_api.FAKE_CONTAINER_ID, - binds={mount_origin: { - "bind": mount_dest, - "ro": True - }}) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds={mount_origin: { + "bind": mount_dest, + "mode": "z", + }} + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args self.assertEqual(args[0][0], url_prefix + - 'containers/3cc2351ab11b/start') - self.assertEqual(json.loads(args[1]['data']), - {"Binds": ["/tmp:/mnt:ro"], - "PublishAllPorts": False, - "Privileged": False}) + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = ["/tmp:/mnt:z"] + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS) + DEFAULT_TIMEOUT_SECONDS + ) - def test_start_container_with_binds_rw(self): + def test_create_container_with_binds_mode_and_ro_error(self): try: mount_dest = '/mnt' mount_origin = '/tmp' - self.client.start(fake_api.FAKE_CONTAINER_ID, - binds={mount_origin: { - "bind": mount_dest, "ro": False}}) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds={mount_origin: { + "bind": mount_dest, + "mode": "z", + "ro": True, + }} + ) + ) + except ValueError: + return + + self.fail('Command should raise ValueError') + + def test_create_container_with_binds_list(self): + try: + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + binds=[ + "/tmp:/mnt/1:ro", + "/tmp:/mnt/2", + ], + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args self.assertEqual(args[0][0], url_prefix + - 'containers/3cc2351ab11b/start') - self.assertEqual(json.loads(args[1]['data']), - {"Binds": ["/tmp:/mnt:rw"], - "PublishAllPorts": False, - "Privileged": False}) + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = [ + "/tmp:/mnt/1:ro", + "/tmp:/mnt/2", + ] + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_port_binds(self): + def test_create_container_with_port_binds(self): self.maxDiff = None try: - self.client.start(fake_api.FAKE_CONTAINER_ID, port_bindings={ - 1111: None, - 2222: 2222, - '3333/udp': (3333,), - 4444: ('127.0.0.1',), - 5555: ('127.0.0.1', 5555), - 6666: [('127.0.0.1',), ('192.168.0.1',)] - }) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + port_bindings={ + 1111: None, + 2222: 2222, + '3333/udp': (3333,), + 4444: ('127.0.0.1',), + 5555: ('127.0.0.1', 5555), + 6666: [('127.0.0.1',), ('192.168.0.1',)] + } + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args - self.assertEqual(args[0][0], url_prefix + - 'containers/3cc2351ab11b/start') + self.assertEqual(args[0][0], url_prefix + 'containers/create') data = json.loads(args[1]['data']) - self.assertEqual(data['PublishAllPorts'], False) - self.assertTrue('1111/tcp' in data['PortBindings']) - self.assertTrue('2222/tcp' in data['PortBindings']) - self.assertTrue('3333/udp' in data['PortBindings']) - self.assertTrue('4444/tcp' in data['PortBindings']) - self.assertTrue('5555/tcp' in data['PortBindings']) - self.assertTrue('6666/tcp' in data['PortBindings']) + port_bindings = data['HostConfig']['PortBindings'] + self.assertTrue('1111/tcp' in port_bindings) + self.assertTrue('2222/tcp' in port_bindings) + self.assertTrue('3333/udp' in port_bindings) + self.assertTrue('4444/tcp' in port_bindings) + self.assertTrue('5555/tcp' in port_bindings) + self.assertTrue('6666/tcp' in port_bindings) self.assertEqual( [{"HostPort": "", "HostIp": ""}], - data['PortBindings']['1111/tcp'] + port_bindings['1111/tcp'] ) self.assertEqual( [{"HostPort": "2222", "HostIp": ""}], - data['PortBindings']['2222/tcp'] + port_bindings['2222/tcp'] ) self.assertEqual( [{"HostPort": "3333", "HostIp": ""}], - data['PortBindings']['3333/udp'] + port_bindings['3333/udp'] ) self.assertEqual( [{"HostPort": "", "HostIp": "127.0.0.1"}], - data['PortBindings']['4444/tcp'] + port_bindings['4444/tcp'] ) self.assertEqual( [{"HostPort": "5555", "HostIp": "127.0.0.1"}], - data['PortBindings']['5555/tcp'] + port_bindings['5555/tcp'] ) - self.assertEqual(len(data['PortBindings']['6666/tcp']), 2) + self.assertEqual(len(port_bindings['6666/tcp']), 2) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_links(self): - # one link + def test_create_container_with_mac_address(self): + try: + mac_address_expected = "02:42:ac:11:00:0a" + container = self.client.create_container( + 'busybox', ['sleep', '60'], mac_address=mac_address_expected) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + res = self.client.inspect_container(container['Id']) + self.assertEqual(mac_address_expected, + res['NetworkSettings']['MacAddress']) + + def test_create_container_with_links(self): try: link_path = 'path' alias = 'alias' - self.client.start(fake_api.FAKE_CONTAINER_ID, - links={link_path: alias}) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + links={link_path: alias} + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False, - "Links": ["path:alias"]} + args[0][0], url_prefix + 'containers/create' ) + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Links'] = ['path:alias'] + + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) - def test_start_container_with_multiple_links(self): + def test_create_container_with_multiple_links(self): try: link_path = 'path' alias = 'alias' - self.client.start( - fake_api.FAKE_CONTAINER_ID, - links={ - link_path + '1': alias + '1', - link_path + '2': alias + '2' - } + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + links={ + link_path + '1': alias + '1', + link_path + '2': alias + '2' + } + ) ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Links'] = [ + 'path1:alias1', 'path2:alias2' + ] + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - { - "PublishAllPorts": False, - "Privileged": False, - "Links": ["path1:alias1", "path2:alias2"] - } - ) - self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) - def test_start_container_with_links_as_list_of_tuples(self): - # one link + def test_create_container_with_links_as_list_of_tuples(self): try: link_path = 'path' alias = 'alias' - self.client.start(fake_api.FAKE_CONTAINER_ID, - links=[(link_path, alias)]) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + links=[(link_path, alias)] + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Links'] = ['path:alias'] + + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False, - "Links": ["path:alias"]} - ) - self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) - def test_start_container_privileged(self): + def test_create_container_privileged(self): try: - self.client.start(fake_api.FAKE_CONTAINER_ID, privileged=True) + self.client.create_container( + 'busybox', 'true', + host_config=create_host_config(privileged=True) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Privileged'] = True args = fake_request.call_args - self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual(json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": True}) + self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + DEFAULT_TIMEOUT_SECONDS ) + def test_start_container_with_lxc_conf(self): + if six.PY2: + try: + self.client.start( + fake_api.FAKE_CONTAINER_ID, + lxc_conf={'lxc.conf.k': 'lxc.conf.value'} + ) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start( + fake_api.FAKE_CONTAINER_ID, + lxc_conf={'lxc.conf.k': 'lxc.conf.value'} + ) + + def test_start_container_with_lxc_conf_compat(self): + if six.PY2: + try: + self.client.start( + fake_api.FAKE_CONTAINER_ID, + lxc_conf=[{'Key': 'lxc.conf.k', 'Value': 'lxc.conf.value'}] + ) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start( + fake_api.FAKE_CONTAINER_ID, + lxc_conf=[{'Key': 'lxc.conf.k', 'Value': 'lxc.conf.value'}] + ) + + def test_start_container_with_binds_ro(self): + mount_dest = '/mnt' + mount_origin = '/tmp' + + if six.PY2: + try: + self.client.start( + fake_api.FAKE_CONTAINER_ID, binds={ + mount_origin: { + "bind": mount_dest, + "ro": True + } + } + ) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start( + fake_api.FAKE_CONTAINER_ID, binds={ + mount_origin: { + "bind": mount_dest, + "ro": True + } + } + ) + + def test_start_container_with_binds_rw(self): + mount_dest = '/mnt' + mount_origin = '/tmp' + if six.PY2: + try: + self.client.start( + fake_api.FAKE_CONTAINER_ID, binds={ + mount_origin: {"bind": mount_dest, "ro": False} + } + ) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start( + fake_api.FAKE_CONTAINER_ID, binds={ + mount_origin: {"bind": mount_dest, "ro": False} + } + ) + + def test_start_container_with_port_binds(self): + self.maxDiff = None + if six.PY2: + try: + self.client.start(fake_api.FAKE_CONTAINER_ID, port_bindings={ + 1111: None, + 2222: 2222, + '3333/udp': (3333,), + 4444: ('127.0.0.1',), + 5555: ('127.0.0.1', 5555), + 6666: [('127.0.0.1',), ('192.168.0.1',)] + }) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start(fake_api.FAKE_CONTAINER_ID, port_bindings={ + 1111: None, + 2222: 2222, + '3333/udp': (3333,), + 4444: ('127.0.0.1',), + 5555: ('127.0.0.1', 5555), + 6666: [('127.0.0.1',), ('192.168.0.1',)] + }) + + def test_start_container_with_links(self): + # one link + link_path = 'path' + alias = 'alias' + + if six.PY2: + try: + self.client.start(fake_api.FAKE_CONTAINER_ID, + links={link_path: alias}) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start( + fake_api.FAKE_CONTAINER_ID, links={link_path: alias} + ) + + def test_start_container_with_multiple_links(self): + link_path = 'path' + alias = 'alias' + if six.PY2: + try: + self.client.start( + fake_api.FAKE_CONTAINER_ID, + links={ + link_path + '1': alias + '1', + link_path + '2': alias + '2' + } + ) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start( + fake_api.FAKE_CONTAINER_ID, + links={ + link_path + '1': alias + '1', + link_path + '2': alias + '2' + } + ) + + def test_start_container_with_links_as_list_of_tuples(self): + # one link + link_path = 'path' + alias = 'alias' + if six.PY2: + try: + self.client.start(fake_api.FAKE_CONTAINER_ID, + links=[(link_path, alias)]) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start(fake_api.FAKE_CONTAINER_ID, + links=[(link_path, alias)]) + + def test_start_container_privileged(self): + if six.PY2: + try: + self.client.start(fake_api.FAKE_CONTAINER_ID, privileged=True) + except DeprecationWarning as e: + return + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + else: + self.fail('Expected a DeprecationWarning') + else: + with self.assertWarns(DeprecationWarning): + self.client.start(fake_api.FAKE_CONTAINER_ID, privileged=True) + def test_start_container_with_dict_instead_of_id(self): try: self.client.start({'Id': fake_api.FAKE_CONTAINER_ID}) @@ -840,130 +1277,196 @@ args[0][0], url_prefix + 'containers/3cc2351ab11b/start' ) + self.assertEqual(json.loads(args[1]['data']), {}) self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False} - ) - self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_restart_policy(self): + def test_create_container_with_restart_policy(self): try: - self.client.start(fake_api.FAKE_CONTAINER_ID, - restart_policy={ - "Name": "always", - "MaximumRetryCount": 0 - }) + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + restart_policy={ + "Name": "always", + "MaximumRetryCount": 0 + } + ) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['RestartPolicy'] = { + "MaximumRetryCount": 0, "Name": "always" + } + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False, - "RestartPolicy": {"MaximumRetryCount": 0, "Name": "always"}} - ) - self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_added_capabilities(self): + def test_create_container_with_added_capabilities(self): try: - self.client.start(fake_api.FAKE_CONTAINER_ID, - cap_add=['MKNOD']) + self.client.create_container( + 'busybox', 'true', + host_config=create_host_config(cap_add=['MKNOD']) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['CapAdd'] = ['MKNOD'] + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False, - "CapAdd": ["MKNOD"]} - ) - self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_dropped_capabilities(self): + def test_create_container_with_dropped_capabilities(self): try: - self.client.start(fake_api.FAKE_CONTAINER_ID, - cap_drop=['MKNOD']) + self.client.create_container( + 'busybox', 'true', + host_config=create_host_config(cap_drop=['MKNOD']) + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['CapDrop'] = ['MKNOD'] + self.assertEqual(json.loads(args[1]['data']), expected_payload) self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False, - "CapDrop": ["MKNOD"]} + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) + + def test_create_container_with_devices(self): + try: + self.client.create_container( + 'busybox', 'true', host_config=create_host_config( + devices=['/dev/sda:/dev/xvda:rwm', + '/dev/sdb:/dev/xvdb', + '/dev/sdc'] + ) + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Devices'] = [ + {'CgroupPermissions': 'rwm', + 'PathInContainer': '/dev/xvda', + 'PathOnHost': '/dev/sda'}, + {'CgroupPermissions': 'rwm', + 'PathInContainer': '/dev/xvdb', + 'PathOnHost': '/dev/sdb'}, + {'CgroupPermissions': 'rwm', + 'PathInContainer': '/dev/sdc', + 'PathOnHost': '/dev/sdc'} + ] + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual( + args[1]['headers'], {'Content-Type': 'application/json'} + ) + self.assertEqual( + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS + ) + + def test_create_container_with_labels_dict(self): + labels_dict = { + six.text_type('foo'): six.text_type('1'), + six.text_type('bar'): six.text_type('2'), + } + try: + self.client.create_container( + 'busybox', 'true', + labels=labels_dict, + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(json.loads(args[1]['data'])['Labels'], labels_dict) self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) - def test_start_container_with_devices(self): + def test_create_container_with_labels_list(self): + labels_list = [ + six.text_type('foo'), + six.text_type('bar'), + ] + labels_dict = { + six.text_type('foo'): six.text_type(), + six.text_type('bar'): six.text_type(), + } try: - self.client.start(fake_api.FAKE_CONTAINER_ID, - devices=['/dev/sda:/dev/xvda:rwm', - '/dev/sdb:/dev/xvdb', - '/dev/sdc']) + self.client.create_container( + 'busybox', 'true', + labels=labels_list, + ) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + 'containers/create') + self.assertEqual(json.loads(args[1]['data'])['Labels'], labels_dict) self.assertEqual( - args[0][0], - url_prefix + 'containers/3cc2351ab11b/start' - ) - self.assertEqual( - json.loads(args[1]['data']), - {"PublishAllPorts": False, "Privileged": False, - "Devices": [{'CgroupPermissions': 'rwm', - 'PathInContainer': '/dev/sda:/dev/xvda:rwm', - 'PathOnHost': '/dev/sda:/dev/xvda:rwm'}, - {'CgroupPermissions': 'rwm', - 'PathInContainer': '/dev/sdb:/dev/xvdb', - 'PathOnHost': '/dev/sdb:/dev/xvdb'}, - {'CgroupPermissions': 'rwm', - 'PathInContainer': '/dev/sdc', - 'PathOnHost': '/dev/sdc'}]} + args[1]['headers'], {'Content-Type': 'application/json'} ) self.assertEqual( - args[1]['headers'], - {'Content-Type': 'application/json'} + args[1]['timeout'], DEFAULT_TIMEOUT_SECONDS ) + + def test_create_container_with_named_volume(self): + try: + mount_dest = '/mnt' + volume_name = 'name' + self.client.create_container( + 'busybox', 'true', + host_config=create_host_config( + binds={volume_name: { + "bind": mount_dest, + "ro": False + }}), + volume_driver='foodriver', + ) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], url_prefix + + 'containers/create') + expected_payload = self.base_create_payload() + expected_payload['VolumeDriver'] = 'foodriver' + expected_payload['HostConfig'] = create_host_config() + expected_payload['HostConfig']['Binds'] = ["name:/mnt:rw"] + self.assertEqual(json.loads(args[1]['data']), expected_payload) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) self.assertEqual( args[1]['timeout'], - docker.client.DEFAULT_TIMEOUT_SECONDS + DEFAULT_TIMEOUT_SECONDS ) def test_resize_container(self): @@ -979,7 +1482,22 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/resize', params={'h': 15, 'w': 120}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS + ) + + def test_rename_container(self): + try: + self.client.rename( + {'Id': fake_api.FAKE_CONTAINER_ID}, + name='foobar' + ) + except Exception as e: + self.fail('Command shold not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'containers/3cc2351ab11b/rename', + params={'name': 'foobar'}, + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_wait(self): @@ -1005,20 +1523,24 @@ timeout=None ) + def _socket_path_for_client_session(self, client): + socket_adapter = client.get_adapter('http+docker://') + return socket_adapter.socket_path + def test_url_compatibility_unix(self): c = docker.Client(base_url="unix://socket") - assert c.base_url == "http+unix://socket" + assert self._socket_path_for_client_session(c) == '/socket' def test_url_compatibility_unix_triple_slash(self): c = docker.Client(base_url="unix:///socket") - assert c.base_url == "http+unix://socket" + assert self._socket_path_for_client_session(c) == '/socket' def test_url_compatibility_http_unix_triple_slash(self): c = docker.Client(base_url="http+unix:///socket") - assert c.base_url == "http+unix://socket" + assert self._socket_path_for_client_session(c) == '/socket' def test_url_compatibility_http(self): c = docker.Client(base_url="http://hostname:1234") @@ -1032,7 +1554,9 @@ def test_logs(self): try: - logs = self.client.logs(fake_api.FAKE_CONTAINER_ID) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + logs = self.client.logs(fake_api.FAKE_CONTAINER_ID) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1040,7 +1564,7 @@ url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS, + timeout=DEFAULT_TIMEOUT_SECONDS, stream=False ) @@ -1051,7 +1575,9 @@ def test_logs_with_dict_instead_of_id(self): try: - logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID}) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + logs = self.client.logs({'Id': fake_api.FAKE_CONTAINER_ID}) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1059,7 +1585,7 @@ url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS, + timeout=DEFAULT_TIMEOUT_SECONDS, stream=False ) @@ -1070,7 +1596,9 @@ def test_log_streaming(self): try: - self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=True) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1078,13 +1606,16 @@ url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1, 'tail': 'all'}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS, + timeout=DEFAULT_TIMEOUT_SECONDS, stream=True ) def test_log_tail(self): try: - self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, tail=10) + with mock.patch('docker.Client.inspect_container', + fake_inspect_container): + self.client.logs(fake_api.FAKE_CONTAINER_ID, stream=False, + tail=10) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -1092,10 +1623,31 @@ url_prefix + 'containers/3cc2351ab11b/logs', params={'timestamps': 0, 'follow': 0, 'stderr': 1, 'stdout': 1, 'tail': 10}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS, + timeout=DEFAULT_TIMEOUT_SECONDS, stream=False ) + def test_log_tty(self): + try: + m = mock.Mock() + with mock.patch('docker.Client.inspect_container', + fake_inspect_container_tty): + with mock.patch('docker.Client._stream_raw_result', + m): + self.client.logs(fake_api.FAKE_CONTAINER_ID, + stream=True) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + self.assertTrue(m.called) + fake_request.assert_called_with( + url_prefix + 'containers/3cc2351ab11b/logs', + params={'timestamps': 0, 'follow': 1, 'stderr': 1, 'stdout': 1, + 'tail': 'all'}, + timeout=DEFAULT_TIMEOUT_SECONDS, + stream=True + ) + def test_diff(self): try: self.client.diff(fake_api.FAKE_CONTAINER_ID) @@ -1104,7 +1656,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/changes', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_diff_with_dict_instead_of_id(self): @@ -1115,7 +1667,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/changes', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_port(self): @@ -1126,7 +1678,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/json', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_stop_container(self): @@ -1139,7 +1691,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/stop', params={'t': timeout}, - timeout=(docker.client.DEFAULT_TIMEOUT_SECONDS + timeout) + timeout=(DEFAULT_TIMEOUT_SECONDS + timeout) ) def test_stop_container_with_dict_instead_of_id(self): @@ -1153,34 +1705,86 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/stop', params={'t': timeout}, - timeout=(docker.client.DEFAULT_TIMEOUT_SECONDS + timeout) + timeout=(DEFAULT_TIMEOUT_SECONDS + timeout) ) - def test_execute_command(self): + def test_exec_create(self): try: - self.client.execute(fake_api.FAKE_CONTAINER_ID, ['ls', '-1']) + self.client.exec_create(fake_api.FAKE_CONTAINER_ID, ['ls', '-1']) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) args = fake_request.call_args - self.assertEqual(args[0][0], - url_prefix + 'exec/3cc2351ab11b/start') + self.assertEqual( + args[0][0], url_prefix + 'containers/{0}/exec'.format( + fake_api.FAKE_CONTAINER_ID + ) + ) - self.assertEqual(json.loads(args[1]['data']), - json.loads('''{ - "Tty": false, - "AttachStderr": true, - "Container": "3cc2351ab11b", - "Cmd": ["ls", "-1"], - "AttachStdin": false, - "User": "", - "Detach": false, - "Privileged": false, - "AttachStdout": true}''')) + self.assertEqual( + json.loads(args[1]['data']), { + 'Tty': False, + 'AttachStdout': True, + 'Container': fake_api.FAKE_CONTAINER_ID, + 'Cmd': ['ls', '-1'], + 'Privileged': False, + 'AttachStdin': False, + 'AttachStderr': True, + 'User': '' + } + ) + + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + + def test_exec_start(self): + try: + self.client.exec_start(fake_api.FAKE_EXEC_ID) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual( + args[0][0], url_prefix + 'exec/{0}/start'.format( + fake_api.FAKE_EXEC_ID + ) + ) + + self.assertEqual( + json.loads(args[1]['data']), { + 'Tty': False, + 'Detach': False, + } + ) self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) + def test_exec_inspect(self): + try: + self.client.exec_inspect(fake_api.FAKE_EXEC_ID) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual( + args[0][0], url_prefix + 'exec/{0}/json'.format( + fake_api.FAKE_EXEC_ID + ) + ) + + def test_exec_resize(self): + try: + self.client.exec_resize(fake_api.FAKE_EXEC_ID, height=20, width=60) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'exec/{0}/resize'.format(fake_api.FAKE_EXEC_ID), + params={'h': 20, 'w': 60}, + timeout=DEFAULT_TIMEOUT_SECONDS + ) + def test_pause_container(self): try: self.client.pause(fake_api.FAKE_CONTAINER_ID) @@ -1188,7 +1792,7 @@ self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/pause', - timeout=(docker.client.DEFAULT_TIMEOUT_SECONDS) + timeout=(DEFAULT_TIMEOUT_SECONDS) ) def test_unpause_container(self): @@ -1198,7 +1802,7 @@ self.fail('Command should not raise exception: {0}'.format(e)) fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/unpause', - timeout=(docker.client.DEFAULT_TIMEOUT_SECONDS) + timeout=(DEFAULT_TIMEOUT_SECONDS) ) def test_kill_container(self): @@ -1210,7 +1814,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/kill', params={}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_kill_container_with_dict_instead_of_id(self): @@ -1222,7 +1826,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/kill', params={}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_kill_container_with_signal(self): @@ -1234,7 +1838,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/kill', params={'signal': signal.SIGTERM}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_restart_container(self): @@ -1246,7 +1850,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/restart', params={'t': 2}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_restart_container_with_dict_instead_of_id(self): @@ -1258,7 +1862,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/restart', params={'t': 2}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_remove_container(self): @@ -1270,7 +1874,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b', params={'v': False, 'link': False, 'force': False}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_remove_container_with_dict_instead_of_id(self): @@ -1282,7 +1886,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b', params={'v': False, 'link': False, 'force': False}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_remove_link(self): @@ -1294,7 +1898,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b', params={'v': False, 'link': True, 'force': False}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_export(self): @@ -1306,7 +1910,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/export', stream=True, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_export_with_dict_instead_of_id(self): @@ -1318,7 +1922,7 @@ fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/export', stream=True, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_inspect_container(self): @@ -1326,10 +1930,32 @@ self.client.inspect_container(fake_api.FAKE_CONTAINER_ID) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) - fake_request.assert_called_with( url_prefix + 'containers/3cc2351ab11b/json', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS + ) + + def test_inspect_container_undefined_id(self): + for arg in None, '', {True: True}: + try: + self.client.inspect_container(arg) + except docker.errors.NullResource as e: + self.assertEqual( + e.args[0], 'image or container param is undefined' + ) + else: + self.fail('Command expected NullResource exception') + + def test_container_stats(self): + try: + self.client.stats(fake_api.FAKE_CONTAINER_ID) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + fake_request.assert_called_with( + url_prefix + 'containers/3cc2351ab11b/stats', + timeout=60, + stream=True ) ################## @@ -1387,7 +2013,7 @@ 'container': '3cc2351ab11b', 'author': None }, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_remove_image(self): @@ -1399,7 +2025,7 @@ fake_request.assert_called_with( url_prefix + 'images/e9aa60c60128', params={'force': False, 'noprune': False}, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_image_history(self): @@ -1410,7 +2036,7 @@ fake_request.assert_called_with( url_prefix + 'images/test_image/history', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_import_image(self): @@ -1431,15 +2057,14 @@ 'fromSrc': fake_api.FAKE_TARBALL_PATH }, data=None, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) - def test_import_image_from_file(self): - buf = tempfile.NamedTemporaryFile(delete=False) + def test_import_image_from_bytes(self): + stream = (i for i in range(0, 100)) try: - # pretent the buffer is a file self.client.import_image( - buf.name, + stream, repository=fake_api.FAKE_REPO_NAME, tag=fake_api.FAKE_TAG_NAME ) @@ -1451,13 +2076,14 @@ params={ 'repo': fake_api.FAKE_REPO_NAME, 'tag': fake_api.FAKE_TAG_NAME, - 'fromSrc': '-' + 'fromSrc': '-', }, - data='', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + headers={ + 'Content-Type': 'application/tar', + }, + data=stream, + timeout=DEFAULT_TIMEOUT_SECONDS ) - buf.close() - os.remove(buf.name) def test_import_image_from_image(self): try: @@ -1477,7 +2103,7 @@ 'fromImage': fake_api.FAKE_IMAGE_NAME }, data=None, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_inspect_image(self): @@ -1488,9 +2114,20 @@ fake_request.assert_called_with( url_prefix + 'images/test_image/json', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) + def test_inspect_image_undefined_id(self): + for arg in None, '', {True: True}: + try: + self.client.inspect_image(arg) + except docker.errors.NullResource as e: + self.assertEqual( + e.args[0], 'image or container param is undefined' + ) + else: + self.fail('Command expected NullResource exception') + def test_insert_image(self): try: self.client.insert(fake_api.FAKE_IMAGE_NAME, @@ -1509,7 +2146,7 @@ 'url': fake_api.FAKE_URL, 'path': fake_api.FAKE_PATH }, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_push_image(self): @@ -1528,7 +2165,7 @@ data='{}', headers={'Content-Type': 'application/json'}, stream=False, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_push_image_with_tag(self): @@ -1549,7 +2186,7 @@ data='{}', headers={'Content-Type': 'application/json'}, stream=False, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_push_image_stream(self): @@ -1568,7 +2205,7 @@ data='{}', headers={'Content-Type': 'application/json'}, stream=True, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_tag_image(self): @@ -1584,7 +2221,7 @@ 'repo': 'repo', 'force': 0 }, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_tag_image_tag(self): @@ -1604,7 +2241,7 @@ 'repo': 'repo', 'force': 0 }, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_tag_image_force(self): @@ -1621,7 +2258,7 @@ 'repo': 'repo', 'force': 1 }, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_get_image(self): @@ -1633,7 +2270,7 @@ fake_request.assert_called_with( url_prefix + 'images/e9aa60c60128/get', stream=True, - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) def test_load_image(self): @@ -1645,7 +2282,7 @@ fake_request.assert_called_with( url_prefix + 'images/load', data='Byte Stream....', - timeout=docker.client.DEFAULT_TIMEOUT_SECONDS + timeout=DEFAULT_TIMEOUT_SECONDS ) ################# @@ -1666,6 +2303,20 @@ except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) + def test_build_container_pull(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')) + try: + self.client.build(fileobj=script, pull=True) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + def test_build_container_stream(self): script = io.BytesIO('\n'.join([ 'FROM busybox', @@ -1715,6 +2366,45 @@ except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) + def test_build_remote_with_registry_auth(self): + try: + self.client._auth_configs = { + 'https://example.com': { + 'user': 'example', + 'password': 'example', + 'email': 'example@example.com' + } + } + + self.client.build(path='https://github.com/docker-library/mongo') + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + def test_build_container_with_named_dockerfile(self): + try: + self.client.build('.', dockerfile='nameddockerfile') + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + def test_build_container_with_container_limits(self): + try: + self.client.build('.', container_limits={ + 'memory': 1024 * 1024, + 'cpusetcpus': 1, + 'cpushares': 1000, + 'memswap': 1024 * 1024 * 8 + }) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + def test_build_container_invalid_container_limits(self): + self.assertRaises( + docker.errors.DockerException, + lambda: self.client.build('.', container_limits={ + 'foo': 'bar' + }) + ) + ####################### # PY SPECIFIC TESTS # ####################### @@ -1728,12 +2418,12 @@ def test_load_config(self): folder = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, folder) - f = open(os.path.join(folder, '.dockercfg'), '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(folder) + dockercfg_path = os.path.join(folder, '.dockercfg') + with open(dockercfg_path, 'w') as f: + auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') + f.write('auth = {0}\n'.format(auth_)) + f.write('email = sakuya@scarlet.net') + cfg = docker.auth.load_config(dockercfg_path) self.assertTrue(docker.auth.INDEX_URL in cfg) self.assertNotEqual(cfg[docker.auth.INDEX_URL], None) cfg = cfg[docker.auth.INDEX_URL] @@ -1742,6 +2432,34 @@ self.assertEqual(cfg['email'], 'sakuya@scarlet.net') self.assertEqual(cfg.get('auth'), None) + def test_load_config_with_random_name(self): + folder = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, folder) + + dockercfg_path = os.path.join(folder, + '.{0}.dockercfg'.format( + random.randrange(100000))) + registry = 'https://your.private.registry.io' + auth_ = base64.b64encode(b'sakuya:izayoi').decode('ascii') + config = { + registry: { + 'auth': '{0}'.format(auth_), + 'email': 'sakuya@scarlet.net' + } + } + + with open(dockercfg_path, 'w') as f: + f.write(json.dumps(config)) + + cfg = docker.auth.load_config(dockercfg_path) + self.assertTrue(registry in cfg) + self.assertNotEqual(cfg[registry], None) + cfg = cfg[registry] + self.assertEqual(cfg['username'], 'sakuya') + self.assertEqual(cfg['password'], 'izayoi') + self.assertEqual(cfg['email'], 'sakuya@scarlet.net') + self.assertEqual(cfg.get('auth'), None) + def test_tar_with_excludes(self): base = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base) @@ -1760,18 +2478,18 @@ (['test/foo', 'a.txt'], ['bar', 'bar/a.txt', 'bar/b.py', 'bar/other.png', 'test']), ): - archive = docker.utils.tar(base, exclude=exclude) - tar = tarfile.open(fileobj=archive) - self.assertEqual(sorted(tar.getnames()), names) + with docker.utils.tar(base, exclude=exclude) as archive: + tar = tarfile.open(fileobj=archive) + self.assertEqual(sorted(tar.getnames()), names) def test_tar_with_empty_directory(self): base = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, base) for d in ['foo', 'bar']: os.makedirs(os.path.join(base, d)) - archive = docker.utils.tar(base) - tar = tarfile.open(fileobj=archive) - self.assertEqual(sorted(tar.getnames()), ['bar', 'foo']) + with docker.utils.tar(base) as archive: + tar = tarfile.open(fileobj=archive) + self.assertEqual(sorted(tar.getnames()), ['bar', 'foo']) def test_tar_with_file_symlinks(self): base = tempfile.mkdtemp() @@ -1780,9 +2498,9 @@ f.write("content") os.makedirs(os.path.join(base, 'bar')) os.symlink('../foo', os.path.join(base, 'bar/foo')) - archive = docker.utils.tar(base) - tar = tarfile.open(fileobj=archive) - self.assertEqual(sorted(tar.getnames()), ['bar', 'bar/foo', 'foo']) + with docker.utils.tar(base) as archive: + tar = tarfile.open(fileobj=archive) + self.assertEqual(sorted(tar.getnames()), ['bar', 'bar/foo', 'foo']) def test_tar_with_directory_symlinks(self): base = tempfile.mkdtemp() @@ -1790,9 +2508,124 @@ for d in ['foo', 'bar']: os.makedirs(os.path.join(base, d)) os.symlink('../foo', os.path.join(base, 'bar/foo')) - archive = docker.utils.tar(base) - tar = tarfile.open(fileobj=archive) - self.assertEqual(sorted(tar.getnames()), ['bar', 'bar/foo', 'foo']) + with docker.utils.tar(base) as archive: + tar = tarfile.open(fileobj=archive) + self.assertEqual(sorted(tar.getnames()), ['bar', 'bar/foo', 'foo']) + + ####################### + # HOST CONFIG TESTS # + ####################### + + def test_create_host_config_secopt(self): + security_opt = ['apparmor:test_profile'] + result = create_host_config(security_opt=security_opt) + self.assertIn('SecurityOpt', result) + self.assertEqual(result['SecurityOpt'], security_opt) + + self.assertRaises( + docker.errors.DockerException, create_host_config, + security_opt='wrong' + ) + + +class StreamTest(Cleanup, base.BaseTestCase): + + def setUp(self): + socket_dir = tempfile.mkdtemp() + self.build_context = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, socket_dir) + self.addCleanup(shutil.rmtree, self.build_context) + self.socket_file = os.path.join(socket_dir, 'test_sock.sock') + self.server_socket = self._setup_socket() + self.stop_server = False + server_thread = threading.Thread(target=self.run_server) + server_thread.setDaemon(True) + server_thread.start() + self.response = None + self.request_handler = None + self.addCleanup(server_thread.join) + self.addCleanup(self.stop) + + def stop(self): + self.stop_server = True + + def _setup_socket(self): + server_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + server_sock.bind(self.socket_file) + # Non-blocking mode so that we can shut the test down easily + server_sock.setblocking(0) + server_sock.listen(5) + return server_sock + + def run_server(self): + try: + while not self.stop_server: + try: + connection, client_address = self.server_socket.accept() + except socket.error: + # Probably no connection to accept yet + time.sleep(0.01) + continue + + connection.setblocking(1) + try: + self.request_handler(connection) + finally: + connection.close() + finally: + self.server_socket.close() + + def early_response_sending_handler(self, connection): + data = b'' + headers = None + + connection.sendall(self.response) + while not headers: + data += connection.recv(2048) + parts = data.split(b'\r\n\r\n', 1) + if len(parts) == 2: + headers, data = parts + + mo = re.search(r'Content-Length: ([0-9]+)', headers.decode()) + assert mo + content_length = int(mo.group(1)) + + while True: + if len(data) >= content_length: + break + + data += connection.recv(2048) + + def test_early_stream_response(self): + self.request_handler = self.early_response_sending_handler + lines = [] + for i in range(0, 50): + line = str(i).encode() + lines += [('%x' % len(line)).encode(), line] + lines.append(b'0') + lines.append(b'') + + self.response = ( + b'HTTP/1.1 200 OK\r\n' + b'Transfer-Encoding: chunked\r\n' + b'\r\n' + ) + b'\r\n'.join(lines) + + with docker.Client(base_url="http+unix://" + self.socket_file) \ + as client: + for i in range(5): + try: + stream = client.build( + path=self.build_context, + stream=True + ) + break + except requests.ConnectionError as e: + if i == 4: + raise e + + self.assertEqual(list(stream), [ + str(i).encode() for i in range(50)]) if __name__ == '__main__': unittest.main() diff -Nru docker-py-0.6.0/tests/utils_test.py docker-py-1.3.0/tests/utils_test.py --- docker-py-0.6.0/tests/utils_test.py 2014-11-03 22:59:06.000000000 +0000 +++ docker-py-1.3.0/tests/utils_test.py 2015-06-18 18:06:56.000000000 +0000 @@ -5,11 +5,16 @@ from docker.client import Client from docker.errors import DockerException from docker.utils import ( - parse_repository_tag, parse_host, convert_filters, kwargs_from_env + parse_repository_tag, parse_host, convert_filters, kwargs_from_env, + create_host_config, Ulimit, LogConfig, parse_bytes ) +from docker.utils.ports import build_port_bindings, split_port +from docker.auth import resolve_authconfig +import base -class UtilsTest(unittest.TestCase): + +class UtilsTest(base.BaseTestCase): longMessage = True def setUp(self): @@ -32,6 +37,12 @@ self.assertEqual(parse_repository_tag("url:5000/repo:tag"), ("url:5000/repo", "tag")) + def test_parse_bytes(self): + self.assertEqual(parse_bytes("512MB"), (536870912)) + self.assertEqual(parse_bytes("512M"), (536870912)) + self.assertRaises(DockerException, parse_bytes, "512MK") + self.assertRaises(DockerException, parse_bytes, "512L") + def test_parse_host(self): invalid_hosts = [ '0.0.0.0', @@ -71,16 +82,16 @@ 'testdata/certs'), DOCKER_TLS_VERIFY='1') kwargs = kwargs_from_env(assert_hostname=False) - self.assertEquals('https://192.168.59.103:2376', kwargs['base_url']) + self.assertEqual('https://192.168.59.103:2376', kwargs['base_url']) self.assertTrue('ca.pem' in kwargs['tls'].verify) self.assertTrue('cert.pem' in kwargs['tls'].cert[0]) self.assertTrue('key.pem' in kwargs['tls'].cert[1]) - self.assertEquals(False, kwargs['tls'].assert_hostname) + self.assertEqual(False, kwargs['tls'].assert_hostname) try: client = Client(**kwargs) - self.assertEquals(kwargs['base_url'], client.base_url) - self.assertEquals(kwargs['tls'].verify, client.verify) - self.assertEquals(kwargs['tls'].cert, client.cert) + self.assertEqual(kwargs['base_url'], client.base_url) + self.assertEqual(kwargs['tls'].verify, client.verify) + self.assertEqual(kwargs['tls'].cert, client.cert) except TypeError as e: self.fail(e) @@ -95,6 +106,227 @@ for filters, expected in tests: self.assertEqual(convert_filters(filters), expected) + def test_create_empty_host_config(self): + empty_config = create_host_config() + self.assertEqual(empty_config, {}) + + def test_create_host_config_dict_ulimit(self): + ulimit_dct = {'name': 'nofile', 'soft': 8096} + config = create_host_config(ulimits=[ulimit_dct]) + self.assertIn('Ulimits', config) + self.assertEqual(len(config['Ulimits']), 1) + ulimit_obj = config['Ulimits'][0] + self.assertTrue(isinstance(ulimit_obj, Ulimit)) + self.assertEqual(ulimit_obj.name, ulimit_dct['name']) + self.assertEqual(ulimit_obj.soft, ulimit_dct['soft']) + self.assertEqual(ulimit_obj['Soft'], ulimit_obj.soft) + + def test_create_host_config_dict_ulimit_capitals(self): + ulimit_dct = {'Name': 'nofile', 'Soft': 8096, 'Hard': 8096 * 4} + config = create_host_config(ulimits=[ulimit_dct]) + self.assertIn('Ulimits', config) + self.assertEqual(len(config['Ulimits']), 1) + ulimit_obj = config['Ulimits'][0] + self.assertTrue(isinstance(ulimit_obj, Ulimit)) + self.assertEqual(ulimit_obj.name, ulimit_dct['Name']) + self.assertEqual(ulimit_obj.soft, ulimit_dct['Soft']) + self.assertEqual(ulimit_obj.hard, ulimit_dct['Hard']) + self.assertEqual(ulimit_obj['Soft'], ulimit_obj.soft) + + def test_create_host_config_obj_ulimit(self): + ulimit_dct = Ulimit(name='nofile', soft=8096) + config = create_host_config(ulimits=[ulimit_dct]) + self.assertIn('Ulimits', config) + self.assertEqual(len(config['Ulimits']), 1) + ulimit_obj = config['Ulimits'][0] + self.assertTrue(isinstance(ulimit_obj, Ulimit)) + self.assertEqual(ulimit_obj, ulimit_dct) + + def test_ulimit_invalid_type(self): + self.assertRaises(ValueError, lambda: Ulimit(name=None)) + self.assertRaises(ValueError, lambda: Ulimit(name='hello', soft='123')) + self.assertRaises(ValueError, lambda: Ulimit(name='hello', hard='456')) + + def test_create_host_config_dict_logconfig(self): + dct = {'type': LogConfig.types.SYSLOG, 'config': {'key1': 'val1'}} + config = create_host_config(log_config=dct) + self.assertIn('LogConfig', config) + self.assertTrue(isinstance(config['LogConfig'], LogConfig)) + self.assertEqual(dct['type'], config['LogConfig'].type) + + def test_create_host_config_obj_logconfig(self): + obj = LogConfig(type=LogConfig.types.SYSLOG, config={'key1': 'val1'}) + config = create_host_config(log_config=obj) + self.assertIn('LogConfig', config) + self.assertTrue(isinstance(config['LogConfig'], LogConfig)) + self.assertEqual(obj, config['LogConfig']) + + def test_logconfig_invalid_type(self): + self.assertRaises(ValueError, lambda: LogConfig(type='xxx', config={})) + self.assertRaises(ValueError, lambda: LogConfig( + type=LogConfig.types.JSON, config='helloworld' + )) + + def test_resolve_authconfig(self): + auth_config = { + 'https://index.docker.io/v1/': {'auth': 'indexuser'}, + 'my.registry.net': {'auth': 'privateuser'}, + 'http://legacy.registry.url/v1/': {'auth': 'legacyauth'} + } + # hostname only + self.assertEqual( + resolve_authconfig(auth_config, 'my.registry.net'), + {'auth': 'privateuser'} + ) + # no protocol + self.assertEqual( + resolve_authconfig(auth_config, 'my.registry.net/v1/'), + {'auth': 'privateuser'} + ) + # no path + self.assertEqual( + resolve_authconfig(auth_config, 'http://my.registry.net'), + {'auth': 'privateuser'} + ) + # no path, trailing slash + self.assertEqual( + resolve_authconfig(auth_config, 'http://my.registry.net/'), + {'auth': 'privateuser'} + ) + # no path, wrong secure protocol + self.assertEqual( + resolve_authconfig(auth_config, 'https://my.registry.net'), + {'auth': 'privateuser'} + ) + # no path, wrong insecure protocol + self.assertEqual( + resolve_authconfig(auth_config, 'http://index.docker.io'), + {'auth': 'indexuser'} + ) + # with path, wrong protocol + self.assertEqual( + resolve_authconfig(auth_config, 'https://my.registry.net/v1/'), + {'auth': 'privateuser'} + ) + # default registry + self.assertEqual( + resolve_authconfig(auth_config), {'auth': 'indexuser'} + ) + # default registry (explicit None) + self.assertEqual( + resolve_authconfig(auth_config, None), {'auth': 'indexuser'} + ) + # fully explicit + self.assertEqual( + resolve_authconfig(auth_config, 'http://my.registry.net/v1/'), + {'auth': 'privateuser'} + ) + # legacy entry in config + self.assertEqual( + resolve_authconfig(auth_config, 'legacy.registry.url'), + {'auth': 'legacyauth'} + ) + # no matching entry + self.assertTrue( + resolve_authconfig(auth_config, 'does.not.exist') is None + ) + + def test_split_port_with_host_ip(self): + internal_port, external_port = split_port("127.0.0.1:1000:2000") + self.assertEqual(internal_port, ["2000"]) + self.assertEqual(external_port, [("127.0.0.1", "1000")]) + + def test_split_port_with_protocol(self): + internal_port, external_port = split_port("127.0.0.1:1000:2000/udp") + self.assertEqual(internal_port, ["2000/udp"]) + self.assertEqual(external_port, [("127.0.0.1", "1000")]) + + def test_split_port_with_host_ip_no_port(self): + internal_port, external_port = split_port("127.0.0.1::2000") + self.assertEqual(internal_port, ["2000"]) + self.assertEqual(external_port, [("127.0.0.1", None)]) + + def test_split_port_range_with_host_ip_no_port(self): + internal_port, external_port = split_port("127.0.0.1::2000-2001") + self.assertEqual(internal_port, ["2000", "2001"]) + self.assertEqual(external_port, + [("127.0.0.1", None), ("127.0.0.1", None)]) + + def test_split_port_with_host_port(self): + internal_port, external_port = split_port("1000:2000") + self.assertEqual(internal_port, ["2000"]) + self.assertEqual(external_port, ["1000"]) + + def test_split_port_range_with_host_port(self): + internal_port, external_port = split_port("1000-1001:2000-2001") + self.assertEqual(internal_port, ["2000", "2001"]) + self.assertEqual(external_port, ["1000", "1001"]) + + def test_split_port_no_host_port(self): + internal_port, external_port = split_port("2000") + self.assertEqual(internal_port, ["2000"]) + self.assertEqual(external_port, None) + + def test_split_port_range_no_host_port(self): + internal_port, external_port = split_port("2000-2001") + self.assertEqual(internal_port, ["2000", "2001"]) + self.assertEqual(external_port, None) + + def test_split_port_range_with_protocol(self): + internal_port, external_port = split_port( + "127.0.0.1:1000-1001:2000-2001/udp") + self.assertEqual(internal_port, ["2000/udp", "2001/udp"]) + self.assertEqual(external_port, + [("127.0.0.1", "1000"), ("127.0.0.1", "1001")]) + + def test_split_port_invalid(self): + self.assertRaises(ValueError, + lambda: split_port("0.0.0.0:1000:2000:tcp")) + + def test_non_matching_length_port_ranges(self): + self.assertRaises( + ValueError, + lambda: split_port("0.0.0.0:1000-1010:2000-2002/tcp") + ) + + def test_port_and_range_invalid(self): + self.assertRaises(ValueError, + lambda: split_port("0.0.0.0:1000:2000-2002/tcp")) + + def test_build_port_bindings_with_one_port(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000"]) + self.assertEqual(port_bindings["1000"], [("127.0.0.1", "1000")]) + + def test_build_port_bindings_with_matching_internal_ports(self): + port_bindings = build_port_bindings( + ["127.0.0.1:1000:1000", "127.0.0.1:2000:1000"]) + self.assertEqual(port_bindings["1000"], + [("127.0.0.1", "1000"), ("127.0.0.1", "2000")]) + + def test_build_port_bindings_with_nonmatching_internal_ports(self): + port_bindings = build_port_bindings( + ["127.0.0.1:1000:1000", "127.0.0.1:2000:2000"]) + self.assertEqual(port_bindings["1000"], [("127.0.0.1", "1000")]) + self.assertEqual(port_bindings["2000"], [("127.0.0.1", "2000")]) + + def test_build_port_bindings_with_port_range(self): + port_bindings = build_port_bindings(["127.0.0.1:1000-1001:1000-1001"]) + self.assertEqual(port_bindings["1000"], [("127.0.0.1", "1000")]) + self.assertEqual(port_bindings["1001"], [("127.0.0.1", "1001")]) + + def test_build_port_bindings_with_matching_internal_port_ranges(self): + port_bindings = build_port_bindings( + ["127.0.0.1:1000-1001:1000-1001", "127.0.0.1:2000-2001:1000-1001"]) + self.assertEqual(port_bindings["1000"], + [("127.0.0.1", "1000"), ("127.0.0.1", "2000")]) + self.assertEqual(port_bindings["1001"], + [("127.0.0.1", "1001"), ("127.0.0.1", "2001")]) + + def test_build_port_bindings_with_nonmatching_internal_port_ranges(self): + port_bindings = build_port_bindings( + ["127.0.0.1:1000:1000", "127.0.0.1:2000:2000"]) + self.assertEqual(port_bindings["1000"], [("127.0.0.1", "1000")]) + self.assertEqual(port_bindings["2000"], [("127.0.0.1", "2000")]) if __name__ == '__main__': unittest.main()