diff --git a/changelogs/fragments/3935-add-gitlab-group-runner.yml b/changelogs/fragments/3935-add-gitlab-group-runner.yml new file mode 100644 index 0000000000..eba390180a --- /dev/null +++ b/changelogs/fragments/3935-add-gitlab-group-runner.yml @@ -0,0 +1,2 @@ +minor_changes: + - 'gitlab_runner - allow to register group runner (https://github.com/ansible-collections/community.general/pull/3935).' diff --git a/plugins/modules/gitlab_runner.py b/plugins/modules/gitlab_runner.py index 22d210b6c2..a41b135fc3 100644 --- a/plugins/modules/gitlab_runner.py +++ b/plugins/modules/gitlab_runner.py @@ -44,10 +44,17 @@ attributes: support: none options: + group: + description: + - ID or full path of the group in the form group/subgroup. + - Mutually exclusive with I(owned) and I(project). + type: str + version_added: '6.5.0' project: description: - ID or full path of the project in the form of group/name. - Mutually exclusive with I(owned) since community.general 4.5.0. + - Mutually exclusive with I(group). type: str version_added: '3.7.0' description: @@ -73,6 +80,7 @@ options: description: - Searches only runners available to the user when searching for existing, when false admin token required. - Mutually exclusive with I(project) since community.general 4.5.0. + - Mutually exclusive with I(group). default: false type: bool version_added: 2.0.0 @@ -209,21 +217,23 @@ except NameError: class GitLabRunner(object): - def __init__(self, module, gitlab_instance, project=None): + def __init__(self, module, gitlab_instance, group=None, project=None): self._module = module self._gitlab = gitlab_instance + self.runner_object = None + # Whether to operate on GitLab-instance-wide or project-wide runners # See https://gitlab.com/gitlab-org/gitlab-ce/issues/60774 # for group runner token access if project: self._runners_endpoint = project.runners.list + elif group: + self._runners_endpoint = group.runners.list elif module.params['owned']: self._runners_endpoint = gitlab_instance.runners.list else: self._runners_endpoint = gitlab_instance.runners.all - self.runner_object = None - def create_or_update_runner(self, description, options): changed = False @@ -360,6 +370,7 @@ def main(): maximum_timeout=dict(type='int', default=3600), registration_token=dict(type='str', no_log=True), project=dict(type='str'), + group=dict(type='str'), state=dict(type='str', default="present", choices=["absent", "present"]), )) @@ -372,6 +383,8 @@ def main(): ['api_token', 'api_oauth_token'], ['api_token', 'api_job_token'], ['project', 'owned'], + ['group', 'owned'], + ['project', 'group'], ], required_together=[ ['api_username', 'api_password'], @@ -396,6 +409,7 @@ def main(): maximum_timeout = module.params['maximum_timeout'] registration_token = module.params['registration_token'] project = module.params['project'] + group = module.params['group'] if access_level is None: message = "The option 'access_level' is unspecified, so 'ref_protected' is assumed. "\ @@ -408,13 +422,20 @@ def main(): gitlab_instance = gitlab_authentication(module) gitlab_project = None + gitlab_group = None + if project: try: gitlab_project = gitlab_instance.projects.get(project) except gitlab.exceptions.GitlabGetError as e: module.fail_json(msg='No such a project %s' % project, exception=to_native(e)) + elif group: + try: + gitlab_group = gitlab_instance.groups.get(group) + except gitlab.exceptions.GitlabGetError as e: + module.fail_json(msg='No such a group %s' % group, exception=to_native(e)) - gitlab_runner = GitLabRunner(module, gitlab_instance, gitlab_project) + gitlab_runner = GitLabRunner(module, gitlab_instance, gitlab_group, gitlab_project) runner_exists = gitlab_runner.exists_runner(runner_description) if state == 'absent': diff --git a/tests/unit/plugins/modules/gitlab.py b/tests/unit/plugins/modules/gitlab.py index 13fc35f899..c64d99fff2 100644 --- a/tests/unit/plugins/modules/gitlab.py +++ b/tests/unit/plugins/modules/gitlab.py @@ -213,6 +213,31 @@ def resp_get_group(url, request): return response(200, content, headers, None, 5, request) +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/foo-bar", method="get") +def resp_get_group_by_name(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "name": "Foobar Group", "path": "foo-bar",' + '"description": "An interesting group", "visibility": "public",' + '"lfs_enabled": true, "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",' + '"web_url": "http://localhost:3000/groups/foo-bar", "request_access_enabled": false,' + '"full_name": "Foobar Group", "full_path": "foo-bar",' + '"project_creation_level": "maintainer", "subgroup_creation_level": "maintainer",' + '"require_two_factor_authentication": true,' + '"file_template_project_id": 1, "parent_id": null, "projects": [{"id": 1,"description": null, "default_branch": "master",' + '"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",' + '"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",' + '"web_url": "http://example.com/diaspora/diaspora-client",' + '"readme_url": "http://example.com/diaspora/diaspora-client/blob/master/README.md",' + '"tag_list": ["example","disapora client"],"name": "Diaspora Client",' + '"name_with_namespace": "Diaspora / Diaspora Client","path": "diaspora-client",' + '"path_with_namespace": "diaspora/diaspora-client","created_at": "2013-09-30T13:46:02Z",' + '"last_activity_at": "2013-09-30T13:46:02Z","forks_count": 0,' + '"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",' + '"star_count": 0}]}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + @urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1", method="get") def resp_get_missing_group(url, request): headers = {'content-type': 'application/json'} @@ -600,6 +625,40 @@ def resp_find_runners_list(url, request): return response(200, content, headers, None, 5, request) +@urlmatch(scheme="http", netloc="localhost", path=r'/api/v4/projects/1/runners$', method="get") +def resp_find_project_runners(url, request): + headers = {'content-type': 'application/json', + "X-Page": 1, + "X-Next-Page": 2, + "X-Per-Page": 1, + "X-Total-Pages": 1, + "X-Total": 2} + content = ('[{"active": true,"description": "test-1-20220210","id": 1,' + '"is_shared": false,"ip_address": "127.0.0.1","name": null,' + '"online": true,"status": "online"},{"active": true,' + '"description": "test-2-20220210","id": 2,"ip_address": "127.0.0.1",' + '"is_shared": false,"name": null,"online": false,"status": "offline"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path=r'/api/v4/groups/1/runners$', method="get") +def resp_find_group_runners(url, request): + headers = {'content-type': 'application/json', + "X-Page": 1, + "X-Next-Page": 2, + "X-Per-Page": 1, + "X-Total-Pages": 1, + "X-Total": 2} + content = ('[{"active": true,"description": "test-3-20220210","id": 1,' + '"is_shared": false,"ip_address": "127.0.0.1","name": null,' + '"online": true,"status": "online"},{"active": true,' + '"description": "test-4-20220210","id": 2,"ip_address": "127.0.0.1",' + '"is_shared": false,"name": null,"online": false,"status": "offline"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + @urlmatch(scheme="http", netloc="localhost", path=r'/api/v4/runners/1$', method="put") def resp_update_runner(url, request): headers = {'content-type': 'application/json', diff --git a/tests/unit/plugins/modules/test_gitlab_runner.py b/tests/unit/plugins/modules/test_gitlab_runner.py index a3fb0ecabb..987659e9c9 100644 --- a/tests/unit/plugins/modules/test_gitlab_runner.py +++ b/tests/unit/plugins/modules/test_gitlab_runner.py @@ -5,6 +5,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import (absolute_import, division, print_function) + +import gitlab __metaclass__ = type import pytest @@ -23,9 +25,11 @@ try: from .gitlab import (FakeAnsibleModule, GitlabModuleTestCase, python_version_match_requirement, - resp_find_runners_all, - resp_find_runners_list, resp_get_runner, - resp_create_runner, resp_delete_runner) + resp_find_runners_all, resp_find_runners_list, + resp_find_project_runners, resp_find_group_runners, + resp_get_runner, + resp_create_runner, resp_delete_runner, + resp_get_project_by_name, resp_get_group_by_name) # GitLab module requirements if python_version_match_requirement(): @@ -76,6 +80,37 @@ class TestGitlabRunner(GitlabModuleTestCase): self.assertEqual(rvalue, False) + @with_httmock(resp_find_project_runners) + @with_httmock(resp_get_runner) + @with_httmock(resp_get_project_by_name) + def test_project_runner_exist(self): + gitlab_project = self.gitlab_instance.projects.get('foo-bar/diaspora-client') + module_util = GitLabRunner(module=FakeAnsibleModule(), gitlab_instance=self.gitlab_instance, project=gitlab_project) + + rvalue = module_util.exists_runner("test-1-20220210") + + self.assertEqual(rvalue, True) + + rvalue = module_util.exists_runner("test-3-00000000") + + self.assertEqual(rvalue, False) + + @with_httmock(resp_find_group_runners) + @with_httmock(resp_get_group_by_name) + @with_httmock(resp_get_runner) + @pytest.mark.skipif(gitlab.__version__ < "2.3.0", reason="require python-gitlab >= 2.3.0") + def test_group_runner_exist(self): + gitlab_group = self.gitlab_instance.groups.get('foo-bar') + module_util = GitLabRunner(module=FakeAnsibleModule(), gitlab_instance=self.gitlab_instance, group=gitlab_group) + + rvalue = module_util.exists_runner("test-3-20220210") + + self.assertEqual(rvalue, True) + + rvalue = module_util.exists_runner("test-3-00000000") + + self.assertEqual(rvalue, False) + @with_httmock(resp_create_runner) def test_create_runner(self): runner = self.module_util_all.create_runner({"token": "token", "description": "test-1-20150125"})