Repository: ansible/ansible

Issue:
docker_container: ambiguous parameter "stop_timeout"
##### SUMMARY
I have a container that takes more time to stop than the default stop timeout of 10s. `docker run` command allows me to override this default setting on each container using `--stop-timeout`. I expected the ansible module `docker_container`s parameter `stop_timeout` to behave the same. But after checking the sources, I realized that `stop_timeout` only applies when `state: stopped` is set.

Anyway, eg. the docker host is restarted, the timeout of 10s still applies, which causes the container to be killed and may corrupt data.

1. Can you please clarify the docs that `stop_timeout` only applies when `state: stopped` is set. I can send you a PR if you want.
1. Is there a way using `docker_container` module to actually set `--stop-timeout` on container level?
1. Could we update the API so that `stop_timeout` behaves the same as `docker run --stop-timeout=x` does?

##### ISSUE TYPE
 - Documentation Report
 - Feature Idea

##### COMPONENT NAME
docker_container

##### ANSIBLE VERSION
<!--- Paste, BELOW THIS COMMENT, verbatim output from "ansible --version" between quotes below -->
```
ansible 2.5.4
  config file = /Users/remo.wenger/.ansible.cfg
  configured module search path = [u'/Users/remo.wenger/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/Cellar/ansible/2.5.4/libexec/lib/python2.7/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 2.7.15 (default, Jun 17 2018, 13:05:56) [GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)]
```

##### CONFIGURATION
```
ANSIBLE_NOCOWS(/Users/remo.wenger/.ansible.cfg) = True
DEFAULT_FORKS(/Users/remo.wenger/.ansible.cfg) = 100
DEFAULT_HOST_LIST(/Users/remo.wenger/.ansible.cfg) = [u'/Users/remo.wenger/environments/production']
DEFAULT_REMOTE_USER(/Users/remo.wenger/.ansible.cfg) = remo.wenger
DEFAULT_ROLES_PATH(/Users/remo.wenger/.ansible.cfg) = [u'/Users/remo.wenger/.ansible/roles']
DEFAULT_VAULT_PASSWORD_FILE(/Users/remo.wenger/.ansible.cfg) = /Users/remo.wenger/.ansible/vault_password_file
HOST_KEY_CHECKING(/Users/remo.wenger/.ansible.cfg) = False
RETRY_FILES_ENABLED(/Users/remo.wenger/.ansible.cfg) = False
```

##### OS / ENVIRONMENT
N/A

##### STEPS TO REPRODUCE
If you agree to update the API, the following playbook should behave like this command:
`docker run --stop-timeout=30 -d -ti --name=zkui goodguide/zk-web:latest`

```yaml
- hosts: all
  become: yes
  tasks:
    - name: Launch container
      docker_container:
        name: zkui
        image: "goodguide/zk-web:latest"
        stop_timeout: 30
```


##### EXPECTED RESULTS
```
# docker inspect zkui | grep -i timeout
            "StopTimeout": 30
```

##### ACTUAL RESULTS

```
# docker inspect zkui | grep -i timeout
#
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/docker/docker_container.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/docker/docker_container.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @chouseknecht @cove @dusdanig @joshuaconner @kassiansun @softzilla @zfil
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
1. Yes, but I think option 3 is better.
2. You can refer to `docker-py` documents, but a google shows there're two options, I think we need to handle both?
    1. https://github.com/docker/docker-py/blob/master/docker/api/container.py#L383, this is for `create_container`
    2. http://docker-py.readthedocs.io/en/stable/api.html#docker.api.container.ContainerApiMixin.stop, this is for `stop`.
3. I think this will be the ultimate solution.

So I think we need to use `stop_timeout` to apply to both `stopped` state, which is to override container's `StopTimeout` config. And other states ("present"), to change the container's `StopTimeout` config. Documentation needs updating, too.

Glad if you can send an MR for this :)

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
@@ -391,6 +391,13 @@
   stop_timeout:
     description:
       - Number of seconds to wait for the container to stop before sending SIGKILL.
+        When the container is created by this module, its C(StopTimeout) configuration
+        will be set to this value.
+      - When the container is stopped, will be used as a timeout for stopping the
+        container. In case the container has a custom C(StopTimeout) configuration,
+        the behavior depends on the version of docker. New versions of docker will
+        always use the container's configured C(StopTimeout) value if it has been
+        configured.
   trust_image_content:
     description:
       - If C(yes), skip image verification.
@@ -930,6 +937,9 @@ def create_parameters(self):
             create_params['cpu_shares'] = 'cpu_shares'
             create_params['volume_driver'] = 'volume_driver'
 
+        if self.client.HAS_STOP_TIMEOUT_OPT:
+            create_params['stop_timeout'] = 'stop_timeout'
+
         result = dict(
             host_config=self._host_config(),
             volumes=self._get_mounts(),
@@ -1494,6 +1504,10 @@ def has_different_configuration(self, image):
             # auto_remove is only supported in docker>=2
             config_mapping['auto_remove'] = host_config.get('AutoRemove')
 
+        if self.parameters.client.HAS_STOP_TIMEOUT_OPT:
+            # stop_timeout is only supported in docker>=2.1
+            config_mapping['stop_timeout'] = config.get('StopTimeout')
+
         if HAS_DOCKER_PY_3:
             # volume_driver moved to create_host_config in > 3
             config_mapping['volume_driver'] = host_config.get('VolumeDriver')
@@ -2187,6 +2201,9 @@ def _parse_comparisons(self):
             ulimits='set(dict)',
         )
         all_options = set()  # this is for improving user feedback when a wrong option was specified for comparison
+        default_values = dict(
+            stop_timeout='ignore',
+        )
         for option, data in self.module.argument_spec.items():
             all_options.add(option)
             for alias in data.get('aliases', []):
@@ -2195,7 +2212,7 @@ def _parse_comparisons(self):
             if option in ('docker_host', 'tls_hostname', 'api_version', 'timeout', 'cacert_path', 'cert_path',
                           'key_path', 'ssl_version', 'tls', 'tls_verify', 'debug', 'env_file', 'force_kill',
                           'keep_volumes', 'ignore_image', 'name', 'pull', 'purge_networks', 'recreate',
-                          'restart', 'state', 'stop_timeout', 'trust_image_content', 'networks'):
+                          'restart', 'state', 'trust_image_content', 'networks'):
                 continue
             # Determine option type
             if option in explicit_types:
@@ -2207,7 +2224,9 @@ def _parse_comparisons(self):
             else:
                 type = 'value'
             # Determine comparison type
-            if type in ('list', 'value'):
+            if option in default_values:
+                comparison = default_values[option]
+            elif type in ('list', 'value'):
                 comparison = 'strict'
             else:
                 comparison = 'allow_more_present'
@@ -2284,10 +2303,29 @@ def __init__(self, **kwargs):
             self.fail("docker or docker-py version is %s. Minimum version required is 2.3 to set cpuset_mems option. "
                       "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,))
 
+        stop_timeout_supported = LooseVersion(docker_api_version) >= LooseVersion('1.25')
+        stop_timeout_needed_for_update = self.module.params.get("stop_timeout") is not None and self.module.params.get('state') != 'absent'
+        if stop_timeout_supported:
+            stop_timeout_supported = LooseVersion(docker_version) >= LooseVersion('2.1')
+            if stop_timeout_needed_for_update and not stop_timeout_supported:
+                # We warn (instead of fail) since in older versions, stop_timeout was not used
+                # to update the container's configuration, but only when stopping a container.
+                self.module.warn("docker or docker-py version is %s. Minimum version required is 2.1 to update "
+                                 "the container's stop_timeout configuration. "
+                                 "If you use the 'docker-py' module, you have to switch to the docker 'Python' package." % (docker_version,))
+        else:
+            if stop_timeout_needed_for_update and not stop_timeout_supported:
+                # We warn (instead of fail) since in older versions, stop_timeout was not used
+                # to update the container's configuration, but only when stopping a container.
+                self.module.warn("docker API version is %s. Minimum version required is 1.25 to set or "
+                                 "update the container's stop_timeout configuration." % (docker_api_version,))
+
         self.HAS_INIT_OPT = init_supported
         self.HAS_UTS_MODE_OPT = uts_mode_supported
         self.HAS_BLKIO_WEIGHT_OPT = blkio_weight_supported
         self.HAS_CPUSET_MEMS_OPT = cpuset_mems_supported
+        self.HAS_STOP_TIMEOUT_OPT = stop_timeout_supported
+
         self.HAS_AUTO_REMOVE_OPT = HAS_DOCKER_PY_2 or HAS_DOCKER_PY_3
         if self.module.params.get('auto_remove') and not self.HAS_AUTO_REMOVE_OPT:
             self.fail("'auto_remove' is not compatible with the 'docker-py' Python package. It requires the newer 'docker' Python package.")


Test patch:
diff --git a/test/integration/targets/docker_container/tasks/tests/comparisons.yml b/test/integration/targets/docker_container/tasks/tests/comparisons.yml
--- a/test/integration/targets/docker_container/tasks/tests/comparisons.yml
+++ b/test/integration/targets/docker_container/tasks/tests/comparisons.yml
@@ -381,6 +381,7 @@
     name: "{{ cname }}"
     state: started
     hostname: example.com
+    stop_timeout: 1
     labels:
       ansible.test.1: hello
       ansible.test.2: world
@@ -394,6 +395,7 @@
     name: "{{ cname }}"
     state: started
     hostname: example.org
+    stop_timeout: 2
     labels:
       ansible.test.1: hello
       ansible.test.4: ignore
diff --git a/test/integration/targets/docker_container/tasks/tests/options.yml b/test/integration/targets/docker_container/tasks/tests/options.yml
--- a/test/integration/targets/docker_container/tasks/tests/options.yml
+++ b/test/integration/targets/docker_container/tasks/tests/options.yml
@@ -2626,7 +2626,45 @@
 ## stop_timeout ####################################################
 ####################################################################
 
-# TODO: - stop_timeout
+- name: stop_timeout
+  docker_container:
+    image: alpine:3.8
+    command: '/bin/sh -c "sleep 10m"'
+    name: "{{ cname }}"
+    stop_timeout: 2
+    state: started
+  register: stop_timeout_1
+
+- name: stop_timeout (idempotency)
+  docker_container:
+    image: alpine:3.8
+    command: '/bin/sh -c "sleep 10m"'
+    name: "{{ cname }}"
+    stop_timeout: 2
+    state: started
+  register: stop_timeout_2
+
+- name: stop_timeout (no change)
+  # stop_timeout changes are ignored by default
+  docker_container:
+    image: alpine:3.8
+    command: '/bin/sh -c "sleep 10m"'
+    name: "{{ cname }}"
+    stop_timeout: 1
+    state: started
+  register: stop_timeout_3
+
+- name: cleanup
+  docker_container:
+    name: "{{ cname }}"
+    state: absent
+    stop_timeout: 1
+
+- assert:
+    that:
+    - stop_timeout_1 is changed
+    - stop_timeout_2 is not changed
+    - stop_timeout_3 is not changed
 
 ####################################################################
 ## sysctls #########################################################