summaryrefslogtreecommitdiff
path: root/docker
diff options
context:
space:
mode:
authorTianon Gravi <tianon@debian.org>2015-11-08 10:31:39 -0800
committerTianon Gravi <tianon@debian.org>2015-11-08 10:31:39 -0800
commit0db60a1e341b25c36bebb5903f0ce87126d7eee4 (patch)
tree601d639621dd6d0a0301861c1279f8fcc866528c /docker
parent90baa554e7b2f26eb2b43bb528d82b170b505903 (diff)
Import python-docker_1.5.0.orig.tar.gz
Diffstat (limited to 'docker')
-rw-r--r--docker/api/__init__.py8
-rw-r--r--docker/api/build.py132
-rw-r--r--docker/api/container.py382
-rw-r--r--docker/api/daemon.py78
-rw-r--r--docker/api/exec_api.py70
-rw-r--r--docker/api/image.py271
-rw-r--r--docker/api/network.py55
-rw-r--r--docker/api/volume.py36
-rw-r--r--docker/auth/auth.py37
-rw-r--r--docker/client.py1070
-rw-r--r--docker/clientbase.py277
-rw-r--r--docker/constants.py2
-rw-r--r--docker/utils/__init__.py7
-rw-r--r--docker/utils/decorators.py16
-rw-r--r--docker/utils/ports/ports.py20
-rw-r--r--docker/utils/types.py22
-rw-r--r--docker/utils/utils.py225
-rw-r--r--docker/version.py2
18 files changed, 1565 insertions, 1145 deletions
diff --git a/docker/api/__init__.py b/docker/api/__init__.py
new file mode 100644
index 0000000..9e74428
--- /dev/null
+++ b/docker/api/__init__.py
@@ -0,0 +1,8 @@
+# flake8: noqa
+from .build import BuildApiMixin
+from .container import ContainerApiMixin
+from .daemon import DaemonApiMixin
+from .exec_api import ExecApiMixin
+from .image import ImageApiMixin
+from .volume import VolumeApiMixin
+from .network import NetworkApiMixin
diff --git a/docker/api/build.py b/docker/api/build.py
new file mode 100644
index 0000000..b303ba6
--- /dev/null
+++ b/docker/api/build.py
@@ -0,0 +1,132 @@
+import logging
+import os
+import re
+
+from .. import constants
+from .. import errors
+from .. import auth
+from .. import utils
+
+
+log = logging.getLogger(__name__)
+
+
+class BuildApiMixin(object):
+ def build(self, path=None, tag=None, quiet=False, fileobj=None,
+ nocache=False, rm=False, stream=False, timeout=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")
+ context = fileobj
+ elif fileobj is not None:
+ context = utils.mkbuildcontext(fileobj)
+ elif path.startswith(('http://', 'https://',
+ 'git://', 'github.com/', 'git@')):
+ remote = path
+ elif not os.path.isdir(path):
+ raise TypeError("You must specify a directory to build in path")
+ else:
+ dockerignore = os.path.join(path, '.dockerignore')
+ exclude = None
+ if os.path.exists(dockerignore):
+ with open(dockerignore, 'r') as f:
+ exclude = list(filter(bool, f.read().splitlines()))
+ context = utils.tar(path, exclude=exclude, dockerfile=dockerfile)
+
+ 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,
+ 'forcerm': forcerm,
+ 'pull': pull,
+ 'dockerfile': dockerfile,
+ }
+ params.update(container_limits)
+
+ if context is not None:
+ headers = {'Content-Type': 'application/tar'}
+ if encoding:
+ headers['Content-Encoding'] = encoding
+
+ if utils.compare_version('1.9', self._version) >= 0:
+ self._set_auth_headers(headers)
+
+ response = self._post(
+ u,
+ data=context,
+ params=params,
+ headers=headers,
+ stream=stream,
+ timeout=timeout,
+ )
+
+ if context is not None and not custom_context:
+ context.close()
+
+ if stream:
+ return self._stream_helper(response, decode=decode)
+ else:
+ output = self._result(response)
+ srch = r'Successfully built ([0-9a-f]+)'
+ match = re.search(srch, output)
+ if not match:
+ return None, output
+ return match.group(1), output
+
+ def _set_auth_headers(self, headers):
+ log.debug('Looking for auth config')
+
+ # 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:
+ log.debug("No auth config in memory - loading from filesystem")
+ self._auth_configs = auth.load_config()
+
+ # Send the full auth configuration (if any exists), since the build
+ # could use any (or all) of the registries.
+ if self._auth_configs:
+ log.debug(
+ 'Sending auth config ({0})'.format(
+ ', '.join(repr(k) for k in self._auth_configs.keys())
+ )
+ )
+ if headers is None:
+ headers = {}
+ if utils.compare_version('1.19', self._version) >= 0:
+ headers['X-Registry-Config'] = auth.encode_header(
+ self._auth_configs
+ )
+ else:
+ headers['X-Registry-Config'] = auth.encode_header({
+ 'configs': self._auth_configs
+ })
+ else:
+ log.debug('No auth config found')
diff --git a/docker/api/container.py b/docker/api/container.py
new file mode 100644
index 0000000..142bd0f
--- /dev/null
+++ b/docker/api/container.py
@@ -0,0 +1,382 @@
+import six
+import warnings
+
+from .. import errors
+from .. import utils
+
+
+class ContainerApiMixin(object):
+ @utils.check_resource
+ def attach(self, container, stdout=True, stderr=True,
+ stream=False, logs=False):
+ params = {
+ 'logs': logs and 1 or 0,
+ 'stdout': stdout and 1 or 0,
+ 'stderr': stderr and 1 or 0,
+ 'stream': stream and 1 or 0,
+ }
+ u = self._url("/containers/{0}/attach", container)
+ response = self._post(u, params=params, stream=stream)
+
+ return self._get_result(container, stream, response)
+
+ @utils.check_resource
+ def attach_socket(self, container, params=None, ws=False):
+ if params is None:
+ params = {
+ 'stdout': 1,
+ 'stderr': 1,
+ 'stream': 1
+ }
+
+ if ws:
+ return self._attach_websocket(container, params)
+
+ u = self._url("/containers/{0}/attach", container)
+ return self._get_raw_response_socket(self.post(
+ u, None, params=self._attach_params(params), stream=True))
+
+ @utils.check_resource
+ def commit(self, container, repository=None, tag=None, message=None,
+ author=None, conf=None):
+ params = {
+ 'container': container,
+ 'repo': repository,
+ 'tag': tag,
+ 'comment': message,
+ 'author': author
+ }
+ u = self._url("/commit")
+ return self._result(self._post_json(u, data=conf, params=params),
+ json=True)
+
+ def containers(self, quiet=False, all=False, trunc=False, latest=False,
+ since=None, before=None, limit=-1, size=False,
+ filters=None):
+ params = {
+ 'limit': 1 if latest else limit,
+ 'all': 1 if all else 0,
+ 'size': 1 if size else 0,
+ 'trunc_cmd': 1 if trunc else 0,
+ 'since': since,
+ 'before': before
+ }
+ if filters:
+ params['filters'] = utils.convert_filters(filters)
+ u = self._url("/containers/json")
+ res = self._result(self._get(u, params=params), True)
+
+ if quiet:
+ return [{'Id': x['Id']} for x in res]
+ if trunc:
+ for x in res:
+ x['Id'] = x['Id'][:12]
+ return res
+
+ @utils.check_resource
+ def copy(self, container, resource):
+ if utils.version_gte(self._version, '1.20'):
+ warnings.warn(
+ 'Client.copy() is deprecated for API version >= 1.20, '
+ 'please use get_archive() instead',
+ DeprecationWarning
+ )
+ res = self._post_json(
+ self._url("/containers/{0}/copy".format(container)),
+ data={"Resource": resource},
+ stream=True
+ )
+ self._raise_for_status(res)
+ return res.raw
+
+ def create_container(self, image, command=None, 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, name=None, 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(volumes, six.string_types):
+ volumes = [volumes, ]
+
+ 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 = self.create_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, host_config, mac_address, labels,
+ volume_driver
+ )
+ return self.create_container_from_config(config, name)
+
+ def create_container_config(self, *args, **kwargs):
+ return utils.create_container_config(self._version, *args, **kwargs)
+
+ def create_container_from_config(self, config, name=None):
+ u = self._url("/containers/create")
+ params = {
+ 'name': name
+ }
+ res = self._post_json(u, data=config, params=params)
+ return self._result(res, True)
+
+ def create_host_config(self, *args, **kwargs):
+ if not kwargs:
+ kwargs = {}
+ if 'version' in kwargs:
+ raise TypeError(
+ "create_host_config() got an unexpected "
+ "keyword argument 'version'"
+ )
+ kwargs['version'] = self._version
+ return utils.create_host_config(*args, **kwargs)
+
+ @utils.check_resource
+ def diff(self, container):
+ return self._result(
+ self._get(self._url("/containers/{0}/changes", container)), True
+ )
+
+ @utils.check_resource
+ def export(self, container):
+ res = self._get(
+ self._url("/containers/{0}/export", container), stream=True
+ )
+ self._raise_for_status(res)
+ return res.raw
+
+ @utils.check_resource
+ @utils.minimum_version('1.20')
+ def get_archive(self, container, path):
+ params = {
+ 'path': path
+ }
+ url = self._url('/containers/{0}/archive', container)
+ res = self._get(url, params=params, stream=True)
+ self._raise_for_status(res)
+ encoded_stat = res.headers.get('x-docker-container-path-stat')
+ return (
+ res.raw,
+ utils.decode_json_header(encoded_stat) if encoded_stat else None
+ )
+
+ @utils.check_resource
+ def inspect_container(self, container):
+ return self._result(
+ self._get(self._url("/containers/{0}/json", container)), True
+ )
+
+ @utils.check_resource
+ def kill(self, container, signal=None):
+ url = self._url("/containers/{0}/kill", container)
+ params = {}
+ if signal is not None:
+ params['signal'] = signal
+ res = self._post(url, params=params)
+
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def logs(self, container, stdout=True, stderr=True, stream=False,
+ timestamps=False, tail='all'):
+ if utils.compare_version('1.11', self._version) >= 0:
+ params = {'stderr': stderr and 1 or 0,
+ 'stdout': stdout and 1 or 0,
+ 'timestamps': timestamps and 1 or 0,
+ 'follow': stream and 1 or 0,
+ }
+ if utils.compare_version('1.13', self._version) >= 0:
+ if tail != 'all' and (not isinstance(tail, int) or tail <= 0):
+ tail = 'all'
+ params['tail'] = tail
+ url = self._url("/containers/{0}/logs", container)
+ res = self._get(url, params=params, stream=stream)
+ return self._get_result(container, stream, res)
+ return self.attach(
+ container,
+ stdout=stdout,
+ stderr=stderr,
+ stream=stream,
+ logs=True
+ )
+
+ @utils.check_resource
+ def pause(self, container):
+ url = self._url('/containers/{0}/pause', container)
+ res = self._post(url)
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def port(self, container, private_port):
+ res = self._get(self._url("/containers/{0}/json", container))
+ self._raise_for_status(res)
+ json_ = res.json()
+ private_port = str(private_port)
+ h_ports = None
+
+ # 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
+
+ if '/' in private_port:
+ return port_settings.get(private_port)
+
+ h_ports = port_settings.get(private_port + '/tcp')
+ if h_ports is None:
+ h_ports = port_settings.get(private_port + '/udp')
+
+ return h_ports
+
+ @utils.check_resource
+ @utils.minimum_version('1.20')
+ def put_archive(self, container, path, data):
+ params = {'path': path}
+ url = self._url('/containers/{0}/archive', container)
+ res = self._put(url, params=params, data=data)
+ self._raise_for_status(res)
+ return res.status_code == 200
+
+ @utils.check_resource
+ def remove_container(self, container, v=False, link=False, force=False):
+ params = {'v': v, 'link': link, 'force': force}
+ res = self._delete(
+ self._url("/containers/{0}", container), params=params
+ )
+ self._raise_for_status(res)
+
+ @utils.minimum_version('1.17')
+ @utils.check_resource
+ def rename(self, container, name):
+ url = self._url("/containers/{0}/rename", container)
+ params = {'name': name}
+ res = self._post(url, params=params)
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def resize(self, container, height, width):
+ params = {'h': height, 'w': width}
+ url = self._url("/containers/{0}/resize", container)
+ res = self._post(url, params=params)
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def restart(self, container, timeout=10):
+ params = {'t': timeout}
+ url = self._url("/containers/{0}/restart", container)
+ res = self._post(url, params=params)
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def start(self, container, binds=None, port_bindings=None, lxc_conf=None,
+ publish_all_ports=None, links=None, privileged=None,
+ 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):
+
+ if utils.compare_version('1.10', self._version) < 0:
+ if dns is not None:
+ raise errors.InvalidVersion(
+ 'dns is only supported for API version >= 1.10'
+ )
+ if volumes_from is not None:
+ raise errors.InvalidVersion(
+ 'volumes_from is only supported for API version >= 1.10'
+ )
+
+ 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 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 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'
+ )
+
+ start_config_kwargs = dict(
+ 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
+ )
+ start_config = None
+
+ if any(v is not None for v in start_config_kwargs.values()):
+ if 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
+ )
+ start_config = self.create_host_config(**start_config_kwargs)
+
+ url = self._url("/containers/{0}/start", container)
+ res = self._post_json(url, data=start_config)
+ self._raise_for_status(res)
+
+ @utils.minimum_version('1.17')
+ @utils.check_resource
+ def stats(self, container, decode=None):
+ url = self._url("/containers/{0}/stats", container)
+ return self._stream_helper(self._get(url, stream=True), decode=decode)
+
+ @utils.check_resource
+ def stop(self, container, timeout=10):
+ params = {'t': timeout}
+ url = self._url("/containers/{0}/stop", container)
+
+ res = self._post(url, params=params,
+ timeout=(timeout + (self.timeout or 0)))
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def top(self, container, ps_args=None):
+ u = self._url("/containers/{0}/top", container)
+ params = {}
+ if ps_args is not None:
+ params['ps_args'] = ps_args
+ return self._result(self._get(u, params=params), True)
+
+ @utils.check_resource
+ def unpause(self, container):
+ url = self._url('/containers/{0}/unpause', container)
+ res = self._post(url)
+ self._raise_for_status(res)
+
+ @utils.check_resource
+ def wait(self, container, timeout=None):
+ url = self._url("/containers/{0}/wait", container)
+ res = self._post(url, timeout=timeout)
+ self._raise_for_status(res)
+ json_ = res.json()
+ if 'StatusCode' in json_:
+ return json_['StatusCode']
+ return -1
diff --git a/docker/api/daemon.py b/docker/api/daemon.py
new file mode 100644
index 0000000..a149e5e
--- /dev/null
+++ b/docker/api/daemon.py
@@ -0,0 +1,78 @@
+import os
+import warnings
+from datetime import datetime
+
+from ..auth import auth
+from ..constants import INSECURE_REGISTRY_DEPRECATION_WARNING
+from ..utils import utils
+
+
+class DaemonApiMixin(object):
+ 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)
+
+ params = {
+ 'since': since,
+ 'until': until,
+ 'filters': filters
+ }
+
+ return self._stream_helper(
+ self.get(self._url('/events'), params=params, stream=True),
+ decode=decode
+ )
+
+ def info(self):
+ return self._result(self._get(self._url("/info")), True)
+
+ def login(self, username, password=None, email=None, registry=None,
+ reauth=False, insecure_registry=False, dockercfg_path=None):
+ if insecure_registry:
+ warnings.warn(
+ INSECURE_REGISTRY_DEPRECATION_WARNING.format('login()'),
+ DeprecationWarning
+ )
+
+ # 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 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
+
+ authcfg = auth.resolve_authconfig(self._auth_configs, registry)
+ # If we found an existing auth config for this registry and username
+ # combination, we can return it immediately unless reauth is requested.
+ if authcfg and authcfg.get('username', None) == username \
+ and not reauth:
+ return authcfg
+
+ req_data = {
+ 'username': username,
+ 'password': password,
+ 'email': email,
+ 'serveraddress': registry,
+ }
+
+ response = self._post_json(self._url('/auth'), data=req_data)
+ if response.status_code == 200:
+ self._auth_configs[registry] = req_data
+ return self._result(response, json=True)
+
+ def ping(self):
+ return self._result(self._get(self._url('/_ping')))
+
+ def version(self, api_version=True):
+ url = self._url("/version", versioned_api=api_version)
+ return self._result(self._get(url), json=True)
diff --git a/docker/api/exec_api.py b/docker/api/exec_api.py
new file mode 100644
index 0000000..c66b9dd
--- /dev/null
+++ b/docker/api/exec_api.py
@@ -0,0 +1,70 @@
+import shlex
+
+import six
+
+from .. import errors
+from .. import utils
+
+
+class ExecApiMixin(object):
+ @utils.minimum_version('1.15')
+ @utils.check_resource
+ def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
+ privileged=False, user=''):
+ if privileged and utils.compare_version('1.19', self._version) < 0:
+ raise errors.InvalidVersion(
+ 'Privileged exec is not supported in API < 1.19'
+ )
+ if user and utils.compare_version('1.19', self._version) < 0:
+ raise errors.InvalidVersion(
+ 'User-specific exec is not supported in API < 1.19'
+ )
+ if isinstance(cmd, six.string_types):
+ cmd = shlex.split(str(cmd))
+
+ data = {
+ 'Container': container,
+ 'User': user,
+ 'Privileged': privileged,
+ 'Tty': tty,
+ 'AttachStdin': False,
+ 'AttachStdout': stdout,
+ 'AttachStderr': stderr,
+ 'Cmd': cmd
+ }
+
+ url = self._url('/containers/{0}/exec', container)
+ res = self._post_json(url, data=data)
+ return self._result(res, True)
+
+ @utils.minimum_version('1.16')
+ def exec_inspect(self, exec_id):
+ if isinstance(exec_id, dict):
+ exec_id = exec_id.get('Id')
+ res = self._get(self._url("/exec/{0}/json", exec_id))
+ return self._result(res, True)
+
+ @utils.minimum_version('1.15')
+ def exec_resize(self, exec_id, height=None, width=None):
+ if isinstance(exec_id, dict):
+ exec_id = exec_id.get('Id')
+
+ params = {'h': height, 'w': width}
+ url = self._url("/exec/{0}/resize", exec_id)
+ res = self._post(url, params=params)
+ self._raise_for_status(res)
+
+ @utils.minimum_version('1.15')
+ def exec_start(self, exec_id, detach=False, tty=False, stream=False):
+ 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', exec_id), data=data, stream=stream
+ )
+ return self._get_result_tty(stream, res, tty)
diff --git a/docker/api/image.py b/docker/api/image.py
new file mode 100644
index 0000000..f891e21
--- /dev/null
+++ b/docker/api/image.py
@@ -0,0 +1,271 @@
+import logging
+import six
+import warnings
+
+from ..auth import auth
+from ..constants import INSECURE_REGISTRY_DEPRECATION_WARNING
+from .. import utils
+from .. import errors
+
+log = logging.getLogger(__name__)
+
+
+class ImageApiMixin(object):
+
+ @utils.check_resource
+ def get_image(self, image):
+ res = self._get(self._url("/images/{0}/get", image), stream=True)
+ self._raise_for_status(res)
+ return res.raw
+
+ @utils.check_resource
+ def history(self, image):
+ res = self._get(self._url("/images/{0}/history", image))
+ return self._result(res, True)
+
+ def images(self, name=None, quiet=False, all=False, viz=False,
+ filters=None):
+ if viz:
+ if utils.compare_version('1.7', self._version) >= 0:
+ raise Exception('Viz output is not supported in API >= 1.7!')
+ return self._result(self._get(self._url("images/viz")))
+ params = {
+ 'filter': name,
+ 'only_ids': 1 if quiet else 0,
+ 'all': 1 if all else 0,
+ }
+ if filters:
+ params['filters'] = utils.convert_filters(filters)
+ res = self._result(self._get(self._url("/images/json"), params=params),
+ True)
+ if quiet:
+ return [x['Id'] for x in res]
+ 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))
+
+ 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))
+
+ 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))
+
+ @utils.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/{0}/insert", image)
+ params = {
+ 'url': url,
+ 'path': path
+ }
+ return self._result(self._post(api_url, params=params))
+
+ @utils.check_resource
+ def inspect_image(self, image):
+ return self._result(
+ self._get(self._url("/images/{0}/json", image)), True
+ )
+
+ def load_image(self, data):
+ res = self._post(self._url("/images/load"), data=data)
+ self._raise_for_status(res)
+
+ def pull(self, repository, tag=None, stream=False,
+ insecure_registry=False, auth_config=None):
+ if insecure_registry:
+ warnings.warn(
+ INSECURE_REGISTRY_DEPRECATION_WARNING.format('pull()'),
+ DeprecationWarning
+ )
+
+ if not tag:
+ repository, tag = utils.parse_repository_tag(repository)
+ registry, repo_name = auth.resolve_repository_name(repository)
+ if repo_name.count(":") == 1:
+ repository, tag = repository.rsplit(":", 1)
+
+ params = {
+ 'tag': tag,
+ 'fromImage': repository
+ }
+ headers = {}
+
+ 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 auth_config is None:
+ log.debug('Looking for auth config')
+ if not self._auth_configs:
+ log.debug(
+ "No auth config in memory - loading from filesystem")
+ 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:
+ log.debug('Found auth config')
+ # 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:
+ log.debug('No auth config found')
+ else:
+ log.debug('Sending supplied auth config')
+ headers['X-Registry-Auth'] = auth.encode_header(auth_config)
+
+ 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)
+
+ return self._result(response)
+
+ def push(self, repository, tag=None, stream=False,
+ insecure_registry=False):
+ if insecure_registry:
+ warnings.warn(
+ INSECURE_REGISTRY_DEPRECATION_WARNING.format('push()'),
+ DeprecationWarning
+ )
+
+ if not tag:
+ repository, tag = utils.parse_repository_tag(repository)
+ registry, repo_name = auth.resolve_repository_name(repository)
+ u = self._url("/images/{0}/push", repository)
+ params = {
+ 'tag': tag
+ }
+ headers = {}
+
+ 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)
+
+ # 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_json(
+ u, None, headers=headers, stream=stream, params=params
+ )
+
+ self._raise_for_status(response)
+
+ if stream:
+ return self._stream_helper(response)
+
+ return self._result(response)
+
+ @utils.check_resource
+ def remove_image(self, image, force=False, noprune=False):
+ params = {'force': force, 'noprune': noprune}
+ res = self._delete(self._url("/images/{0}", image), params=params)
+ self._raise_for_status(res)
+
+ def search(self, term):
+ return self._result(
+ self._get(self._url("/images/search"), params={'term': term}),
+ True
+ )
+
+ @utils.check_resource
+ def tag(self, image, repository, tag=None, force=False):
+ params = {
+ 'tag': tag,
+ 'repo': repository,
+ 'force': 1 if force else 0
+ }
+ url = self._url("/images/{0}/tag", image)
+ res = self._post(url, params=params)
+ self._raise_for_status(res)
+ return res.status_code == 201
diff --git a/docker/api/network.py b/docker/api/network.py
new file mode 100644
index 0000000..2dea679
--- /dev/null
+++ b/docker/api/network.py
@@ -0,0 +1,55 @@
+import json
+
+from ..utils import check_resource, minimum_version
+
+
+class NetworkApiMixin(object):
+ @minimum_version('1.21')
+ def networks(self, names=None, ids=None):
+ filters = {}
+ if names:
+ filters['name'] = names
+ if ids:
+ filters['id'] = ids
+
+ params = {'filters': json.dumps(filters)}
+
+ url = self._url("/networks")
+ res = self._get(url, params=params)
+ return self._result(res, json=True)
+
+ @minimum_version('1.21')
+ def create_network(self, name, driver=None):
+ data = {
+ 'name': name,
+ 'driver': driver,
+ }
+ url = self._url("/networks/create")
+ res = self._post_json(url, data=data)
+ return self._result(res, json=True)
+
+ @minimum_version('1.21')
+ def remove_network(self, net_id):
+ url = self._url("/networks/{0}", net_id)
+ res = self._delete(url)
+ self._raise_for_status(res)
+
+ @minimum_version('1.21')
+ def inspect_network(self, net_id):
+ url = self._url("/networks/{0}", net_id)
+ res = self._get(url)
+ return self._result(res, json=True)
+
+ @check_resource
+ @minimum_version('1.21')
+ def connect_container_to_network(self, container, net_id):
+ data = {"container": container}
+ url = self._url("/networks/{0}/connect", net_id)
+ self._post_json(url, data=data)
+
+ @check_resource
+ @minimum_version('1.21')
+ def disconnect_container_from_network(self, container, net_id):
+ data = {"container": container}
+ url = self._url("/networks/{0}/disconnect", net_id)
+ self._post_json(url, data=data)
diff --git a/docker/api/volume.py b/docker/api/volume.py
new file mode 100644
index 0000000..e9e7127
--- /dev/null
+++ b/docker/api/volume.py
@@ -0,0 +1,36 @@
+from .. import utils
+
+
+class VolumeApiMixin(object):
+ @utils.minimum_version('1.21')
+ def volumes(self, filters=None):
+ params = {
+ 'filter': utils.convert_filters(filters) if filters else None
+ }
+ url = self._url('/volumes')
+ return self._result(self._get(url, params=params), True)
+
+ @utils.minimum_version('1.21')
+ def create_volume(self, name, driver=None, driver_opts=None):
+ url = self._url('/volumes')
+ if driver_opts is not None and not isinstance(driver_opts, dict):
+ raise TypeError('driver_opts must be a dictionary')
+
+ data = {
+ 'Name': name,
+ 'Driver': driver,
+ 'DriverOpts': driver_opts,
+ }
+ return self._result(self._post_json(url, data=data), True)
+
+ @utils.minimum_version('1.21')
+ def inspect_volume(self, name):
+ url = self._url('/volumes/{0}', name)
+ return self._result(self._get(url), True)
+
+ @utils.minimum_version('1.21')
+ def remove_volume(self, name):
+ url = self._url('/volumes/{0}', name)
+ resp = self._delete(url)
+ self._raise_for_status(resp)
+ return True
diff --git a/docker/auth/auth.py b/docker/auth/auth.py
index 4af741e..1ee9f81 100644
--- a/docker/auth/auth.py
+++ b/docker/auth/auth.py
@@ -15,6 +15,7 @@
import base64
import fileinput
import json
+import logging
import os
import warnings
@@ -28,6 +29,8 @@ INDEX_URL = 'https://{0}/v1/'.format(INDEX_NAME)
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
+log = logging.getLogger(__name__)
+
def resolve_repository_name(repo_name, insecure=False):
if insecure:
@@ -65,14 +68,18 @@ def resolve_authconfig(authconfig, registry=None):
"""
# Default to the public index server
registry = convert_to_hostname(registry) if registry else INDEX_NAME
+ log.debug("Looking for auth entry for {0}".format(repr(registry)))
if registry in authconfig:
+ log.debug("Found {0}".format(repr(registry)))
return authconfig[registry]
for key, config in six.iteritems(authconfig):
if convert_to_hostname(key) == registry:
+ log.debug("Found {0}".format(repr(key)))
return config
+ log.debug("No entry found")
return None
@@ -95,7 +102,7 @@ def decode_auth(auth):
def encode_header(auth):
auth_json = json.dumps(auth).encode('ascii')
- return base64.b64encode(auth_json)
+ return base64.urlsafe_b64encode(auth_json)
def parse_auth(entries):
@@ -112,6 +119,10 @@ def parse_auth(entries):
conf = {}
for registry, entry in six.iteritems(entries):
username, password = decode_auth(entry['auth'])
+ log.debug(
+ 'Found entry (registry={0}, username={1})'
+ .format(repr(registry), repr(username))
+ )
conf[registry] = {
'username': username,
'password': password,
@@ -133,31 +144,41 @@ def load_config(config_path=None):
config_file = config_path or os.path.join(os.path.expanduser('~'),
DOCKER_CONFIG_FILENAME)
+ log.debug("Trying {0}".format(config_file))
+
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
+ log.debug("Found 'auths' section")
return parse_auth(data)
- except (IOError, KeyError, ValueError):
+ log.debug("Couldn't find 'auths' section")
+ except (IOError, KeyError, ValueError) as e:
# Likely missing new Docker config file or it's in an
# unknown format, continue to attempt to read old location
# and format.
+ log.debug(e)
pass
+ else:
+ log.debug("File doesn't exist")
config_file = config_path or os.path.join(os.path.expanduser('~'),
LEGACY_DOCKER_CONFIG_FILENAME)
- # if config path doesn't exist return empty config
+ log.debug("Trying {0}".format(config_file))
+
if not os.path.exists(config_file):
+ log.debug("File doesn't exist - returning empty config")
return {}
- # Try reading legacy location as JSON.
+ log.debug("Attempting to parse as JSON")
try:
with open(config_file) as f:
return parse_auth(json.load(f))
- except:
+ except Exception as e:
+ log.debug(e)
pass
# If that fails, we assume the configuration file contains a single
@@ -165,6 +186,7 @@ def load_config(config_path=None):
#
# auth = AUTH_TOKEN
# email = email@domain.com
+ log.debug("Attempting to parse legacy auth file format")
try:
data = []
for line in fileinput.input(config_file):
@@ -182,8 +204,9 @@ def load_config(config_path=None):
'serveraddress': INDEX_URL,
}
return conf
- except:
+ except Exception as e:
+ log.debug(e)
pass
- # If all fails, return an empty config
+ log.debug("All parsing attempts failed - returning empty config")
return {}
diff --git a/docker/client.py b/docker/client.py
index e4712c2..d219472 100644
--- a/docker/client.py
+++ b/docker/client.py
@@ -12,845 +12,307 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import os
-import re
-import shlex
-import warnings
-from datetime import datetime
+import json
+import struct
+import sys
+import requests
+import requests.exceptions
import six
+import websocket
-from . import clientbase
+
+from . import api
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 .constants import INSECURE_REGISTRY_DEPRECATION_WARNING
-
-
-class Client(clientbase.ClientBase):
- @check_resource
- def attach(self, container, stdout=True, stderr=True,
- stream=False, logs=False):
- params = {
- 'logs': logs and 1 or 0,
- 'stdout': stdout and 1 or 0,
- 'stderr': stderr and 1 or 0,
- 'stream': stream and 1 or 0,
- }
- u = self._url("/containers/{0}/attach".format(container))
- response = self._post(u, params=params, stream=stream)
-
- return self._get_result(container, stream, response)
-
- @check_resource
- def attach_socket(self, container, params=None, ws=False):
- if params is None:
- params = {
- 'stdout': 1,
- 'stderr': 1,
- '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))
-
- def build(self, path=None, tag=None, quiet=False, fileobj=None,
- nocache=False, rm=False, stream=False, timeout=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")
- context = fileobj
- elif fileobj is not None:
- context = utils.mkbuildcontext(fileobj)
- elif path.startswith(('http://', 'https://',
- 'git://', 'github.com/', 'git@')):
- remote = path
- elif not os.path.isdir(path):
- raise TypeError("You must specify a directory to build in path")
+from .tls import TLSConfig
+
+
+class Client(
+ requests.Session,
+ api.BuildApiMixin,
+ api.ContainerApiMixin,
+ api.DaemonApiMixin,
+ api.ExecApiMixin,
+ api.ImageApiMixin,
+ api.VolumeApiMixin,
+ api.NetworkApiMixin):
+ def __init__(self, base_url=None, version=None,
+ timeout=constants.DEFAULT_TIMEOUT_SECONDS, tls=False):
+ super(Client, 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, sys.platform)
+ 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:
- dockerignore = os.path.join(path, '.dockerignore')
- exclude = None
- if os.path.exists(dockerignore):
- with open(dockerignore, 'r') as f:
- 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,
- 'forcerm': forcerm,
- 'pull': pull,
- 'dockerfile': dockerfile,
- }
- params.update(container_limits)
-
- if context is not None:
- headers = {'Content-Type': 'application/tar'}
- if encoding:
- headers['Content-Encoding'] = encoding
-
- if utils.compare_version('1.9', 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()
-
- # 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 = {}
- if utils.compare_version('1.19', self._version) >= 0:
- headers['X-Registry-Config'] = auth.encode_header(
- self._auth_configs
- )
- else:
- headers['X-Registry-Config'] = auth.encode_header({
- 'configs': self._auth_configs
- })
-
- response = self._post(
- u,
- data=context,
- params=params,
- headers=headers,
- stream=stream,
- timeout=timeout,
- )
-
- if context is not None and not custom_context:
- context.close()
-
- if stream:
- return self._stream_helper(response, decode=decode)
+ # 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:
- output = self._result(response)
- srch = r'Successfully built ([0-9a-f]+)'
- match = re.search(srch, output)
- if not match:
- 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 = {
- 'container': container,
- 'repo': repository,
- 'tag': tag,
- 'comment': message,
- 'author': author
- }
- u = self._url("/commit")
- return self._result(self._post_json(u, data=conf, params=params),
- json=True)
-
- def containers(self, quiet=False, all=False, trunc=False, latest=False,
- since=None, before=None, limit=-1, size=False,
- filters=None):
- params = {
- 'limit': 1 if latest else limit,
- 'all': 1 if all else 0,
- 'size': 1 if size else 0,
- 'trunc_cmd': 1 if trunc else 0,
- 'since': since,
- 'before': before
- }
- if filters:
- params['filters'] = utils.convert_filters(filters)
- u = self._url("/containers/json")
- res = self._result(self._get(u, params=params), True)
-
- 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):
- res = self._post_json(
- self._url("/containers/{0}/copy".format(container)),
- data={"Resource": resource},
- stream=True
- )
- self._raise_for_status(res)
- return res.raw
-
- def create_container(self, image, command=None, 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, name=None, 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(volumes, six.string_types):
- volumes = [volumes, ]
-
- if host_config and utils.compare_version('1.15', self._version) < 0:
- raise errors.InvalidVersion(
- 'host_config is not supported in API < 1.15'
+ raise errors.DockerException(
+ 'Version parameter must be a string or None. Found {0}'.format(
+ type(version).__name__
+ )
)
- 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)
-
- def create_container_from_config(self, config, name=None):
- u = self._url("/containers/create")
- params = {
- 'name': name
- }
- res = self._post_json(u, data=config, params=params)
- return self._result(res, True)
-
- @check_resource
- def diff(self, container):
- return self._result(self._get(self._url("/containers/{0}/changes".
- format(container))), 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)
-
- params = {
- 'since': since,
- 'until': until,
- 'filters': filters
- }
-
- return self._stream_helper(
- self.get(self._url('/events'), params=params, stream=True),
- decode=decode
- )
-
- @check_resource
- def exec_create(self, container, cmd, stdout=True, stderr=True, tty=False,
- privileged=False):
- if utils.compare_version('1.15', self._version) < 0:
- 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'
+ 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.'
)
- if isinstance(cmd, six.string_types):
- cmd = shlex.split(str(cmd))
-
- data = {
- 'Container': container,
- 'User': '',
- 'Privileged': privileged,
- 'Tty': tty,
- 'AttachStdin': False,
- 'AttachStdout': stdout,
- 'AttachStderr': stderr,
- 'Cmd': cmd
- }
-
- 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)
-
- 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)
- return self._get_result_tty(stream, res, tty)
-
- @check_resource
- def export(self, container):
- 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)
-
- def images(self, name=None, quiet=False, all=False, viz=False,
- filters=None):
- if viz:
- if utils.compare_version('1.7', self._version) >= 0:
- raise Exception('Viz output is not supported in API >= 1.7!')
- return self._result(self._get(self._url("images/viz")))
- params = {
- 'filter': name,
- 'only_ids': 1 if quiet else 0,
- 'all': 1 if all else 0,
- }
- if filters:
- params['filters'] = utils.convert_filters(filters)
- res = self._result(self._get(self._url("/images/json"), params=params),
- True)
- if quiet:
- return [x['Id'] for x in res]
- 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))
-
- 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))
-
- 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'
+ except Exception as e:
+ raise errors.DockerException(
+ 'Error while fetching server API version: {0}'.format(e)
)
- 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):
- return self._result(
- self._get(self._url("/containers/{0}/json".format(container))),
- True)
-
- @check_resource
- def inspect_image(self, image):
- return self._result(
- self._get(
- self._url("/images/{0}/json".format(image.replace('/', '%2F')))
- ),
- True
- )
-
- @check_resource
- def kill(self, container, signal=None):
- url = self._url("/containers/{0}/kill".format(container))
- params = {}
- if signal is not None:
- params['signal'] = signal
- res = self._post(url, params=params)
-
- self._raise_for_status(res)
-
- def load_image(self, data):
- res = self._post(self._url("/images/load"), data=data)
- self._raise_for_status(res)
+ 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 login(self, username, password=None, email=None, registry=None,
- reauth=False, insecure_registry=False, dockercfg_path=None):
- if insecure_registry:
- warnings.warn(
- INSECURE_REGISTRY_DEPRECATION_WARNING.format('login()'),
- DeprecationWarning
- )
+ def _post(self, url, **kwargs):
+ return self.post(url, **self._set_request_timeout(kwargs))
- # 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 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
-
- authcfg = auth.resolve_authconfig(self._auth_configs, registry)
- # If we found an existing auth config for this registry and username
- # combination, we can return it immediately unless reauth is requested.
- if authcfg and authcfg.get('username', None) == username \
- and not reauth:
- return authcfg
-
- req_data = {
- 'username': username,
- 'password': password,
- 'email': email,
- 'serveraddress': registry,
- }
+ def _get(self, url, **kwargs):
+ return self.get(url, **self._set_request_timeout(kwargs))
- response = self._post_json(self._url('/auth'), data=req_data)
- if response.status_code == 200:
- self._auth_configs[registry] = req_data
- return self._result(response, json=True)
+ def _put(self, url, **kwargs):
+ return self.put(url, **self._set_request_timeout(kwargs))
- @check_resource
- def logs(self, container, stdout=True, stderr=True, stream=False,
- timestamps=False, tail='all'):
- if utils.compare_version('1.11', self._version) >= 0:
- params = {'stderr': stderr and 1 or 0,
- 'stdout': stdout and 1 or 0,
- 'timestamps': timestamps and 1 or 0,
- 'follow': stream and 1 or 0,
- }
- if utils.compare_version('1.13', self._version) >= 0:
- if tail != 'all' and (not isinstance(tail, int) or tail <= 0):
- tail = 'all'
- params['tail'] = tail
- url = self._url("/containers/{0}/logs".format(container))
- res = self._get(url, params=params, stream=stream)
- return self._get_result(container, stream, res)
- return self.attach(
- container,
- stdout=stdout,
- stderr=stderr,
- stream=stream,
- logs=True
- )
+ def _delete(self, url, **kwargs):
+ return self.delete(url, **self._set_request_timeout(kwargs))
- @check_resource
- def pause(self, container):
- url = self._url('/containers/{0}/pause'.format(container))
- res = self._post(url)
- self._raise_for_status(res)
+ def _url(self, pathfmt, *args, **kwargs):
+ for arg in args:
+ if not isinstance(arg, six.string_types):
+ raise ValueError(
+ 'Expected a string but found {0} ({1}) '
+ 'instead'.format(arg, type(arg))
+ )
- def ping(self):
- return self._result(self._get(self._url('/_ping')))
+ args = map(six.moves.urllib.parse.quote_plus, args)
- @check_resource
- def port(self, container, private_port):
- 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
-
- # 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 = port_settings.get(s_port + '/tcp')
-
- return h_ports
-
- def pull(self, repository, tag=None, stream=False,
- insecure_registry=False, auth_config=None):
- if insecure_registry:
- warnings.warn(
- INSECURE_REGISTRY_DEPRECATION_WARNING.format('pull()'),
- DeprecationWarning
+ if kwargs.get('versioned_api', True):
+ return '{0}/v{1}{2}'.format(
+ self.base_url, self._version, pathfmt.format(*args)
)
-
- if not tag:
- repository, tag = utils.parse_repository_tag(repository)
- registry, repo_name = auth.resolve_repository_name(repository)
- if repo_name.count(":") == 1:
- repository, tag = repository.rsplit(":", 1)
-
- params = {
- 'tag': tag,
- 'fromImage': repository
- }
- headers = {}
-
- 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 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)
-
- response = self._post(
- self._url('/images/create'), params=params, headers=headers,
- stream=stream, timeout=None
- )
-
+ else:
+ return '{0}{1}'.format(self.base_url, pathfmt.format(*args))
+
+ 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:
+ if e.response.status_code == 404:
+ raise errors.NotFound(e, response, explanation=explanation)
+ 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 stream:
- return self._stream_helper(response)
-
- return self._result(response)
-
- def push(self, repository, tag=None, stream=False,
- insecure_registry=False):
- if insecure_registry:
- warnings.warn(
- INSECURE_REGISTRY_DEPRECATION_WARNING.format('push()'),
- DeprecationWarning
- )
-
- if not tag:
- repository, tag = utils.parse_repository_tag(repository)
- registry, repo_name = auth.resolve_repository_name(repository)
- u = self._url("/images/{0}/push".format(repository))
- params = {
- 'tag': tag
+ 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
}
- headers = {}
-
- 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)
-
- # 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_json(
- u, None, headers=headers, stream=stream, params=params
- )
+ @check_resource
+ def _attach_websocket(self, container, params=None):
+ url = self._url("/containers/{0}/attach/ws", 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:
+ continue
+ 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
- if stream:
- return self._stream_helper(response)
-
- return self._result(response)
-
- @check_resource
- def remove_container(self, container, v=False, link=False, force=False):
- params = {'v': v, 'link': link, 'force': force}
- res = self._delete(self._url("/containers/" + container),
- params=params)
- self._raise_for_status(res)
+ def _get_result(self, container, stream, res):
+ cont = self.inspect_container(container)
+ return self._get_result_tty(stream, res, cont['Config']['Tty'])
- @check_resource
- def remove_image(self, image, force=False, noprune=False):
- params = {'force': force, 'noprune': noprune}
- res = self._delete(self._url("/images/" + image), params=params)
- self._raise_for_status(res)
+ 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)
- @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)
+ # 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)
- @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):
- params = {'t': timeout}
- url = self._url("/containers/{0}/restart".format(container))
- res = self._post(url, params=params)
- self._raise_for_status(res)
-
- def search(self, term):
- return self._result(self._get(self._url("/images/search"),
- 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,
- extra_hosts=None, read_only=None, pid_mode=None, ipc_mode=None,
- security_opt=None, ulimits=None):
-
- if utils.compare_version('1.10', self._version) < 0:
- if dns is not None:
- raise errors.InvalidVersion(
- 'dns is only supported for API version >= 1.10'
- )
- if volumes_from is not None:
- raise errors.InvalidVersion(
- 'volumes_from is only supported for API version >= 1.10'
- )
-
- 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 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 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'
- )
-
- 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
+ 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)]
)
- res = self._post_json(url, data=start_config)
- self._raise_for_status(res)
- @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!')
-
- 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):
- params = {'t': timeout}
- url = self._url("/containers/{0}/stop".format(container))
-
- res = self._post(url, params=params,
- 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,
- 'repo': repository,
- 'force': 1 if force else 0
- }
- url = self._url("/images/{0}/tag".format(image))
- res = self._post(url, params=params)
- 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, 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):
- url = self._url('/containers/{0}/unpause'.format(container))
- res = self._post(url)
- self._raise_for_status(res)
+ def get_adapter(self, url):
+ try:
+ return super(Client, self).get_adapter(url)
+ except requests.exceptions.InvalidSchema as e:
+ if self._custom_adapter:
+ return self._custom_adapter
+ else:
+ raise e
- @check_resource
- def wait(self, container, timeout=None):
- url = self._url("/containers/{0}/wait".format(container))
- res = self._post(url, timeout=timeout)
- self._raise_for_status(res)
- json_ = res.json()
- if 'StatusCode' in json_:
- return json_['StatusCode']
- return -1
+ @property
+ def api_version(self):
+ return self._version
class AutoVersionClient(Client):
diff --git a/docker/clientbase.py b/docker/clientbase.py
deleted file mode 100644
index ce52ffa..0000000
--- a/docker/clientbase.py
+++ /dev/null
@@ -1,277 +0,0 @@
-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:
- if e.response.status_code == 404:
- raise errors.NotFound(e, response, explanation=explanation)
- 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:
- continue
- 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 --git a/docker/constants.py b/docker/constants.py
index 10a2fee..3647a3b 100644
--- a/docker/constants.py
+++ b/docker/constants.py
@@ -1,4 +1,4 @@
-DEFAULT_DOCKER_API_VERSION = '1.19'
+DEFAULT_DOCKER_API_VERSION = '1.20'
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
CONTAINER_LIMITS_KEYS = [
diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py
index 81cc8a6..92e03e9 100644
--- a/docker/utils/__init__.py
+++ b/docker/utils/__init__.py
@@ -1,9 +1,10 @@
from .utils import (
compare_version, convert_port_bindings, convert_volume_binds,
- mkbuildcontext, tar, parse_repository_tag, parse_host,
+ mkbuildcontext, tar, exclude_paths, parse_repository_tag, parse_host,
kwargs_from_env, convert_filters, create_host_config,
- create_container_config, parse_bytes, ping_registry
+ create_container_config, parse_bytes, ping_registry, parse_env_file,
+ version_lt, version_gte, decode_json_header
) # flake8: noqa
from .types import Ulimit, LogConfig # flake8: noqa
-from .decorators import check_resource #flake8: noqa
+from .decorators import check_resource, minimum_version #flake8: noqa
diff --git a/docker/utils/decorators.py b/docker/utils/decorators.py
index 3c42fe4..7d3b01a 100644
--- a/docker/utils/decorators.py
+++ b/docker/utils/decorators.py
@@ -1,6 +1,7 @@
import functools
from .. import errors
+from . import utils
def check_resource(f):
@@ -19,3 +20,18 @@ def check_resource(f):
)
return f(self, resource_id, *args, **kwargs)
return wrapped
+
+
+def minimum_version(version):
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapper(self, *args, **kwargs):
+ if utils.version_lt(self._version, version):
+ raise errors.InvalidVersion(
+ '{0} is not available for version < {1}'.format(
+ f.__name__, version
+ )
+ )
+ return f(self, *args, **kwargs)
+ return wrapper
+ return decorator
diff --git a/docker/utils/ports/ports.py b/docker/utils/ports/ports.py
index 6a0a862..326ef94 100644
--- a/docker/utils/ports/ports.py
+++ b/docker/utils/ports/ports.py
@@ -1,5 +1,4 @@
-
def add_port_mapping(port_bindings, internal_port, external):
if internal_port in port_bindings:
port_bindings[internal_port].append(external)
@@ -33,9 +32,8 @@ def to_port_range(port):
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)
+ _raise_invalid_port(port)
+
port, protocol = parts
protocol = "/" + protocol
@@ -52,11 +50,17 @@ def to_port_range(port):
'port or startport-endport' % port)
+def _raise_invalid_port(port):
+ raise ValueError('Invalid port "%s", should be '
+ '[[remote_ip:]remote_port[-remote_port]:]'
+ 'port[/protocol]' % 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)
+ _raise_invalid_port(port)
if len(parts) == 1:
internal_port, = parts
@@ -66,6 +70,10 @@ def split_port(port):
internal_range = to_port_range(internal_port)
external_range = to_port_range(external_port)
+
+ if internal_range is None or external_range is None:
+ _raise_invalid_port(port)
+
if len(internal_range) != len(external_range):
raise ValueError('Port ranges don\'t match in length')
diff --git a/docker/utils/types.py b/docker/utils/types.py
index d742fd0..ea9f06d 100644
--- a/docker/utils/types.py
+++ b/docker/utils/types.py
@@ -5,9 +5,12 @@ class LogConfigTypesEnum(object):
_values = (
'json-file',
'syslog',
+ 'journald',
+ 'gelf',
+ 'fluentd',
'none'
)
- JSON, SYSLOG, NONE = _values
+ JSON, SYSLOG, JOURNALD, GELF, FLUENTD, NONE = _values
class DictType(dict):
@@ -20,18 +23,15 @@ 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)
- ))
+ log_driver_type = kwargs.get('type', kwargs.get('Type'))
+ config = kwargs.get('config', kwargs.get('Config')) or {}
+
if config and not isinstance(config, dict):
raise ValueError("LogConfig.config must be a dictionary")
super(LogConfig, self).__init__({
- 'Type': type_,
- 'Config': config or {}
+ 'Type': log_driver_type,
+ 'Config': config
})
@property
@@ -40,10 +40,6 @@ class LogConfig(DictType):
@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
diff --git a/docker/utils/utils.py b/docker/utils/utils.py
index a714c97..89837b7 100644
--- a/docker/utils/utils.py
+++ b/docker/utils/utils.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import base64
import io
import os
import os.path
@@ -27,6 +28,7 @@ from datetime import datetime
import requests
import six
+from .. import constants
from .. import errors
from .. import tls
from .types import Ulimit, LogConfig
@@ -65,39 +67,89 @@ def mkbuildcontext(dockerfile):
return f
-def fnmatch_any(relpath, patterns):
- return any([fnmatch(relpath, pattern) for pattern in patterns])
+def decode_json_header(header):
+ data = base64.b64decode(header)
+ if six.PY3:
+ data = data.decode('utf-8')
+ return json.loads(data)
-def tar(path, exclude=None):
+def tar(path, exclude=None, dockerfile=None):
f = tempfile.NamedTemporaryFile()
t = tarfile.open(mode='w', fileobj=f)
- for dirpath, dirnames, filenames in os.walk(path):
- relpath = os.path.relpath(dirpath, path)
- if relpath == '.':
- relpath = ''
- if exclude is None:
- fnames = filenames
- else:
- dirnames[:] = [d for d in dirnames
- if not fnmatch_any(os.path.join(relpath, d),
- exclude)]
- fnames = [name for name in filenames
- if not fnmatch_any(os.path.join(relpath, name),
- exclude)]
- 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:
- arcname = os.path.join(relpath, name)
- t.add(os.path.join(path, arcname),
- arcname=arcname, recursive=False)
+
+ root = os.path.abspath(path)
+ exclude = exclude or []
+
+ for path in sorted(exclude_paths(root, exclude, dockerfile=dockerfile)):
+ t.add(os.path.join(root, path), arcname=path, recursive=False)
+
t.close()
f.seek(0)
return f
+def exclude_paths(root, patterns, dockerfile=None):
+ """
+ Given a root directory path and a list of .dockerignore patterns, return
+ an iterator of all paths (both regular files and directories) in the root
+ directory that do *not* match any of the patterns.
+
+ All paths returned are relative to the root.
+ """
+ if dockerfile is None:
+ dockerfile = 'Dockerfile'
+
+ exceptions = [p for p in patterns if p.startswith('!')]
+
+ include_patterns = [p[1:] for p in exceptions]
+ include_patterns += [dockerfile, '.dockerignore']
+
+ exclude_patterns = list(set(patterns) - set(exceptions))
+
+ all_paths = get_paths(root)
+
+ # Remove all paths that are matched by any exclusion pattern
+ paths = [
+ p for p in all_paths
+ if not any(match_path(p, pattern) for pattern in exclude_patterns)
+ ]
+
+ # Add back the set of paths that are matched by any inclusion pattern.
+ # Include parent dirs - if we add back 'foo/bar', add 'foo' as well
+ for p in all_paths:
+ if any(match_path(p, pattern) for pattern in include_patterns):
+ components = p.split('/')
+ paths += [
+ '/'.join(components[:end])
+ for end in range(1, len(components) + 1)
+ ]
+
+ return set(paths)
+
+
+def get_paths(root):
+ paths = []
+
+ for parent, dirs, files in os.walk(root, followlinks=False):
+ parent = os.path.relpath(parent, root)
+ if parent == '.':
+ parent = ''
+ for path in dirs:
+ paths.append(os.path.join(parent, path))
+ for path in files:
+ paths.append(os.path.join(parent, path))
+
+ return paths
+
+
+def match_path(path, pattern):
+ pattern = pattern.rstrip('/')
+ pattern_components = pattern.split('/')
+ path_components = path.split('/')[:len(pattern_components)]
+ return fnmatch('/'.join(path_components), pattern)
+
+
def compare_version(v1, v2):
"""Compare docker versions
@@ -120,6 +172,14 @@ def compare_version(v1, v2):
return 1
+def version_lt(v1, v2):
+ return compare_version(v1, v2) > 0
+
+
+def version_gte(v1, v2):
+ return not version_lt(v1, v2)
+
+
def ping_registry(url):
warnings.warn(
'The `ping_registry` method is deprecated and will be removed.',
@@ -190,6 +250,9 @@ def convert_volume_binds(binds):
result = []
for k, v in binds.items():
+ if isinstance(k, six.binary_type):
+ k = k.decode('utf-8')
+
if isinstance(v, dict):
if 'ro' in v and 'mode' in v:
raise ValueError(
@@ -197,6 +260,10 @@ def convert_volume_binds(binds):
.format(repr(v))
)
+ bind = v['bind']
+ if isinstance(bind, six.binary_type):
+ bind = bind.decode('utf-8')
+
if 'ro' in v:
mode = 'ro' if v['ro'] else 'rw'
elif 'mode' in v:
@@ -204,11 +271,15 @@ def convert_volume_binds(binds):
else:
mode = 'rw'
- result.append('{0}:{1}:{2}'.format(
- k, v['bind'], mode
- ))
+ result.append(
+ six.text_type('{0}:{1}:{2}').format(k, bind, mode)
+ )
else:
- result.append('{0}:{1}:rw'.format(k, v))
+ if isinstance(v, six.binary_type):
+ v = v.decode('utf-8')
+ result.append(
+ six.text_type('{0}:{1}:rw').format(k, v)
+ )
return result
@@ -228,10 +299,15 @@ def parse_repository_tag(repo):
# fd:// protocol unsupported (for obvious reasons)
# Added support for http and https
# Protocol translation: tcp -> http, unix -> http+unix
-def parse_host(addr):
+def parse_host(addr, platform=None):
proto = "http+unix"
host = DEFAULT_HTTP_HOST
port = None
+ path = ''
+
+ if not addr and platform == 'win32':
+ addr = '{0}:{1}'.format(DEFAULT_HTTP_HOST, 2375)
+
if not addr or addr.strip() == 'unix://':
return DEFAULT_UNIX_SOCKET
@@ -270,8 +346,12 @@ def parse_host(addr):
if host_parts[0]:
host = host_parts[0]
+ port = host_parts[1]
+ if '/' in port:
+ port, path = port.split('/', 1)
+ path = '/{0}'.format(path)
try:
- port = int(host_parts[1])
+ port = int(port)
except Exception:
raise errors.DockerException(
"Invalid port: %s", addr
@@ -285,7 +365,7 @@ def parse_host(addr):
if proto == "http+unix":
return "{0}://{1}".format(proto, host)
- return "{0}://{1}:{2}".format(proto, host, port)
+ return "{0}://{1}:{2}{3}".format(proto, host, port, path)
def parse_devices(devices):
@@ -314,9 +394,14 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None):
tls_verify = os.environ.get('DOCKER_TLS_VERIFY')
params = {}
+
if host:
params['base_url'] = (host.replace('tcp://', 'https://')
if tls_verify else host)
+
+ if tls_verify and not cert_path:
+ cert_path = os.path.join(os.path.expanduser('~'), '.docker')
+
if tls_verify and cert_path:
params['tls'] = tls.TLSConfig(
client_cert=(os.path.join(cert_path, 'cert.pem'),
@@ -325,6 +410,7 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None):
verify=True,
ssl_version=ssl_version,
assert_hostname=assert_hostname)
+
return params
@@ -390,10 +476,18 @@ def create_host_config(
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
+ memswap_limit=None, cgroup_parent=None, group_add=None, cpu_quota=None,
+ cpu_period=None, version=None
):
host_config = {}
+ if not version:
+ warnings.warn(
+ 'docker.utils.create_host_config() is deprecated. Please use '
+ 'Client.create_host_config() instead.'
+ )
+ version = constants.DEFAULT_DOCKER_API_VERSION
+
if mem_limit is not None:
if isinstance(mem_limit, six.string_types):
mem_limit = parse_bytes(mem_limit)
@@ -428,6 +522,8 @@ def create_host_config(
if network_mode:
host_config['NetworkMode'] = network_mode
+ elif network_mode is None and compare_version('1.19', version) > 0:
+ host_config['NetworkMode'] = 'default'
if restart_policy:
host_config['RestartPolicy'] = restart_policy
@@ -441,6 +537,13 @@ def create_host_config(
if devices:
host_config['Devices'] = parse_devices(devices)
+ if group_add:
+ if version_lt(version, '1.20'):
+ raise errors.InvalidVersion(
+ 'group_add param not supported for API version < 1.20'
+ )
+ host_config['GroupAdd'] = [six.text_type(grp) for grp in group_add]
+
if dns is not None:
host_config['Dns'] = dns
@@ -493,6 +596,9 @@ def create_host_config(
if lxc_conf is not None:
host_config['LxcConf'] = lxc_conf
+ if cgroup_parent is not None:
+ host_config['CgroupParent'] = cgroup_parent
+
if ulimits is not None:
if not isinstance(ulimits, list):
raise errors.DockerException(
@@ -515,9 +621,58 @@ def create_host_config(
log_config = LogConfig(**log_config)
host_config['LogConfig'] = log_config
+ if cpu_quota:
+ if not isinstance(cpu_quota, int):
+ raise TypeError(
+ 'Invalid type for cpu_quota param: expected int but'
+ ' found {0}'.format(type(cpu_quota))
+ )
+ if version_lt(version, '1.19'):
+ raise errors.InvalidVersion(
+ 'cpu_quota param not supported for API version < 1.19'
+ )
+ host_config['CpuQuota'] = cpu_quota
+
+ if cpu_period:
+ if not isinstance(cpu_period, int):
+ raise TypeError(
+ 'Invalid type for cpu_period param: expected int but'
+ ' found {0}'.format(type(cpu_period))
+ )
+ if version_lt(version, '1.19'):
+ raise errors.InvalidVersion(
+ 'cpu_period param not supported for API version < 1.19'
+ )
+ host_config['CpuPeriod'] = cpu_period
+
return host_config
+def parse_env_file(env_file):
+ """
+ Reads a line-separated environment file.
+ The format of each line should be "key=value".
+ """
+ environment = {}
+
+ with open(env_file, 'r') as f:
+ for line in f:
+
+ if line[0] == '#':
+ continue
+
+ parse_line = line.strip().split('=')
+ if len(parse_line) == 2:
+ k, v = parse_line
+ environment[k] = v
+ else:
+ raise errors.DockerException(
+ 'Invalid line in environment file {0}:\n{1}'.format(
+ env_file, line))
+
+ return environment
+
+
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,
@@ -528,6 +683,10 @@ def create_container_config(
):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
+
+ if isinstance(entrypoint, six.string_types):
+ entrypoint = shlex.split(str(entrypoint))
+
if isinstance(environment, dict):
environment = [
six.text_type('{0}={1}').format(k, v)
@@ -619,7 +778,7 @@ def create_container_config(
'Hostname': hostname,
'Domainname': domainname,
'ExposedPorts': ports,
- 'User': user,
+ 'User': six.text_type(user) if user else None,
'Tty': tty,
'OpenStdin': stdin_open,
'StdinOnce': stdin_once,
diff --git a/docker/version.py b/docker/version.py
index bc778b9..4ebafdd 100644
--- a/docker/version.py
+++ b/docker/version.py
@@ -1,2 +1,2 @@
-version = "1.3.1"
+version = "1.5.0"
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])