diff --git a/docker/models/containers.py b/docker/models/containers.py index 44bb92a0..85e9c66a 100644 --- a/docker/models/containers.py +++ b/docker/models/containers.py @@ -2,16 +2,16 @@ import copy import ntpath from collections import namedtuple +from .images import Image +from .resource import Collection, Model from ..api import APIClient from ..constants import DEFAULT_DATA_CHUNK_SIZE from ..errors import ( ContainerError, DockerException, ImageNotFound, NotFound, create_unexpected_kwargs_error ) -from ..types import HostConfig +from ..types import HostConfig, NetworkingConfig from ..utils import version_gte -from .images import Image -from .resource import Collection, Model class Container(Model): @@ -21,6 +21,7 @@ class Container(Model): query the Docker daemon for the current properties, causing :py:attr:`attrs` to be refreshed. """ + @property def name(self): """ @@ -681,10 +682,14 @@ class ContainerCollection(Collection): This mode is incompatible with ``ports``. Incompatible with ``network``. - network_driver_opt (dict): A dictionary of options to provide - to the network driver. Defaults to ``None``. Used in - conjuction with ``network``. Incompatible - with ``network_mode``. + networking_config (Dict[str, EndpointConfig]): + Dictionary of EndpointConfig objects for each container network. + The key is the name of the network. + Defaults to ``None``. + + Used in conjuction with ``network``. + + Incompatible with ``network_mode``. oom_kill_disable (bool): Whether to disable OOM killer. oom_score_adj (int): An integer value containing the score given to the container in order to tune OOM killer preferences. @@ -849,9 +854,9 @@ class ContainerCollection(Collection): 'together.' ) - if kwargs.get('network_driver_opt') and not kwargs.get('network'): + if kwargs.get('networking_config') and not kwargs.get('network'): raise RuntimeError( - 'The options "network_driver_opt" can not be used ' + 'The option "networking_config" can not be used ' 'without "network".' ) @@ -1007,6 +1012,7 @@ class ContainerCollection(Collection): def prune(self, filters=None): return self.client.api.prune_containers(filters=filters) + prune.__doc__ = APIClient.prune_containers.__doc__ @@ -1125,12 +1131,17 @@ def _create_container_args(kwargs): host_config_kwargs['binds'] = volumes network = kwargs.pop('network', None) - network_driver_opt = kwargs.pop('network_driver_opt', None) + networking_config = kwargs.pop('networking_config', None) if network: - network_configuration = {'driver_opt': network_driver_opt} \ - if network_driver_opt else None + if networking_config: + # Sanity check: check if the network is defined in the + # networking config dict, otherwise switch to None + if network not in networking_config: + networking_config = None - create_kwargs['networking_config'] = {network: network_configuration} + create_kwargs['networking_config'] = NetworkingConfig( + networking_config + ) if networking_config else {network: None} host_config_kwargs['network_mode'] = network # All kwargs should have been consumed by this point, so raise diff --git a/tests/integration/models_containers_test.py b/tests/integration/models_containers_test.py index 4d33e622..8fde851a 100644 --- a/tests/integration/models_containers_test.py +++ b/tests/integration/models_containers_test.py @@ -5,10 +5,10 @@ import threading import pytest import docker -from ..helpers import random_name -from ..helpers import requires_api_version from .base import BaseIntegrationTest from .base import TEST_API_VERSION +from ..helpers import random_name +from ..helpers import requires_api_version class ContainerCollectionTest(BaseIntegrationTest): @@ -104,6 +104,96 @@ class ContainerCollectionTest(BaseIntegrationTest): assert 'Networks' in attrs['NetworkSettings'] assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + def test_run_with_networking_config(self): + net_name = random_name() + client = docker.from_env(version=TEST_API_VERSION) + client.networks.create(net_name) + self.tmp_networks.append(net_name) + + test_aliases = ['hello'] + test_driver_opt = {'key1': 'a'} + + networking_config = { + net_name: client.api.create_endpoint_config( + aliases=test_aliases, + driver_opt=test_driver_opt + ) + } + + container = client.containers.run( + 'alpine', 'echo hello world', network=net_name, + networking_config=networking_config, + detach=True + ) + self.tmp_containers.append(container.id) + + attrs = container.attrs + + assert 'NetworkSettings' in attrs + assert 'Networks' in attrs['NetworkSettings'] + assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] == \ + test_aliases + assert attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] \ + == test_driver_opt + + def test_run_with_networking_config_with_undeclared_network(self): + net_name = random_name() + client = docker.from_env(version=TEST_API_VERSION) + client.networks.create(net_name) + self.tmp_networks.append(net_name) + + test_aliases = ['hello'] + test_driver_opt = {'key1': 'a'} + + networking_config = { + net_name: client.api.create_endpoint_config( + aliases=test_aliases, + driver_opt=test_driver_opt + ), + 'bar': client.api.create_endpoint_config( + aliases=['test'], + driver_opt={'key2': 'b'} + ), + } + + with pytest.raises(docker.errors.APIError) as e: + container = client.containers.run( + 'alpine', 'echo hello world', network=net_name, + networking_config=networking_config, + detach=True + ) + self.tmp_containers.append(container.id) + + def test_run_with_networking_config_only_undeclared_network(self): + net_name = random_name() + client = docker.from_env(version=TEST_API_VERSION) + client.networks.create(net_name) + self.tmp_networks.append(net_name) + + networking_config = { + 'bar': client.api.create_endpoint_config( + aliases=['hello'], + driver_opt={'key1': 'a'} + ), + } + + container = client.containers.run( + 'alpine', 'echo hello world', network=net_name, + networking_config=networking_config, + detach=True + ) + self.tmp_containers.append(container.id) + + attrs = container.attrs + + assert 'NetworkSettings' in attrs + assert 'Networks' in attrs['NetworkSettings'] + assert list(attrs['NetworkSettings']['Networks'].keys()) == [net_name] + assert attrs['NetworkSettings']['Networks'][net_name]['Aliases'] is None + assert (attrs['NetworkSettings']['Networks'][net_name]['DriverOpts'] + is None) + def test_run_with_none_driver(self): client = docker.from_env(version=TEST_API_VERSION) @@ -187,7 +277,7 @@ class ContainerCollectionTest(BaseIntegrationTest): container = client.containers.run("alpine", "sleep 300", detach=True) self.tmp_containers.append(container.id) assert client.containers.get(container.id).attrs[ - 'Config']['Image'] == "alpine" + 'Config']['Image'] == "alpine" def test_list(self): client = docker.from_env(version=TEST_API_VERSION) diff --git a/tests/unit/models_containers_test.py b/tests/unit/models_containers_test.py index 2eabd268..bd3092b6 100644 --- a/tests/unit/models_containers_test.py +++ b/tests/unit/models_containers_test.py @@ -1,12 +1,15 @@ -import docker -from docker.constants import DEFAULT_DATA_CHUNK_SIZE -from docker.models.containers import Container, _create_container_args -from docker.models.images import Image import unittest +import pytest + +import docker +from docker.constants import DEFAULT_DATA_CHUNK_SIZE, \ + DEFAULT_DOCKER_API_VERSION +from docker.models.containers import Container, _create_container_args +from docker.models.images import Image +from docker.types import EndpointConfig from .fake_api import FAKE_CONTAINER_ID, FAKE_IMAGE_ID, FAKE_EXEC_ID from .fake_api_client import make_fake_client -import pytest class ContainerCollectionTest(unittest.TestCase): @@ -31,77 +34,84 @@ class ContainerCollectionTest(unittest.TestCase): ) def test_create_container_args(self): - create_kwargs = _create_container_args({ - "image": 'alpine', - "command": 'echo hello world', - "blkio_weight_device": [{'Path': 'foo', 'Weight': 3}], - "blkio_weight": 2, - "cap_add": ['foo'], - "cap_drop": ['bar'], - "cgroup_parent": 'foobar', - "cgroupns": 'host', - "cpu_period": 1, - "cpu_quota": 2, - "cpu_shares": 5, - "cpuset_cpus": '0-3', - "detach": False, - "device_read_bps": [{'Path': 'foo', 'Rate': 3}], - "device_read_iops": [{'Path': 'foo', 'Rate': 3}], - "device_write_bps": [{'Path': 'foo', 'Rate': 3}], - "device_write_iops": [{'Path': 'foo', 'Rate': 3}], - "devices": ['/dev/sda:/dev/xvda:rwm'], - "dns": ['8.8.8.8'], - "domainname": 'example.com', - "dns_opt": ['foo'], - "dns_search": ['example.com'], - "entrypoint": '/bin/sh', - "environment": {'FOO': 'BAR'}, - "extra_hosts": {'foo': '1.2.3.4'}, - "group_add": ['blah'], - "ipc_mode": 'foo', - "kernel_memory": 123, - "labels": {'key': 'value'}, - "links": {'foo': 'bar'}, - "log_config": {'Type': 'json-file', 'Config': {}}, - "lxc_conf": {'foo': 'bar'}, - "healthcheck": {'test': 'true'}, - "hostname": 'somehost', - "mac_address": 'abc123', - "mem_limit": 123, - "mem_reservation": 123, - "mem_swappiness": 2, - "memswap_limit": 456, - "name": 'somename', - "network_disabled": False, - "network": 'foo', - "network_driver_opt": {'key1': 'a'}, - "oom_kill_disable": True, - "oom_score_adj": 5, - "pid_mode": 'host', - "pids_limit": 500, - "platform": 'linux', - "ports": { + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + + create_kwargs = _create_container_args(dict( + image='alpine', + command='echo hello world', + blkio_weight_device=[{'Path': 'foo', 'Weight': 3}], + blkio_weight=2, + cap_add=['foo'], + cap_drop=['bar'], + cgroup_parent='foobar', + cgroupns='host', + cpu_period=1, + cpu_quota=2, + cpu_shares=5, + cpuset_cpus='0-3', + detach=False, + device_read_bps=[{'Path': 'foo', 'Rate': 3}], + device_read_iops=[{'Path': 'foo', 'Rate': 3}], + device_write_bps=[{'Path': 'foo', 'Rate': 3}], + device_write_iops=[{'Path': 'foo', 'Rate': 3}], + devices=['/dev/sda:/dev/xvda:rwm'], + dns=['8.8.8.8'], + domainname='example.com', + dns_opt=['foo'], + dns_search=['example.com'], + entrypoint='/bin/sh', + environment={'FOO': 'BAR'}, + extra_hosts={'foo': '1.2.3.4'}, + group_add=['blah'], + ipc_mode='foo', + kernel_memory=123, + labels={'key': 'value'}, + links={'foo': 'bar'}, + log_config={'Type': 'json-file', 'Config': {}}, + lxc_conf={'foo': 'bar'}, + healthcheck={'test': 'true'}, + hostname='somehost', + mac_address='abc123', + mem_limit=123, + mem_reservation=123, + mem_swappiness=2, + memswap_limit=456, + name='somename', + network_disabled=False, + network='foo', + networking_config=networking_config, + oom_kill_disable=True, + oom_score_adj=5, + pid_mode='host', + pids_limit=500, + platform='linux', + ports={ 1111: 4567, 2222: None }, - "privileged": True, - "publish_all_ports": True, - "read_only": True, - "restart_policy": {'Name': 'always'}, - "security_opt": ['blah'], - "shm_size": 123, - "stdin_open": True, - "stop_signal": 9, - "sysctls": {'foo': 'bar'}, - "tmpfs": {'/blah': ''}, - "tty": True, - "ulimits": [{"Name": "nofile", "Soft": 1024, "Hard": 2048}], - "user": 'bob', - "userns_mode": 'host', - "uts_mode": 'host', - "version": '1.23', - "volume_driver": 'some_driver', - "volumes": [ + privileged=True, + publish_all_ports=True, + read_only=True, + restart_policy={'Name': 'always'}, + security_opt=['blah'], + shm_size=123, + stdin_open=True, + stop_signal=9, + sysctls={'foo': 'bar'}, + tmpfs={'/blah': ''}, + tty=True, + ulimits=[{"Name": "nofile", "Soft": 1024, "Hard": 2048}], + user='bob', + userns_mode='host', + uts_mode='host', + version=DEFAULT_DOCKER_API_VERSION, + volume_driver='some_driver', + volumes=[ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', 'volumename:/mnt/vol3r', @@ -109,18 +119,18 @@ class ContainerCollectionTest(unittest.TestCase): '/anothervolumewithnohostpath:ro', 'C:\\windows\\path:D:\\hello\\world:rw' ], - "volumes_from": ['container'], - "working_dir": '/code' - }) + volumes_from=['container'], + working_dir='/code' + )) - expected = { - "image": 'alpine', - "command": 'echo hello world', - "domainname": 'example.com', - "detach": False, - "entrypoint": '/bin/sh', - "environment": {'FOO': 'BAR'}, - "host_config": { + expected = dict( + image='alpine', + command='echo hello world', + domainname='example.com', + detach=False, + entrypoint='/bin/sh', + environment={'FOO': 'BAR'}, + host_config={ 'Binds': [ '/home/user1/:/mnt/vol2', '/var/www:/mnt/vol1:ro', @@ -183,20 +193,22 @@ class ContainerCollectionTest(unittest.TestCase): 'VolumeDriver': 'some_driver', 'VolumesFrom': ['container'], }, - "healthcheck": {'test': 'true'}, - "hostname": 'somehost', - "labels": {'key': 'value'}, - "mac_address": 'abc123', - "name": 'somename', - "network_disabled": False, - "networking_config": {'foo': {'driver_opt': {'key1': 'a'}}}, - "platform": 'linux', - "ports": [('1111', 'tcp'), ('2222', 'tcp')], - "stdin_open": True, - "stop_signal": 9, - "tty": True, - "user": 'bob', - "volumes": [ + healthcheck={'test': 'true'}, + hostname='somehost', + labels={'key': 'value'}, + mac_address='abc123', + name='somename', + network_disabled=False, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, + platform='linux', + ports=[('1111', 'tcp'), ('2222', 'tcp')], + stdin_open=True, + stop_signal=9, + tty=True, + user='bob', + volumes=[ '/mnt/vol2', '/mnt/vol1', '/mnt/vol3r', @@ -204,8 +216,8 @@ class ContainerCollectionTest(unittest.TestCase): '/anothervolumewithnohostpath', 'D:\\hello\\world' ], - "working_dir": '/code' - } + working_dir='/code' + ) assert create_kwargs == expected @@ -346,39 +358,105 @@ class ContainerCollectionTest(unittest.TestCase): host_config={'NetworkMode': 'default'}, ) - def test_run_network_driver_opts_without_network(self): + def test_run_networking_config_without_network(self): client = make_fake_client() with pytest.raises(RuntimeError): client.containers.run( image='alpine', - network_driver_opt={'key1': 'a'} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) - def test_run_network_driver_opts_with_network_mode(self): + def test_run_networking_config_with_network_mode(self): client = make_fake_client() with pytest.raises(RuntimeError): client.containers.run( image='alpine', network_mode='none', - network_driver_opt={'key1': 'a'} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) - def test_run_network_driver_opts(self): + def test_run_networking_config(self): client = make_fake_client() + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.run( image='alpine', network='foo', - network_driver_opt={'key1': 'a'} + networking_config=networking_config ) client.api.create_container.assert_called_with( detach=False, image='alpine', command=None, - networking_config={'foo': {'driver_opt': {'key1': 'a'}}}, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, + host_config={'NetworkMode': 'foo'} + ) + + def test_run_networking_config_with_undeclared_network(self): + client = make_fake_client() + + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test_foo'], + driver_opt={'key2': 'b'} + ), + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + + client.containers.run( + image='alpine', + network='foo', + networking_config=networking_config + ) + + client.api.create_container.assert_called_with( + detach=False, + image='alpine', + command=None, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test_foo'], 'DriverOpts': {'key2': 'b'}}, + 'bar': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}, + }}, + host_config={'NetworkMode': 'foo'} + ) + + def test_run_networking_config_only_undeclared_network(self): + client = make_fake_client() + + networking_config = { + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + + client.containers.run( + image='alpine', + network='foo', + networking_config=networking_config + ) + + client.api.create_container.assert_called_with( + detach=False, + image='alpine', + command=None, + networking_config={'foo': None}, host_config={'NetworkMode': 'foo'} ) @@ -409,12 +487,13 @@ class ContainerCollectionTest(unittest.TestCase): host_config={'NetworkMode': 'default'} ) - def test_create_network_driver_opts_without_network(self): + def test_create_networking_config_without_network(self): client = make_fake_client() client.containers.create( image='alpine', - network_driver_opt={'key1': 'a'} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( @@ -423,13 +502,14 @@ class ContainerCollectionTest(unittest.TestCase): host_config={'NetworkMode': 'default'} ) - def test_create_network_driver_opts_with_network_mode(self): + def test_create_networking_config_with_network_mode(self): client = make_fake_client() client.containers.create( image='alpine', network_mode='none', - network_driver_opt={'key1': 'a'} + networking_config={'aliases': ['test'], + 'driver_opt': {'key1': 'a'}} ) client.api.create_container.assert_called_with( @@ -438,19 +518,81 @@ class ContainerCollectionTest(unittest.TestCase): host_config={'NetworkMode': 'none'} ) - def test_create_network_driver_opts(self): + def test_create_networking_config(self): client = make_fake_client() + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + client.containers.create( image='alpine', network='foo', - network_driver_opt={'key1': 'a'} + networking_config=networking_config ) client.api.create_container.assert_called_with( image='alpine', command=None, - networking_config={'foo': {'driver_opt': {'key1': 'a'}}}, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}} + }, + host_config={'NetworkMode': 'foo'} + ) + + def test_create_networking_config_with_undeclared_network(self): + client = make_fake_client() + + networking_config = { + 'foo': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test_foo'], + driver_opt={'key2': 'b'} + ), + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + + client.containers.create( + image='alpine', + network='foo', + networking_config=networking_config + ) + + client.api.create_container.assert_called_with( + image='alpine', + command=None, + networking_config={'EndpointsConfig': { + 'foo': {'Aliases': ['test_foo'], 'DriverOpts': {'key2': 'b'}}, + 'bar': {'Aliases': ['test'], 'DriverOpts': {'key1': 'a'}}, + }}, + host_config={'NetworkMode': 'foo'} + ) + + def test_create_networking_config_only_undeclared_network(self): + client = make_fake_client() + + networking_config = { + 'bar': EndpointConfig( + DEFAULT_DOCKER_API_VERSION, aliases=['test'], + driver_opt={'key1': 'a'} + ) + } + + client.containers.create( + image='alpine', + network='foo', + networking_config=networking_config + ) + + client.api.create_container.assert_called_with( + image='alpine', + command=None, + networking_config={'foo': None}, host_config={'NetworkMode': 'foo'} ) @@ -479,6 +621,7 @@ class ContainerCollectionTest(unittest.TestCase): def test_list_ignore_removed(self): def side_effect(*args, **kwargs): raise docker.errors.NotFound('Container not found') + client = make_fake_client({ 'inspect_container.side_effect': side_effect })