From e96549c95d44120d885bcacfeacd3d6a56fce579 Mon Sep 17 00:00:00 2001
From: Andrew Pashkin <andrew.pashkin@gmx.co.uk>
Date: Fri, 2 Oct 2015 00:44:52 +0300
Subject: [PATCH 1/2] Harden matching running containers by "command" in the
 Docker module

Before this patch:

  - Command was matched if 'Command' field of docker-py
    representation of Docker container ends with 'command' passed
    to Ansible docker module by user.
  - That can give false positives and false negatives.
  - For example:
      a) If 'command' was set up with more than one spaces,
         like 'command=sleep  123', it would be never matched again
         with a container(s) launched by this task.
         Because after launching, command would be normalized and
         appear, in docker-py API call, just as 'sleep 123' - with one
         space. This is false negative case.
      b) If 'entrypoint + command = command', for example
         'sleep + 123 = sleep 123', module would give false positive
         match.

This patch fixes it, by making matching more explicit - against
'Config'->Cmd' field of 'docker inspect' output, provided by docker-py
API and with proper normalization of user input by splitting it to
tokens with 'shlex.split()'.
---
 cloud/docker/docker.py | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py
index 0ab564208ba..cefae3db3df 100644
--- a/cloud/docker/docker.py
+++ b/cloud/docker/docker.py
@@ -1314,8 +1314,8 @@ class DockerManager(object):
         """
 
         command = self.module.params.get('command')
-        if command:
-            command = command.strip()
+        if command is not None:
+            command = shlex.split(command)
         name = self.module.params.get('name')
         if name and not name.startswith('/'):
             name = '/' + name
@@ -1342,13 +1342,10 @@ class DockerManager(object):
                 details = _docker_id_quirk(details)
 
                 running_image = normalize_image(details['Config']['Image'])
-                running_command = container['Command'].strip()
 
                 image_matches = running_image in repo_tags
 
-                # if a container has an entrypoint, `command` will actually equal
-                # '{} {}'.format(entrypoint, command)
-                command_matches = (not command or running_command.endswith(command))
+                command_matches = command == details['Config']['Cmd']
 
                 matches = image_matches and command_matches
 

From cee7e928fc2cb911480aae0c3ed53501034f4611 Mon Sep 17 00:00:00 2001
From: Andrew Pashkin <andrew.pashkin@gmx.co.uk>
Date: Fri, 2 Oct 2015 01:09:08 +0300
Subject: [PATCH 2/2] Add 'entrypoint' parameter to Docker module

---
 cloud/docker/docker.py | 34 +++++++++++++++++++++++++++++++++-
 1 file changed, 33 insertions(+), 1 deletion(-)

diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py
index cefae3db3df..3bc42629709 100644
--- a/cloud/docker/docker.py
+++ b/cloud/docker/docker.py
@@ -46,6 +46,14 @@ options:
     default: missing
     choices: [ "missing", "always" ]
     version_added: "1.9"
+  entrypoint:
+    description:
+      - Corresponds to ``--entrypoint`` option of ``docker run`` command and
+        ``ENTRYPOINT`` directive of Dockerfile.
+        Used to match and launch containers.
+    default: null
+    required: false
+    version_added: "2.0"
   command:
     description:
       - Command used to match and launch containers.
@@ -1043,6 +1051,21 @@ class DockerManager(object):
                 differing.append(container)
                 continue
 
+            # ENTRYPOINT
+
+            expected_entrypoint = self.module.params.get('entrypoint')
+            if expected_entrypoint:
+                expected_entrypoint = shlex.split(expected_entrypoint)
+                actual_entrypoint = container["Config"]["Entrypoint"]
+
+                if actual_entrypoint != expected_entrypoint:
+                    self.reload_reasons.append(
+                        'entrypoint ({0} => {1})'
+                        .format(actual_entrypoint, expected_entrypoint)
+                    )
+                    differing.append(container)
+                    continue
+
             # COMMAND
 
             expected_command = self.module.params.get('command')
@@ -1313,6 +1336,9 @@ class DockerManager(object):
         Return any matching containers that are already present.
         """
 
+        entrypoint = self.module.params.get('entrypoint')
+        if entrypoint is not None:
+            entrypoint = shlex.split(entrypoint)
         command = self.module.params.get('command')
         if command is not None:
             command = shlex.split(command)
@@ -1346,8 +1372,12 @@ class DockerManager(object):
                 image_matches = running_image in repo_tags
 
                 command_matches = command == details['Config']['Cmd']
+                entrypoint_matches = (
+                    entrypoint == details['Config']['Entrypoint']
+                )
 
-                matches = image_matches and command_matches
+                matches = (image_matches and command_matches and
+                           entrypoint_matches)
 
             if matches:
                 if not details:
@@ -1407,6 +1437,7 @@ class DockerManager(object):
         api_version = self.client.version()['ApiVersion']
 
         params = {'image':        self.module.params.get('image'),
+                  'entrypoint':   self.module.params.get('entrypoint'),
                   'command':      self.module.params.get('command'),
                   'ports':        self.exposed_ports,
                   'volumes':      self.volumes,
@@ -1619,6 +1650,7 @@ def main():
             count           = dict(default=1),
             image           = dict(required=True),
             pull            = dict(required=False, default='missing', choices=['missing', 'always']),
+            entrypoint      = dict(required=False, default=None, type='str'),
             command         = dict(required=False, default=None),
             expose          = dict(required=False, default=None, type='list'),
             ports           = dict(required=False, default=None, type='list'),