Repository: ansible-collections/community.general

Issue:
New Docker Stack Task Info Module
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
I would like to list all stacks tasks from a given stack name like `docker stack ps` in order to run some setup containers if a particular stack is not present.

##### ISSUE TYPE

- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->

docker_stack_task_info.py

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml
   - name: List all docker stacks
     docker_stack_task_info:
       name: test_stack      # return list tasks from specific stack name
```

<!--- HINT: You can also paste gist.github.com links for larger files -->


Discussion:
Files identified in the description:
* [`plugins/modules/cloud/docker/docker_config.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_config.py)
* [`plugins/modules/cloud/docker/docker_container.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_container.py)
* [`plugins/modules/cloud/docker/docker_container_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_container_info.py)
* [`plugins/modules/cloud/docker/docker_host_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_host_info.py)
* [`plugins/modules/cloud/docker/docker_image.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_image.py)
* [`plugins/modules/cloud/docker/docker_image_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_image_info.py)
* [`plugins/modules/cloud/docker/docker_network.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_network.py)
* [`plugins/modules/cloud/docker/docker_network_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_network_info.py)
* [`plugins/modules/cloud/docker/docker_node_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_node_info.py)
* [`plugins/modules/cloud/docker/docker_secret.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_secret.py)
* [`plugins/modules/cloud/docker/docker_stack.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_stack.py)
* [`plugins/modules/cloud/docker/docker_stack_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_stack_info.py)
* [`plugins/modules/cloud/docker/docker_swarm.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_swarm.py)
* [`plugins/modules/cloud/docker/docker_swarm_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_swarm_info.py)
* [`plugins/modules/cloud/docker/docker_swarm_service.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_swarm_service.py)
* [`plugins/modules/cloud/docker/docker_swarm_service_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_swarm_service_info.py)
* [`plugins/modules/cloud/docker/docker_volume_info.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/docker/docker_volume_info.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 @DBendit @WojciechowskiPiotr @akshay196 @chouseknecht @danihodovic @dariko @dusdanig @felixfontein @hannseman @jwitko @kassiansun @keitwb @olsaki @softzilla @ssbarnea @tbouvet @ushuz @zfil
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/cloud/docker/docker_stack_task_info.py b/plugins/modules/cloud/docker/docker_stack_task_info.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/cloud/docker/docker_stack_task_info.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Jose Angel Munoz (@imjoseangel)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: docker_stack_task_info
+author: "Jose Angel Munoz (@imjoseangel)"
+short_description: Return information of the tasks on a docker stack
+description:
+  - Retrieve information on docker stacks tasks using the C(docker stack) command
+    on the target node (see examples).
+options:
+  name:
+    description:
+      - Stack name.
+    type: str
+    required: yes
+version_added: "1.1.0"
+'''
+
+RETURN = '''
+results:
+    description: |
+        List of dictionaries containing the list of tasks associated
+        to a stack name.
+    sample: >
+        [{"CurrentState":"Running","DesiredState":"Running","Error":"","ID":"7wqv6m02ugkw","Image":"busybox","Name":"test_stack.1","Node":"swarm","Ports":""}]
+    returned: always
+    type: list
+    elements: dict
+'''
+
+EXAMPLES = '''
+  - name: Shows stack info
+    community.general.docker_stack_task_info:
+      name: test_stack
+    register: result
+
+  - name: Show results
+    ansible.builtin.debug:
+      var: result.results
+'''
+
+import json
+from ansible.module_utils.basic import AnsibleModule
+
+
+def docker_stack_task(module, stack_name):
+    docker_bin = module.get_bin_path('docker', required=True)
+    rc, out, err = module.run_command(
+        [docker_bin, "stack", "ps", stack_name, "--format={{json .}}"])
+
+    return rc, out.strip(), err.strip()
+
+
+def main():
+    module = AnsibleModule(
+        argument_spec={
+            'name': dict(type='str', required=True)
+        },
+        supports_check_mode=False
+    )
+
+    name = module.params['name']
+
+    rc, out, err = docker_stack_task(module, name)
+
+    if rc != 0:
+        module.fail_json(msg="Error running docker stack. {0}".format(err),
+                         rc=rc, stdout=out, stderr=err)
+    else:
+        if out:
+            ret = list(
+                json.loads(outitem)
+                for outitem in out.splitlines())
+
+        else:
+            ret = []
+
+        module.exit_json(changed=False,
+                         rc=rc,
+                         stdout=out,
+                         stderr=err,
+                         results=ret)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/plugins/modules/docker_stack_task_info.py b/plugins/modules/docker_stack_task_info.py
new file mode 120000
--- /dev/null
+++ b/plugins/modules/docker_stack_task_info.py
@@ -0,0 +1 @@
+./cloud/docker/docker_stack_task_info.py
\ No newline at end of file


Test patch:
diff --git a/tests/integration/targets/docker_stack_task_info/aliases b/tests/integration/targets/docker_stack_task_info/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/aliases
@@ -0,0 +1,8 @@
+shippable/posix/group1
+skip/aix
+skip/osx
+skip/freebsd
+destructive
+skip/docker  # The tests sometimes make docker daemon unstable; hence,
+             # we skip all docker-based CI runs to avoid disrupting
+             # the whole CI system.
diff --git a/tests/integration/targets/docker_stack_task_info/files/stack_compose_base.yml b/tests/integration/targets/docker_stack_task_info/files/stack_compose_base.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/files/stack_compose_base.yml
@@ -0,0 +1,5 @@
+version: '3'
+services:
+    busybox:
+        image: busybox:latest
+        command: sleep 3600
diff --git a/tests/integration/targets/docker_stack_task_info/files/stack_compose_overrides.yml b/tests/integration/targets/docker_stack_task_info/files/stack_compose_overrides.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/files/stack_compose_overrides.yml
@@ -0,0 +1,5 @@
+version: '3'
+services:
+    busybox:
+        environment:
+            envvar: value
diff --git a/tests/integration/targets/docker_stack_task_info/meta/main.yml b/tests/integration/targets/docker_stack_task_info/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+  - setup_docker
diff --git a/tests/integration/targets/docker_stack_task_info/tasks/main.yml b/tests/integration/targets/docker_stack_task_info/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/tasks/main.yml
@@ -0,0 +1,5 @@
+- include_tasks: test_stack_task_info.yml
+  when: docker_api_version is version('1.25', '>=')
+
+- fail: msg="Too old docker / docker-py version to run docker_stack tests!"
+  when: not(docker_api_version is version('1.25', '>=')) and (ansible_distribution != 'CentOS' or ansible_distribution_major_version|int > 6)
diff --git a/tests/integration/targets/docker_stack_task_info/tasks/test_stack_task_info.yml b/tests/integration/targets/docker_stack_task_info/tasks/test_stack_task_info.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/tasks/test_stack_task_info.yml
@@ -0,0 +1,80 @@
+---
+- block:
+  - name: Make sure we're not already using Docker swarm
+    docker_swarm:
+      state: absent
+      force: true
+
+  - name: Get docker_stack_info when docker is not running in swarm mode
+    docker_stack_info:
+    ignore_errors: true
+    register: output
+
+  - name: Assert failure when called when swarm is not running
+    assert:
+      that:
+        - 'output is failed'
+        - '"Error running docker stack" in output.msg'
+
+  - name: Create a swarm cluster
+    docker_swarm:
+      state: present
+      advertise_addr: "{{ ansible_default_ipv4.address }}"
+
+  - name: Get docker_stack_info when docker is running and not stack available
+    docker_stack_info:
+    register: output
+
+  - name: Assert stack facts
+    assert:
+      that:
+        - 'output.results | type_debug == "list"'
+        - 'output.results | length == 0'
+
+  - name: Copy compose files
+    copy:
+      src: "{{ item }}"
+      dest: "{{ output_dir }}/"
+    with_items:
+      - stack_compose_base.yml
+      - stack_compose_overrides.yml
+
+  - name: Install docker_stack python requirements
+    pip:
+      name: jsondiff,pyyaml
+
+  - name: Create stack with compose file
+    register: output
+    docker_stack:
+      state: present
+      name: test_stack
+      compose:
+        - "{{ output_dir }}/stack_compose_base.yml"
+
+  - name: Assert test_stack changed on stack creation with compose file
+    assert:
+      that:
+        - output is changed
+
+  - name: Get docker_stack_info when docker is running
+    docker_stack_info:
+    register: output
+
+  - name: Get docker_stack_task_info first element
+    docker_stack_task_info:
+      name: "{{ output.results[0].Name }}"
+    register: output
+
+  - name: assert stack facts
+    assert:
+      that:
+        - 'output.results | type_debug == "list"'
+        - 'output.results[0].DesiredState == "Running"'
+        - 'output.results[0].Image == "busybox:latest"'
+        - 'output.results[0].Name == "test_stack_busybox.1"'
+
+  always:
+    - name: Cleanup
+      docker_swarm:
+        state: absent
+        force: true
diff --git a/tests/integration/targets/docker_stack_task_info/vars/main.yml b/tests/integration/targets/docker_stack_task_info/vars/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/docker_stack_task_info/vars/main.yml
@@ -0,0 +1,15 @@
+stack_compose_base:
+    version: '3'
+    services:
+        busybox:
+            image: busybox:latest
+            command: sleep 3600
+
+stack_compose_overrides:
+    version: '3'
+    services:
+        busybox:
+            environment:
+                envvar: value
+
+stack_update_expected_diff: '{"test_stack_busybox": {"TaskTemplate": {"ContainerSpec": {"Env": ["envvar=value"]}}}}'