Repository: ansible-collections/community.general

Issue:
Module: gitlab_runner registration token optional
### Summary

Runner is registered using both API Token/User Password and registration token currently. But token can be queried via API + Project/Group (id or path). Using the already provided fields (+ maybe one extra field to allow/disallow querying the token automatically) nothing needs to be changed at interface/task level.

Pros:

* Easier to use and enhances readability as it specifies to which group/project the runner belongs
* Avoids redundancy for something that can be retrieved automatically
* Avoids specifying and storing an extra secret

Cons:

* Do not see any

### Issue Type

Feature Idea

### Component Name

gitlab_runner

### Additional Information

Gitlab allows registering runners via API (+ queried token) or  just the token. Querying information, updating or deleting runners only works via the API, afaik. Since we have to check presence before doing any modifications, we need the API access anyway.
Currently (according to the doc) the id/path is only used for filtering runners, but can be used for querying the token as well.

Querying https://gitlab.example.com/api/v4/groups/my%2Fsample%2Fgroup https://gitlab.example.com/api/v4/proejcts/my%2Fsample%2Fproject  may return (special json chars omitted for readability):
```
id: 1337
....
runners_token: secretToken
```
In the case the token is not specified in the task the new flow would be:
* Query runners and check presence/need to create or update
* Since token is not specified query token via group (if not disabled)
* Register runner with retrieved token

In the case the token was specified the flow remains the same and it need not be queried.

Maybe I have time to take a shot at implementing it, whoever is faster be my guest.

Best Regards

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/source_control/gitlab/gitlab_runner.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/source_control/gitlab/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 @marwatk @metanovii @scodeman @sh0shin @waheedi @zanssa
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Hi @devrite,

I just started working on your issue.

The `runners_token` value can be retrieved from `GET /api/v4/groups/:id` and `GET /api/v4/projects/:id` resources as you said in the description, but I can't find an equivalent to retrieve the instance-level value.

Do you have an idea ?
Hi @lgatellier, 

afaik you can only query via group or project. Since shared runners can only be managed by admin you probably need the admin API and token. For the shared instance runner.

Probably what the ansible module property owned is for to distinguish admin vs user access?




cc @nejch
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @suukit
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
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 --->
This seems more relevant now that registration tokens are now deprecated in GitLab as of 16.0 and will be removed completely in 17.0. It is moving to a per runner authentication token which can be retrieved from the API.

[Deprecation - Support for registration tokens and server-side runner configuration parameters in `gitlab-runner register` command](https://gitlab.com/gitlab-org/gitlab/-/issues/380872)

If you feel the above should be a new issue I'm happy to raise it.
@tvigers The runner registration API may be deprecated in a future 16.x version (before deletion in 17.0).
Thus, this module may be deprecated too.

The new runner registration workflow only allow runner creation using the Web UI. I don't think a new issue/module will be necessary for the moment.
Not entirely sure yet. There might be an API endpoint for creating a runner that returns the token. Instead of the registration token you will need your users API token.

https://gitlab.com/gitlab-org/gitlab/-/issues/380872#note_1323931180
Hi,

Just opened a PR on `python-gitlab` library to support new runner creation API endpoint : https://github.com/python-gitlab/python-gitlab/pull/2635.

Once it's merged, we can make `registration_token` optional, and handle the new `runner_type` attribute.

Not sure about deprecating `registration_token`, as there's some GitLab feature proposal talking about keeping registration token feature.

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
@@ -48,6 +48,7 @@
     description:
       - ID or full path of the group in the form group/subgroup.
       - Mutually exclusive with O(owned) and O(project).
+      - Must be group's numeric ID if O(registration_token) is not set and O(state=present).
     type: str
     version_added: '6.5.0'
   project:
@@ -55,6 +56,7 @@
       - ID or full path of the project in the form of group/name.
       - Mutually exclusive with O(owned) since community.general 4.5.0.
       - Mutually exclusive with O(group).
+      - Must be project's numeric ID if O(registration_token) is not set and O(state=present).
     type: str
     version_added: '3.7.0'
   description:
@@ -73,8 +75,11 @@
     type: str
   registration_token:
     description:
-      - The registration token is used to register new runners.
-      - Required if O(state=present).
+      - The registration token is used to register new runners before GitLab 16.0.
+      - Required if O(state=present) for GitLab < 16.0.
+      - If set, the runner will be created using the old runner creation workflow.
+      - If not set, the runner will be created using the new runner creation workflow, introduced in GitLab 16.0.
+      - If not set, requires python-gitlab >= 4.0.0.
     type: str
   owned:
     description:
@@ -87,9 +92,18 @@
   active:
     description:
       - Define if the runners is immediately active after creation.
+      - Mutually exclusive with O(paused).
     required: false
     default: true
     type: bool
+  paused:
+    description:
+      - Define if the runners is active or paused after creation.
+      - Mutually exclusive with O(active).
+    required: false
+    default: false
+    type: bool
+    version_added: 8.1.0
   locked:
     description:
       - Determines if the runner is locked or not.
@@ -210,6 +224,9 @@
 )
 
 
+from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
+
+
 class GitLabRunner(object):
     def __init__(self, module, gitlab_instance, group=None, project=None):
         self._module = module
@@ -232,18 +249,32 @@ 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'],
         }
+
+        if options.get('paused') is not None:
+            arguments['paused'] = options['paused']
+        else:
+            arguments['active'] = options['active']
+
         if options.get('access_level') is not None:
             arguments['access_level'] = options['access_level']
         # Because we have already call userExists in main()
         if self.runner_object is None:
             arguments['description'] = description
-            arguments['token'] = options['registration_token']
+            if options.get('registration_token') is not None:
+                arguments['token'] = options['registration_token']
+            elif options.get('group') is not None:
+                arguments['runner_type'] = 'group_type'
+                arguments['group_id'] = options['group']
+            elif options.get('project') is not None:
+                arguments['runner_type'] = 'project_type'
+                arguments['project_id'] = options['project']
+            else:
+                arguments['runner_type'] = 'instance_type'
 
             access_level_on_creation = self._module.params['access_level_on_creation']
             if not access_level_on_creation:
@@ -253,19 +284,17 @@ def create_or_update_runner(self, description, options):
             changed = True
         else:
             changed, runner = self.update_runner(self.runner_object, arguments)
+            if changed:
+                if self._module.check_mode:
+                    self._module.exit_json(changed=True, msg="Successfully updated the runner %s" % description)
+
+                try:
+                    runner.save()
+                except Exception as e:
+                    self._module.fail_json(msg="Failed to update runner: %s " % to_native(e))
 
         self.runner_object = runner
-        if changed:
-            if self._module.check_mode:
-                self._module.exit_json(changed=True, msg="Successfully created or updated the runner %s" % description)
-
-            try:
-                runner.save()
-            except Exception as e:
-                self._module.fail_json(msg="Failed to update runner: %s " % to_native(e))
-            return True
-        else:
-            return False
+        return changed
 
     '''
     @param arguments Attributes of the runner
@@ -275,7 +304,12 @@ def create_runner(self, arguments):
             return True
 
         try:
-            runner = self._gitlab.runners.create(arguments)
+            if arguments.get('token') is not None:
+                runner = self._gitlab.runners.create(arguments)
+            elif LooseVersion(gitlab.__version__) < LooseVersion('4.0.0'):
+                self._module.fail_json(msg="New runner creation workflow requires python-gitlab 4.0.0 or higher")
+            else:
+                runner = self._gitlab.user.runners.create(arguments)
         except (gitlab.exceptions.GitlabCreateError) as e:
             self._module.fail_json(msg="Failed to create runner: %s " % to_native(e))
 
@@ -348,6 +382,7 @@ def main():
     argument_spec.update(dict(
         description=dict(type='str', required=True, aliases=["name"]),
         active=dict(type='bool', default=True),
+        paused=dict(type='bool', default=False),
         owned=dict(type='bool', default=False),
         tag_list=dict(type='list', elements='str', default=[]),
         run_untagged=dict(type='bool', default=True),
@@ -372,6 +407,7 @@ def main():
             ['project', 'owned'],
             ['group', 'owned'],
             ['project', 'group'],
+            ['active', 'paused'],
         ],
         required_together=[
             ['api_username', 'api_password'],
@@ -379,9 +415,6 @@ def main():
         required_one_of=[
             ['api_username', 'api_token', 'api_oauth_token', 'api_job_token'],
         ],
-        required_if=[
-            ('state', 'present', ['registration_token']),
-        ],
         supports_check_mode=True,
     )
     ensure_gitlab_package(module)
@@ -432,6 +465,8 @@ def main():
             "access_level": access_level,
             "maximum_timeout": maximum_timeout,
             "registration_token": registration_token,
+            "group": group,
+            "project": project,
         }):
             module.exit_json(changed=True, runner=gitlab_runner.runner_object._attrs,
                              msg="Successfully created or updated the runner %s" % runner_description)


Test patch: