Merge pull request #2331 from docker/4.0.0-release

4.0.0 release
This commit is contained in:
Joffrey F 2019-05-18 18:35:54 -07:00 committed by GitHub
commit 5d42ab81c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 918 additions and 249 deletions

View File

@ -4,8 +4,6 @@ matrix:
include: include:
- python: 2.7 - python: 2.7
env: TOXENV=py27 env: TOXENV=py27
- python: 3.4
env: TOXENV=py34
- python: 3.5 - python: 3.5
env: TOXENV=py35 env: TOXENV=py35
- python: 3.6 - python: 3.6

4
Jenkinsfile vendored
View File

@ -24,8 +24,8 @@ def buildImages = { ->
imageNamePy2 = "${imageNameBase}:py2-${gitCommit()}" imageNamePy2 = "${imageNameBase}:py2-${gitCommit()}"
imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}" imageNamePy3 = "${imageNameBase}:py3-${gitCommit()}"
buildImage(imageNamePy2, ".", "py2.7") buildImage(imageNamePy2, "-f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 .", "py2.7")
buildImage(imageNamePy3, "-f Dockerfile-py3 .", "py3.6") buildImage(imageNamePy3, "-f tests/Dockerfile --build-arg PYTHON_VERSION=3.6 .", "py3.6")
} }
} }
} }

View File

@ -6,3 +6,4 @@ include LICENSE
recursive-include tests *.py recursive-include tests *.py
recursive-include tests/unit/testdata * recursive-include tests/unit/testdata *
recursive-include tests/integration/testdata * recursive-include tests/integration/testdata *
recursive-include tests/gpg-keys *

View File

@ -8,11 +8,11 @@ clean:
.PHONY: build .PHONY: build
build: build:
docker build -t docker-sdk-python . docker build -t docker-sdk-python -f tests/Dockerfile --build-arg PYTHON_VERSION=2.7 .
.PHONY: build-py3 .PHONY: build-py3
build-py3: build-py3:
docker build -t docker-sdk-python3 -f Dockerfile-py3 . docker build -t docker-sdk-python3 -f tests/Dockerfile .
.PHONY: build-docs .PHONY: build-docs
build-docs: build-docs:
@ -39,10 +39,10 @@ integration-test: build
.PHONY: integration-test-py3 .PHONY: integration-test-py3
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 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.35
TEST_ENGINE_VERSION ?= 17.12.0-ce TEST_ENGINE_VERSION ?= 18.09.5
.PHONY: setup-network .PHONY: setup-network
setup-network: setup-network:

View File

@ -20,7 +20,7 @@ class BuildApiMixin(object):
decode=False, buildargs=None, gzip=False, shmsize=None, decode=False, buildargs=None, gzip=False, shmsize=None,
labels=None, cache_from=None, target=None, network_mode=None, labels=None, cache_from=None, target=None, network_mode=None,
squash=None, extra_hosts=None, platform=None, isolation=None, squash=None, extra_hosts=None, platform=None, isolation=None,
use_config_proxy=False): use_config_proxy=True):
""" """
Similar to the ``docker build`` command. Either ``path`` or ``fileobj`` Similar to the ``docker build`` command. Either ``path`` or ``fileobj``
needs to be set. ``path`` can be a local path (to a directory needs to be set. ``path`` can be a local path (to a directory
@ -121,6 +121,7 @@ class BuildApiMixin(object):
remote = context = None remote = context = None
headers = {} headers = {}
container_limits = container_limits or {} container_limits = container_limits or {}
buildargs = buildargs or {}
if path is None and fileobj is None: if path is None and fileobj is None:
raise TypeError("Either path or fileobj needs to be provided.") raise TypeError("Either path or fileobj needs to be provided.")
if gzip and encoding is not None: if gzip and encoding is not None:

View File

@ -82,7 +82,7 @@ class APIClient(
base_url (str): URL to the Docker server. For example, base_url (str): URL to the Docker server. For example,
``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``. ``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``.
version (str): The version of the API to use. Set to ``auto`` to version (str): The version of the API to use. Set to ``auto`` to
automatically detect the server's version. Default: ``1.30`` automatically detect the server's version. Default: ``1.35``
timeout (int): Default timeout for API calls, in seconds. timeout (int): Default timeout for API calls, in seconds.
tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass
``True`` to enable it with default options, or pass a ``True`` to enable it with default options, or pass a

View File

@ -1,13 +1,15 @@
import six
from datetime import datetime from datetime import datetime
import six
from .. import errors from .. import errors
from .. import utils from .. import utils
from ..constants import DEFAULT_DATA_CHUNK_SIZE from ..constants import DEFAULT_DATA_CHUNK_SIZE
from ..types import ( from ..types import CancellableStream
CancellableStream, ContainerConfig, EndpointConfig, HostConfig, from ..types import ContainerConfig
NetworkingConfig from ..types import EndpointConfig
) from ..types import HostConfig
from ..types import NetworkingConfig
class ContainerApiMixin(object): class ContainerApiMixin(object):
@ -222,7 +224,7 @@ class ContainerApiMixin(object):
mac_address=None, labels=None, stop_signal=None, mac_address=None, labels=None, stop_signal=None,
networking_config=None, healthcheck=None, networking_config=None, healthcheck=None,
stop_timeout=None, runtime=None, stop_timeout=None, runtime=None,
use_config_proxy=False): use_config_proxy=True):
""" """
Creates a container. Parameters are similar to those for the ``docker Creates a container. Parameters are similar to those for the ``docker
run`` command except it doesn't support the attach options (``-a``). run`` command except it doesn't support the attach options (``-a``).
@ -414,7 +416,7 @@ class ContainerApiMixin(object):
if use_config_proxy: if use_config_proxy:
environment = self._proxy_configs.inject_proxy_environment( environment = self._proxy_configs.inject_proxy_environment(
environment environment
) ) or None
config = self.create_container_config( config = self.create_container_config(
image, command, hostname, user, detach, stdin_open, tty, image, command, hostname, user, detach, stdin_open, tty,
@ -487,7 +489,6 @@ class ContainerApiMixin(object):
IDs that the container process will run as. IDs that the container process will run as.
init (bool): Run an init inside the container that forwards init (bool): Run an init inside the container that forwards
signals and reaps processes signals and reaps processes
init_path (str): Path to the docker-init binary
ipc_mode (str): Set the IPC mode for the container. ipc_mode (str): Set the IPC mode for the container.
isolation (str): Isolation technology to use. Default: ``None``. isolation (str): Isolation technology to use. Default: ``None``.
links (dict): Mapping of links using the links (dict): Mapping of links using the
@ -512,7 +513,7 @@ class ContainerApiMixin(object):
network_mode (str): One of: network_mode (str): One of:
- ``bridge`` Create a new network stack for the container on - ``bridge`` Create a new network stack for the container on
on the bridge network. the bridge network.
- ``none`` No networking for this container. - ``none`` No networking for this container.
- ``container:<name|id>`` Reuse another container's network - ``container:<name|id>`` Reuse another container's network
stack. stack.
@ -915,9 +916,10 @@ class ContainerApiMixin(object):
if '/' in private_port: if '/' in private_port:
return port_settings.get(private_port) return port_settings.get(private_port)
h_ports = port_settings.get(private_port + '/tcp') for protocol in ['tcp', 'udp', 'sctp']:
if h_ports is None: h_ports = port_settings.get(private_port + '/' + protocol)
h_ports = port_settings.get(private_port + '/udp') if h_ports:
break
return h_ports return h_ports

View File

@ -137,7 +137,8 @@ class ExecApiMixin(object):
(generator or str or tuple): If ``stream=True``, a generator (generator or str or tuple): If ``stream=True``, a generator
yielding response chunks. If ``socket=True``, a socket object for yielding response chunks. If ``socket=True``, a socket object for
the connection. A string containing response data otherwise. If the connection. A string containing response data otherwise. If
``demux=True``, stdout and stderr are separated. ``demux=True``, a tuple with two elements of type byte: stdout and
stderr.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`

View File

@ -247,12 +247,15 @@ class ImageApiMixin(object):
@utils.minimum_version('1.30') @utils.minimum_version('1.30')
@utils.check_resource('image') @utils.check_resource('image')
def inspect_distribution(self, image): def inspect_distribution(self, image, auth_config=None):
""" """
Get image digest and platform information by contacting the registry. Get image digest and platform information by contacting the registry.
Args: Args:
image (str): The image name to inspect image (str): The image name to inspect
auth_config (dict): Override the credentials that are found in the
config for this request. ``auth_config`` should contain the
``username`` and ``password`` keys to be valid.
Returns: Returns:
(dict): A dict containing distribution data (dict): A dict containing distribution data
@ -261,9 +264,21 @@ class ImageApiMixin(object):
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
If the server returns an error. If the server returns an error.
""" """
registry, _ = auth.resolve_repository_name(image)
headers = {}
if auth_config is None:
header = auth.get_config_header(self, registry)
if header:
headers['X-Registry-Auth'] = header
else:
log.debug('Sending supplied auth config')
headers['X-Registry-Auth'] = auth.encode_header(auth_config)
url = self._url("/distribution/{0}/json", image)
return self._result( return self._result(
self._get(self._url("/distribution/{0}/json", image)), True self._get(url, headers=headers), True
) )
def load_image(self, data, quiet=None): def load_image(self, data, quiet=None):
@ -336,10 +351,9 @@ class ImageApiMixin(object):
tag (str): The tag to pull tag (str): The tag to pull
stream (bool): Stream the output as a generator. Make sure to stream (bool): Stream the output as a generator. Make sure to
consume the generator, otherwise pull might get cancelled. consume the generator, otherwise pull might get cancelled.
auth_config (dict): Override the credentials that auth_config (dict): Override the credentials that are found in the
:py:meth:`~docker.api.daemon.DaemonApiMixin.login` has set for config for this request. ``auth_config`` should contain the
this request. ``auth_config`` should contain the ``username`` ``username`` and ``password`` keys to be valid.
and ``password`` keys to be valid.
decode (bool): Decode the JSON data from the server into dicts. decode (bool): Decode the JSON data from the server into dicts.
Only applies with ``stream=True`` Only applies with ``stream=True``
platform (str): Platform in the format ``os[/arch[/variant]]`` platform (str): Platform in the format ``os[/arch[/variant]]``
@ -414,10 +428,9 @@ class ImageApiMixin(object):
repository (str): The repository to push to repository (str): The repository to push to
tag (str): An optional tag to push tag (str): An optional tag to push
stream (bool): Stream the output as a blocking generator stream (bool): Stream the output as a blocking generator
auth_config (dict): Override the credentials that auth_config (dict): Override the credentials that are found in the
:py:meth:`~docker.api.daemon.DaemonApiMixin.login` has set for config for this request. ``auth_config`` should contain the
this request. ``auth_config`` should contain the ``username`` ``username`` and ``password`` keys to be valid.
and ``password`` keys to be valid.
decode (bool): Decode the JSON data from the server into dicts. decode (bool): Decode the JSON data from the server into dicts.
Only applies with ``stream=True`` Only applies with ``stream=True``

View File

@ -88,6 +88,10 @@ def _check_api_features(version, task_template, update_config, endpoint_spec,
if container_spec.get('Isolation') is not None: if container_spec.get('Isolation') is not None:
raise_version_error('ContainerSpec.isolation', '1.35') raise_version_error('ContainerSpec.isolation', '1.35')
if utils.version_lt(version, '1.38'):
if container_spec.get('Init') is not None:
raise_version_error('ContainerSpec.init', '1.38')
if task_template.get('Resources'): if task_template.get('Resources'):
if utils.version_lt(version, '1.32'): if utils.version_lt(version, '1.32'):
if task_template['Resources'].get('GenericResources'): if task_template['Resources'].get('GenericResources'):
@ -387,7 +391,7 @@ class ServiceApiMixin(object):
current specification of the service. Default: ``False`` current specification of the service. Default: ``False``
Returns: Returns:
``True`` if successful. A dictionary containing a ``Warnings`` key.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -471,5 +475,4 @@ class ServiceApiMixin(object):
resp = self._post_json( resp = self._post_json(
url, data=data, params={'version': version}, headers=headers url, data=data, params={'version': version}, headers=headers
) )
self._raise_for_status(resp) return self._result(resp, json=True)
return True

View File

@ -1,5 +1,6 @@
import logging import logging
from six.moves import http_client from six.moves import http_client
from ..constants import DEFAULT_SWARM_ADDR_POOL, DEFAULT_SWARM_SUBNET_SIZE
from .. import errors from .. import errors
from .. import types from .. import types
from .. import utils from .. import utils
@ -82,7 +83,9 @@ class SwarmApiMixin(object):
@utils.minimum_version('1.24') @utils.minimum_version('1.24')
def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377', def init_swarm(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
force_new_cluster=False, swarm_spec=None): force_new_cluster=False, swarm_spec=None,
default_addr_pool=None, subnet_size=None,
data_path_addr=None):
""" """
Initialize a new Swarm using the current connected engine as the first Initialize a new Swarm using the current connected engine as the first
node. node.
@ -107,9 +110,17 @@ class SwarmApiMixin(object):
swarm_spec (dict): Configuration settings of the new Swarm. Use swarm_spec (dict): Configuration settings of the new Swarm. Use
``APIClient.create_swarm_spec`` to generate a valid ``APIClient.create_swarm_spec`` to generate a valid
configuration. Default: None configuration. Default: None
default_addr_pool (list of strings): Default Address Pool specifies
default subnet pools for global scope networks. Each pool
should be specified as a CIDR block, like '10.0.0.0/8'.
Default: None
subnet_size (int): SubnetSize specifies the subnet size of the
networks created from the default subnet pool. Default: None
data_path_addr (string): Address or interface to use for data path
traffic. For example, 192.168.1.1, or an interface, like eth0.
Returns: Returns:
``True`` if successful. (str): The ID of the created node.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -119,15 +130,44 @@ class SwarmApiMixin(object):
url = self._url('/swarm/init') url = self._url('/swarm/init')
if swarm_spec is not None and not isinstance(swarm_spec, dict): if swarm_spec is not None and not isinstance(swarm_spec, dict):
raise TypeError('swarm_spec must be a dictionary') raise TypeError('swarm_spec must be a dictionary')
if default_addr_pool is not None:
if utils.version_lt(self._version, '1.39'):
raise errors.InvalidVersion(
'Address pool is only available for API version >= 1.39'
)
# subnet_size becomes 0 if not set with default_addr_pool
if subnet_size is None:
subnet_size = DEFAULT_SWARM_SUBNET_SIZE
if subnet_size is not None:
if utils.version_lt(self._version, '1.39'):
raise errors.InvalidVersion(
'Subnet size is only available for API version >= 1.39'
)
# subnet_size is ignored if set without default_addr_pool
if default_addr_pool is None:
default_addr_pool = DEFAULT_SWARM_ADDR_POOL
data = { data = {
'AdvertiseAddr': advertise_addr, 'AdvertiseAddr': advertise_addr,
'ListenAddr': listen_addr, 'ListenAddr': listen_addr,
'DefaultAddrPool': default_addr_pool,
'SubnetSize': subnet_size,
'ForceNewCluster': force_new_cluster, 'ForceNewCluster': force_new_cluster,
'Spec': swarm_spec, 'Spec': swarm_spec,
} }
if data_path_addr is not None:
if utils.version_lt(self._version, '1.30'):
raise errors.InvalidVersion(
'Data address path is only available for '
'API version >= 1.30'
)
data['DataPathAddr'] = data_path_addr
response = self._post_json(url, data=data) response = self._post_json(url, data=data)
self._raise_for_status(response) return self._result(response, json=True)
return True
@utils.minimum_version('1.24') @utils.minimum_version('1.24')
def inspect_swarm(self): def inspect_swarm(self):
@ -165,7 +205,7 @@ class SwarmApiMixin(object):
@utils.minimum_version('1.24') @utils.minimum_version('1.24')
def join_swarm(self, remote_addrs, join_token, listen_addr='0.0.0.0:2377', def join_swarm(self, remote_addrs, join_token, listen_addr='0.0.0.0:2377',
advertise_addr=None): advertise_addr=None, data_path_addr=None):
""" """
Make this Engine join a swarm that has already been created. Make this Engine join a swarm that has already been created.
@ -176,7 +216,7 @@ class SwarmApiMixin(object):
listen_addr (string): Listen address used for inter-manager listen_addr (string): Listen address used for inter-manager
communication if the node gets promoted to manager, as well as communication if the node gets promoted to manager, as well as
determining the networking interface used for the VXLAN Tunnel determining the networking interface used for the VXLAN Tunnel
Endpoint (VTEP). Default: ``None`` Endpoint (VTEP). Default: ``'0.0.0.0:2377``
advertise_addr (string): Externally reachable address advertised advertise_addr (string): Externally reachable address advertised
to other nodes. This can either be an address/port combination to other nodes. This can either be an address/port combination
in the form ``192.168.1.1:4567``, or an interface followed by a in the form ``192.168.1.1:4567``, or an interface followed by a
@ -184,6 +224,8 @@ class SwarmApiMixin(object):
the port number from the listen address is used. If the port number from the listen address is used. If
AdvertiseAddr is not specified, it will be automatically AdvertiseAddr is not specified, it will be automatically
detected when possible. Default: ``None`` detected when possible. Default: ``None``
data_path_addr (string): Address or interface to use for data path
traffic. For example, 192.168.1.1, or an interface, like eth0.
Returns: Returns:
``True`` if the request went through. ``True`` if the request went through.
@ -193,11 +235,20 @@ class SwarmApiMixin(object):
If the server returns an error. If the server returns an error.
""" """
data = { data = {
"RemoteAddrs": remote_addrs, 'RemoteAddrs': remote_addrs,
"ListenAddr": listen_addr, 'ListenAddr': listen_addr,
"JoinToken": join_token, 'JoinToken': join_token,
"AdvertiseAddr": advertise_addr, 'AdvertiseAddr': advertise_addr,
} }
if data_path_addr is not None:
if utils.version_lt(self._version, '1.30'):
raise errors.InvalidVersion(
'Data address path is only available for '
'API version >= 1.30'
)
data['DataPathAddr'] = data_path_addr
url = self._url('/swarm/join') url = self._url('/swarm/join')
response = self._post_json(url, data=data) response = self._post_json(url, data=data)
self._raise_for_status(response) self._raise_for_status(response)
@ -355,8 +406,10 @@ class SwarmApiMixin(object):
return True return True
@utils.minimum_version('1.24') @utils.minimum_version('1.24')
def update_swarm(self, version, swarm_spec=None, rotate_worker_token=False, def update_swarm(self, version, swarm_spec=None,
rotate_manager_token=False): rotate_worker_token=False,
rotate_manager_token=False,
rotate_manager_unlock_key=False):
""" """
Update the Swarm's configuration Update the Swarm's configuration
@ -370,6 +423,8 @@ class SwarmApiMixin(object):
``False``. ``False``.
rotate_manager_token (bool): Rotate the manager join token. rotate_manager_token (bool): Rotate the manager join token.
Default: ``False``. Default: ``False``.
rotate_manager_unlock_key (bool): Rotate the manager unlock key.
Default: ``False``.
Returns: Returns:
``True`` if the request went through. ``True`` if the request went through.
@ -378,12 +433,20 @@ class SwarmApiMixin(object):
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
If the server returns an error. If the server returns an error.
""" """
url = self._url('/swarm/update') url = self._url('/swarm/update')
response = self._post_json(url, data=swarm_spec, params={ params = {
'rotateWorkerToken': rotate_worker_token, 'rotateWorkerToken': rotate_worker_token,
'rotateManagerToken': rotate_manager_token, 'rotateManagerToken': rotate_manager_token,
'version': version 'version': version
}) }
if rotate_manager_unlock_key:
if utils.version_lt(self._version, '1.25'):
raise errors.InvalidVersion(
'Rotate manager unlock key '
'is only available for API version >= 1.25'
)
params['rotateManagerUnlockKey'] = rotate_manager_unlock_key
response = self._post_json(url, data=swarm_spec, params=params)
self._raise_for_status(response) self._raise_for_status(response)
return True return True

View File

@ -2,9 +2,9 @@ import base64
import json import json
import logging import logging
import dockerpycreds
import six import six
from . import credentials
from . import errors from . import errors
from .utils import config from .utils import config
@ -273,17 +273,17 @@ class AuthConfig(dict):
'Password': data['Secret'], 'Password': data['Secret'],
}) })
return res return res
except dockerpycreds.CredentialsNotFound: except credentials.CredentialsNotFound:
log.debug('No entry found') log.debug('No entry found')
return None return None
except dockerpycreds.StoreError as e: except credentials.StoreError as e:
raise errors.DockerException( raise errors.DockerException(
'Credentials store error: {0}'.format(repr(e)) 'Credentials store error: {0}'.format(repr(e))
) )
def _get_store_instance(self, name): def _get_store_instance(self, name):
if name not in self._stores: if name not in self._stores:
self._stores[name] = dockerpycreds.Store( self._stores[name] = credentials.Store(
name, environment=self._credstore_env name, environment=self._credstore_env
) )
return self._stores[name] return self._stores[name]

View File

@ -26,7 +26,7 @@ class DockerClient(object):
base_url (str): URL to the Docker server. For example, base_url (str): URL to the Docker server. For example,
``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``. ``unix:///var/run/docker.sock`` or ``tcp://127.0.0.1:1234``.
version (str): The version of the API to use. Set to ``auto`` to version (str): The version of the API to use. Set to ``auto`` to
automatically detect the server's version. Default: ``1.30`` automatically detect the server's version. Default: ``1.35``
timeout (int): Default timeout for API calls, in seconds. timeout (int): Default timeout for API calls, in seconds.
tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass tls (bool or :py:class:`~docker.tls.TLSConfig`): Enable TLS. Pass
``True`` to enable it with default options, or pass a ``True`` to enable it with default options, or pass a
@ -62,7 +62,7 @@ class DockerClient(object):
Args: Args:
version (str): The version of the API to use. Set to ``auto`` to version (str): The version of the API to use. Set to ``auto`` to
automatically detect the server's version. Default: ``1.30`` automatically detect the server's version. Default: ``1.35``
timeout (int): Default timeout for API calls, in seconds. timeout (int): Default timeout for API calls, in seconds.
ssl_version (int): A valid `SSL version`_. ssl_version (int): A valid `SSL version`_.
assert_hostname (bool): Verify the hostname of the server. assert_hostname (bool): Verify the hostname of the server.

View File

@ -25,3 +25,6 @@ DEFAULT_NUM_POOLS = 25
DEFAULT_NUM_POOLS_SSH = 9 DEFAULT_NUM_POOLS_SSH = 9
DEFAULT_DATA_CHUNK_SIZE = 1024 * 2048 DEFAULT_DATA_CHUNK_SIZE = 1024 * 2048
DEFAULT_SWARM_ADDR_POOL = ['10.0.0.0/8']
DEFAULT_SWARM_SUBNET_SIZE = 24

View File

@ -0,0 +1,4 @@
# flake8: noqa
from .store import Store
from .errors import StoreError, CredentialsNotFound
from .constants import *

View File

@ -0,0 +1,4 @@
PROGRAM_PREFIX = 'docker-credential-'
DEFAULT_LINUX_STORE = 'secretservice'
DEFAULT_OSX_STORE = 'osxkeychain'
DEFAULT_WIN32_STORE = 'wincred'

View File

@ -0,0 +1,25 @@
class StoreError(RuntimeError):
pass
class CredentialsNotFound(StoreError):
pass
class InitializationError(StoreError):
pass
def process_store_error(cpe, program):
message = cpe.output.decode('utf-8')
if 'credentials not found in native keychain' in message:
return CredentialsNotFound(
'No matching credentials in {}'.format(
program
)
)
return StoreError(
'Credentials store {} exited with "{}".'.format(
program, cpe.output.decode('utf-8').strip()
)
)

107
docker/credentials/store.py Normal file
View File

@ -0,0 +1,107 @@
import json
import os
import subprocess
import six
from . import constants
from . import errors
from .utils import create_environment_dict
from .utils import find_executable
class Store(object):
def __init__(self, program, environment=None):
""" Create a store object that acts as an interface to
perform the basic operations for storing, retrieving
and erasing credentials using `program`.
"""
self.program = constants.PROGRAM_PREFIX + program
self.exe = find_executable(self.program)
self.environment = environment
if self.exe is None:
raise errors.InitializationError(
'{} not installed or not available in PATH'.format(
self.program
)
)
def get(self, server):
""" Retrieve credentials for `server`. If no credentials are found,
a `StoreError` will be raised.
"""
if not isinstance(server, six.binary_type):
server = server.encode('utf-8')
data = self._execute('get', server)
result = json.loads(data.decode('utf-8'))
# docker-credential-pass will return an object for inexistent servers
# whereas other helpers will exit with returncode != 0. For
# consistency, if no significant data is returned,
# raise CredentialsNotFound
if result['Username'] == '' and result['Secret'] == '':
raise errors.CredentialsNotFound(
'No matching credentials in {}'.format(self.program)
)
return result
def store(self, server, username, secret):
""" Store credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
data_input = json.dumps({
'ServerURL': server,
'Username': username,
'Secret': secret
}).encode('utf-8')
return self._execute('store', data_input)
def erase(self, server):
""" Erase credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
if not isinstance(server, six.binary_type):
server = server.encode('utf-8')
self._execute('erase', server)
def list(self):
""" List stored credentials. Requires v0.4.0+ of the helper.
"""
data = self._execute('list', None)
return json.loads(data.decode('utf-8'))
def _execute(self, subcmd, data_input):
output = None
env = create_environment_dict(self.environment)
try:
if six.PY3:
output = subprocess.check_output(
[self.exe, subcmd], input=data_input, env=env,
)
else:
process = subprocess.Popen(
[self.exe, subcmd], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, env=env,
)
output, err = process.communicate(data_input)
if process.returncode != 0:
raise subprocess.CalledProcessError(
returncode=process.returncode, cmd='', output=output
)
except subprocess.CalledProcessError as e:
raise errors.process_store_error(e, self.program)
except OSError as e:
if e.errno == os.errno.ENOENT:
raise errors.StoreError(
'{} not installed or not available in PATH'.format(
self.program
)
)
else:
raise errors.StoreError(
'Unexpected OS error "{}", errno={}'.format(
e.strerror, e.errno
)
)
return output

View File

@ -0,0 +1,38 @@
import distutils.spawn
import os
import sys
def find_executable(executable, path=None):
"""
As distutils.spawn.find_executable, but on Windows, look up
every extension declared in PATHEXT instead of just `.exe`
"""
if sys.platform != 'win32':
return distutils.spawn.find_executable(executable, path)
if path is None:
path = os.environ['PATH']
paths = path.split(os.pathsep)
extensions = os.environ.get('PATHEXT', '.exe').split(os.pathsep)
base, ext = os.path.splitext(executable)
if not os.path.isfile(executable):
for p in paths:
for ext in extensions:
f = os.path.join(p, base + ext)
if os.path.isfile(f):
return f
return None
else:
return executable
def create_environment_dict(overrides):
"""
Create and return a copy of os.environ with the specified overrides
"""
result = os.environ.copy()
result.update(overrides or {})
return result

View File

@ -62,6 +62,13 @@ class Container(Model):
return self.attrs['State']['Status'] return self.attrs['State']['Status']
return self.attrs['State'] return self.attrs['State']
@property
def ports(self):
"""
The ports that the container exposes as a dictionary.
"""
return self.attrs.get('NetworkSettings', {}).get('Ports', {})
def attach(self, **kwargs): def attach(self, **kwargs):
""" """
Attach to this container. Attach to this container.
@ -172,10 +179,11 @@ class Container(Model):
(ExecResult): A tuple of (exit_code, output) (ExecResult): A tuple of (exit_code, output)
exit_code: (int): exit_code: (int):
Exit code for the executed command or ``None`` if Exit code for the executed command or ``None`` if
either ``stream```or ``socket`` is ``True``. either ``stream`` or ``socket`` is ``True``.
output: (generator or bytes): output: (generator, bytes, or tuple):
If ``stream=True``, a generator yielding response chunks. If ``stream=True``, a generator yielding response chunks.
If ``socket=True``, a socket object for the connection. If ``socket=True``, a socket object for the connection.
If ``demux=True``, a tuple of two bytes: stdout and stderr.
A bytestring containing response data otherwise. A bytestring containing response data otherwise.
Raises: Raises:
@ -540,12 +548,15 @@ class ContainerCollection(Collection):
cap_add (list of str): Add kernel capabilities. For example, cap_add (list of str): Add kernel capabilities. For example,
``["SYS_ADMIN", "MKNOD"]``. ``["SYS_ADMIN", "MKNOD"]``.
cap_drop (list of str): Drop kernel capabilities. cap_drop (list of str): Drop kernel capabilities.
cgroup_parent (str): Override the default parent cgroup.
cpu_count (int): Number of usable CPUs (Windows only). cpu_count (int): Number of usable CPUs (Windows only).
cpu_percent (int): Usable percentage of the available CPUs cpu_percent (int): Usable percentage of the available CPUs
(Windows only). (Windows only).
cpu_period (int): The length of a CPU period in microseconds. cpu_period (int): The length of a CPU period in microseconds.
cpu_quota (int): Microseconds of CPU time that the container can cpu_quota (int): Microseconds of CPU time that the container can
get in a CPU period. get in a CPU period.
cpu_rt_period (int): Limit CPU real-time period in microseconds.
cpu_rt_runtime (int): Limit CPU real-time runtime in microseconds.
cpu_shares (int): CPU shares (relative weight). cpu_shares (int): CPU shares (relative weight).
cpuset_cpus (str): CPUs in which to allow execution (``0-3``, cpuset_cpus (str): CPUs in which to allow execution (``0-3``,
``0,1``). ``0,1``).
@ -589,6 +600,7 @@ class ContainerCollection(Collection):
init_path (str): Path to the docker-init binary init_path (str): Path to the docker-init binary
ipc_mode (str): Set the IPC mode for the container. ipc_mode (str): Set the IPC mode for the container.
isolation (str): Isolation technology to use. Default: `None`. isolation (str): Isolation technology to use. Default: `None`.
kernel_memory (int or str): Kernel memory limit
labels (dict or list): A dictionary of name-value labels (e.g. labels (dict or list): A dictionary of name-value labels (e.g.
``{"label1": "value1", "label2": "value2"}``) or a list of ``{"label1": "value1", "label2": "value2"}``) or a list of
names of labels to set with empty values (e.g. names of labels to set with empty values (e.g.
@ -598,6 +610,7 @@ class ContainerCollection(Collection):
Containers declared in this dict will be linked to the new Containers declared in this dict will be linked to the new
container using the provided alias. Default: ``None``. container using the provided alias. Default: ``None``.
log_config (LogConfig): Logging configuration. log_config (LogConfig): Logging configuration.
lxc_conf (dict): LXC config.
mac_address (str): MAC address to assign to the container. mac_address (str): MAC address to assign to the container.
mem_limit (int or str): Memory limit. Accepts float values mem_limit (int or str): Memory limit. Accepts float values
(which represent the memory limit of the created container in (which represent the memory limit of the created container in
@ -605,6 +618,7 @@ class ContainerCollection(Collection):
(``100000b``, ``1000k``, ``128m``, ``1g``). If a string is (``100000b``, ``1000k``, ``128m``, ``1g``). If a string is
specified without a units character, bytes are assumed as an specified without a units character, bytes are assumed as an
intended unit. intended unit.
mem_reservation (int or str): Memory soft limit
mem_swappiness (int): Tune a container's memory swappiness mem_swappiness (int): Tune a container's memory swappiness
behavior. Accepts number between 0 and 100. behavior. Accepts number between 0 and 100.
memswap_limit (str or int): Maximum amount of memory + swap a memswap_limit (str or int): Maximum amount of memory + swap a
@ -643,8 +657,8 @@ class ContainerCollection(Collection):
The keys of the dictionary are the ports to bind inside the The keys of the dictionary are the ports to bind inside the
container, either as an integer or a string in the form container, either as an integer or a string in the form
``port/protocol``, where the protocol is either ``tcp`` or ``port/protocol``, where the protocol is either ``tcp``,
``udp``. ``udp``, or ``sctp``.
The values of the dictionary are the corresponding ports to The values of the dictionary are the corresponding ports to
open on the host, which can be either: open on the host, which can be either:
@ -718,6 +732,10 @@ class ContainerCollection(Collection):
userns_mode (str): Sets the user namespace mode for the container userns_mode (str): Sets the user namespace mode for the container
when user namespace remapping option is enabled. Supported when user namespace remapping option is enabled. Supported
values are: ``host`` values are: ``host``
uts_mode (str): Sets the UTS namespace mode for the container.
Supported values are: ``host``
version (str): The version of the API to use. Set to ``auto`` to
automatically detect the server's version. Default: ``1.35``
volume_driver (str): The name of a volume driver/plugin. volume_driver (str): The name of a volume driver/plugin.
volumes (dict or list): A dictionary to configure volumes mounted volumes (dict or list): A dictionary to configure volumes mounted
inside the container. The key is either the host path or a inside the container. The key is either the host path or a
@ -953,7 +971,6 @@ RUN_CREATE_KWARGS = [
'tty', 'tty',
'use_config_proxy', 'use_config_proxy',
'user', 'user',
'volume_driver',
'working_dir', 'working_dir',
] ]
@ -1017,6 +1034,7 @@ RUN_HOST_CONFIG_KWARGS = [
'userns_mode', 'userns_mode',
'uts_mode', 'uts_mode',
'version', 'version',
'volume_driver',
'volumes_from', 'volumes_from',
'runtime' 'runtime'
] ]

View File

@ -315,22 +315,26 @@ class ImageCollection(Collection):
""" """
return self.prepare_model(self.client.api.inspect_image(name)) return self.prepare_model(self.client.api.inspect_image(name))
def get_registry_data(self, name): def get_registry_data(self, name, auth_config=None):
""" """
Gets the registry data for an image. Gets the registry data for an image.
Args: Args:
name (str): The name of the image. name (str): The name of the image.
auth_config (dict): Override the credentials that are found in the
config for this request. ``auth_config`` should contain the
``username`` and ``password`` keys to be valid.
Returns: Returns:
(:py:class:`RegistryData`): The data object. (:py:class:`RegistryData`): The data object.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
If the server returns an error. If the server returns an error.
""" """
return RegistryData( return RegistryData(
image_name=name, image_name=name,
attrs=self.client.api.inspect_distribution(name), attrs=self.client.api.inspect_distribution(name, auth_config),
client=self.client, client=self.client,
collection=self, collection=self,
) )
@ -404,10 +408,9 @@ class ImageCollection(Collection):
Args: Args:
repository (str): The repository to pull repository (str): The repository to pull
tag (str): The tag to pull tag (str): The tag to pull
auth_config (dict): Override the credentials that auth_config (dict): Override the credentials that are found in the
:py:meth:`~docker.client.DockerClient.login` has set for config for this request. ``auth_config`` should contain the
this request. ``auth_config`` should contain the ``username`` ``username`` and ``password`` keys to be valid.
and ``password`` keys to be valid.
platform (str): Platform in the format ``os[/arch[/variant]]`` platform (str): Platform in the format ``os[/arch[/variant]]``
Returns: Returns:

View File

@ -165,6 +165,8 @@ class ServiceCollection(Collection):
env (list of str): Environment variables, in the form env (list of str): Environment variables, in the form
``KEY=val``. ``KEY=val``.
hostname (string): Hostname to set on the container. hostname (string): Hostname to set on the container.
init (boolean): Run an init inside the container that forwards
signals and reaps processes
isolation (string): Isolation technology used by the service's isolation (string): Isolation technology used by the service's
containers. Only used for Windows containers. containers. Only used for Windows containers.
labels (dict): Labels to apply to the service. labels (dict): Labels to apply to the service.
@ -280,6 +282,7 @@ CONTAINER_SPEC_KWARGS = [
'hostname', 'hostname',
'hosts', 'hosts',
'image', 'image',
'init',
'isolation', 'isolation',
'labels', 'labels',
'mounts', 'mounts',

View File

@ -34,7 +34,8 @@ class Swarm(Model):
get_unlock_key.__doc__ = APIClient.get_unlock_key.__doc__ get_unlock_key.__doc__ = APIClient.get_unlock_key.__doc__
def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377', def init(self, advertise_addr=None, listen_addr='0.0.0.0:2377',
force_new_cluster=False, **kwargs): force_new_cluster=False, default_addr_pool=None,
subnet_size=None, data_path_addr=None, **kwargs):
""" """
Initialize a new swarm on this Engine. Initialize a new swarm on this Engine.
@ -56,6 +57,14 @@ class Swarm(Model):
is used. Default: ``0.0.0.0:2377`` is used. Default: ``0.0.0.0:2377``
force_new_cluster (bool): Force creating a new Swarm, even if force_new_cluster (bool): Force creating a new Swarm, even if
already part of one. Default: False already part of one. Default: False
default_addr_pool (list of str): Default Address Pool specifies
default subnet pools for global scope networks. Each pool
should be specified as a CIDR block, like '10.0.0.0/8'.
Default: None
subnet_size (int): SubnetSize specifies the subnet size of the
networks created from the default subnet pool. Default: None
data_path_addr (string): Address or interface to use for data path
traffic. For example, 192.168.1.1, or an interface, like eth0.
task_history_retention_limit (int): Maximum number of tasks task_history_retention_limit (int): Maximum number of tasks
history stored. history stored.
snapshot_interval (int): Number of logs entries between snapshot. snapshot_interval (int): Number of logs entries between snapshot.
@ -89,7 +98,7 @@ class Swarm(Model):
created in the orchestrator. created in the orchestrator.
Returns: Returns:
``True`` if the request went through. (str): The ID of the created node.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
@ -99,7 +108,8 @@ class Swarm(Model):
>>> client.swarm.init( >>> client.swarm.init(
advertise_addr='eth0', listen_addr='0.0.0.0:5000', advertise_addr='eth0', listen_addr='0.0.0.0:5000',
force_new_cluster=False, snapshot_interval=5000, force_new_cluster=False, default_addr_pool=['10.20.0.0/16],
subnet_size=24, snapshot_interval=5000,
log_entries_for_slow_followers=1200 log_entries_for_slow_followers=1200
) )
@ -107,12 +117,15 @@ class Swarm(Model):
init_kwargs = { init_kwargs = {
'advertise_addr': advertise_addr, 'advertise_addr': advertise_addr,
'listen_addr': listen_addr, 'listen_addr': listen_addr,
'force_new_cluster': force_new_cluster 'force_new_cluster': force_new_cluster,
'default_addr_pool': default_addr_pool,
'subnet_size': subnet_size,
'data_path_addr': data_path_addr,
} }
init_kwargs['swarm_spec'] = self.client.api.create_swarm_spec(**kwargs) init_kwargs['swarm_spec'] = self.client.api.create_swarm_spec(**kwargs)
self.client.api.init_swarm(**init_kwargs) node_id = self.client.api.init_swarm(**init_kwargs)
self.reload() self.reload()
return True return node_id
def join(self, *args, **kwargs): def join(self, *args, **kwargs):
return self.client.api.join_swarm(*args, **kwargs) return self.client.api.join_swarm(*args, **kwargs)
@ -138,7 +151,7 @@ class Swarm(Model):
unlock.__doc__ = APIClient.unlock_swarm.__doc__ unlock.__doc__ = APIClient.unlock_swarm.__doc__
def update(self, rotate_worker_token=False, rotate_manager_token=False, def update(self, rotate_worker_token=False, rotate_manager_token=False,
**kwargs): rotate_manager_unlock_key=False, **kwargs):
""" """
Update the swarm's configuration. Update the swarm's configuration.
@ -151,7 +164,8 @@ class Swarm(Model):
``False``. ``False``.
rotate_manager_token (bool): Rotate the manager join token. rotate_manager_token (bool): Rotate the manager join token.
Default: ``False``. Default: ``False``.
rotate_manager_unlock_key (bool): Rotate the manager unlock key.
Default: ``False``.
Raises: Raises:
:py:class:`docker.errors.APIError` :py:class:`docker.errors.APIError`
If the server returns an error. If the server returns an error.
@ -165,5 +179,6 @@ class Swarm(Model):
version=self.version, version=self.version,
swarm_spec=self.client.api.create_swarm_spec(**kwargs), swarm_spec=self.client.api.create_swarm_spec(**kwargs),
rotate_worker_token=rotate_worker_token, rotate_worker_token=rotate_worker_token,
rotate_manager_token=rotate_manager_token rotate_manager_token=rotate_manager_token,
rotate_manager_unlock_key=rotate_manager_unlock_key
) )

View File

@ -110,13 +110,15 @@ class ContainerSpec(dict):
privileges (Privileges): Security options for the service's containers. privileges (Privileges): Security options for the service's containers.
isolation (string): Isolation technology used by the service's isolation (string): Isolation technology used by the service's
containers. Only used for Windows containers. containers. Only used for Windows containers.
init (boolean): Run an init inside the container that forwards signals
and reaps processes.
""" """
def __init__(self, image, command=None, args=None, hostname=None, env=None, def __init__(self, image, command=None, args=None, hostname=None, env=None,
workdir=None, user=None, labels=None, mounts=None, workdir=None, user=None, labels=None, mounts=None,
stop_grace_period=None, secrets=None, tty=None, groups=None, stop_grace_period=None, secrets=None, tty=None, groups=None,
open_stdin=None, read_only=None, stop_signal=None, open_stdin=None, read_only=None, stop_signal=None,
healthcheck=None, hosts=None, dns_config=None, configs=None, healthcheck=None, hosts=None, dns_config=None, configs=None,
privileges=None, isolation=None): privileges=None, isolation=None, init=None):
self['Image'] = image self['Image'] = image
if isinstance(command, six.string_types): if isinstance(command, six.string_types):
@ -183,6 +185,9 @@ class ContainerSpec(dict):
if isolation is not None: if isolation is not None:
self['Isolation'] = isolation self['Isolation'] = isolation
if init is not None:
self['Init'] = init
class Mount(dict): class Mount(dict):
""" """
@ -692,7 +697,7 @@ class PlacementPreference(dict):
'PlacementPreference strategy value is invalid ({}):' 'PlacementPreference strategy value is invalid ({}):'
' must be "spread".'.format(strategy) ' must be "spread".'.format(strategy)
) )
self['SpreadOver'] = descriptor self['Spread'] = {'SpreadDescriptor': descriptor}
class DNSConfig(dict): class DNSConfig(dict):

View File

@ -7,7 +7,7 @@ PORT_SPEC = re.compile(
r"(?P<ext>[\d]*)(-(?P<ext_end>[\d]+))?:" # External range r"(?P<ext>[\d]*)(-(?P<ext_end>[\d]+))?:" # External range
")?" ")?"
r"(?P<int>[\d]+)(-(?P<int_end>[\d]+))?" # Internal range r"(?P<int>[\d]+)(-(?P<int_end>[\d]+))?" # Internal range
"(?P<proto>/(udp|tcp))?" # Protocol "(?P<proto>/(udp|tcp|sctp))?" # Protocol
"$" # Match full string "$" # Match full string
) )

View File

@ -1,2 +1,2 @@
version = "3.7.2" version = "4.0.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(".")])

View File

@ -1,6 +1,43 @@
Change log Change log
========== ==========
4.0.0
-----
[List of PRs / issues for this release](https://github.com/docker/docker-py/milestone/57?closed=1)
### Breaking changes
- Support for Python 3.3 and Python 3.4 has been dropped
- `APIClient.update_service`, `APIClient.init_swarm`, and
`DockerClient.swarm.init` now return a `dict` from the API's response body
- In `APIClient.build` and `DockerClient.images.build`, the `use_config_proxy`
parameter now defaults to True
- `init_path` is no longer a valid parameter for `HostConfig`
### Features
- It is now possible to provide `SCTP` ports for port mappings
- `ContainerSpec`s now support the `init` parameter
- `DockerClient.swarm.init` and `APIClient.init_swarm` now support the
`data_path_addr` parameter
- `APIClient.update_swarm` and `DockerClient.swarm.update` now support the
`rotate_manager_unlock_key` parameter
- `APIClient.update_service` returns the API's response body as a `dict`
- `APIClient.init_swarm`, and `DockerClient.swarm.init` now return the API's
response body as a `dict`
### Bugfixes
- Fixed `PlacementPreference` instances to produce a valid API type
- Fixed a bug where not setting a value for `buildargs` in `build` could cause
the library to attempt accessing attributes of a `None` value
- Fixed a bug where setting the `volume_driver` parameter in
`DockerClient.containers.create` would result in an error
- `APIClient.inspect_distribution` now correctly sets the authentication
headers on the request, allowing it to be used with private repositories
This change also applies to `DockerClient.get_registry_data`
3.7.2 3.7.2
----- -----

View File

@ -2,9 +2,7 @@ appdirs==1.4.3
asn1crypto==0.22.0 asn1crypto==0.22.0
backports.ssl-match-hostname==3.5.0.1 backports.ssl-match-hostname==3.5.0.1
cffi==1.10.0 cffi==1.10.0
cryptography==1.9; python_version == '3.3' cryptography==2.3
cryptography==2.3; python_version > '3.3'
docker-pycreds==0.4.0
enum34==1.1.6 enum34==1.1.6
idna==2.5 idna==2.5
ipaddress==1.0.18 ipaddress==1.0.18
@ -17,5 +15,5 @@ pypiwin32==219; sys_platform == 'win32' and python_version < '3.6'
pypiwin32==223; sys_platform == 'win32' and python_version >= '3.6' 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
websocket-client==0.40.0 websocket-client==0.40.0
urllib3==1.21.1; python_version == '3.3'

7
scripts/versions.py Normal file → Executable file
View File

@ -26,8 +26,8 @@ class Version(namedtuple('_Version', 'major minor patch stage edition')):
edition = stage edition = stage
stage = None stage = None
elif '-' in stage: elif '-' in stage:
edition, stage = stage.split('-') edition, stage = stage.split('-', 1)
major, minor, patch = version.split('.', 3) major, minor, patch = version.split('.', 2)
return cls(major, minor, patch, stage, edition) return cls(major, minor, patch, stage, edition)
@property @property
@ -63,7 +63,7 @@ def main():
res = requests.get(url) res = requests.get(url)
content = res.text content = res.text
versions = [Version.parse(v) for v in re.findall( versions = [Version.parse(v) for v in re.findall(
r'"docker-([0-9]+\.[0-9]+\.[0-9]+)-?.*tgz"', content r'"docker-([0-9]+\.[0-9]+\.[0-9]+-?.*)\.tgz"', content
)] )]
sorted_versions = sorted( sorted_versions = sorted(
versions, reverse=True, key=operator.attrgetter('order') versions, reverse=True, key=operator.attrgetter('order')
@ -72,5 +72,6 @@ def main():
results.add(str(latest)) results.add(str(latest))
print(' '.join(results)) print(' '.join(results))
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View File

@ -4,7 +4,8 @@ from __future__ import print_function
import codecs import codecs
import os import os
from setuptools import setup, find_packages from setuptools import find_packages
from setuptools import setup
ROOT_DIR = os.path.dirname(__file__) ROOT_DIR = os.path.dirname(__file__)
SOURCE_DIR = os.path.join(ROOT_DIR) SOURCE_DIR = os.path.join(ROOT_DIR)
@ -12,7 +13,6 @@ SOURCE_DIR = os.path.join(ROOT_DIR)
requirements = [ requirements = [
'six >= 1.4.0', 'six >= 1.4.0',
'websocket-client >= 0.32.0', 'websocket-client >= 0.32.0',
'docker-pycreds >= 0.4.0',
'requests >= 2.14.2, != 2.18.0', 'requests >= 2.14.2, != 2.18.0',
] ]
@ -29,9 +29,6 @@ extras_require = {
':sys_platform == "win32" and python_version < "3.6"': 'pypiwin32==219', ':sys_platform == "win32" and python_version < "3.6"': 'pypiwin32==219',
':sys_platform == "win32" and python_version >= "3.6"': 'pypiwin32==223', ':sys_platform == "win32" and python_version >= "3.6"': 'pypiwin32==223',
# urllib3 drops support for Python 3.3 in 1.23
':python_version == "3.3"': 'urllib3 < 1.23',
# 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.
@ -75,7 +72,7 @@ setup(
install_requires=requirements, install_requires=requirements,
tests_require=test_requirements, tests_require=test_requirements,
extras_require=extras_require, extras_require=extras_require,
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*', python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*',
zip_safe=False, zip_safe=False,
test_suite='tests', test_suite='tests',
classifiers=[ classifiers=[
@ -87,8 +84,6 @@ setup(
'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',

View File

@ -1,9 +1,6 @@
coverage==4.5.2 coverage==4.5.2
flake8==3.6.0; python_version != '3.3' flake8==3.6.0
flake8==3.4.1; python_version == '3.3'
mock==1.0.1 mock==1.0.1
pytest==2.9.1; python_version == '3.3' pytest==4.1.0
pytest==4.1.0; python_version != '3.3' pytest-cov==2.6.1
pytest-cov==2.6.1; python_version != '3.3'
pytest-cov==2.5.1; python_version == '3.3'
pytest-timeout==1.3.3 pytest-timeout==1.3.3

28
tests/Dockerfile Normal file
View File

@ -0,0 +1,28 @@
ARG PYTHON_VERSION=3.6
FROM python:$PYTHON_VERSION-jessie
RUN apt-get update && apt-get -y install \
gnupg2 \
pass \
curl
COPY ./tests/gpg-keys /gpg-keys
RUN gpg2 --import gpg-keys/secret
RUN gpg2 --import-ownertrust gpg-keys/ownertrust
RUN yes | pass init $(gpg2 --no-auto-check-trustdb --list-secret-keys | grep ^sec | cut -d/ -f2 | cut -d" " -f1)
RUN gpg2 --check-trustdb
ARG CREDSTORE_VERSION=v0.6.0
RUN curl -sSL -o /opt/docker-credential-pass.tar.gz \
https://github.com/docker/docker-credential-helpers/releases/download/$CREDSTORE_VERSION/docker-credential-pass-$CREDSTORE_VERSION-amd64.tar.gz && \
tar -xf /opt/docker-credential-pass.tar.gz -O > /usr/local/bin/docker-credential-pass && \
rm -rf /opt/docker-credential-pass.tar.gz && \
chmod +x /usr/local/bin/docker-credential-pass
WORKDIR /src
COPY requirements.txt /src/requirements.txt
RUN pip install -r requirements.txt
COPY test-requirements.txt /src/test-requirements.txt
RUN pip install -r test-requirements.txt
COPY . /src
RUN pip install .

View File

@ -0,0 +1,3 @@
# List of assigned trustvalues, created Wed 25 Apr 2018 01:28:17 PM PDT
# (Use "gpg --import-ownertrust" to restore them)
9781B87DAB042E6FD51388A5464ED987A7B21401:6:

BIN
tests/gpg-keys/secret Normal file

Binary file not shown.

View File

@ -2,16 +2,16 @@ import functools
import os import os
import os.path import os.path
import random import random
import re
import socket
import tarfile import tarfile
import tempfile import tempfile
import time import time
import re
import six
import socket
import docker import docker
import paramiko import paramiko
import pytest import pytest
import six
def make_tree(dirs, files): def make_tree(dirs, files):
@ -119,13 +119,18 @@ def assert_cat_socket_detached_with_keys(sock, inputs):
# If we're using a Unix socket, the sock.send call will fail with a # If we're using a Unix socket, the sock.send call will fail with a
# BrokenPipeError ; INET sockets will just stop receiving / sending data # BrokenPipeError ; INET sockets will just stop receiving / sending data
# but will not raise an error # but will not raise an error
if getattr(sock, 'family', -9) == getattr(socket, 'AF_UNIX', -1): if isinstance(sock, paramiko.Channel):
with pytest.raises(socket.error):
sock.sendall(b'make sure the socket is closed\n')
elif isinstance(sock, paramiko.Channel):
with pytest.raises(OSError): with pytest.raises(OSError):
sock.sendall(b'make sure the socket is closed\n') sock.sendall(b'make sure the socket is closed\n')
else: else:
if getattr(sock, 'family', -9) == getattr(socket, 'AF_UNIX', -1):
# We do not want to use pytest.raises here because future versions
# of the daemon no longer cause this to raise an error.
try:
sock.sendall(b'make sure the socket is closed\n')
except socket.error:
return
sock.sendall(b"make sure the socket is closed\n") sock.sendall(b"make sure the socket is closed\n")
data = sock.recv(128) data = sock.recv(128)
# New in 18.06: error message is broadcast over the socket when reading # New in 18.06: error message is broadcast over the socket when reading

View File

@ -5,21 +5,20 @@ import tempfile
import threading import threading
from datetime import datetime from datetime import datetime
import docker
from docker.constants import IS_WINDOWS_PLATFORM
from docker.utils.socket import next_frame_header
from docker.utils.socket import read_exactly
import pytest import pytest
import requests import requests
import six import six
from .base import BUSYBOX, BaseAPIIntegrationTest import docker
from .. import helpers from .. import helpers
from ..helpers import ( from ..helpers import assert_cat_socket_detached_with_keys
requires_api_version, ctrl_with, assert_cat_socket_detached_with_keys from ..helpers import ctrl_with
) from ..helpers import requires_api_version
from .base import BaseAPIIntegrationTest
from .base import BUSYBOX
from docker.constants import IS_WINDOWS_PLATFORM
from docker.utils.socket import next_frame_header
from docker.utils.socket import read_exactly
class ListContainersTest(BaseAPIIntegrationTest): class ListContainersTest(BaseAPIIntegrationTest):
@ -38,7 +37,7 @@ class ListContainersTest(BaseAPIIntegrationTest):
assert 'Command' in retrieved assert 'Command' in retrieved
assert retrieved['Command'] == six.text_type('true') assert retrieved['Command'] == six.text_type('true')
assert 'Image' in retrieved assert 'Image' in retrieved
assert re.search(r'busybox:.*', retrieved['Image']) assert re.search(r'alpine:.*', retrieved['Image'])
assert 'Status' in retrieved assert 'Status' in retrieved
@ -368,10 +367,9 @@ class CreateContainerTest(BaseAPIIntegrationTest):
) )
self.tmp_containers.append(container['Id']) self.tmp_containers.append(container['Id'])
config = self.client.inspect_container(container['Id']) config = self.client.inspect_container(container['Id'])
assert ( assert 'Foo' in config['Config']['Env']
sorted(config['Config']['Env']) == assert 'Other=one' in config['Config']['Env']
sorted(['Foo', 'Other=one', 'Blank=']) assert 'Blank=' in config['Config']['Env']
)
@requires_api_version('1.22') @requires_api_version('1.22')
def test_create_with_tmpfs(self): def test_create_with_tmpfs(self):
@ -448,19 +446,6 @@ class CreateContainerTest(BaseAPIIntegrationTest):
config = self.client.inspect_container(ctnr) config = self.client.inspect_container(ctnr)
assert config['HostConfig']['Init'] is True assert config['HostConfig']['Init'] is True
@pytest.mark.xfail(True, reason='init-path removed in 17.05.0')
@requires_api_version('1.25')
def test_create_with_init_path(self):
ctnr = self.client.create_container(
BUSYBOX, 'true',
host_config=self.client.create_host_config(
init_path="/usr/libexec/docker-init"
)
)
self.tmp_containers.append(ctnr['Id'])
config = self.client.inspect_container(ctnr)
assert config['HostConfig']['InitPath'] == "/usr/libexec/docker-init"
@requires_api_version('1.24') @requires_api_version('1.24')
@pytest.mark.xfail(not os.path.exists('/sys/fs/cgroup/cpu.rt_runtime_us'), @pytest.mark.xfail(not os.path.exists('/sys/fs/cgroup/cpu.rt_runtime_us'),
reason='CONFIG_RT_GROUP_SCHED isn\'t enabled') reason='CONFIG_RT_GROUP_SCHED isn\'t enabled')
@ -1082,11 +1067,17 @@ class PortTest(BaseAPIIntegrationTest):
def test_port(self): def test_port(self):
port_bindings = { port_bindings = {
'1111': ('127.0.0.1', '4567'), '1111': ('127.0.0.1', '4567'),
'2222': ('127.0.0.1', '4568') '2222': ('127.0.0.1', '4568'),
'3333/udp': ('127.0.0.1', '4569'),
} }
ports = [
1111,
2222,
(3333, 'udp'),
]
container = self.client.create_container( container = self.client.create_container(
BUSYBOX, ['sleep', '60'], ports=list(port_bindings.keys()), BUSYBOX, ['sleep', '60'], ports=ports,
host_config=self.client.create_host_config( host_config=self.client.create_host_config(
port_bindings=port_bindings, network_mode='bridge' port_bindings=port_bindings, network_mode='bridge'
) )
@ -1097,13 +1088,15 @@ class PortTest(BaseAPIIntegrationTest):
# Call the port function on each biding and compare expected vs actual # Call the port function on each biding and compare expected vs actual
for port in port_bindings: for port in port_bindings:
port, _, protocol = port.partition('/')
actual_bindings = self.client.port(container, port) actual_bindings = self.client.port(container, port)
port_binding = actual_bindings.pop() port_binding = actual_bindings.pop()
ip, host_port = port_binding['HostIp'], port_binding['HostPort'] ip, host_port = port_binding['HostIp'], port_binding['HostPort']
assert ip == port_bindings[port][0] port_binding = port if not protocol else port + "/" + protocol
assert host_port == port_bindings[port][1] assert ip == port_bindings[port_binding][0]
assert host_port == port_bindings[port_binding][1]
self.client.kill(id) self.client.kill(id)
@ -1168,10 +1161,10 @@ class RestartContainerTest(BaseAPIIntegrationTest):
def test_restart_with_low_timeout(self): def test_restart_with_low_timeout(self):
container = self.client.create_container(BUSYBOX, ['sleep', '9999']) container = self.client.create_container(BUSYBOX, ['sleep', '9999'])
self.client.start(container) self.client.start(container)
self.client.timeout = 1 self.client.timeout = 3
self.client.restart(container, timeout=3) self.client.restart(container, timeout=1)
self.client.timeout = None self.client.timeout = None
self.client.restart(container, timeout=3) self.client.restart(container, timeout=1)
self.client.kill(container) self.client.kill(container)
def test_restart_with_dict_instead_of_id(self): def test_restart_with_dict_instead_of_id(self):
@ -1256,7 +1249,7 @@ class AttachContainerTest(BaseAPIIntegrationTest):
output = self.client.attach(container, stream=False, logs=True) output = self.client.attach(container, stream=False, logs=True)
assert output == 'hello\n'.encode(encoding='ascii') assert output == 'hello\n'.encode(encoding='ascii')
@pytest.mark.timeout(5) @pytest.mark.timeout(10)
@pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'), @pytest.mark.skipif(os.environ.get('DOCKER_HOST', '').startswith('ssh://'),
reason='No cancellable streams over SSH') reason='No cancellable streams over SSH')
@pytest.mark.xfail(condition=os.environ.get('DOCKER_TLS_VERIFY') or @pytest.mark.xfail(condition=os.environ.get('DOCKER_TLS_VERIFY') or
@ -1264,14 +1257,14 @@ class AttachContainerTest(BaseAPIIntegrationTest):
reason='Flaky test on TLS') reason='Flaky test on TLS')
def test_attach_stream_and_cancel(self): def test_attach_stream_and_cancel(self):
container = self.client.create_container( container = self.client.create_container(
BUSYBOX, 'sh -c "echo hello && sleep 60"', BUSYBOX, 'sh -c "sleep 2 && echo hello && sleep 60"',
tty=True tty=True
) )
self.tmp_containers.append(container) self.tmp_containers.append(container)
self.client.start(container) self.client.start(container)
output = self.client.attach(container, stream=True, logs=True) output = self.client.attach(container, stream=True, logs=True)
threading.Timer(1, output.close).start() threading.Timer(3, output.close).start()
lines = [] lines = []
for line in output: for line in output:

View File

@ -1,11 +1,11 @@
from ..helpers import assert_cat_socket_detached_with_keys
from ..helpers import ctrl_with
from ..helpers import requires_api_version
from .base import BaseAPIIntegrationTest
from .base import BUSYBOX
from docker.utils.proxy import ProxyConfig
from docker.utils.socket import next_frame_header from docker.utils.socket import next_frame_header
from docker.utils.socket import read_exactly from docker.utils.socket import read_exactly
from docker.utils.proxy import ProxyConfig
from .base import BaseAPIIntegrationTest, BUSYBOX
from ..helpers import (
requires_api_version, ctrl_with, assert_cat_socket_detached_with_keys
)
class ExecTest(BaseAPIIntegrationTest): class ExecTest(BaseAPIIntegrationTest):
@ -17,7 +17,6 @@ class ExecTest(BaseAPIIntegrationTest):
container = self.client.create_container( container = self.client.create_container(
BUSYBOX, 'cat', detach=True, stdin_open=True, BUSYBOX, 'cat', detach=True, stdin_open=True,
use_config_proxy=True,
) )
self.client.start(container) self.client.start(container)
self.tmp_containers.append(container) self.tmp_containers.append(container)
@ -81,11 +80,11 @@ class ExecTest(BaseAPIIntegrationTest):
self.client.start(id) self.client.start(id)
self.tmp_containers.append(id) self.tmp_containers.append(id)
res = self.client.exec_create(id, 'whoami', user='default') res = self.client.exec_create(id, 'whoami', user='postgres')
assert 'Id' in res assert 'Id' in res
exec_log = self.client.exec_start(res) exec_log = self.client.exec_start(res)
assert exec_log == b'default\n' assert exec_log == b'postgres\n'
def test_exec_command_as_root(self): def test_exec_command_as_root(self):
container = self.client.create_container(BUSYBOX, 'cat', container = self.client.create_container(BUSYBOX, 'cat',
@ -115,75 +114,6 @@ class ExecTest(BaseAPIIntegrationTest):
res += chunk res += chunk
assert res == b'hello\nworld\n' assert res == b'hello\nworld\n'
def test_exec_command_demux(self):
container = self.client.create_container(
BUSYBOX, 'cat', detach=True, stdin_open=True)
id = container['Id']
self.client.start(id)
self.tmp_containers.append(id)
script = ' ; '.join([
# Write something on stdout
'echo hello out',
# Busybox's sleep does not handle sub-second times.
# This loops takes ~0.3 second to execute on my machine.
'for i in $(seq 1 50000); do echo $i>/dev/null; done',
# Write something on stderr
'echo hello err >&2'])
cmd = 'sh -c "{}"'.format(script)
# tty=False, stream=False, demux=False
res = self.client.exec_create(id, cmd)
exec_log = self.client.exec_start(res)
assert exec_log == b'hello out\nhello err\n'
# tty=False, stream=True, demux=False
res = self.client.exec_create(id, cmd)
exec_log = self.client.exec_start(res, stream=True)
assert next(exec_log) == b'hello out\n'
assert next(exec_log) == b'hello err\n'
with self.assertRaises(StopIteration):
next(exec_log)
# tty=False, stream=False, demux=True
res = self.client.exec_create(id, cmd)
exec_log = self.client.exec_start(res, demux=True)
assert exec_log == (b'hello out\n', b'hello err\n')
# tty=False, stream=True, demux=True
res = self.client.exec_create(id, cmd)
exec_log = self.client.exec_start(res, demux=True, stream=True)
assert next(exec_log) == (b'hello out\n', None)
assert next(exec_log) == (None, b'hello err\n')
with self.assertRaises(StopIteration):
next(exec_log)
# tty=True, stream=False, demux=False
res = self.client.exec_create(id, cmd, tty=True)
exec_log = self.client.exec_start(res)
assert exec_log == b'hello out\r\nhello err\r\n'
# tty=True, stream=True, demux=False
res = self.client.exec_create(id, cmd, tty=True)
exec_log = self.client.exec_start(res, stream=True)
assert next(exec_log) == b'hello out\r\n'
assert next(exec_log) == b'hello err\r\n'
with self.assertRaises(StopIteration):
next(exec_log)
# tty=True, stream=False, demux=True
res = self.client.exec_create(id, cmd, tty=True)
exec_log = self.client.exec_start(res, demux=True)
assert exec_log == (b'hello out\r\nhello err\r\n', None)
# tty=True, stream=True, demux=True
res = self.client.exec_create(id, cmd, tty=True)
exec_log = self.client.exec_start(res, demux=True, stream=True)
assert next(exec_log) == (b'hello out\r\n', None)
assert next(exec_log) == (b'hello err\r\n', None)
with self.assertRaises(StopIteration):
next(exec_log)
def test_exec_start_socket(self): def test_exec_start_socket(self):
container = self.client.create_container(BUSYBOX, 'cat', container = self.client.create_container(BUSYBOX, 'cat',
detach=True, stdin_open=True) detach=True, stdin_open=True)
@ -257,9 +187,9 @@ class ExecTest(BaseAPIIntegrationTest):
self.tmp_containers.append(container) self.tmp_containers.append(container)
self.client.start(container) self.client.start(container)
res = self.client.exec_create(container, 'pwd', workdir='/var/www') res = self.client.exec_create(container, 'pwd', workdir='/var/opt')
exec_log = self.client.exec_start(res) exec_log = self.client.exec_start(res)
assert exec_log == b'/var/www\n' assert exec_log == b'/var/opt\n'
def test_detach_with_default(self): def test_detach_with_default(self):
container = self.client.create_container( container = self.client.create_container(
@ -313,3 +243,88 @@ class ExecTest(BaseAPIIntegrationTest):
self.addCleanup(sock.close) self.addCleanup(sock.close)
assert_cat_socket_detached_with_keys(sock, [ctrl_with('x')]) assert_cat_socket_detached_with_keys(sock, [ctrl_with('x')])
class ExecDemuxTest(BaseAPIIntegrationTest):
cmd = 'sh -c "{}"'.format(' ; '.join([
# Write something on stdout
'echo hello out',
# Busybox's sleep does not handle sub-second times.
# This loops takes ~0.3 second to execute on my machine.
'sleep 0.5',
# Write something on stderr
'echo hello err >&2'])
)
def setUp(self):
super(ExecDemuxTest, self).setUp()
self.container = self.client.create_container(
BUSYBOX, 'cat', detach=True, stdin_open=True
)
self.client.start(self.container)
self.tmp_containers.append(self.container)
def test_exec_command_no_stream_no_demux(self):
# tty=False, stream=False, demux=False
res = self.client.exec_create(self.container, self.cmd)
exec_log = self.client.exec_start(res)
assert b'hello out\n' in exec_log
assert b'hello err\n' in exec_log
def test_exec_command_stream_no_demux(self):
# tty=False, stream=True, demux=False
res = self.client.exec_create(self.container, self.cmd)
exec_log = list(self.client.exec_start(res, stream=True))
assert len(exec_log) == 2
assert b'hello out\n' in exec_log
assert b'hello err\n' in exec_log
def test_exec_command_no_stream_demux(self):
# tty=False, stream=False, demux=True
res = self.client.exec_create(self.container, self.cmd)
exec_log = self.client.exec_start(res, demux=True)
assert exec_log == (b'hello out\n', b'hello err\n')
def test_exec_command_stream_demux(self):
# tty=False, stream=True, demux=True
res = self.client.exec_create(self.container, self.cmd)
exec_log = list(self.client.exec_start(res, demux=True, stream=True))
assert len(exec_log) == 2
assert (b'hello out\n', None) in exec_log
assert (None, b'hello err\n') in exec_log
def test_exec_command_tty_no_stream_no_demux(self):
# tty=True, stream=False, demux=False
res = self.client.exec_create(self.container, self.cmd, tty=True)
exec_log = self.client.exec_start(res)
assert exec_log == b'hello out\r\nhello err\r\n'
def test_exec_command_tty_stream_no_demux(self):
# tty=True, stream=True, demux=False
res = self.client.exec_create(self.container, self.cmd, tty=True)
exec_log = list(self.client.exec_start(res, stream=True))
assert b'hello out\r\n' in exec_log
if len(exec_log) == 2:
assert b'hello err\r\n' in exec_log
else:
assert len(exec_log) == 3
assert b'hello err' in exec_log
assert b'\r\n' in exec_log
def test_exec_command_tty_no_stream_demux(self):
# tty=True, stream=False, demux=True
res = self.client.exec_create(self.container, self.cmd, tty=True)
exec_log = self.client.exec_start(res, demux=True)
assert exec_log == (b'hello out\r\nhello err\r\n', None)
def test_exec_command_tty_stream_demux(self):
# tty=True, stream=True, demux=True
res = self.client.exec_create(self.container, self.cmd, tty=True)
exec_log = list(self.client.exec_start(res, demux=True, stream=True))
assert (b'hello out\r\n', None) in exec_log
if len(exec_log) == 2:
assert (b'hello err\r\n', None) in exec_log
else:
assert len(exec_log) == 3
assert (b'hello err', None) in exec_log
assert (b'\r\n', None) in exec_log

View File

@ -427,6 +427,21 @@ class ServiceTest(BaseAPIIntegrationTest):
assert 'Placement' in svc_info['Spec']['TaskTemplate'] assert 'Placement' in svc_info['Spec']['TaskTemplate']
assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt
@requires_api_version('1.27')
def test_create_service_with_placement_preferences_tuple(self):
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
placemt = docker.types.Placement(preferences=(
('spread', 'com.dockerpy.test'),
))
task_tmpl = docker.types.TaskTemplate(
container_spec, placement=placemt
)
name = self.get_service_name()
svc_id = self.client.create_service(task_tmpl, name=name)
svc_info = self.client.inspect_service(svc_id)
assert 'Placement' in svc_info['Spec']['TaskTemplate']
assert svc_info['Spec']['TaskTemplate']['Placement'] == placemt
def test_create_service_with_endpoint_spec(self): def test_create_service_with_endpoint_spec(self):
container_spec = docker.types.ContainerSpec(BUSYBOX, ['true']) container_spec = docker.types.ContainerSpec(BUSYBOX, ['true'])
task_tmpl = docker.types.TaskTemplate(container_spec) task_tmpl = docker.types.TaskTemplate(container_spec)
@ -835,6 +850,20 @@ class ServiceTest(BaseAPIIntegrationTest):
) )
assert privileges['SELinuxContext']['Disable'] is True assert privileges['SELinuxContext']['Disable'] is True
@requires_api_version('1.38')
def test_create_service_with_init(self):
container_spec = docker.types.ContainerSpec(
'busybox', ['sleep', '999'], init=True
)
task_tmpl = docker.types.TaskTemplate(container_spec)
name = self.get_service_name()
svc_id = self.client.create_service(task_tmpl, name=name)
svc_info = self.client.inspect_service(svc_id)
assert 'Init' in svc_info['Spec']['TaskTemplate']['ContainerSpec']
assert (
svc_info['Spec']['TaskTemplate']['ContainerSpec']['Init'] is True
)
@requires_api_version('1.25') @requires_api_version('1.25')
def test_update_service_with_defaults_name(self): def test_update_service_with_defaults_name(self):
container_spec = docker.types.ContainerSpec( container_spec = docker.types.ContainerSpec(

View File

@ -35,6 +35,35 @@ class SwarmTest(BaseAPIIntegrationTest):
version_2 = self.client.inspect_swarm()['Version']['Index'] version_2 = self.client.inspect_swarm()['Version']['Index']
assert version_2 != version_1 assert version_2 != version_1
@requires_api_version('1.39')
def test_init_swarm_custom_addr_pool_defaults(self):
assert self.init_swarm()
results = self.client.inspect_swarm()
assert set(results['DefaultAddrPool']) == {'10.0.0.0/8'}
assert results['SubnetSize'] == 24
@requires_api_version('1.39')
def test_init_swarm_custom_addr_pool_only_pool(self):
assert self.init_swarm(default_addr_pool=['2.0.0.0/16'])
results = self.client.inspect_swarm()
assert set(results['DefaultAddrPool']) == {'2.0.0.0/16'}
assert results['SubnetSize'] == 24
@requires_api_version('1.39')
def test_init_swarm_custom_addr_pool_only_subnet_size(self):
assert self.init_swarm(subnet_size=26)
results = self.client.inspect_swarm()
assert set(results['DefaultAddrPool']) == {'10.0.0.0/8'}
assert results['SubnetSize'] == 26
@requires_api_version('1.39')
def test_init_swarm_custom_addr_pool_both_args(self):
assert self.init_swarm(default_addr_pool=['2.0.0.0/16', '3.0.0.0/16'],
subnet_size=28)
results = self.client.inspect_swarm()
assert set(results['DefaultAddrPool']) == {'2.0.0.0/16', '3.0.0.0/16'}
assert results['SubnetSize'] == 28
@requires_api_version('1.24') @requires_api_version('1.24')
def test_init_already_in_cluster(self): def test_init_already_in_cluster(self):
assert self.init_swarm() assert self.init_swarm()
@ -157,12 +186,14 @@ class SwarmTest(BaseAPIIntegrationTest):
@requires_api_version('1.24') @requires_api_version('1.24')
def test_inspect_node(self): def test_inspect_node(self):
assert self.init_swarm() node_id = self.init_swarm()
assert node_id
nodes_list = self.client.nodes() nodes_list = self.client.nodes()
assert len(nodes_list) == 1 assert len(nodes_list) == 1
node = nodes_list[0] node = nodes_list[0]
node_data = self.client.inspect_node(node['ID']) node_data = self.client.inspect_node(node['ID'])
assert node['ID'] == node_data['ID'] assert node['ID'] == node_data['ID']
assert node_id == node['ID']
assert node['Version'] == node_data['Version'] assert node['Version'] == node_data['Version']
@requires_api_version('1.24') @requires_api_version('1.24')
@ -204,3 +235,20 @@ class SwarmTest(BaseAPIIntegrationTest):
self.client.remove_node(node_id, True) self.client.remove_node(node_id, True)
assert e.value.response.status_code >= 400 assert e.value.response.status_code >= 400
@requires_api_version('1.25')
def test_rotate_manager_unlock_key(self):
spec = self.client.create_swarm_spec(autolock_managers=True)
assert self.init_swarm(swarm_spec=spec)
swarm_info = self.client.inspect_swarm()
key_1 = self.client.get_unlock_key()
assert self.client.update_swarm(
version=swarm_info['Version']['Index'],
rotate_manager_unlock_key=True
)
key_2 = self.client.get_unlock_key()
assert key_1['UnlockKey'] != key_2['UnlockKey']
@requires_api_version('1.30')
def test_init_swarm_data_path_addr(self):
assert self.init_swarm(data_path_addr='eth0')

View File

@ -3,11 +3,10 @@ import shutil
import unittest import unittest
import docker import docker
from .. import helpers
from docker.utils import kwargs_from_env from docker.utils import kwargs_from_env
from .. import helpers BUSYBOX = 'alpine:3.9.3' # FIXME: this should probably be renamed
BUSYBOX = 'busybox:buildroot-2014.02'
TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION') TEST_API_VERSION = os.environ.get('DOCKER_TEST_API_VERSION')

View File

@ -0,0 +1,12 @@
#!/usr/bin/sh
haveged
gpg --batch --gen-key <<-EOF
%echo Generating a standard key
Key-Type: DSA
Key-Length: 1024
Subkey-Type: ELG-E
Subkey-Length: 1024
Name-Real: Sakuya Izayoi
Name-Email: sakuya@gensokyo.jp
Expire-Date: 0
EOF

View File

@ -0,0 +1,87 @@
import os
import random
import sys
import pytest
import six
from distutils.spawn import find_executable
from docker.credentials import (
CredentialsNotFound, Store, StoreError, DEFAULT_LINUX_STORE,
DEFAULT_OSX_STORE
)
class TestStore(object):
def teardown_method(self):
for server in self.tmp_keys:
try:
self.store.erase(server)
except StoreError:
pass
def setup_method(self):
self.tmp_keys = []
if sys.platform.startswith('linux'):
if find_executable('docker-credential-' + DEFAULT_LINUX_STORE):
self.store = Store(DEFAULT_LINUX_STORE)
elif find_executable('docker-credential-pass'):
self.store = Store('pass')
else:
raise Exception('No supported docker-credential store in PATH')
elif sys.platform.startswith('darwin'):
self.store = Store(DEFAULT_OSX_STORE)
def get_random_servername(self):
res = 'pycreds_test_{:x}'.format(random.getrandbits(32))
self.tmp_keys.append(res)
return res
def test_store_and_get(self):
key = self.get_random_servername()
self.store.store(server=key, username='user', secret='pass')
data = self.store.get(key)
assert data == {
'ServerURL': key,
'Username': 'user',
'Secret': 'pass'
}
def test_get_nonexistent(self):
key = self.get_random_servername()
with pytest.raises(CredentialsNotFound):
self.store.get(key)
def test_store_and_erase(self):
key = self.get_random_servername()
self.store.store(server=key, username='user', secret='pass')
self.store.erase(key)
with pytest.raises(CredentialsNotFound):
self.store.get(key)
def test_unicode_strings(self):
key = self.get_random_servername()
key = six.u(key)
self.store.store(server=key, username='user', secret='pass')
data = self.store.get(key)
assert data
self.store.erase(key)
with pytest.raises(CredentialsNotFound):
self.store.get(key)
def test_list(self):
names = (self.get_random_servername(), self.get_random_servername())
self.store.store(names[0], username='sakuya', secret='izayoi')
self.store.store(names[1], username='reimu', secret='hakurei')
data = self.store.list()
assert names[0] in data
assert data[names[0]] == 'sakuya'
assert names[1] in data
assert data[names[1]] == 'reimu'
def test_execute_with_env_override(self):
self.store.exe = 'env'
self.store.environment = {'FOO': 'bar'}
data = self.store._execute('--null', '')
assert b'\0FOO=bar\0' in data
assert 'FOO' not in os.environ

View File

@ -0,0 +1,22 @@
import os
from docker.credentials.utils import create_environment_dict
try:
from unittest import mock
except ImportError:
import mock
@mock.patch.dict(os.environ)
def test_create_environment_dict():
base = {'FOO': 'bar', 'BAZ': 'foobar'}
os.environ = base
assert create_environment_dict({'FOO': 'baz'}) == {
'FOO': 'baz', 'BAZ': 'foobar',
}
assert create_environment_dict({'HELLO': 'world'}) == {
'FOO': 'bar', 'BAZ': 'foobar', 'HELLO': 'world',
}
assert os.environ == base

View File

@ -2,10 +2,13 @@ import os
import tempfile import tempfile
import threading import threading
import docker
import pytest import pytest
from .base import BaseIntegrationTest, TEST_API_VERSION
from ..helpers import random_name, requires_api_version import docker
from ..helpers import random_name
from ..helpers import requires_api_version
from .base import BaseIntegrationTest
from .base import TEST_API_VERSION
class ContainerCollectionTest(BaseIntegrationTest): class ContainerCollectionTest(BaseIntegrationTest):
@ -123,7 +126,9 @@ class ContainerCollectionTest(BaseIntegrationTest):
def test_run_with_auto_remove(self): def test_run_with_auto_remove(self):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)
out = client.containers.run( out = client.containers.run(
'alpine', 'echo hello', auto_remove=True # sleep(2) to allow any communication with the container
# before it gets removed by the host.
'alpine', 'sh -c "echo hello && sleep 2"', auto_remove=True
) )
assert out == b'hello\n' assert out == b'hello\n'
@ -132,7 +137,10 @@ class ContainerCollectionTest(BaseIntegrationTest):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)
with pytest.raises(docker.errors.ContainerError) as e: with pytest.raises(docker.errors.ContainerError) as e:
client.containers.run( client.containers.run(
'alpine', 'sh -c ">&2 echo error && exit 1"', auto_remove=True # sleep(2) to allow any communication with the container
# before it gets removed by the host.
'alpine', 'sh -c ">&2 echo error && sleep 2 && exit 1"',
auto_remove=True
) )
assert e.value.exit_status == 1 assert e.value.exit_status == 1
assert e.value.stderr is None assert e.value.stderr is None
@ -169,9 +177,7 @@ class ContainerCollectionTest(BaseIntegrationTest):
ftp='sakuya.jp:4967' ftp='sakuya.jp:4967'
) )
out = client.containers.run( out = client.containers.run('alpine', 'sh -c "env"')
'alpine', 'sh -c "env"', use_config_proxy=True
)
assert b'FTP_PROXY=sakuya.jp:4967\n' in out assert b'FTP_PROXY=sakuya.jp:4967\n' in out
assert b'ftp_proxy=sakuya.jp:4967\n' in out assert b'ftp_proxy=sakuya.jp:4967\n' in out
@ -341,6 +347,66 @@ class ContainerTest(BaseIntegrationTest):
'memory_stats', 'blkio_stats']: 'memory_stats', 'blkio_stats']:
assert key in stats assert key in stats
def test_ports_target_none(self):
client = docker.from_env(version=TEST_API_VERSION)
ports = None
target_ports = {'2222/tcp': ports}
container = client.containers.run(
"alpine", "sleep 100", detach=True,
ports=target_ports
)
self.tmp_containers.append(container.id)
container.reload() # required to get auto-assigned ports
actual_ports = container.ports
assert sorted(target_ports.keys()) == sorted(actual_ports.keys())
for target_client, target_host in target_ports.items():
for actual_port in actual_ports[target_client]:
actual_keys = sorted(actual_port.keys())
assert sorted(['HostIp', 'HostPort']) == actual_keys
assert target_host is ports
assert int(actual_port['HostPort']) > 0
client.close()
def test_ports_target_tuple(self):
client = docker.from_env(version=TEST_API_VERSION)
ports = ('127.0.0.1', 1111)
target_ports = {'2222/tcp': ports}
container = client.containers.run(
"alpine", "sleep 100", detach=True,
ports=target_ports
)
self.tmp_containers.append(container.id)
container.reload() # required to get auto-assigned ports
actual_ports = container.ports
assert sorted(target_ports.keys()) == sorted(actual_ports.keys())
for target_client, target_host in target_ports.items():
for actual_port in actual_ports[target_client]:
actual_keys = sorted(actual_port.keys())
assert sorted(['HostIp', 'HostPort']) == actual_keys
assert target_host == ports
assert int(actual_port['HostPort']) > 0
client.close()
def test_ports_target_list(self):
client = docker.from_env(version=TEST_API_VERSION)
ports = [1234, 4567]
target_ports = {'2222/tcp': ports}
container = client.containers.run(
"alpine", "sleep 100", detach=True,
ports=target_ports
)
self.tmp_containers.append(container.id)
container.reload() # required to get auto-assigned ports
actual_ports = container.ports
assert sorted(target_ports.keys()) == sorted(actual_ports.keys())
for target_client, target_host in target_ports.items():
for actual_port in actual_ports[target_client]:
actual_keys = sorted(actual_port.keys())
assert sorted(['HostIp', 'HostPort']) == actual_keys
assert target_host == ports
assert int(actual_port['HostPort']) > 0
client.close()
def test_stop(self): def test_stop(self):
client = docker.from_env(version=TEST_API_VERSION) client = docker.from_env(version=TEST_API_VERSION)
container = client.containers.run("alpine", "top", detach=True) container = client.containers.run("alpine", "top", detach=True)
@ -378,3 +444,13 @@ class ContainerTest(BaseIntegrationTest):
detach=True) detach=True)
self.tmp_containers.append(container.id) self.tmp_containers.append(container.id)
assert container.wait()['StatusCode'] == 1 assert container.wait()['StatusCode'] == 1
def test_create_with_volume_driver(self):
client = docker.from_env(version=TEST_API_VERSION)
container = client.containers.create(
'alpine',
'sleep 300',
volume_driver='foo'
)
self.tmp_containers.append(container.id)
assert container.attrs['HostConfig']['VolumeDriver'] == 'foo'

View File

@ -31,3 +31,15 @@ class SwarmTest(unittest.TestCase):
cm.value.response.status_code == 406 or cm.value.response.status_code == 406 or
cm.value.response.status_code == 503 cm.value.response.status_code == 503
) )
def test_join_on_already_joined_swarm(self):
client = docker.from_env(version=TEST_API_VERSION)
client.swarm.init()
join_token = client.swarm.attrs['JoinTokens']['Manager']
with pytest.raises(docker.errors.APIError) as cm:
client.swarm.join(
remote_addrs=['127.0.0.1'],
join_token=join_token,
)
assert cm.value.response.status_code == 503
assert 'This node is already part of a swarm.' in cm.value.explanation

View File

@ -9,8 +9,7 @@ import shutil
import tempfile import tempfile
import unittest import unittest
from docker import auth, errors from docker import auth, credentials, errors
import dockerpycreds
import pytest import pytest
try: try:
@ -661,7 +660,7 @@ class CredstoreTest(unittest.TestCase):
} }
class InMemoryStore(dockerpycreds.Store): class InMemoryStore(credentials.Store):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.__store = {} self.__store = {}
@ -669,7 +668,7 @@ class InMemoryStore(dockerpycreds.Store):
try: try:
return self.__store[server] return self.__store[server]
except KeyError: except KeyError:
raise dockerpycreds.errors.CredentialsNotFound() raise credentials.errors.CredentialsNotFound()
def store(self, server, username, secret): def store(self, server, username, secret):
self.__store[server] = { self.__store[server] = {

View File

@ -176,6 +176,7 @@ class ContainerCollectionTest(unittest.TestCase):
'Ulimits': [{"Name": "nofile", "Soft": 1024, "Hard": 2048}], 'Ulimits': [{"Name": "nofile", "Soft": 1024, "Hard": 2048}],
'UsernsMode': 'host', 'UsernsMode': 'host',
'UTSMode': 'host', 'UTSMode': 'host',
'VolumeDriver': 'some_driver',
'VolumesFrom': ['container'], 'VolumesFrom': ['container'],
}, },
healthcheck={'test': 'true'}, healthcheck={'test': 'true'},
@ -190,7 +191,6 @@ class ContainerCollectionTest(unittest.TestCase):
stop_signal=9, stop_signal=9,
tty=True, tty=True,
user='bob', user='bob',
volume_driver='some_driver',
volumes=[ volumes=[
'/mnt/vol2', '/mnt/vol2',
'/mnt/vol1', '/mnt/vol1',

View File

@ -495,9 +495,12 @@ class PortsTest(unittest.TestCase):
assert external_port == [("127.0.0.1", "1000")] assert external_port == [("127.0.0.1", "1000")]
def test_split_port_with_protocol(self): def test_split_port_with_protocol(self):
internal_port, external_port = split_port("127.0.0.1:1000:2000/udp") for protocol in ['tcp', 'udp', 'sctp']:
assert internal_port == ["2000/udp"] internal_port, external_port = split_port(
assert external_port == [("127.0.0.1", "1000")] "127.0.0.1:1000:2000/" + protocol
)
assert internal_port == ["2000/" + protocol]
assert external_port == [("127.0.0.1", "1000")]
def test_split_port_with_host_ip_no_port(self): def test_split_port_with_host_ip_no_port(self):
internal_port, external_port = split_port("127.0.0.1::2000") internal_port, external_port = split_port("127.0.0.1::2000")
@ -550,6 +553,10 @@ class PortsTest(unittest.TestCase):
with pytest.raises(ValueError): with pytest.raises(ValueError):
split_port("0.0.0.0:1000:2000:tcp") split_port("0.0.0.0:1000:2000:tcp")
def test_split_port_invalid_protocol(self):
with pytest.raises(ValueError):
split_port("0.0.0.0:1000:2000/ftp")
def test_non_matching_length_port_ranges(self): def test_non_matching_length_port_ranges(self):
with pytest.raises(ValueError): with pytest.raises(ValueError):
split_port("0.0.0.0:1000-1010:2000-2002/tcp") split_port("0.0.0.0:1000-1010:2000-2002/tcp")

View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py27, py34, py35, py36, py37, flake8 envlist = py27, py35, py36, py37, flake8
skipsdist=True skipsdist=True
[testenv] [testenv]

View File

@ -1 +0,0 @@
-r requirements.txt