Kaydet (Commit) ec5a28a4 authored tarafından Joffrey F's avatar Joffrey F

Merge pull request #428 from docker/host_config

Support for host config
......@@ -34,7 +34,7 @@ from .tls import TLSConfig
if not six.PY3:
import websocket
DEFAULT_DOCKER_API_VERSION = '1.15'
DEFAULT_DOCKER_API_VERSION = '1.16'
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
......@@ -110,7 +110,8 @@ class Client(requests.Session):
volumes=None, volumes_from=None,
network_disabled=False, entrypoint=None,
cpu_shares=None, working_dir=None,
domainname=None, memswap_limit=0, cpuset=None):
domainname=None, memswap_limit=0, cpuset=None,
host_config=None):
if isinstance(command, six.string_types):
command = shlex.split(str(command))
if isinstance(environment, dict):
......@@ -225,7 +226,8 @@ class Client(requests.Session):
'CpuShares': cpu_shares,
'Cpuset': cpuset,
'WorkingDir': working_dir,
'MemorySwap': memswap_limit
'MemorySwap': memswap_limit,
'HostConfig': host_config
}
def _post_json(self, url, data, **kwargs):
......@@ -263,6 +265,12 @@ class Client(requests.Session):
def _create_websocket_connection(self, url):
return websocket.create_connection(url)
def _warn_deprecated(self, arg_name, version):
warning_message = (
'{0!r} is deprecated for API version >= {1}'
).format(arg_name, version)
warnings.warn(warning_message, DeprecationWarning)
def _get_raw_response_socket(self, response):
self._raise_for_status(response)
if six.PY3:
......@@ -536,17 +544,20 @@ class Client(requests.Session):
mem_limit=0, ports=None, environment=None, dns=None,
volumes=None, volumes_from=None,
network_disabled=False, name=None, entrypoint=None,
cpu_shares=None, working_dir=None,
domainname=None, memswap_limit=0, cpuset=None):
cpu_shares=None, working_dir=None, domainname=None,
memswap_limit=0, cpuset=None, host_config=None):
if isinstance(volumes, six.string_types):
volumes = [volumes, ]
if host_config and utils.compare_version('1.15', self._version) < 0:
raise errors.APIError('host_config is not supported in API < 1.15')
config = self._container_config(
image, command, hostname, user, detach, stdin_open, tty, mem_limit,
ports, environment, dns, volumes, volumes_from, network_disabled,
entrypoint, cpu_shares, working_dir, domainname,
memswap_limit, cpuset
memswap_limit, cpuset, host_config
)
return self.create_container_from_config(config, name)
......@@ -570,7 +581,7 @@ class Client(requests.Session):
def execute(self, container, cmd, detach=False, stdout=True, stderr=True,
stream=False, tty=False):
if utils.compare_version('1.15', self._version) < 0:
raise Exception('Exec is not supported in API < 1.15!')
raise errors.APIError('Exec is not supported in API < 1.15')
if isinstance(container, dict):
container = container.get('Id')
if isinstance(cmd, six.string_types):
......@@ -911,6 +922,9 @@ class Client(requests.Session):
dns=None, dns_search=None, volumes_from=None, network_mode=None,
restart_policy=None, cap_add=None, cap_drop=None, devices=None,
extra_hosts=None):
start_config = {}
if isinstance(container, dict):
container = container.get('Id')
......@@ -920,9 +934,9 @@ class Client(requests.Session):
formatted.append({'Key': k, 'Value': str(v)})
lxc_conf = formatted
start_config = {
'LxcConf': lxc_conf
}
if lxc_conf:
start_config['LxcConf'] = lxc_conf
if binds:
start_config['Binds'] = utils.convert_volume_binds(binds)
......@@ -931,7 +945,8 @@ class Client(requests.Session):
port_bindings
)
start_config['PublishAllPorts'] = publish_all_ports
if publish_all_ports:
start_config['PublishAllPorts'] = publish_all_ports
if links:
if isinstance(links, dict):
......@@ -953,7 +968,8 @@ class Client(requests.Session):
start_config['ExtraHosts'] = formatted_extra_hosts
start_config['Privileged'] = privileged
if privileged:
start_config['Privileged'] = privileged
if utils.compare_version('1.10', self._version) >= 0:
if dns is not None:
......@@ -963,16 +979,15 @@ class Client(requests.Session):
volumes_from = volumes_from.split(',')
start_config['VolumesFrom'] = volumes_from
else:
warning_message = ('{0!r} parameter is discarded. It is only'
' available for API version greater or equal'
' than 1.10')
if dns is not None:
warnings.warn(warning_message.format('dns'),
DeprecationWarning)
raise errors.APIError(
'dns is only supported for API version >= 1.10'
)
if volumes_from is not None:
warnings.warn(warning_message.format('volumes_from'),
DeprecationWarning)
raise errors.APIError(
'volumes_from is only supported for API version >= 1.10'
)
if dns_search:
start_config['DnsSearch'] = dns_search
......@@ -992,6 +1007,8 @@ class Client(requests.Session):
start_config['Devices'] = utils.parse_devices(devices)
url = self._url("/containers/{0}/start".format(container))
if not start_config:
start_config = None
res = self._post_json(url, data=start_config)
self._raise_for_status(res)
......
from .utils import (
compare_version, convert_port_bindings, convert_volume_binds,
mkbuildcontext, ping, tar, parse_repository_tag, parse_host,
kwargs_from_env, convert_filters
kwargs_from_env, convert_filters, create_host_config
) # flake8: noqa
......@@ -294,3 +294,70 @@ def convert_filters(filters):
v = [v, ]
result[k] = v
return json.dumps(result)
def create_host_config(
binds=None, port_bindings=None, lxc_conf=None,
publish_all_ports=False, links=None, privileged=False,
dns=None, dns_search=None, volumes_from=None, network_mode=None,
restart_policy=None, cap_add=None, cap_drop=None, devices=None
):
host_config = {
'Privileged': privileged,
'PublishAllPorts': publish_all_ports,
}
if dns_search:
host_config['DnsSearch'] = dns_search
if network_mode:
host_config['NetworkMode'] = network_mode
if restart_policy:
host_config['RestartPolicy'] = restart_policy
if cap_add:
host_config['CapAdd'] = cap_add
if cap_drop:
host_config['CapDrop'] = cap_drop
if devices:
host_config['Devices'] = parse_devices(devices)
if dns is not None:
host_config['Dns'] = dns
if volumes_from is not None:
if isinstance(volumes_from, six.string_types):
volumes_from = volumes_from.split(',')
host_config['VolumesFrom'] = volumes_from
if binds:
host_config['Binds'] = convert_volume_binds(binds)
if port_bindings:
host_config['PortBindings'] = convert_port_bindings(
port_bindings
)
host_config['PublishAllPorts'] = publish_all_ports
if links:
if isinstance(links, dict):
links = six.iteritems(links)
formatted_links = [
'{0}:{1}'.format(k, v) for k, v in sorted(links)
]
host_config['Links'] = formatted_links
if isinstance(lxc_conf, dict):
formatted = []
for k, v in six.iteritems(lxc_conf):
formatted.append({'Key': k, 'Value': str(v)})
lxc_conf = formatted
host_config['LxcConf'] = lxc_conf
return host_config
......@@ -172,8 +172,9 @@ character, bytes are assumed as an intended unit.
`volumes_from` and `dns` arguments raise [TypeError](
https://docs.python.org/3.4/library/exceptions.html#TypeError) exception if
they are used against v1.10 of the Docker remote API. Those arguments should be
passed to `start()` instead.
they are used against v1.10 and above of the Docker remote API. Those
arguments should be passed to `start()` instead, or as part of the `host_config`
dictionary.
**Params**:
......@@ -200,7 +201,8 @@ from. Optionally a single string joining container id's with commas
* cpu_shares (int or float): CPU shares (relative weight)
* working_dir (str): Path to the working directory
* domainname (str or list): Set custom DNS search domains
* memswap_limit:
* memswap_limit (int):
* host_config (dict): A [HostConfig](hostconfig.md) dictionary
**Returns** (dict): A dictionary with an image 'Id' key and a 'Warnings' key.
......@@ -563,9 +565,13 @@ Similar to the `docker start` command, but doesn't support attach options. Use
`.logs()` to recover `stdout`/`stderr`.
`binds` allows to bind a directory in the host to the container. See [Using
volumes](volumes.md) for more information. `port_bindings` exposes container
ports to the host. See [Port bindings](port-bindings.md) for more information.
volumes](volumes.md) for more information.
`port_bindings` exposes container ports to the host.
See [Port bindings](port-bindings.md) for more information.
`lxc_conf` allows to pass LXC configuration options using a dictionary.
`privileged` starts the container in privileged mode.
[Links](http://docs.docker.io/en/latest/use/working_with_links_names/) can be
......
# HostConfig object
The Docker Remote API introduced [support for HostConfig in version 1.15](http://docs.docker.com/reference/api/docker_remote_api_v1.15/#create-a-container). This object contains all the parameters you can pass to `Client.start`.
## HostConfig helper
### docker.utils.create_host_config
Creates a HostConfig dictionary to be used with `Client.create_container`.
`binds` allows to bind a directory in the host to the container. See [Using
volumes](volumes.md) for more information.
`port_bindings` exposes container ports to the host.
See [Port bindings](port-bindings.md) for more information.
`lxc_conf` allows to pass LXC configuration options using a dictionary.
`privileged` starts the container in privileged mode.
[Links](http://docs.docker.io/en/latest/use/working_with_links_names/) can be
specified with the `links` argument. They can either be specified as a
dictionary mapping name to alias or as a list of `(name, alias)` tuples.
`dns` and `volumes_from` are only available if they are used with version v1.10
of docker remote API. Otherwise they are ignored.
`network_mode` is available since v1.11 and sets the Network mode for the
container ('bridge': creates a new network stack for the container on the
Docker bridge, 'none': no networking for this container, 'container:[name|id]':
reuses another container network stack), 'host': use the host network stack
inside the container.
`restart_policy` is available since v1.2.0 and sets the RestartPolicy for how a
container should or should not be restarted on exit. By default the policy is
set to no meaning do not restart the container when it exits. The user may
specify the restart policy as a dictionary for example:
```python
{
"MaximumRetryCount": 0,
"Name": "always"
}
```
For always restarting the container on exit or can specify to restart the
container to restart on failure and can limit number of restarts. For example:
```python
{
"MaximumRetryCount": 5,
"Name": "on-failure"
}
```
`cap_add` and `cap_drop` are available since v1.2.0 and can be used to add or
drop certain capabilities. The user may specify the capabilities as an array
for example:
```python
[
"SYS_ADMIN",
"MKNOD"
]
```
**Params**
* container (str): The container to start
* binds: Volumes to bind. See [Using volumes](volumes.md) for more information.
* port_bindings (dict): Port bindings. See [Port bindings](port-bindings.md)
for more information.
* lxc_conf (dict): LXC config
* publish_all_ports (bool): Whether to publish all ports to the host
* links (dict or list of tuples): either as a dictionary mapping name to alias or
as a list of `(name, alias)` tuples
* privileged (bool): Give extended privileges to this container
* dns (list): Set custom DNS servers
* dns_search (list): DNS search domains
* volumes_from (str or list): List of container names or Ids to get volumes
from. Optionally a single string joining container id's with commas
* network_mode (str): One of `['bridge', None, 'container:<name|id>', 'host']`
* restart_policy (dict): "Name" param must be one of `['on-failure', 'always']`
* cap_add (list of str): Add kernel capabilities
* cap_drop (list of str): Drop kernel capabilities
**Returns** (dict) HostConfig dictionary
```python
>>> from docker.utils import create_host_config
>>> create_host_config(privileged=True, cap_drop=['MKNOD'], volumes_from=['nostalgic_newton'])
{'CapDrop': ['MKNOD'], 'LxcConf': None, 'Privileged': True, 'VolumesFrom': ['nostalgic_newton'], 'PublishAllPorts': False}
```
\ No newline at end of file
site_name: docker-py Documentation
site_description: An API client for Docker written in Python
site_favicon: favicon_whale.png
# site_url: docker-py.readthedocs.org
site_url: docker-py.readthedocs.org
repo_url: https://github.com/docker/docker-py/
theme: readthedocs
pages:
......
......@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
CURRENT_VERSION = 'v1.15'
CURRENT_VERSION = 'v1.16'
FAKE_CONTAINER_ID = '3cc2351ab11b'
FAKE_IMAGE_ID = 'e9aa60c60128'
......
......@@ -33,6 +33,9 @@ from test import Cleanup
DEFAULT_BASE_URL = os.environ.get('DOCKER_HOST')
warnings.simplefilter('error')
create_host_config = docker.utils.create_host_config
class BaseTestCase(unittest.TestCase):
tmp_imgs = []
......@@ -154,22 +157,51 @@ class TestCreateContainerWithBinds(BaseTestCase):
filename = 'shared.txt'
shared_file = os.path.join(mount_origin, filename)
binds = {
mount_origin: {
'bind': mount_dest,
'ro': False,
},
}
with open(shared_file, 'w'):
container = self.client.create_container(
'busybox',
['ls', mount_dest], volumes={mount_dest: {}}
['ls', mount_dest], volumes={mount_dest: {}},
host_config=create_host_config(binds=binds)
)
container_id = container['Id']
self.client.start(
container_id,
binds={
mount_origin: {
'bind': mount_dest,
'ro': False,
},
},
self.client.start(container_id)
self.tmp_containers.append(container_id)
exitcode = self.client.wait(container_id)
self.assertEqual(exitcode, 0)
logs = self.client.logs(container_id)
os.unlink(shared_file)
self.assertIn(filename, logs)
class TestStartContainerWithBinds(BaseTestCase):
def runTest(self):
mount_dest = '/mnt'
mount_origin = tempfile.mkdtemp()
self.tmp_folders.append(mount_origin)
filename = 'shared.txt'
shared_file = os.path.join(mount_origin, filename)
binds = {
mount_origin: {
'bind': mount_dest,
'ro': False,
},
}
with open(shared_file, 'w'):
container = self.client.create_container(
'busybox', ['ls', mount_dest], volumes={mount_dest: {}}
)
container_id = container['Id']
self.client.start(container_id, binds=binds)
self.tmp_containers.append(container_id)
exitcode = self.client.wait(container_id)
self.assertEqual(exitcode, 0)
......@@ -225,6 +257,31 @@ class TestStartContainerWithDictInsteadOfId(BaseTestCase):
self.assertEqual(inspect['State']['ExitCode'], 0)
class TestCreateContainerPrivileged(BaseTestCase):
def runTest(self):
res = self.client.create_container(
'busybox', 'true', host_config=create_host_config(privileged=True)
)
self.assertIn('Id', res)
self.tmp_containers.append(res['Id'])
self.client.start(res['Id'])
inspect = self.client.inspect_container(res['Id'])
self.assertIn('Config', inspect)
self.assertIn('Id', inspect)
self.assertTrue(inspect['Id'].startswith(res['Id']))
self.assertIn('Image', inspect)
self.assertIn('State', inspect)
self.assertIn('Running', inspect['State'])
if not inspect['State']['Running']:
self.assertIn('ExitCode', inspect['State'])
self.assertEqual(inspect['State']['ExitCode'], 0)
# Since Nov 2013, the Privileged flag is no longer part of the
# container's config exposed via the API (safety concerns?).
#
if 'Privileged' in inspect['Config']:
self.assertEqual(inspect['Config']['Privileged'], True)
class TestStartContainerPrivileged(BaseTestCase):
def runTest(self):
res = self.client.create_container('busybox', 'true')
......@@ -457,6 +514,35 @@ class TestKillWithSignal(BaseTestCase):
class TestPort(BaseTestCase):
def runTest(self):
port_bindings = {
1111: ('127.0.0.1', '4567'),
2222: ('127.0.0.1', '4568')
}
container = self.client.create_container(
'busybox', ['sleep', '60'], ports=port_bindings.keys(),
host_config=create_host_config(port_bindings=port_bindings)
)
id = container['Id']
self.client.start(container)
# Call the port function on each biding and compare expected vs actual
for port in port_bindings:
actual_bindings = self.client.port(container, port)
port_binding = actual_bindings.pop()
ip, host_port = port_binding['HostIp'], port_binding['HostPort']
self.assertEqual(ip, port_bindings[port][0])
self.assertEqual(host_port, port_bindings[port][1])
self.client.kill(id)
class TestStartWithPortBindings(BaseTestCase):
def runTest(self):
port_bindings = {
......@@ -551,6 +637,88 @@ class TestRemoveContainerWithDictInsteadOfId(BaseTestCase):
self.assertEqual(len(res), 0)
class TestCreateContainerWithVolumesFrom(BaseTestCase):
def runTest(self):
vol_names = ['foobar_vol0', 'foobar_vol1']
res0 = self.client.create_container(
'busybox', 'true', name=vol_names[0]
)
container1_id = res0['Id']
self.tmp_containers.append(container1_id)
self.client.start(container1_id)
res1 = self.client.create_container(
'busybox', 'true', name=vol_names[1]
)
container2_id = res1['Id']
self.tmp_containers.append(container2_id)
self.client.start(container2_id)
with self.assertRaises(docker.errors.DockerException):
self.client.create_container(
'busybox', 'cat', detach=True, stdin_open=True,
volumes_from=vol_names
)
res2 = self.client.create_container(
'busybox', 'cat', detach=True, stdin_open=True,
host_config=create_host_config(volumes_from=vol_names)
)
container3_id = res2['Id']
self.tmp_containers.append(container3_id)
self.client.start(container3_id)
info = self.client.inspect_container(res2['Id'])
self.assertItemsEqual(info['HostConfig']['VolumesFrom'], vol_names)
class TestCreateContainerWithLinks(BaseTestCase):
def runTest(self):
res0 = self.client.create_container(
'busybox', 'cat',
detach=True, stdin_open=True,
environment={'FOO': '1'})
container1_id = res0['Id']
self.tmp_containers.append(container1_id)
self.client.start(container1_id)
res1 = self.client.create_container(
'busybox', 'cat',
detach=True, stdin_open=True,
environment={'FOO': '1'})
container2_id = res1['Id']
self.tmp_containers.append(container2_id)
self.client.start(container2_id)
# we don't want the first /
link_path1 = self.client.inspect_container(container1_id)['Name'][1:]
link_alias1 = 'mylink1'
link_env_prefix1 = link_alias1.upper()
link_path2 = self.client.inspect_container(container2_id)['Name'][1:]
link_alias2 = 'mylink2'
link_env_prefix2 = link_alias2.upper()
res2 = self.client.create_container(
'busybox', 'env', host_config=create_host_config(
links={link_path1: link_alias1, link_path2: link_alias2}
)
)
container3_id = res2['Id']
self.tmp_containers.append(container3_id)
self.client.start(container3_id)
self.assertEqual(self.client.wait(container3_id), 0)
logs = self.client.logs(container3_id)
self.assertIn('{0}_NAME='.format(link_env_prefix1), logs)
self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix1), logs)
self.assertIn('{0}_NAME='.format(link_env_prefix2), logs)
self.assertIn('{0}_ENV_FOO=1'.format(link_env_prefix2), logs)
class TestStartContainerWithVolumesFrom(BaseTestCase):
def runTest(self):
vol_names = ['foobar_vol0', 'foobar_vol1']
......@@ -633,12 +801,13 @@ class TestStartContainerWithLinks(BaseTestCase):
class TestRestartingContainer(BaseTestCase):
def runTest(self):
container = self.client.create_container('busybox', ['false'])
container = self.client.create_container(
'busybox', ['false'], host_config=create_host_config(
restart_policy={"Name": "on-failure", "MaximumRetryCount": 1}
)
)
id = container['Id']
self.client.start(id, restart_policy={
"Name": "on-failure",
"MaximumRetryCount": 1
})
self.client.start(id)
self.client.wait(id)
self.client.remove_container(id)
containers = self.client.containers(all=True)
......@@ -726,7 +895,8 @@ class TestRemoveLink(BaseTestCase):
def runTest(self):
# Create containers
container1 = self.client.create_container(
'busybox', 'cat', detach=True, stdin_open=True)
'busybox', 'cat', detach=True, stdin_open=True
)
container1_id = container1['Id']
self.tmp_containers.append(container1_id)
self.client.start(container1_id)
......@@ -736,10 +906,14 @@ class TestRemoveLink(BaseTestCase):
link_path = self.client.inspect_container(container1_id)['Name'][1:]
link_alias = 'mylink'
container2 = self.client.create_container('busybox', 'cat')
container2 = self.client.create_container(
'busybox', 'cat', host_config=create_host_config(
links={link_path: link_alias}
)
)
container2_id = container2['Id']
self.tmp_containers.append(container2_id)
self.client.start(container2_id, links={link_path: link_alias})
self.client.start(container2_id)
# Remove link
linked_name = self.client.inspect_container(container2_id)['Name'][1:]
......
This diff is collapsed.
......@@ -71,16 +71,16 @@ class UtilsTest(unittest.TestCase):
'testdata/certs'),
DOCKER_TLS_VERIFY='1')
kwargs = kwargs_from_env(assert_hostname=False)
self.assertEquals('https://192.168.59.103:2376', kwargs['base_url'])
self.assertEqual('https://192.168.59.103:2376', kwargs['base_url'])
self.assertTrue('ca.pem' in kwargs['tls'].verify)
self.assertTrue('cert.pem' in kwargs['tls'].cert[0])
self.assertTrue('key.pem' in kwargs['tls'].cert[1])
self.assertEquals(False, kwargs['tls'].assert_hostname)
self.assertEqual(False, kwargs['tls'].assert_hostname)
try:
client = Client(**kwargs)
self.assertEquals(kwargs['base_url'], client.base_url)
self.assertEquals(kwargs['tls'].verify, client.verify)
self.assertEquals(kwargs['tls'].cert, client.cert)
self.assertEqual(kwargs['base_url'], client.base_url)
self.assertEqual(kwargs['tls'].verify, client.verify)
self.assertEqual(kwargs['tls'].cert, client.cert)
except TypeError as e:
self.fail(e)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment