Repository: ansible/ansible-modules-core

Issue:
docker module: Default network behavior does not mimic docker run
##### Issue Type:

Bug Report
##### Ansible Version:

1.8
##### Environment:

CentOS 6.5
##### Summary:

I'm filing this issue, though this may be a corner case that a majority of users may never see.

When starting a container with docker run from an image that does not expose ports, docker run will automatically expose the ports defined in -p option passed to the docker run command. With Ansible, just defining the ports for the container does not properly set up networking. The expose option must also be used to list what ports should be exposed. 
##### Steps To Reproduce:

Networking does not get setup.

```

---
- hosts: all
  tasks:
  - name: Start Network container
    docker: image=kubernetes/pause
            name=network
            ports=8080:8080,8443:8443
```

Docker run command automatically exposes the listed ports and sets up networking properly.

```
docker run -d -p 8080:8080 -p 8443:8443 --name network kubernetes/pause
```
##### Expected Results:

Container should have the port exposed and be listed in iptables showing routes for those ports to the container that was started.
##### Actual Results:

Container does not have networking routing setup unless 'expose' option is explicitly defined in the playbook.



Discussion:
I can confirm this. Using the docker module is very frustrating, because it often behaves different than the command line client, and the docs are not helping. Case in point, only this bug report helped me solve my networking issue with expose.

Instead of using the ansible docker module, one often has to use ansible shell running docker command, because this will work.

I can confirm this as well, it is actually very subtle and confusing bug. At least it would make sense to add this behavior to module docs. See https://github.com/ansible/ansible/issues/9648#issuecomment-68571501 for more details.

_sigh_ the docker module is just a PITA

This is urgent! Just cost us an hour to come here....


Resolving patch:
diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py
--- a/cloud/docker/docker_container.py
+++ b/cloud/docker/docker_container.py
@@ -727,8 +727,10 @@ def __init__(self, client):
                 except ValueError as exc:
                     self.fail("Failed to convert %s to bytes: %s" % (param_name, exc))
 
-        self.ports = self._parse_exposed_ports()
         self.published_ports = self._parse_publish_ports()
+        self.ports = self._parse_exposed_ports(self.published_ports)
+        self.log("expose ports:")
+        self.log(self.ports, pretty_print=True)
         self.publish_all_ports = None
         if self.published_ports == 'all':
             self.publish_all_ports = True
@@ -963,22 +965,38 @@ def _get_volume_binds(volumes):
                     )
         return result
 
-    def _parse_exposed_ports(self):
+    def _parse_exposed_ports(self, published_ports):
         '''
         Parse exposed ports from docker CLI-style ports syntax.
         '''
-        if self.exposed_ports is None:
-            return None
-
         exposed = []
-        for port in self.exposed_ports:
-            port = str(port).strip()
-            if port.endswith('/tcp') or port.endswith('/udp'):
-                port_with_proto = tuple(port.split('/'))
-            else:
-                # assume tcp protocol if not specified
-                port_with_proto = (port, 'tcp')
-            exposed.append(port_with_proto)
+        if self.exposed_ports:
+            for port in self.exposed_ports:
+                port = str(port).strip()
+                protocol = 'tcp'
+                match = re.search(r'(/.+$)', port)
+                if match:
+                    protocol = match.group(1)
+                exposed.append((port, protocol))
+        if published_ports:
+            # Any published port should also be exposed
+            for publish_port in published_ports:
+                match = False
+                if isinstance(publish_port, basestring) and '/' in publish_port:
+                    port, protocol = publish_port.split('/')
+                    port = int(port)
+                else:
+                    protocol = 'tcp'
+                    port = int(publish_port)
+                for exposed_port in exposed:
+                    if isinstance(exposed_port[0], basestring) and '-' in exposed_port[0]:
+                        start_port, end_port = exposed_port[0].split('-')
+                        if int(start_port) <= port <= int(end_port):
+                            match = True
+                    elif exposed_port[0] == port:
+                        match = True
+                if not match:
+                    exposed.append((port, protocol))
         return exposed
 
     @staticmethod
@@ -1502,11 +1520,12 @@ def _get_expected_exposed(self, image):
         image_ports = []
         if image:
             image_ports = [re.sub(r'/.+$', '', p) for p in (image['ContainerConfig'].get('ExposedPorts') or {}).keys()]
-        if self.parameters.exposed_ports:
-            param_ports = [str(p) for p in self.parameters.exposed_ports]
-        else:
-            param_ports = []
-        return list(set(image_ports + param_ports))
+        param_ports = []
+        if self.parameters.ports:
+            param_ports = [str(p[0]) for p in self.parameters.ports]
+        result = list(set(image_ports + param_ports))
+        self.log(result, pretty_print=True)
+        return result
 
     def _get_expected_ulimits(self, config_ulimits):
         self.log('_get_expected_ulimits')


Test patch: