Repository: ansible-collections/community.general

Issue:
gitlab_runner is not idempotent on first run after runner creation
### Summary

On the first run after a GitLab runner creation, Ansible reports a change, although the configuration has not changed in the meanwhile. All subsequent runs on the other hand are idempotent.

### Issue Type

Bug Report

### Component Name

gitlab_runner

### Ansible Version

```console (paste below)
ansible [core 2.14.1]
  config file = None
  configured module search path = ['/home/ansible/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ansible/.local/lib/python3.10/site-packages/ansible
  ansible collection location = /home/ansible/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/ansible/.local/bin/ansible
  python version = 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] (/usr/bin/python3)
  jinja version = 3.0.3
  libyaml = True
```


### Community.general Version

```console (paste below)
Collection        Version
----------------- -------
community.general 6.3.0
```


### Configuration

```console (paste below)
CONFIG_FILE() = None
```


### OS / Environment

Ubuntu 22.04

### Steps to Reproduce

Just create a GitLab runner and re-execute the playbook.

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: Test playbook
  hosts: localhost
  become: false
  gather_facts: false
  tasks:
    - name: Ensure GitLab runner
      community.general.gitlab_runner:
        api_url: https://git.mycompany.com/
        api_token: "{{ gitlab_runner_api_token }}"
        registration_token: "{{ gitlab_runner_registration_token }}"
        name: Test GitLab Runner
        state: present
        project: loc/test
```


### Expected Results

Ansible should report no change on subsequent runs when the runner configuration has not changed.

### Actual Results

```console (paste below)
$ ansible-playbook test-gitlab-runner.yml -vvvv
ansible-playbook [core 2.14.1]
  config file = /home/ansible/automation/projects/provisioning/ansible.cfg
  configured module search path = ['/home/ansible/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ansible/.local/lib/python3.10/site-packages/ansible
  ansible collection location = /home/ansible/automation/projects/provisioning/collections
  executable location = /home/ansible/.local/bin/ansible-playbook
  python version = 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] (/usr/bin/python3)
  jinja version = 3.0.3
  libyaml = True
Using /home/ansible/automation/projects/provisioning/ansible.cfg as config file
setting up inventory plugins
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
yaml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
ini declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
toml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
Loading collection community.general from /home/ansible/automation/projects/provisioning/collections/ansible_collections/community/general
Loading callback plugin default of type stdout, v2.0 from /home/ansible/.local/lib/python3.10/site-packages/ansible/plugins/callback/default.py
Loading collection ansible.posix from /home/ansible/.local/lib/python3.10/site-packages/ansible_collections/ansible/posix
Skipping callback 'default', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
Loading callback plugin ansible.posix.profile_tasks of type aggregate, v2.0 from /home/ansible/.local/lib/python3.10/site-packages/ansible_collections/ansible/posix/plugins/callback/profile_tasks.py

PLAYBOOK: test-gitlab-runner.yml ***************************************************************************************************************************************************
Positional arguments: test-gitlab-runner.yml
verbosity: 4
connection: smart
timeout: 60
become_method: sudo
tags: ('all',)
inventory: ('/etc/ansible/hosts',)
forks: 5
1 plays in test-gitlab-runner.yml

PLAY [Test playbook] ***************************************************************************************************************************************************************

TASK [Ensure GitLab runner] ********************************************************************************************************************************************************
task path: /home/ansible/automation/projects/provisioning/test-gitlab-runner.yml:7
Saturday 28 January 2023  12:34:22 +0100 (0:00:00.053)       0:00:00.053 ******
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: ansible
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /tmp/.ansible-$USER/tmp `"&& mkdir "` echo /tmp/.ansible-$USER/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039 `" && echo ansible-tmp-1674905662.979175-3040807-195011873830039="` echo /tmp/.ansible-$USER/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039 `" ) && sleep 0'
Using module file /home/ansible/automation/projects/provisioning/collections/ansible_collections/community/general/plugins/modules/gitlab_runner.py
<127.0.0.1> PUT /home/ansible/.ansible/tmp/ansible-local-3040798bs03wu04/tmpq8stvhl6 TO /tmp/.ansible-ansible/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039/AnsiballZ_gitlab_runner.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /tmp/.ansible-ansible/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039/ /tmp/.ansible-ansible/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039/AnsiballZ_gitlab_runner.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python3 /tmp/.ansible-ansible/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039/AnsiballZ_gitlab_runner.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /tmp/.ansible-ansible/tmp/ansible-tmp-1674905662.979175-3040807-195011873830039/ > /dev/null 2>&1 && sleep 0'
changed: [localhost] => {
    "changed": true,
    "invocation": {
        "module_args": {
            "access_level": "ref_protected",
            "active": true,
            "api_job_token": null,
            "api_oauth_token": null,
            "api_password": null,
            "api_token": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "api_url": "https://git.mycompany.com/",
            "api_username": null,
            "description": "Test GitLab Runner",
            "locked": false,
            "maximum_timeout": 3600,
            "name": "Test GitLab Runner",
            "owned": false,
            "project": "loc/test",
            "registration_token": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "run_untagged": true,
            "state": "present",
            "tag_list": [],
            "validate_certs": true
        }
    },
    "msg": "Successfully created or updated the runner Test GitLab Runner",
    "runner": {
        "id": 1232,
        "token": "7Qr-JaAzYS2LH9eAi2HV",
        "token_expires_at": null
    }
}

PLAY RECAP *************************************************************************************************************************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Saturday 28 January 2023  12:34:23 +0100 (0:00:01.004)       0:00:01.057 ******
===============================================================================
Ensure GitLab runner -------------------------------------------------------------------------------------------------------------------------------------------------------- 1.00s
/home/ansible/automation/projects/provisioning/test-gitlab-runner.yml:7 -----------------------------------------------------------------------------------------------------------

$ ansible-playbook test-gitlab-runner.yml -vvvv
ansible-playbook [core 2.14.1]
  config file = /home/ansible/automation/projects/provisioning/ansible.cfg
  configured module search path = ['/home/ansible/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ansible/.local/lib/python3.10/site-packages/ansible
  ansible collection location = /home/ansible/automation/projects/provisioning/collections
  executable location = /home/ansible/.local/bin/ansible-playbook
  python version = 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] (/usr/bin/python3)
  jinja version = 3.0.3
  libyaml = True
Using /home/ansible/automation/projects/provisioning/ansible.cfg as config file
setting up inventory plugins
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
yaml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
ini declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Skipping due to inventory source not existing or not being readable by the current user
toml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
Loading collection community.general from /home/ansible/automation/projects/provisioning/collections/ansible_collections/community/general
Loading callback plugin default of type stdout, v2.0 from /home/ansible/.local/lib/python3.10/site-packages/ansible/plugins/callback/default.py
Loading collection ansible.posix from /home/ansible/.local/lib/python3.10/site-packages/ansible_collections/ansible/posix
Skipping callback 'default', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
Loading callback plugin ansible.posix.profile_tasks of type aggregate, v2.0 from /home/ansible/.local/lib/python3.10/site-packages/ansible_collections/ansible/posix/plugins/callback/profile_tasks.py

PLAYBOOK: test-gitlab-runner.yml ***************************************************************************************************************************************************
Positional arguments: test-gitlab-runner.yml
verbosity: 4
connection: smart
timeout: 60
become_method: sudo
tags: ('all',)
inventory: ('/etc/ansible/hosts',)
forks: 5
1 plays in test-gitlab-runner.yml

PLAY [Test playbook] ***************************************************************************************************************************************************************

TASK [Ensure GitLab runner] ********************************************************************************************************************************************************
task path: /home/ansible/automation/projects/provisioning/test-gitlab-runner.yml:7
Saturday 28 January 2023  12:34:26 +0100 (0:00:00.051)       0:00:00.051 ******
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: ansible
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /tmp/.ansible-$USER/tmp `"&& mkdir "` echo /tmp/.ansible-$USER/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431 `" && echo ansible-tmp-1674905666.289351-3040847-56819068716431="` echo /tmp/.ansible-$USER/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431 `" ) && sleep 0'
Using module file /home/ansible/automation/projects/provisioning/collections/ansible_collections/community/general/plugins/modules/gitlab_runner.py
<127.0.0.1> PUT /home/ansible/.ansible/tmp/ansible-local-30408433yz6d20y/tmplkxru1sf TO /tmp/.ansible-ansible/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431/AnsiballZ_gitlab_runner.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /tmp/.ansible-ansible/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431/ /tmp/.ansible-ansible/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431/AnsiballZ_gitlab_runner.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python3 /tmp/.ansible-ansible/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431/AnsiballZ_gitlab_runner.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /tmp/.ansible-ansible/tmp/ansible-tmp-1674905666.289351-3040847-56819068716431/ > /dev/null 2>&1 && sleep 0'
changed: [localhost] => {
    "changed": true,
    "invocation": {
        "module_args": {
            "access_level": "ref_protected",
            "active": true,
            "api_job_token": null,
            "api_oauth_token": null,
            "api_password": null,
            "api_token": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "api_url": "https://git.mycompany.com/",
            "api_username": null,
            "description": "Test GitLab Runner",
            "locked": false,
            "maximum_timeout": 3600,
            "name": "Test GitLab Runner",
            "owned": false,
            "project": "loc/test",
            "registration_token": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "run_untagged": true,
            "state": "present",
            "tag_list": [],
            "validate_certs": true
        }
    },
    "msg": "Successfully created or updated the runner Test GitLab Runner",
    "runner": {
        "access_level": "ref_protected",
        "active": true,
        "architecture": null,
        "contacted_at": null,
        "description": "Test GitLab Runner",
        "groups": [],
        "id": 1232,
        "ip_address": "10.2.4.14",
        "is_shared": false,
        "locked": false,
        "maximum_timeout": 3600,
        "name": null,
        "online": null,
        "paused": false,
        "platform": null,
        "projects": [
            {
                "avatar_url": null,
                "created_at": "2023-01-27T12:32:44.132+01:00",
                "default_branch": "main",
                "description": null,
                "forks_count": 0,
                "http_url_to_repo": "https://git.mycompany.com/loc/test.git",
                "id": 1910,
                "last_activity_at": "2023-01-27T13:54:22.868+01:00",
                "name": "test",
                "name_with_namespace": "loc / test",
                "namespace": {
                    "avatar_url": null,
                    "full_path": "loc",
                    "id": 539,
                    "kind": "group",
                    "name": "loc",
                    "parent_id": null,
                    "path": "loc",
                    "web_url": "https://git.mycompany.com/groups/loc"
                },
                "path": "test",
                "path_with_namespace": "loc/test",
                "readme_url": "https://git.mycompany.com/loc/test/-/blob/main/README.md",
                "ssh_url_to_repo": "git@git.mycompany.com:loc/test.git",
                "star_count": 0,
                "tag_list": [],
                "topics": [],
                "web_url": "https://git.mycompany.com/loc/test"
            }
        ],
        "revision": null,
        "run_untagged": true,
        "runner_type": "project_type",
        "status": "never_contacted",
        "tag_list": [],
        "version": null
    }
}

PLAY RECAP *************************************************************************************************************************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Saturday 28 January 2023  12:34:27 +0100 (0:00:01.050)       0:00:01.102 ******
===============================================================================
Ensure GitLab runner -------------------------------------------------------------------------------------------------------------------------------------------------------- 1.05s
/home/ansible/automation/projects/provisioning/test-gitlab-runner.yml:7 -----------------------------------------------------------------------------------------------------------

```


### Code of Conduct

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


Discussion:
Files identified in the description:
* [`plugins/modules/gitlab_runner.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/gitlab_runner.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 @Lunik @SamyCoenen @Shaps @dj-wasabi @lgatellier @marwatk @metanovii @nejch @scodeman @sh0shin @suukit @waheedi @zanssa
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/gitlab_runner.py b/plugins/modules/gitlab_runner.py
--- a/plugins/modules/gitlab_runner.py
+++ b/plugins/modules/gitlab_runner.py
@@ -84,12 +84,23 @@
   access_level:
     description:
       - Determines if a runner can pick up jobs only from protected branches.
+      - If I(access_level_on_creation) is not explicitly set to C(true), this option is ignored on registration and
+        is only applied on updates.
       - If set to C(ref_protected), runner can pick up jobs only from protected branches.
       - If set to C(not_protected), runner can pick up jobs from both protected and unprotected branches.
     required: false
     default: ref_protected
     choices: ["ref_protected", "not_protected"]
     type: str
+  access_level_on_creation:
+    description:
+      - Whether the runner should be registered with an access level or not.
+      - If set to C(true), the value of I(access_level) is used for runner registration.
+      - If set to C(false), GitLab registers the runner with the default access level.
+      - The current default of this option is C(false). This default is deprecated and will change to C(true) in commuinty.general 7.0.0.
+    required: false
+    type: bool
+    version_added: 6.3.0
   maximum_timeout:
     description:
       - The maximum time that a runner has to complete a specific job.
@@ -207,27 +218,34 @@ def __init__(self, module, gitlab_instance, project=None):
     def create_or_update_runner(self, description, options):
         changed = False
 
+        arguments = {
+            'active': options['active'],
+            'locked': options['locked'],
+            'run_untagged': options['run_untagged'],
+            'maximum_timeout': options['maximum_timeout'],
+            'tag_list': options['tag_list'],
+        }
         # Because we have already call userExists in main()
         if self.runner_object is None:
-            runner = self.create_runner({
-                'description': description,
-                'active': options['active'],
-                'token': options['registration_token'],
-                'locked': options['locked'],
-                'run_untagged': options['run_untagged'],
-                'maximum_timeout': options['maximum_timeout'],
-                'tag_list': options['tag_list'],
-            })
+            arguments['description'] = description
+            arguments['token'] = options['registration_token']
+
+            access_level_on_creation = self._module.params['access_level_on_creation']
+            if access_level_on_creation is None:
+                message = "The option 'access_level_on_creation' is unspecified, so 'false' is assumed. "\
+                          "That means any value of 'access_level' is ignored and GitLab registers the runner with its default value. "\
+                          "The option 'access_level_on_creation' will switch to 'true' in community.general 7.0.0"
+                self._module.deprecate(message, version='7.0.0', collection_name='community.general')
+                access_level_on_creation = False
+
+            if access_level_on_creation:
+                arguments['access_level'] = options['access_level']
+
+            runner = self.create_runner(arguments)
             changed = True
         else:
-            changed, runner = self.update_runner(self.runner_object, {
-                'active': options['active'],
-                'locked': options['locked'],
-                'run_untagged': options['run_untagged'],
-                'maximum_timeout': options['maximum_timeout'],
-                'access_level': options['access_level'],
-                'tag_list': options['tag_list'],
-            })
+            arguments['access_level'] = options['access_level']
+            changed, runner = self.update_runner(self.runner_object, arguments)
 
         self.runner_object = runner
         if changed:
@@ -328,6 +346,7 @@ def main():
         run_untagged=dict(type='bool', default=True),
         locked=dict(type='bool', default=False),
         access_level=dict(type='str', default='ref_protected', choices=["not_protected", "ref_protected"]),
+        access_level_on_creation=dict(type='bool'),
         maximum_timeout=dict(type='int', default=3600),
         registration_token=dict(type='str', no_log=True),
         project=dict(type='str'),


Test patch: