diff --git a/README.md b/README.md index 1d5f1d3b..c7f4c432 100644 --- a/README.md +++ b/README.md @@ -27,18 +27,14 @@ Identical to the `docker ps` command. * `c.copy(container, resource)` Identical to the `docker cp` command. -* c.create_container(image, command=None, hostname=None, user=None, detach=False, - stdin_open=False, tty=False, mem_limit=0, ports=None, environment=None, - dns=None, volumes=None, volumes_from=None, privileged=False, name=None) +* c.create_container(image, command=None, hostname=None, user=None, + detach=False, stdin_open=False, tty=False, mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, + privileged=False, name=None) Creates a container that can then be `start`ed. Parameters are similar to those for the `docker run` command except it doesn't support the attach options (`-a`) -In order to create volumes that can be rebinded at start time, use the -following syntax: `volumes={"/srv": {}}`. The `ports` parameter is a -dictionary whose key is the port to expose and the value is an empty -dictionary: `ports={"2181/tcp": {}}`. Note, this will simply expose the ports in -the container, but does not make them available on the host. See `start` -below. +See "Port bindings" and "Using volumes" below for more information on how to +create port bindings and volume mappings. * `c.diff(container)` Identical to the `docker diff` command. @@ -105,16 +101,10 @@ Identical to the `docker search` command. * `c.start(container, binds=None, port_bindings=None, lxc_conf=None)` Similar to the `docker start` command, but doesn't support attach options. Use `docker logs` to recover `stdout`/`stderr` -`binds` Allows to bind a directory in the host to the container. - Similar to the `docker run` command with option `-v="/host:/mnt"`. -Note that you must declare "blank" volumes at container creation to use binds. -Example of binds mapping from host to container: `{'/mnt/srv/': '/srv'}` -`port_bindings` Exposes container ports to the host. This is a -dictionary whose key is the container's port and the value is a `[{'HostIp': '' -'HostPort': ''}]` list. Leaving `HostIp` blank will expose the port on -all host interfaces. By leaving the `HostPort` blank, Docker will -automatically assign a port. For example: `port_bindings={"2181/tcp": [{'HostIp': '', -'HostPort': ''}]}`. +`binds` Allows to bind a directory in the host to the container. See +"Using volumes" below for more information. +`port_bindings` Exposes container ports to the host. See "Port bindings" below +for more information. `lxc_conf` allows to pass LXC configuration options using a dictionary. * `c.stop(container, timeout=10)` @@ -133,3 +123,52 @@ Identical to the `docker version` command. Wait for a container and return its exit code. Similar to the `docker wait` command. + + +Port bindings +============= + +Port bindings is done in two parts. Firstly, by providing a list of ports to +open inside the container in the `Client.create_container` method. + + client.create_container('busybox', 'ls', ports=[1111, 2222]) + +If you wish to use UDP instead of TCP (default), you can declare it like such: + + client.create_container('busybox', 'ls', ports=[(1111, 'udp'), 2222]) + +Bindings are then declared in the `Client.start` method. + + client.start(container_id, port_bindings={ + 1111: 4567, + 2222: None + }) + +You can limit the host address on which the port will be exposed like such: + + client.start(container_id, port_bindings={ + 1111: ('127.0.0.1', 4567) + }) + +or without host port assignment: + + client.start(container_id, port_bindings={ + 1111: ('127.0.0.1',) + }) + + + +Using volumes +============= + +Similarly, volume declaration is done in two parts. First, you have to provide +a list of mountpoints to the `Client.create_container` method. + + client.create_container('busybox', 'ls', volumes=['/mnt/vol1', '/mnt/vol2']) + +Volume mappings are then declared inside the `Client.start` method like this: + + client.start(container_id, bindings={ + '/mnt/vol2': '/home/user1/', + '/mnt/vol1': '/var/www' + }) \ No newline at end of file diff --git a/docker/client.py b/docker/client.py index d9242d3a..90f430ca 100644 --- a/docker/client.py +++ b/docker/client.py @@ -133,6 +133,27 @@ class Client(requests.Session): '{0}={1}'.format(k, v) for k, v in environment.items() ] + if ports and isinstance(ports, list): + exposed_ports = {} + for port_definition in ports: + port = port_definition + proto = None + if isinstance(port_definition, tuple): + if len(port_definition) == 2: + proto = port_definition[1] + port = port_definition[0] + exposed_ports['{0}{1}'.format( + port, + '/' + proto if proto else '' + )] = {} + ports = exposed_ports + + if volumes and isinstance(volumes, list): + volumes_dict = {} + for vol in volumes: + volumes_dict[vol] = {} + volumes = volumes_dict + attach_stdin = False attach_stdout = False attach_stderr = False @@ -598,7 +619,9 @@ class Client(requests.Session): start_config['Binds'] = bind_pairs if port_bindings: - start_config['PortBindings'] = port_bindings + start_config['PortBindings'] = utils.convert_port_bindings( + port_bindings + ) start_config['PublishAllPorts'] = publish_all_ports diff --git a/docker/utils/__init__.py b/docker/utils/__init__.py index 82f94413..386a01af 100644 --- a/docker/utils/__init__.py +++ b/docker/utils/__init__.py @@ -1 +1,3 @@ -from .utils import compare_version, mkbuildcontext, ping, tar # flake8: noqa +from .utils import ( + compare_version, convert_port_bindings, mkbuildcontext, ping, tar +) # flake8: noqa diff --git a/docker/utils/utils.py b/docker/utils/utils.py index 1aa86aab..8fd9e947 100644 --- a/docker/utils/utils.py +++ b/docker/utils/utils.py @@ -60,3 +60,37 @@ def ping(url): return res.status >= 400 except Exception: return False + + +def _convert_port_binding(binding): + result = {'HostIp': '', 'HostPort': ''} + if isinstance(binding, tuple): + if len(binding) == 2: + result['HostPort'] = binding[1] + result['HostIp'] = binding[0] + elif isinstance(binding[0], six.string_types): + result['HostIp'] = binding[0] + else: + result['HostPort'] = binding[0] + else: + result['HostPort'] = binding + + if result['HostPort'] is None: + result['HostPort'] = '' + else: + result['HostPort'] = str(result['HostPort']) + + return result + + +def convert_port_bindings(port_bindings): + result = {} + for k, v in six.iteritems(port_bindings): + key = str(k) + if '/' not in key: + key = key + '/tcp' + if isinstance(v, list): + result[key] = [_convert_port_binding(binding) for binding in v] + else: + result[key] = [_convert_port_binding(v)] + return result diff --git a/tests/test.py b/tests/test.py index e058d287..7417842b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -190,7 +190,7 @@ class DockerClientTest(unittest.TestCase): try: self.client.create_container('busybox', ['ls', mount_dest], - volumes={mount_dest: {}}) + volumes=[mount_dest]) except Exception as e: self.fail('Command should not raise exception: {0}'.format(e)) @@ -207,6 +207,30 @@ class DockerClientTest(unittest.TestCase): self.assertEqual(args[1]['headers'], {'Content-Type': 'application/json'}) + def test_create_container_with_ports(self): + try: + self.client.create_container('busybox', 'ls', + ports=[1111, (2222, 'udp'), (3333,)]) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], + 'unix://var/run/docker.sock/v1.6/containers/create') + self.assertEqual(json.loads(args[1]['data']), + json.loads(''' + {"Tty": false, "Image": "busybox", + "Cmd": ["ls"], "AttachStdin": false, + "Memory": 0, "ExposedPorts": { + "1111": {}, + "2222/udp": {}, + "3333": {} + }, + "AttachStderr": true, "Privileged": false, + "AttachStdout": true, "OpenStdin": false}''')) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + def test_create_container_privileged(self): try: self.client.create_container('busybox', 'true', privileged=True) @@ -324,6 +348,53 @@ class DockerClientTest(unittest.TestCase): docker.client.DEFAULT_TIMEOUT_SECONDS ) + def test_start_container_with_port_binds(self): + self.maxDiff = None + try: + self.client.start(fake_api.FAKE_CONTAINER_ID, port_bindings={ + 1111: None, + 2222: 2222, + '3333/udp': (3333,), + 4444: ('127.0.0.1',), + 5555: ('127.0.0.1', 5555), + 6666: [('127.0.0.1',), ('192.168.0.1',)] + }) + except Exception as e: + self.fail('Command should not raise exception: {0}'.format(e)) + + args = fake_request.call_args + self.assertEqual(args[0][0], 'unix://var/run/docker.sock/v1.6/' + 'containers/3cc2351ab11b/start') + self.assertEqual(json.loads(args[1]['data']), { + "PublishAllPorts": False, + "PortBindings": { + "1111/tcp": [{"HostPort": "", "HostIp": ""}], + "2222/tcp": [{"HostPort": "2222", "HostIp": ""}], + "3333/udp": [{"HostPort": "3333", "HostIp": ""}], + "4444/tcp": [{ + "HostPort": "", + "HostIp": "127.0.0.1" + }], + "5555/tcp": [{ + "HostPort": "5555", + "HostIp": "127.0.0.1" + }], + "6666/tcp": [{ + "HostPort": "", + "HostIp": "127.0.0.1" + }, { + "HostPort": "", + "HostIp": "192.168.0.1" + }] + } + }) + self.assertEqual(args[1]['headers'], + {'Content-Type': 'application/json'}) + self.assertEqual( + args[1]['timeout'], + docker.client.DEFAULT_TIMEOUT_SECONDS + ) + def test_start_container_with_links(self): # one link try: