Repository: ansible/ansible

Issue:
docker_container 's entrypoint directive incompatibility between 2.2.0.0 and 2.1.3.0
_From @stepanv on November 23, 2016 14:37_

<!--- Verify first that your issue/request is not already reported in GitHub -->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report


##### COMPONENT NAME
docker_container

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.2.0.0
  config file =
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
Linux and MacOS, probably unrelated

##### SUMMARY
<!--- Explain the problem briefly -->
`docker_container`'s `entrypoint` directive 

- in 2.2.0.0, it doesn't work with *exec form* parameters correctly
- in 2.2.0.0, it works ok with *shell form* parameters
- in 2.1.3.0, it didn't work with *shell form* parameters
- in 2.1.3.0, it worked with *exec form* parameters

In other words, in Ansible 2.2.0.0, `entrypoint` directive works with *shell form*, such as `/bin/sh -c date` (which is not preferred by docker) but when an array is used, such as `["/bin/sh", "-c", "date"]`, the container is unable to start. It looks like the complex value of `entrypoint` is incorrectly passed to docker.

In Ansible 2.1.3.0, it worked exactly the other way around. The `/bin/sh -c date` didn't work, but the `["bin/sh", "-c", "date"]` worked ok.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

Execute:
```
docker info  # it is expected that Docker is installed
docker pull alpine  # On MacOS, this is required because with `-c local`, `docker_container` incorrectly pulls images (which is just another bug)
ansible-playbook /tmp/reproducer.yml
```

<!--- Paste example playbooks or commands between quotes below -->
File `/tmp/reproducer.yml` content:
```
---

- name: docker_container module entrypoint directive regression
  hosts: localhost
  connection: local

  tasks:
    - name: "Show that running docker container with 'entrypoint' param does not always work"
      docker_container:
        image: alpine
        entrypoint: ["/bin/sh", "-c", "date"] # works in 2.1.3.0 but stopped working in 2.2.0.0
        #entrypoint: "/bin/sh -c date" # works in 2.2.0.0 but did not work in 2.1.3.0
        name: my_test

```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
I would expect Ansible 2.2.0.0 to work the same way as version 2.1.3.0 where a container was created and its logs contains output of `/bin/sh -c date` command:
```
$ docker logs my_test
Wed Nov 23 14:14:28 UTC 2016
```

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
<!--- Paste verbatim command output between quotes below -->
With Ansible 2.2.0.0, when `["/bin/sh", "-c", "date"]` is used:
```
ansible-playbook /tmp/reproducer.yml

PLAY [docker_container module entrypoint directive regression] *****************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [Show that running docker container with 'entrypoint' param does not always work] ***
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "Error starting container 86b656ecf976370936e2c0d3c63ba51bdea47f04f9e77346d0db9b90c993563a: 404 Client Error: Not Found (\"{\"message\":\"oci runtime error: exec: \\\"[/bin/sh,\\\": stat [/bin/sh,: no such file or directory\"}\")"}
	to retry, use: --limit @/tmp/reproducer.retry

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1
```

As you can see, the executable was parsed as `[/bin/sh,` instead, it was supposed to be just `/bin/sh`.

With Ansible 2.1.3.0, when `"/bin/sh -c date"` is used, it's obvious the whole string was used as the executable:
```
fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "Error starting container 09976976506a02149ea346032b15845e162cace65cf1e94e20881f6f6442120e: 404 Client Error: Not Found (\"{\"message\":\"oci runtime error: exec: \\\"/bin/sh -c date\\\": stat /bin/sh -c date: no such file or directory\"}\")"}
```


_Copied from original issue: ansible/ansible-modules-core#5711_


Discussion:
_From @ansibot on November 23, 2016 15:18_

<!-- @stepanv: Thanks for filing this bug report! -->
@ansible ping, this issue is waiting for your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: issue_bug_new --->
_From @jctanner on November 23, 2016 19:50_

@stepanv

The behavior changed because of https://github.com/ansible/ansible-modules-core/commit/a75ee76aa3c359e1b87a136fab71c59f17fe8f9a

 Here's a workaround if you can't make your commands into strings prior to the task call ...

```
entrypoint: '{{ ["/bin/sh", "-c", "date"] | join(" ") }}'
```

@chouseknecht what would the repercussions be if we set the param type back to list and then joined them later?
_From @ansibot on December 7, 2016 20:38_

This repository has been locked. All new issues and pull requests should be filed in https://github.com/ansible/ansible

Please read through the <a href="http://docs.ansible.com/ansible/dev_guide/repomerge.html#move-issues-and-prs-to-new-repo">repomerge</a> page in the dev guide. The guide contains links to tools which automatically move your issue or pull request to the ansible/ansible repo.

<!-- boilerplate: repomerge -->
Just for the record, wouldn't the ` [ .. ] | join(" ")` forget to escape spaces in the array items such as
```
entrypoint: '{{ ["/path with spaces/sh", "-c", "date", "param with spaces"] | join(" ") }}'
```
That's the very reason why `Dockerfile`'s `ENTRYPOINT` prefers the _exec_ form (over the _shell_ one) as specified at https://docs.docker.com/engine/reference/builder/#entrypoint

If you really stopped supporting the _exec_ (array) form, I honestly think that's quite short-sighted.
I'll test it, and see. I don't remember why it changed from a list to a string. It seems we *should* be able to support both.

Resolving patch:
diff --git a/lib/ansible/modules/cloud/docker/docker_container.py b/lib/ansible/modules/cloud/docker/docker_container.py
--- a/lib/ansible/modules/cloud/docker/docker_container.py
+++ b/lib/ansible/modules/cloud/docker/docker_container.py
@@ -574,7 +574,7 @@
   docker_container:
     name: sleepy
     image: ubuntu:14.04
-    command: sleep infinity
+    command: ["sleep", "infinity"]
 
 - name: Add container to networks
   docker_container:
@@ -801,6 +801,14 @@ def __init__(self, client):
                 if network.get('links'):
                     network['links'] = self._parse_links(network['links'])
 
+        if self.entrypoint:
+            # convert from list to str.
+            self.entrypoint = ' '.join([str(x) for x in self.entrypoint])
+
+        if self.command:
+            # convert from list to str
+            self.command = ' '.join([str(x) for x in self.command])
+
     def fail(self, msg):
         self.client.module.fail_json(msg=msg)
 
@@ -1483,7 +1491,6 @@ def _get_expected_devices(self):
         return expected_devices
 
     def _get_expected_entrypoint(self):
-        self.log('_get_expected_entrypoint')
         if not self.parameters.entrypoint:
             return None
         return shlex.split(self.parameters.entrypoint)
@@ -1940,7 +1947,7 @@ def main():
         blkio_weight=dict(type='int'),
         capabilities=dict(type='list'),
         cleanup=dict(type='bool', default=False),
-        command=dict(type='str'),
+        command=dict(type='list'),
         cpu_period=dict(type='int'),
         cpu_quota=dict(type='int'),
         cpuset_cpus=dict(type='str'),
@@ -1953,7 +1960,7 @@ def main():
         dns_search_domains=dict(type='list'),
         env=dict(type='dict'),
         env_file=dict(type='path'),
-        entrypoint=dict(type='str'),
+        entrypoint=dict(type='list'),
         etc_hosts=dict(type='dict'),
         exposed_ports=dict(type='list', aliases=['exposed', 'expose']),
         force_kill=dict(type='bool', default=False, aliases=['forcekill']),


Test patch: