mirror of https://github.com/docker/docker-py.git
commit
30089ec681
|
@ -31,7 +31,7 @@ def buildImages = { ->
|
||||||
}
|
}
|
||||||
|
|
||||||
def getDockerVersions = { ->
|
def getDockerVersions = { ->
|
||||||
def dockerVersions = ["19.03.5"]
|
def dockerVersions = ["19.03.12"]
|
||||||
wrappedNode(label: "amd64 && ubuntu-1804 && overlay2") {
|
wrappedNode(label: "amd64 && ubuntu-1804 && overlay2") {
|
||||||
def result = sh(script: """docker run --rm \\
|
def result = sh(script: """docker run --rm \\
|
||||||
--entrypoint=python \\
|
--entrypoint=python \\
|
||||||
|
@ -66,7 +66,7 @@ def runTests = { Map settings ->
|
||||||
throw new Exception("Need test image object, e.g.: `runTests(testImage: img)`")
|
throw new Exception("Need test image object, e.g.: `runTests(testImage: img)`")
|
||||||
}
|
}
|
||||||
if (!dockerVersion) {
|
if (!dockerVersion) {
|
||||||
throw new Exception("Need Docker version to test, e.g.: `runTests(dockerVersion: '1.12.3')`")
|
throw new Exception("Need Docker version to test, e.g.: `runTests(dockerVersion: '19.03.12')`")
|
||||||
}
|
}
|
||||||
if (!pythonVersion) {
|
if (!pythonVersion) {
|
||||||
throw new Exception("Need Python version being tested, e.g.: `runTests(pythonVersion: 'py2.7')`")
|
throw new Exception("Need Python version being tested, e.g.: `runTests(pythonVersion: 'py2.7')`")
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -41,8 +41,8 @@ integration-test: build
|
||||||
integration-test-py3: build-py3
|
integration-test-py3: build-py3
|
||||||
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file}
|
docker run -t --rm -v /var/run/docker.sock:/var/run/docker.sock docker-sdk-python3 py.test -v tests/integration/${file}
|
||||||
|
|
||||||
TEST_API_VERSION ?= 1.35
|
TEST_API_VERSION ?= 1.39
|
||||||
TEST_ENGINE_VERSION ?= 19.03.5
|
TEST_ENGINE_VERSION ?= 19.03.12
|
||||||
|
|
||||||
.PHONY: setup-network
|
.PHONY: setup-network
|
||||||
setup-network:
|
setup-network:
|
||||||
|
|
|
@ -480,6 +480,9 @@ class ContainerApiMixin(object):
|
||||||
For example, ``/dev/sda:/dev/xvda:rwm`` allows the container
|
For example, ``/dev/sda:/dev/xvda:rwm`` allows the container
|
||||||
to have read-write access to the host's ``/dev/sda`` via a
|
to have read-write access to the host's ``/dev/sda`` via a
|
||||||
node named ``/dev/xvda`` inside the container.
|
node named ``/dev/xvda`` inside the container.
|
||||||
|
device_requests (:py:class:`list`): Expose host resources such as
|
||||||
|
GPUs to the container, as a list of
|
||||||
|
:py:class:`docker.types.DeviceRequest` instances.
|
||||||
dns (:py:class:`list`): Set custom DNS servers.
|
dns (:py:class:`list`): Set custom DNS servers.
|
||||||
dns_opt (:py:class:`list`): Additional options to be added to the
|
dns_opt (:py:class:`list`): Additional options to be added to the
|
||||||
container's ``resolv.conf`` file
|
container's ``resolv.conf`` file
|
||||||
|
@ -636,6 +639,8 @@ class ContainerApiMixin(object):
|
||||||
network, using the IPv6 protocol. Defaults to ``None``.
|
network, using the IPv6 protocol. Defaults to ``None``.
|
||||||
link_local_ips (:py:class:`list`): A list of link-local (IPv4/IPv6)
|
link_local_ips (:py:class:`list`): A list of link-local (IPv4/IPv6)
|
||||||
addresses.
|
addresses.
|
||||||
|
driver_opt (dict): A dictionary of options to provide to the
|
||||||
|
network driver. Defaults to ``None``.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(dict) An endpoint config.
|
(dict) An endpoint config.
|
||||||
|
@ -694,7 +699,8 @@ class ContainerApiMixin(object):
|
||||||
return self._stream_raw_result(res, chunk_size, False)
|
return self._stream_raw_result(res, chunk_size, False)
|
||||||
|
|
||||||
@utils.check_resource('container')
|
@utils.check_resource('container')
|
||||||
def get_archive(self, container, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
|
def get_archive(self, container, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE,
|
||||||
|
encode_stream=False):
|
||||||
"""
|
"""
|
||||||
Retrieve a file or folder from a container in the form of a tar
|
Retrieve a file or folder from a container in the form of a tar
|
||||||
archive.
|
archive.
|
||||||
|
@ -705,6 +711,8 @@ class ContainerApiMixin(object):
|
||||||
chunk_size (int): The number of bytes returned by each iteration
|
chunk_size (int): The number of bytes returned by each iteration
|
||||||
of the generator. If ``None``, data will be streamed as it is
|
of the generator. If ``None``, data will be streamed as it is
|
||||||
received. Default: 2 MB
|
received. Default: 2 MB
|
||||||
|
encode_stream (bool): Determines if data should be encoded
|
||||||
|
(gzip-compressed) during transmission. Default: False
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(tuple): First element is a raw tar data stream. Second element is
|
(tuple): First element is a raw tar data stream. Second element is
|
||||||
|
@ -729,8 +737,13 @@ class ContainerApiMixin(object):
|
||||||
params = {
|
params = {
|
||||||
'path': path
|
'path': path
|
||||||
}
|
}
|
||||||
|
headers = {
|
||||||
|
"Accept-Encoding": "gzip, deflate"
|
||||||
|
} if encode_stream else {
|
||||||
|
"Accept-Encoding": "identity"
|
||||||
|
}
|
||||||
url = self._url('/containers/{0}/archive', container)
|
url = self._url('/containers/{0}/archive', container)
|
||||||
res = self._get(url, params=params, stream=True)
|
res = self._get(url, params=params, stream=True, headers=headers)
|
||||||
self._raise_for_status(res)
|
self._raise_for_status(res)
|
||||||
encoded_stat = res.headers.get('x-docker-container-path-stat')
|
encoded_stat = res.headers.get('x-docker-container-path-stat')
|
||||||
return (
|
return (
|
||||||
|
@ -1120,7 +1133,7 @@ class ContainerApiMixin(object):
|
||||||
else:
|
else:
|
||||||
if decode:
|
if decode:
|
||||||
raise errors.InvalidArgument(
|
raise errors.InvalidArgument(
|
||||||
"decode is only available in conjuction with stream=True"
|
"decode is only available in conjunction with stream=True"
|
||||||
)
|
)
|
||||||
return self._result(self._get(url, params={'stream': False}),
|
return self._result(self._get(url, params={'stream': False}),
|
||||||
json=True)
|
json=True)
|
||||||
|
|
|
@ -216,7 +216,7 @@ class NetworkApiMixin(object):
|
||||||
def connect_container_to_network(self, container, net_id,
|
def connect_container_to_network(self, container, net_id,
|
||||||
ipv4_address=None, ipv6_address=None,
|
ipv4_address=None, ipv6_address=None,
|
||||||
aliases=None, links=None,
|
aliases=None, links=None,
|
||||||
link_local_ips=None):
|
link_local_ips=None, driver_opt=None):
|
||||||
"""
|
"""
|
||||||
Connect a container to a network.
|
Connect a container to a network.
|
||||||
|
|
||||||
|
@ -240,7 +240,8 @@ class NetworkApiMixin(object):
|
||||||
"Container": container,
|
"Container": container,
|
||||||
"EndpointConfig": self.create_endpoint_config(
|
"EndpointConfig": self.create_endpoint_config(
|
||||||
aliases=aliases, links=links, ipv4_address=ipv4_address,
|
aliases=aliases, links=links, ipv4_address=ipv4_address,
|
||||||
ipv6_address=ipv6_address, link_local_ips=link_local_ips
|
ipv6_address=ipv6_address, link_local_ips=link_local_ips,
|
||||||
|
driver_opt=driver_opt
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
from .version import version
|
from .version import version
|
||||||
|
|
||||||
DEFAULT_DOCKER_API_VERSION = '1.35'
|
DEFAULT_DOCKER_API_VERSION = '1.39'
|
||||||
MINIMUM_DOCKER_API_VERSION = '1.21'
|
MINIMUM_DOCKER_API_VERSION = '1.21'
|
||||||
DEFAULT_TIMEOUT_SECONDS = 60
|
DEFAULT_TIMEOUT_SECONDS = 60
|
||||||
STREAM_HEADER_SIZE_BYTES = 8
|
STREAM_HEADER_SIZE_BYTES = 8
|
||||||
|
|
|
@ -225,7 +225,8 @@ class Container(Model):
|
||||||
"""
|
"""
|
||||||
return self.client.api.export(self.id, chunk_size)
|
return self.client.api.export(self.id, chunk_size)
|
||||||
|
|
||||||
def get_archive(self, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE):
|
def get_archive(self, path, chunk_size=DEFAULT_DATA_CHUNK_SIZE,
|
||||||
|
encode_stream=False):
|
||||||
"""
|
"""
|
||||||
Retrieve a file or folder from the container in the form of a tar
|
Retrieve a file or folder from the container in the form of a tar
|
||||||
archive.
|
archive.
|
||||||
|
@ -235,6 +236,8 @@ class Container(Model):
|
||||||
chunk_size (int): The number of bytes returned by each iteration
|
chunk_size (int): The number of bytes returned by each iteration
|
||||||
of the generator. If ``None``, data will be streamed as it is
|
of the generator. If ``None``, data will be streamed as it is
|
||||||
received. Default: 2 MB
|
received. Default: 2 MB
|
||||||
|
encode_stream (bool): Determines if data should be encoded
|
||||||
|
(gzip-compressed) during transmission. Default: False
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(tuple): First element is a raw tar data stream. Second element is
|
(tuple): First element is a raw tar data stream. Second element is
|
||||||
|
@ -255,7 +258,8 @@ class Container(Model):
|
||||||
... f.write(chunk)
|
... f.write(chunk)
|
||||||
>>> f.close()
|
>>> f.close()
|
||||||
"""
|
"""
|
||||||
return self.client.api.get_archive(self.id, path, chunk_size)
|
return self.client.api.get_archive(self.id, path,
|
||||||
|
chunk_size, encode_stream)
|
||||||
|
|
||||||
def kill(self, signal=None):
|
def kill(self, signal=None):
|
||||||
"""
|
"""
|
||||||
|
@ -579,6 +583,9 @@ class ContainerCollection(Collection):
|
||||||
For example, ``/dev/sda:/dev/xvda:rwm`` allows the container
|
For example, ``/dev/sda:/dev/xvda:rwm`` allows the container
|
||||||
to have read-write access to the host's ``/dev/sda`` via a
|
to have read-write access to the host's ``/dev/sda`` via a
|
||||||
node named ``/dev/xvda`` inside the container.
|
node named ``/dev/xvda`` inside the container.
|
||||||
|
device_requests (:py:class:`list`): Expose host resources such as
|
||||||
|
GPUs to the container, as a list of
|
||||||
|
:py:class:`docker.types.DeviceRequest` instances.
|
||||||
dns (:py:class:`list`): Set custom DNS servers.
|
dns (:py:class:`list`): Set custom DNS servers.
|
||||||
dns_opt (:py:class:`list`): Additional options to be added to the
|
dns_opt (:py:class:`list`): Additional options to be added to the
|
||||||
container's ``resolv.conf`` file.
|
container's ``resolv.conf`` file.
|
||||||
|
@ -998,6 +1005,7 @@ RUN_HOST_CONFIG_KWARGS = [
|
||||||
'device_write_bps',
|
'device_write_bps',
|
||||||
'device_write_iops',
|
'device_write_iops',
|
||||||
'devices',
|
'devices',
|
||||||
|
'device_requests',
|
||||||
'dns_opt',
|
'dns_opt',
|
||||||
'dns_search',
|
'dns_search',
|
||||||
'dns',
|
'dns',
|
||||||
|
|
|
@ -46,6 +46,8 @@ class Network(Model):
|
||||||
network, using the IPv6 protocol. Defaults to ``None``.
|
network, using the IPv6 protocol. Defaults to ``None``.
|
||||||
link_local_ips (:py:class:`list`): A list of link-local (IPv4/IPv6)
|
link_local_ips (:py:class:`list`): A list of link-local (IPv4/IPv6)
|
||||||
addresses.
|
addresses.
|
||||||
|
driver_opt (dict): A dictionary of options to provide to the
|
||||||
|
network driver. Defaults to ``None``.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
:py:class:`docker.errors.APIError`
|
:py:class:`docker.errors.APIError`
|
||||||
|
|
|
@ -32,7 +32,7 @@ class TLSConfig(object):
|
||||||
# https://docs.docker.com/engine/articles/https/
|
# https://docs.docker.com/engine/articles/https/
|
||||||
# This diverges from the Docker CLI in that users can specify 'tls'
|
# This diverges from the Docker CLI in that users can specify 'tls'
|
||||||
# here, but also disable any public/default CA pool verification by
|
# here, but also disable any public/default CA pool verification by
|
||||||
# leaving tls_verify=False
|
# leaving verify=False
|
||||||
|
|
||||||
self.assert_hostname = assert_hostname
|
self.assert_hostname = assert_hostname
|
||||||
self.assert_fingerprint = assert_fingerprint
|
self.assert_fingerprint = assert_fingerprint
|
||||||
|
@ -62,7 +62,7 @@ class TLSConfig(object):
|
||||||
# https://github.com/docker/docker-py/issues/963
|
# https://github.com/docker/docker-py/issues/963
|
||||||
self.ssl_version = ssl.PROTOCOL_TLSv1
|
self.ssl_version = ssl.PROTOCOL_TLSv1
|
||||||
|
|
||||||
# "tls" and "tls_verify" must have both or neither cert/key files In
|
# "client_cert" must have both or neither cert/key files. In
|
||||||
# either case, Alert the user when both are expected, but any are
|
# either case, Alert the user when both are expected, but any are
|
||||||
# missing.
|
# missing.
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ class TLSConfig(object):
|
||||||
tls_cert, tls_key = client_cert
|
tls_cert, tls_key = client_cert
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise errors.TLSParameterError(
|
raise errors.TLSParameterError(
|
||||||
'client_config must be a tuple of'
|
'client_cert must be a tuple of'
|
||||||
' (client certificate, key file)'
|
' (client certificate, key file)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ class TLSConfig(object):
|
||||||
not os.path.isfile(tls_key)):
|
not os.path.isfile(tls_key)):
|
||||||
raise errors.TLSParameterError(
|
raise errors.TLSParameterError(
|
||||||
'Path to a certificate and key files must be provided'
|
'Path to a certificate and key files must be provided'
|
||||||
' through the client_config param'
|
' through the client_cert param'
|
||||||
)
|
)
|
||||||
self.cert = (tls_cert, tls_key)
|
self.cert = (tls_cert, tls_key)
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ class TLSConfig(object):
|
||||||
self.ca_cert = ca_cert
|
self.ca_cert = ca_cert
|
||||||
if self.verify and self.ca_cert and not os.path.isfile(self.ca_cert):
|
if self.verify and self.ca_cert and not os.path.isfile(self.ca_cert):
|
||||||
raise errors.TLSParameterError(
|
raise errors.TLSParameterError(
|
||||||
'Invalid CA certificate provided for `tls_ca_cert`.'
|
'Invalid CA certificate provided for `ca_cert`.'
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_client(self, client):
|
def configure_client(self, client):
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# flake8: noqa
|
# flake8: noqa
|
||||||
from .containers import ContainerConfig, HostConfig, LogConfig, Ulimit
|
from .containers import (
|
||||||
|
ContainerConfig, HostConfig, LogConfig, Ulimit, DeviceRequest
|
||||||
|
)
|
||||||
from .daemon import CancellableStream
|
from .daemon import CancellableStream
|
||||||
from .healthcheck import Healthcheck
|
from .healthcheck import Healthcheck
|
||||||
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
|
from .networks import EndpointConfig, IPAMConfig, IPAMPool, NetworkingConfig
|
||||||
|
|
|
@ -154,6 +154,104 @@ class Ulimit(DictType):
|
||||||
self['Hard'] = value
|
self['Hard'] = value
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRequest(DictType):
|
||||||
|
"""
|
||||||
|
Create a device request to be used with
|
||||||
|
:py:meth:`~docker.api.container.ContainerApiMixin.create_host_config`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
driver (str): Which driver to use for this device. Optional.
|
||||||
|
count (int): Number or devices to request. Optional.
|
||||||
|
Set to -1 to request all available devices.
|
||||||
|
device_ids (list): List of strings for device IDs. Optional.
|
||||||
|
Set either ``count`` or ``device_ids``.
|
||||||
|
capabilities (list): List of lists of strings to request
|
||||||
|
capabilities. Optional. The global list acts like an OR,
|
||||||
|
and the sub-lists are AND. The driver will try to satisfy
|
||||||
|
one of the sub-lists.
|
||||||
|
Available capabilities for the ``nvidia`` driver can be found
|
||||||
|
`here <https://github.com/NVIDIA/nvidia-container-runtime>`_.
|
||||||
|
options (dict): Driver-specific options. Optional.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
driver = kwargs.get('driver', kwargs.get('Driver'))
|
||||||
|
count = kwargs.get('count', kwargs.get('Count'))
|
||||||
|
device_ids = kwargs.get('device_ids', kwargs.get('DeviceIDs'))
|
||||||
|
capabilities = kwargs.get('capabilities', kwargs.get('Capabilities'))
|
||||||
|
options = kwargs.get('options', kwargs.get('Options'))
|
||||||
|
|
||||||
|
if driver is None:
|
||||||
|
driver = ''
|
||||||
|
elif not isinstance(driver, six.string_types):
|
||||||
|
raise ValueError('DeviceRequest.driver must be a string')
|
||||||
|
if count is None:
|
||||||
|
count = 0
|
||||||
|
elif not isinstance(count, int):
|
||||||
|
raise ValueError('DeviceRequest.count must be an integer')
|
||||||
|
if device_ids is None:
|
||||||
|
device_ids = []
|
||||||
|
elif not isinstance(device_ids, list):
|
||||||
|
raise ValueError('DeviceRequest.device_ids must be a list')
|
||||||
|
if capabilities is None:
|
||||||
|
capabilities = []
|
||||||
|
elif not isinstance(capabilities, list):
|
||||||
|
raise ValueError('DeviceRequest.capabilities must be a list')
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
elif not isinstance(options, dict):
|
||||||
|
raise ValueError('DeviceRequest.options must be a dict')
|
||||||
|
|
||||||
|
super(DeviceRequest, self).__init__({
|
||||||
|
'Driver': driver,
|
||||||
|
'Count': count,
|
||||||
|
'DeviceIDs': device_ids,
|
||||||
|
'Capabilities': capabilities,
|
||||||
|
'Options': options
|
||||||
|
})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def driver(self):
|
||||||
|
return self['Driver']
|
||||||
|
|
||||||
|
@driver.setter
|
||||||
|
def driver(self, value):
|
||||||
|
self['Driver'] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self):
|
||||||
|
return self['Count']
|
||||||
|
|
||||||
|
@count.setter
|
||||||
|
def count(self, value):
|
||||||
|
self['Count'] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_ids(self):
|
||||||
|
return self['DeviceIDs']
|
||||||
|
|
||||||
|
@device_ids.setter
|
||||||
|
def device_ids(self, value):
|
||||||
|
self['DeviceIDs'] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def capabilities(self):
|
||||||
|
return self['Capabilities']
|
||||||
|
|
||||||
|
@capabilities.setter
|
||||||
|
def capabilities(self, value):
|
||||||
|
self['Capabilities'] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
return self['Options']
|
||||||
|
|
||||||
|
@options.setter
|
||||||
|
def options(self, value):
|
||||||
|
self['Options'] = value
|
||||||
|
|
||||||
|
|
||||||
class HostConfig(dict):
|
class HostConfig(dict):
|
||||||
def __init__(self, version, binds=None, port_bindings=None,
|
def __init__(self, version, binds=None, port_bindings=None,
|
||||||
lxc_conf=None, publish_all_ports=False, links=None,
|
lxc_conf=None, publish_all_ports=False, links=None,
|
||||||
|
@ -176,7 +274,7 @@ class HostConfig(dict):
|
||||||
volume_driver=None, cpu_count=None, cpu_percent=None,
|
volume_driver=None, cpu_count=None, cpu_percent=None,
|
||||||
nano_cpus=None, cpuset_mems=None, runtime=None, mounts=None,
|
nano_cpus=None, cpuset_mems=None, runtime=None, mounts=None,
|
||||||
cpu_rt_period=None, cpu_rt_runtime=None,
|
cpu_rt_period=None, cpu_rt_runtime=None,
|
||||||
device_cgroup_rules=None):
|
device_cgroup_rules=None, device_requests=None):
|
||||||
|
|
||||||
if mem_limit is not None:
|
if mem_limit is not None:
|
||||||
self['Memory'] = parse_bytes(mem_limit)
|
self['Memory'] = parse_bytes(mem_limit)
|
||||||
|
@ -536,6 +634,19 @@ class HostConfig(dict):
|
||||||
)
|
)
|
||||||
self['DeviceCgroupRules'] = device_cgroup_rules
|
self['DeviceCgroupRules'] = device_cgroup_rules
|
||||||
|
|
||||||
|
if device_requests is not None:
|
||||||
|
if version_lt(version, '1.40'):
|
||||||
|
raise host_config_version_error('device_requests', '1.40')
|
||||||
|
if not isinstance(device_requests, list):
|
||||||
|
raise host_config_type_error(
|
||||||
|
'device_requests', device_requests, 'list'
|
||||||
|
)
|
||||||
|
self['DeviceRequests'] = []
|
||||||
|
for req in device_requests:
|
||||||
|
if not isinstance(req, DeviceRequest):
|
||||||
|
req = DeviceRequest(**req)
|
||||||
|
self['DeviceRequests'].append(req)
|
||||||
|
|
||||||
|
|
||||||
def host_config_type_error(param, param_value, expected):
|
def host_config_type_error(param, param_value, expected):
|
||||||
error_msg = 'Invalid type for {0} param: expected {1} but found {2}'
|
error_msg = 'Invalid type for {0} param: expected {1} but found {2}'
|
||||||
|
|
|
@ -4,7 +4,7 @@ from ..utils import normalize_links, version_lt
|
||||||
|
|
||||||
class EndpointConfig(dict):
|
class EndpointConfig(dict):
|
||||||
def __init__(self, version, aliases=None, links=None, ipv4_address=None,
|
def __init__(self, version, aliases=None, links=None, ipv4_address=None,
|
||||||
ipv6_address=None, link_local_ips=None):
|
ipv6_address=None, link_local_ips=None, driver_opt=None):
|
||||||
if version_lt(version, '1.22'):
|
if version_lt(version, '1.22'):
|
||||||
raise errors.InvalidVersion(
|
raise errors.InvalidVersion(
|
||||||
'Endpoint config is not supported for API version < 1.22'
|
'Endpoint config is not supported for API version < 1.22'
|
||||||
|
@ -33,6 +33,15 @@ class EndpointConfig(dict):
|
||||||
if ipam_config:
|
if ipam_config:
|
||||||
self['IPAMConfig'] = ipam_config
|
self['IPAMConfig'] = ipam_config
|
||||||
|
|
||||||
|
if driver_opt:
|
||||||
|
if version_lt(version, '1.32'):
|
||||||
|
raise errors.InvalidVersion(
|
||||||
|
'DriverOpts is not supported for API version < 1.32'
|
||||||
|
)
|
||||||
|
if not isinstance(driver_opt, dict):
|
||||||
|
raise TypeError('driver_opt must be a dictionary')
|
||||||
|
self['DriverOpts'] = driver_opt
|
||||||
|
|
||||||
|
|
||||||
class NetworkingConfig(dict):
|
class NetworkingConfig(dict):
|
||||||
def __init__(self, endpoints_config=None):
|
def __init__(self, endpoints_config=None):
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
version = "4.2.2"
|
version = "4.3.0"
|
||||||
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])
|
version_info = tuple([int(d) for d in version.split("-")[0].split(".")])
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/64?closed=1)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add `DeviceRequest` type to expose host resources such as GPUs
|
||||||
|
- Add support for `DriverOpts` in EndpointConfig
|
||||||
|
- Disable compression by default when using container.get_archive method
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
- Update default API version to v1.39
|
||||||
|
- Update test engine version to 19.03.12
|
||||||
|
|
||||||
|
|
||||||
4.2.2
|
4.2.2
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -36,7 +51,6 @@ Change log
|
||||||
|
|
||||||
- Add support for docker contexts through `docker.ContextAPI`
|
- Add support for docker contexts through `docker.ContextAPI`
|
||||||
|
|
||||||
|
|
||||||
4.1.0
|
4.1.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -130,7 +144,7 @@ Change log
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
||||||
* Fix base_url to keep TCP protocol on utils.py by letting the responsability of changing the
|
* Fix base_url to keep TCP protocol on utils.py by letting the responsibility of changing the
|
||||||
protocol to `parse_host` afterwards, letting `base_url` with the original value.
|
protocol to `parse_host` afterwards, letting `base_url` with the original value.
|
||||||
* XFAIL test_attach_stream_and_cancel on TLS
|
* XFAIL test_attach_stream_and_cancel on TLS
|
||||||
|
|
||||||
|
@ -1234,7 +1248,7 @@ like the others
|
||||||
(`Client.volumes`, `Client.create_volume`, `Client.inspect_volume`,
|
(`Client.volumes`, `Client.create_volume`, `Client.inspect_volume`,
|
||||||
`Client.remove_volume`).
|
`Client.remove_volume`).
|
||||||
* Added support for the `group_add` parameter in `create_host_config`.
|
* Added support for the `group_add` parameter in `create_host_config`.
|
||||||
* Added support for the CPU CFS (`cpu_quota` and `cpu_period`) parameteres
|
* Added support for the CPU CFS (`cpu_quota` and `cpu_period`) parameters
|
||||||
in `create_host_config`.
|
in `create_host_config`.
|
||||||
* Added support for the archive API endpoint (`Client.get_archive`,
|
* Added support for the archive API endpoint (`Client.get_archive`,
|
||||||
`Client.put_archive`).
|
`Client.put_archive`).
|
||||||
|
|
|
@ -11,8 +11,7 @@ paramiko==2.4.2
|
||||||
pycparser==2.17
|
pycparser==2.17
|
||||||
pyOpenSSL==18.0.0
|
pyOpenSSL==18.0.0
|
||||||
pyparsing==2.2.0
|
pyparsing==2.2.0
|
||||||
pypiwin32==219; sys_platform == 'win32' and python_version < '3.6'
|
pywin32==227; sys_platform == 'win32'
|
||||||
pypiwin32==223; sys_platform == 'win32' and python_version >= '3.6'
|
|
||||||
requests==2.20.0
|
requests==2.20.0
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
urllib3==1.24.3
|
urllib3==1.24.3
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -24,10 +24,7 @@ extras_require = {
|
||||||
':python_version < "3.3"': 'ipaddress >= 1.0.16',
|
':python_version < "3.3"': 'ipaddress >= 1.0.16',
|
||||||
|
|
||||||
# win32 APIs if on Windows (required for npipe support)
|
# win32 APIs if on Windows (required for npipe support)
|
||||||
# Python 3.6 is only compatible with v220 ; Python < 3.5 is not supported
|
':sys_platform == "win32"': 'pywin32==227',
|
||||||
# on v220 ; ALL versions are broken for v222 (as of 2018-01-26)
|
|
||||||
':sys_platform == "win32" and python_version < "3.6"': 'pypiwin32==219',
|
|
||||||
':sys_platform == "win32" and python_version >= "3.6"': 'pypiwin32==223',
|
|
||||||
|
|
||||||
# If using docker-py over TLS, highly recommend this option is
|
# If using docker-py over TLS, highly recommend this option is
|
||||||
# pip-installed or pinned.
|
# pip-installed or pinned.
|
||||||
|
|
|
@ -4,7 +4,7 @@ FROM python:${PYTHON_VERSION}
|
||||||
|
|
||||||
ARG APT_MIRROR
|
ARG APT_MIRROR
|
||||||
RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \
|
RUN sed -ri "s/(httpredir|deb).debian.org/${APT_MIRROR:-deb.debian.org}/g" /etc/apt/sources.list \
|
||||||
&& sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list
|
&& sed -ri "s/(security).debian.org/${APT_MIRROR:-security.debian.org}/g" /etc/apt/sources.list
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y install \
|
RUN apt-get update && apt-get -y install \
|
||||||
gnupg2 \
|
gnupg2 \
|
||||||
|
|
|
@ -279,7 +279,7 @@ class CreateContainerTest(BaseAPIIntegrationTest):
|
||||||
|
|
||||||
expected_msgs = [
|
expected_msgs = [
|
||||||
"logger: no log driver named 'asdf' is registered",
|
"logger: no log driver named 'asdf' is registered",
|
||||||
"looking up logging plugin asdf: plugin \"asdf\" not found",
|
"error looking up logging plugin asdf: plugin \"asdf\" not found",
|
||||||
]
|
]
|
||||||
with pytest.raises(docker.errors.APIError) as excinfo:
|
with pytest.raises(docker.errors.APIError) as excinfo:
|
||||||
# raises an internal server error 500
|
# raises an internal server error 500
|
||||||
|
|
|
@ -275,6 +275,27 @@ class TestNetworks(BaseAPIIntegrationTest):
|
||||||
assert 'LinkLocalIPs' in net_cfg['IPAMConfig']
|
assert 'LinkLocalIPs' in net_cfg['IPAMConfig']
|
||||||
assert net_cfg['IPAMConfig']['LinkLocalIPs'] == ['169.254.8.8']
|
assert net_cfg['IPAMConfig']['LinkLocalIPs'] == ['169.254.8.8']
|
||||||
|
|
||||||
|
@requires_api_version('1.32')
|
||||||
|
def test_create_with_driveropt(self):
|
||||||
|
container = self.client.create_container(
|
||||||
|
TEST_IMG, 'top',
|
||||||
|
networking_config=self.client.create_networking_config(
|
||||||
|
{
|
||||||
|
'bridge': self.client.create_endpoint_config(
|
||||||
|
driver_opt={'com.docker-py.setting': 'on'}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
host_config=self.client.create_host_config(network_mode='bridge')
|
||||||
|
)
|
||||||
|
self.tmp_containers.append(container)
|
||||||
|
self.client.start(container)
|
||||||
|
container_data = self.client.inspect_container(container)
|
||||||
|
net_cfg = container_data['NetworkSettings']['Networks']['bridge']
|
||||||
|
assert 'DriverOpts' in net_cfg
|
||||||
|
assert 'com.docker-py.setting' in net_cfg['DriverOpts']
|
||||||
|
assert net_cfg['DriverOpts']['com.docker-py.setting'] == 'on'
|
||||||
|
|
||||||
@requires_api_version('1.22')
|
@requires_api_version('1.22')
|
||||||
def test_create_with_links(self):
|
def test_create_with_links(self):
|
||||||
net_name, net_id = self.create_network()
|
net_name, net_id = self.create_network()
|
||||||
|
|
|
@ -5,6 +5,7 @@ import json
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
|
from docker.api import APIClient
|
||||||
import pytest
|
import pytest
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ from . import fake_api
|
||||||
from ..helpers import requires_api_version
|
from ..helpers import requires_api_version
|
||||||
from .api_test import (
|
from .api_test import (
|
||||||
BaseAPIClientTest, url_prefix, fake_request, DEFAULT_TIMEOUT_SECONDS,
|
BaseAPIClientTest, url_prefix, fake_request, DEFAULT_TIMEOUT_SECONDS,
|
||||||
fake_inspect_container
|
fake_inspect_container, url_base
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -767,6 +768,67 @@ class CreateContainerTest(BaseAPIClientTest):
|
||||||
assert args[1]['headers'] == {'Content-Type': 'application/json'}
|
assert args[1]['headers'] == {'Content-Type': 'application/json'}
|
||||||
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
|
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
|
||||||
|
|
||||||
|
def test_create_container_with_device_requests(self):
|
||||||
|
client = APIClient(version='1.40')
|
||||||
|
fake_api.fake_responses.setdefault(
|
||||||
|
'{0}/v1.40/containers/create'.format(fake_api.prefix),
|
||||||
|
fake_api.post_fake_create_container,
|
||||||
|
)
|
||||||
|
client.create_container(
|
||||||
|
'busybox', 'true', host_config=client.create_host_config(
|
||||||
|
device_requests=[
|
||||||
|
{
|
||||||
|
'device_ids': [
|
||||||
|
'0',
|
||||||
|
'GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'driver': 'nvidia',
|
||||||
|
'Count': -1,
|
||||||
|
'capabilities': [
|
||||||
|
['gpu', 'utility']
|
||||||
|
],
|
||||||
|
'options': {
|
||||||
|
'key': 'value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
args = fake_request.call_args
|
||||||
|
assert args[0][1] == url_base + 'v1.40/' + 'containers/create'
|
||||||
|
expected_payload = self.base_create_payload()
|
||||||
|
expected_payload['HostConfig'] = client.create_host_config()
|
||||||
|
expected_payload['HostConfig']['DeviceRequests'] = [
|
||||||
|
{
|
||||||
|
'Driver': '',
|
||||||
|
'Count': 0,
|
||||||
|
'DeviceIDs': [
|
||||||
|
'0',
|
||||||
|
'GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a'
|
||||||
|
],
|
||||||
|
'Capabilities': [],
|
||||||
|
'Options': {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Driver': 'nvidia',
|
||||||
|
'Count': -1,
|
||||||
|
'DeviceIDs': [],
|
||||||
|
'Capabilities': [
|
||||||
|
['gpu', 'utility']
|
||||||
|
],
|
||||||
|
'Options': {
|
||||||
|
'key': 'value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert json.loads(args[1]['data']) == expected_payload
|
||||||
|
assert args[1]['headers']['Content-Type'] == 'application/json'
|
||||||
|
assert set(args[1]['headers']) <= {'Content-Type', 'User-Agent'}
|
||||||
|
assert args[1]['timeout'] == DEFAULT_TIMEOUT_SECONDS
|
||||||
|
|
||||||
def test_create_container_with_labels_dict(self):
|
def test_create_container_with_labels_dict(self):
|
||||||
labels_dict = {
|
labels_dict = {
|
||||||
six.text_type('foo'): six.text_type('1'),
|
six.text_type('foo'): six.text_type('1'),
|
||||||
|
|
|
@ -136,7 +136,8 @@ class NetworkTest(BaseAPIClientTest):
|
||||||
container={'Id': container_id},
|
container={'Id': container_id},
|
||||||
net_id=network_id,
|
net_id=network_id,
|
||||||
aliases=['foo', 'bar'],
|
aliases=['foo', 'bar'],
|
||||||
links=[('baz', 'quux')]
|
links=[('baz', 'quux')],
|
||||||
|
driver_opt={'com.docker-py.setting': 'yes'},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert post.call_args[0][0] == (
|
assert post.call_args[0][0] == (
|
||||||
|
@ -148,6 +149,7 @@ class NetworkTest(BaseAPIClientTest):
|
||||||
'EndpointConfig': {
|
'EndpointConfig': {
|
||||||
'Aliases': ['foo', 'bar'],
|
'Aliases': ['foo', 'bar'],
|
||||||
'Links': ['baz:quux'],
|
'Links': ['baz:quux'],
|
||||||
|
'DriverOpts': {'com.docker-py.setting': 'yes'},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -450,7 +450,7 @@ class ContainerTest(unittest.TestCase):
|
||||||
container = client.containers.get(FAKE_CONTAINER_ID)
|
container = client.containers.get(FAKE_CONTAINER_ID)
|
||||||
container.get_archive('foo')
|
container.get_archive('foo')
|
||||||
client.api.get_archive.assert_called_with(
|
client.api.get_archive.assert_called_with(
|
||||||
FAKE_CONTAINER_ID, 'foo', DEFAULT_DATA_CHUNK_SIZE
|
FAKE_CONTAINER_ID, 'foo', DEFAULT_DATA_CHUNK_SIZE, False
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_image(self):
|
def test_image(self):
|
||||||
|
|
|
@ -335,7 +335,7 @@ class ExcludePathsTest(unittest.TestCase):
|
||||||
# Dockerignore reference stipulates that absolute paths are
|
# Dockerignore reference stipulates that absolute paths are
|
||||||
# equivalent to relative paths, hence /../foo should be
|
# equivalent to relative paths, hence /../foo should be
|
||||||
# equivalent to ../foo. It also stipulates that paths are run
|
# equivalent to ../foo. It also stipulates that paths are run
|
||||||
# through Go's filepath.Clean, which explicitely "replace
|
# through Go's filepath.Clean, which explicitly "replace
|
||||||
# "/.." by "/" at the beginning of a path".
|
# "/.." by "/" at the beginning of a path".
|
||||||
assert exclude_paths(
|
assert exclude_paths(
|
||||||
base,
|
base,
|
||||||
|
|
Loading…
Reference in New Issue