Repository: ansible-collections/community.general

Issue:
Add an option to update any flatpaks if updates are available
### Summary

When I make an adjustment to my Ansible playbook and apply it to an existing system with flatpaks installed, it would be convenient if I could configure it so that the flatpaks are automatically updated when I re-run the playbook.

### Issue Type

Feature Idea

### Component Name

flatpak

### Additional Information

I'm not sure what the convention if any is for doing this but two different approaches to setting this option occured to me:

Have a `state` option which asserts that the flatpaks should be up-to-date:

```yaml
  - name: Install flatpaks
    flatpak:
      name:
        # utilities 
        - com.github.tchx84.Flatseal
      state: uptodate
```

Having a seperate option for enabling auto-updates

```yaml
  - name: Install flatpaks
    flatpak:
      autoupdate: true
      name:
        # utilities 
        - com.github.tchx84.Flatseal
      state: present
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/flatpak.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/flatpak.py)

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @JayKayy @oolongbrothers
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
> ```yaml
>  flatpak:
>      state: uptodate
> ```
Following ansible convention, `state:` would be set to `latest` rather than "uptodate". Otherwise, :+1: I have this use case also.


Resolving patch:
diff --git a/plugins/modules/flatpak.py b/plugins/modules/flatpak.py
--- a/plugins/modules/flatpak.py
+++ b/plugins/modules/flatpak.py
@@ -26,7 +26,9 @@
   - community.general.attributes
 attributes:
   check_mode:
-    support: full
+    support: partial
+    details:
+      - If O(state=latest), the module will always return C(changed=true).
   diff_mode:
     support: none
 options:
@@ -53,12 +55,12 @@
     - Both C(https://) and C(http://) URLs are supported.
     - When supplying a reverse DNS name, you can use the O(remote) option to specify on what remote
       to look for the flatpak. An example for a reverse DNS name is C(org.gnome.gedit).
-    - When used with O(state=absent), it is recommended to specify the name in the reverse DNS
-      format.
-    - When supplying a URL with O(state=absent), the module will try to match the
-      installed flatpak based on the name of the flatpakref to remove it. However, there is no
-      guarantee that the names of the flatpakref file and the reverse DNS name of the installed
-      flatpak do match.
+    - When used with O(state=absent) or O(state=latest), it is recommended to specify the name in
+      the reverse DNS format.
+    - When supplying a URL with O(state=absent) or O(state=latest), the module will try to match the
+      installed flatpak based on the name of the flatpakref to remove or update it. However, there
+      is no guarantee that the names of the flatpakref file and the reverse DNS name of the
+      installed flatpak do match.
     type: list
     elements: str
     required: true
@@ -82,7 +84,8 @@
   state:
     description:
     - Indicates the desired package state.
-    choices: [ absent, present ]
+    - The value V(latest) is supported since community.general 8.6.0.
+    choices: [ absent, present, latest ]
     type: str
     default: present
 '''
@@ -118,6 +121,37 @@
       - org.inkscape.Inkscape
       - org.mozilla.firefox
 
+- name: Update the spotify flatpak
+  community.general.flatpak:
+    name:  https://s3.amazonaws.com/alexlarsson/spotify-repo/spotify.flatpakref
+    state: latest
+
+- name: Update the gedit flatpak package without dependencies (not recommended)
+  community.general.flatpak:
+    name: https://git.gnome.org/browse/gnome-apps-nightly/plain/gedit.flatpakref
+    state: latest
+    no_dependencies: true
+
+- name: Update the gedit package from flathub for current user
+  community.general.flatpak:
+    name: org.gnome.gedit
+    state: latest
+    method: user
+
+- name: Update the Gnome Calendar flatpak from the gnome remote system-wide
+  community.general.flatpak:
+    name: org.gnome.Calendar
+    state: latest
+    remote: gnome
+
+- name: Update multiple packages
+  community.general.flatpak:
+    name:
+      - org.gimp.GIMP
+      - org.inkscape.Inkscape
+      - org.mozilla.firefox
+    state: latest
+
 - name: Remove the gedit flatpak
   community.general.flatpak:
     name: org.gnome.gedit
@@ -195,6 +229,28 @@ def install_flat(module, binary, remote, names, method, no_dependencies):
     result['changed'] = True
 
 
+def update_flat(module, binary, names, method, no_dependencies):
+    """Update existing flatpaks."""
+    global result  # pylint: disable=global-variable-not-assigned
+    installed_flat_names = [
+        _match_installed_flat_name(module, binary, name, method)
+        for name in names
+    ]
+    command = [binary, "update", "--{0}".format(method)]
+    flatpak_version = _flatpak_version(module, binary)
+    if LooseVersion(flatpak_version) < LooseVersion('1.1.3'):
+        command += ["-y"]
+    else:
+        command += ["--noninteractive"]
+    if no_dependencies:
+        command += ["--no-deps"]
+    command += installed_flat_names
+    stdout = _flatpak_command(module, module.check_mode, command)
+    result["changed"] = (
+        True if module.check_mode else stdout.find("Nothing to do.") == -1
+    )
+
+
 def uninstall_flat(module, binary, names, method):
     """Remove existing flatpaks."""
     global result  # pylint: disable=global-variable-not-assigned
@@ -313,7 +369,7 @@ def main():
             method=dict(type='str', default='system',
                         choices=['user', 'system']),
             state=dict(type='str', default='present',
-                       choices=['absent', 'present']),
+                       choices=['absent', 'present', 'latest']),
             no_dependencies=dict(type='bool', default=False),
             executable=dict(type='path', default='flatpak')
         ),
@@ -338,10 +394,13 @@ def main():
         module.fail_json(msg="Executable '%s' was not found on the system." % executable, **result)
 
     installed, not_installed = flatpak_exists(module, binary, name, method)
-    if state == 'present' and not_installed:
-        install_flat(module, binary, remote, not_installed, method, no_dependencies)
-    elif state == 'absent' and installed:
+    if state == 'absent' and installed:
         uninstall_flat(module, binary, installed, method)
+    else:
+        if state == 'latest' and installed:
+            update_flat(module, binary, installed, method, no_dependencies)
+        if state in ('present', 'latest') and not_installed:
+            install_flat(module, binary, remote, not_installed, method, no_dependencies)
 
     module.exit_json(**result)
 


Test patch:
diff --git a/tests/integration/targets/flatpak/tasks/check_mode.yml b/tests/integration/targets/flatpak/tasks/check_mode.yml
--- a/tests/integration/targets/flatpak/tasks/check_mode.yml
+++ b/tests/integration/targets/flatpak/tasks/check_mode.yml
@@ -52,6 +52,38 @@
       - removal_result is not changed
     msg: "Removing an absent flatpak shall mark module execution as not changed"
 
+# state=latest on absent flatpak
+
+- name: Test state=latest of absent flatpak (check mode)
+  flatpak:
+    name: com.dummy.App1
+    remote: dummy-remote
+    state: latest
+  register: latest_result
+  check_mode: true
+
+- name: Verify state=latest of absent flatpak test result (check mode)
+  assert:
+    that:
+      - latest_result is changed
+    msg: "state=latest an absent flatpak shall mark module execution as changed"
+
+- name: Test non-existent idempotency of state=latest of absent flatpak (check mode)
+  flatpak:
+    name: com.dummy.App1
+    remote: dummy-remote
+    state: latest
+  register: double_latest_result
+  check_mode: true
+
+- name: Verify non-existent idempotency of state=latest of absent flatpak test result (check mode)
+  assert:
+    that:
+      - double_latest_result is changed
+    msg: |
+        state=latest an absent flatpak a second time shall still mark module execution
+        as changed in check mode
+
 # state=present with url on absent flatpak
 
 - name: Test addition of absent flatpak with url (check mode)
@@ -101,6 +133,40 @@
       - url_removal_result is not changed
     msg: "Removing an absent flatpak shall mark module execution as not changed"
 
+# state=latest with url on absent flatpak
+
+- name: Test state=latest of absent flatpak with url (check mode)
+  flatpak:
+    name: http://127.0.0.1:8000/repo/com.dummy.App1.flatpakref
+    remote: dummy-remote
+    state: latest
+  register: url_latest_result
+  check_mode: true
+
+- name: Verify state=latest of absent flatpak with url test result (check mode)
+  assert:
+    that:
+      - url_latest_result is changed
+    msg: "state=latest an absent flatpak from URL shall mark module execution as changed"
+
+- name: Test non-existent idempotency of state=latest of absent flatpak with url (check mode)
+  flatpak:
+    name: http://127.0.0.1:8000/repo/com.dummy.App1.flatpakref
+    remote: dummy-remote
+    state: latest
+  register: double_url_latest_result
+  check_mode: true
+
+- name: >
+      Verify non-existent idempotency of additionof state=latest flatpak with url test
+      result (check mode)
+  assert:
+    that:
+      - double_url_latest_result is changed
+    msg: |
+        state=latest an absent flatpak from URL a second time shall still mark module execution
+        as changed in check mode
+
 # - Tests with present flatpak -------------------------------------------------
 
 # state=present on present flatpak
@@ -149,6 +215,22 @@
         Removing a present flatpak a second time shall still mark module execution
         as changed in check mode
 
+# state=latest on present flatpak
+
+- name: Test state=latest of present flatpak (check mode)
+  flatpak:
+    name: com.dummy.App2
+    remote: dummy-remote
+    state: latest
+  register: latest_present_result
+  check_mode: true
+
+- name: Verify latest test result of present flatpak (check mode)
+  assert:
+    that:
+      - latest_present_result is changed
+    msg: "state=latest an present flatpak shall mark module execution as changed"
+
 # state=present with url on present flatpak
 
 - name: Test addition with url of present flatpak (check mode)
@@ -195,3 +277,19 @@
     that:
       - double_url_removal_present_result is changed
     msg: Removing an absent flatpak a second time shall still mark module execution as changed
+
+# state=latest with url on present flatpak
+
+- name: Test state=latest with url of present flatpak (check mode)
+  flatpak:
+    name: http://127.0.0.1:8000/repo/com.dummy.App2.flatpakref
+    remote: dummy-remote
+    state: latest
+  register: url_latest_present_result
+  check_mode: true
+
+- name: Verify state=latest with url of present flatpak test result (check mode)
+  assert:
+    that:
+      - url_latest_present_result is changed
+    msg: "state=latest a present flatpak from URL shall mark module execution as changed"
diff --git a/tests/integration/targets/flatpak/tasks/test.yml b/tests/integration/targets/flatpak/tasks/test.yml
--- a/tests/integration/targets/flatpak/tasks/test.yml
+++ b/tests/integration/targets/flatpak/tasks/test.yml
@@ -65,6 +65,45 @@
       - double_removal_result is not changed
     msg: "state=absent shall not do anything when flatpak is not present"
 
+# state=latest
+
+- name: Test state=latest - {{ method }}
+  flatpak:
+    name: com.dummy.App1
+    remote: dummy-remote
+    state: present
+    method: "{{ method }}"
+    no_dependencies: true
+  register: latest_result
+
+- name: Verify state=latest test result - {{ method }}
+  assert:
+    that:
+      - latest_result is changed
+    msg: "state=latest shall add flatpak when absent"
+
+- name: Test idempotency of state=latest - {{ method }}
+  flatpak:
+    name: com.dummy.App1
+    remote: dummy-remote
+    state: present
+    method: "{{ method }}"
+    no_dependencies: true
+  register: double_latest_result
+
+- name: Verify idempotency of state=latest test result - {{ method }}
+  assert:
+    that:
+      - double_latest_result is not changed
+    msg: "state=latest shall not do anything when flatpak is already present"
+
+- name: Cleanup after state=present test - {{ method }}
+  flatpak:
+    name: com.dummy.App1
+    state: absent
+    method: "{{ method }}"
+    no_dependencies: true
+
 # state=present with url as name
 
 - name: Test addition with url - {{ method }}
@@ -152,6 +191,45 @@
     method: "{{ method }}"
     no_dependencies: true
 
+# state=latest with url as name
+
+- name: Test state=latest with url - {{ method }}
+  flatpak:
+    name: http://127.0.0.1:8000/repo/com.dummy.App1.flatpakref
+    remote: dummy-remote
+    state: latest
+    method: "{{ method }}"
+    no_dependencies: true
+  register: url_latest_result
+
+- name: Verify state=latest test result - {{ method }}
+  assert:
+    that:
+      - url_latest_result is changed
+    msg: "state=present with url as name shall add flatpak when absent"
+
+- name: Test idempotency of state=latest with url - {{ method }}
+  flatpak:
+    name: http://127.0.0.1:8000/repo/com.dummy.App1.flatpakref
+    remote: dummy-remote
+    state: latest
+    method: "{{ method }}"
+    no_dependencies: true
+  register: double_url_latest_result
+
+- name: Verify idempotency of state=latest with url test result - {{ method }}
+  assert:
+    that:
+      - double_url_latest_result is not changed
+    msg: "state=present with url as name shall not do anything when flatpak is already present"
+
+- name: Cleanup after state=present with url test - {{ method }}
+  flatpak:
+    name: com.dummy.App1
+    state: absent
+    method: "{{ method }}"
+    no_dependencies: true
+
 # state=present with list of packages
 
 - name: Test addition with list - {{ method }}
@@ -287,3 +365,84 @@
     that:
       - double_removal_result is not changed
     msg: "state=absent shall not do anything when flatpak is not present"
+
+# state=latest with list of packages
+
+- name: Test state=latest with list - {{ method }}
+  flatpak:
+    name:
+      - com.dummy.App1
+      - http://127.0.0.1:8000/repo/com.dummy.App2.flatpakref
+    remote: dummy-remote
+    state: latest
+    method: "{{ method }}"
+    no_dependencies: true
+  register: latest_result
+
+- name: Verify state=latest with list test result - {{ method }}
+  assert:
+    that:
+      - latest_result is changed
+    msg: "state=present shall add flatpak when absent"
+
+- name: Test idempotency of state=latest with list - {{ method }}
+  flatpak:
+    name:
+      - com.dummy.App1
+      - http://127.0.0.1:8000/repo/com.dummy.App2.flatpakref
+    remote: dummy-remote
+    state: latest
+    method: "{{ method }}"
+    no_dependencies: true
+  register: double_latest_result
+
+- name: Verify idempotency of state=latest with list test result - {{ method }}
+  assert:
+    that:
+      - double_latest_result is not changed
+    msg: "state=present shall not do anything when flatpak is already present"
+
+- name: Test state=latest with list partially installed - {{ method }}
+  flatpak:
+    name:
+      - com.dummy.App1
+      - http://127.0.0.1:8000/repo/com.dummy.App2.flatpakref
+      - com.dummy.App3
+    remote: dummy-remote
+    state: latest
+    method: "{{ method }}"
+    no_dependencies: true
+  register: latest_result
+
+- name: Verify state=latest with list partially installed test result - {{ method }}
+  assert:
+    that:
+      - latest_result is changed
+    msg: "state=present shall add flatpak when absent"
+
+- name: Test idempotency of state=latest with list partially installed - {{ method }}
+  flatpak:
+    name:
+      - com.dummy.App1
+      - http://127.0.0.1:8000/repo/com.dummy.App2.flatpakref
+      - com.dummy.App3
+    remote: dummy-remote
+    state: latest
+    method: "{{ method }}"
+    no_dependencies: true
+  register: double_latest_result
+
+- name: Verify idempotency of state=latest with list partially installed test result - {{ method }}
+  assert:
+    that:
+      - double_latest_result is not changed
+    msg: "state=present shall not do anything when flatpak is already present"
+
+- name: Cleanup after state=present with list test - {{ method }}
+  flatpak:
+    name:
+      - com.dummy.App1
+      - com.dummy.App2
+      - com.dummy.App3
+    state: absent
+    method: "{{ method }}"