diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 495ae14f044..9fae56365c1 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1312,7 +1312,6 @@ files: maintainers: $team_nxos labels: - networking - test/units/modules/source_control/test_gitlab_: *gitlab test/sanity/pep8/legacy-files.txt: notified: mattclay hacking/report.py: diff --git a/lib/ansible/module_utils/gitlab.py b/lib/ansible/module_utils/gitlab.py index 684997db52e..6194c20a38e 100644 --- a/lib/ansible/module_utils/gitlab.py +++ b/lib/ansible/module_utils/gitlab.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- -# (c) 2018, Marcus Watkins +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) +# Copyright: (c) 2018, Marcus Watkins # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import import json from ansible.module_utils.urls import fetch_url @@ -35,3 +37,25 @@ def request(module, api_url, project, path, access_token, private_token, rawdata return True, json.loads(content) else: return False, str(status) + ": " + content + + +def findProject(gitlab_instance, identifier): + try: + project = gitlab_instance.projects.get(identifier) + except Exception as e: + current_user = gitlab_instance.user + try: + project = gitlab_instance.projects.get(current_user.username + '/' + identifier) + except Exception as e: + return None + + return project + + +def findGroup(gitlab_instance, identifier): + try: + project = gitlab_instance.groups.get(identifier) + except Exception as e: + return None + + return project diff --git a/lib/ansible/modules/source_control/gitlab_deploy_key.py b/lib/ansible/modules/source_control/gitlab_deploy_key.py index 503c3216078..e84f972f7d0 100644 --- a/lib/ansible/modules/source_control/gitlab_deploy_key.py +++ b/lib/ansible/modules/source_control/gitlab_deploy_key.py @@ -1,91 +1,95 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2018, Marcus Watkins +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) +# Copyright: (c) 2018, Marcus Watkins # Based on code: -# (c) 2013, Phillip Gentry +# Copyright: (c) 2013, Phillip Gentry # 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 - ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - DOCUMENTATION = ''' ---- module: gitlab_deploy_key short_description: Manages GitLab project deploy keys. description: - Adds, updates and removes project deploy keys version_added: "2.6" +author: + - Marcus Watkins (@marwatk) + - Guillaume Martinez (@Lunik) +requirements: + - python >= 2.7 + - python-gitlab python module +extends_documentation_fragment: + - auth_basic options: - api_url: - description: - - GitLab API url, e.g. https://gitlab.example.com/api - required: true - access_token: - description: - - The oauth key provided by GitLab. One of access_token or private_token is required. See https://docs.gitlab.com/ee/api/oauth2.html - required: false - private_token: + api_token: description: - - Personal access token to use. One of private_token or access_token is required. See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html - required: false + - Gitlab token for logging in. + version_added: "2.8" + type: str + aliases: + - private_token + - access_token project: description: - - Numeric project id or name of project in the form of group/name + - Id or Full path of project in the form of group/name required: true + type: str title: description: - Deploy key's title required: true + type: str key: description: - Deploy key required: true + type: str can_push: description: - Whether this key can push to the project type: bool - default: 'no' + default: no state: description: - When C(present) the deploy key added to the project if it doesn't exist. - When C(absent) it will be removed from the project if it exists required: true default: present + type: str choices: [ "present", "absent" ] -author: "Marcus Watkins (@marwatk)" ''' EXAMPLES = ''' -# Example adding a project deploy key -- gitlab_deploy_key: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" +- name: "Adding a project deploy key" + gitlab_deploy_key: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" project: "my_group/my_project" title: "Jenkins CI" state: present key: "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9w..." -# Update the above deploy key to add push access -- gitlab_deploy_key: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" +- name: "Update the above deploy key to add push access" + gitlab_deploy_key: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" project: "my_group/my_project" title: "Jenkins CI" state: present - key: "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9w..." can_push: yes -# Remove the previous deploy key from the project -- gitlab_deploy_key: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" +- name: "Remove the previous deploy key from the project" + gitlab_deploy_key: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" project: "my_group/my_project" state: absent key: "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9w..." @@ -94,140 +98,229 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Success or failure message - returned: always - type: str - sample: "Success" + description: Success or failure message + returned: always + type: str + sample: "Success" result: - description: json parsed response from the server - returned: always - type: dict + description: json parsed response from the server + returned: always + type: dict error: - description: the error message returned by the Gitlab API - returned: failed - type: str - sample: "400: key is already in use" - -previous_version: - description: object describing the state prior to this task - returned: changed - type: dict + description: the error message returned by the Gitlab API + returned: failed + type: str + sample: "400: key is already in use" + +deploy_key: + description: API object + returned: always + type: dict ''' - -import json - -from ansible.module_utils.basic import AnsibleModule -from copy import deepcopy -from ansible.module_utils.gitlab import request - - -def _list(module, api_url, project, access_token, private_token): - path = "/deploy_keys" - return request(module, api_url, project, path, access_token, private_token) - - -def _find(module, api_url, project, key, access_token, private_token): - success, data = _list(module, api_url, project, access_token, private_token) - if success: - for i in data: - if i["key"] == key: - return success, i - return success, None - return success, data - - -def _publish(module, api_url, project, data, access_token, private_token): - path = "/deploy_keys" - method = "POST" - if 'id' in data: - path += "/%s" % str(data["id"]) - method = "PUT" - data = deepcopy(data) - data.pop('id', None) - return request(module, api_url, project, path, access_token, private_token, json.dumps(data, sort_keys=True), method) - - -def _delete(module, api_url, project, key_id, access_token, private_token): - path = "/deploy_keys/%s" % str(key_id) - return request(module, api_url, project, path, access_token, private_token, method='DELETE') - - -def _are_equivalent(input, existing): - for key in ['title', 'key', 'can_push']: - if key in input and key not in existing: - return False - if key not in input and key in existing: - return False - if not input[key] == existing[key]: +import os +import re +import traceback + +GITLAB_IMP_ERR = None +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except Exception: + GITLAB_IMP_ERR = traceback.format_exc() + HAS_GITLAB_PACKAGE = False + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native + +from ansible.module_utils.gitlab import findProject + + +class GitLabDeployKey(object): + def __init__(self, module, gitlab_instance): + self._module = module + self._gitlab = gitlab_instance + self.deployKeyObject = None + + ''' + @param project Project object + @param key_title Title of the key + @param key_key String of the key + @param key_can_push Option of the deployKey + @param options Deploy key options + ''' + def createOrUpdateDeployKey(self, project, key_title, key_key, options): + changed = False + + # Because we have already call existsDeployKey in main() + if self.deployKeyObject is None: + deployKey = self.createDeployKey(project, { + 'title': key_title, + 'key': key_key, + 'can_push': options['can_push']}) + changed = True + else: + changed, deployKey = self.updateDeployKey(self.deployKeyObject, { + 'can_push': options['can_push']}) + + self.deployKeyObject = deployKey + if changed: + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully created or updated the deploy key %s" % key_title) + + try: + deployKey.save() + except Exception as e: + self._module.fail_json(msg="Failed to update deploy key: %s " % e) + return True + else: return False - return True + + ''' + @param project Project Object + @param arguments Attributs of the deployKey + ''' + def createDeployKey(self, project, arguments): + if self._module.check_mode: + return True + + try: + deployKey = project.keys.create(arguments) + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create deploy key: %s " % to_native(e)) + + return deployKey + + ''' + @param deployKey Deploy Key Object + @param arguments Attributs of the deployKey + ''' + def updateDeployKey(self, deployKey, arguments): + changed = False + + for arg_key, arg_value in arguments.items(): + if arguments[arg_key] is not None: + if getattr(deployKey, arg_key) != arguments[arg_key]: + setattr(deployKey, arg_key, arguments[arg_key]) + changed = True + + return (changed, deployKey) + + ''' + @param project Project object + @param key_title Title of the key + ''' + def findDeployKey(self, project, key_title): + deployKeys = project.keys.list() + for deployKey in deployKeys: + if (deployKey.title == key_title): + return deployKey + + ''' + @param project Project object + @param key_title Title of the key + ''' + def existsDeployKey(self, project, key_title): + # When project exists, object will be stored in self.projectObject. + deployKey = self.findDeployKey(project, key_title) + if deployKey: + self.deployKeyObject = deployKey + return True + return False + + def deleteDeployKey(self): + if self._module.check_mode: + return True + + return self.deployKeyObject.delete() + + +def deprecation_warning(module): + deprecated_aliases = ['private_token', 'access_token'] + + module.deprecate("Aliases \'{aliases}\' are deprecated".format(aliases='\', \''.join(deprecated_aliases)), 2.10) def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_token=dict(type='str', no_log=True, aliases=["private_token", "access_token"]), + state=dict(type='str', default="present", choices=["absent", "present"]), + project=dict(type='str', required=True), + key=dict(type='str', required=True), + can_push=dict(type='bool', default=False), + title=dict(type='str', required=True) + )) + module = AnsibleModule( - argument_spec=dict( - api_url=dict(required=True), - access_token=dict(required=False, no_log=True), - private_token=dict(required=False, no_log=True), - project=dict(required=True), - key=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - can_push=dict(default='no', type='bool'), - title=dict(required=True), - ), + argument_spec=argument_spec, mutually_exclusive=[ - ['access_token', 'private_token'] + ['api_username', 'api_token'], + ['api_password', 'api_token'] + ], + required_together=[ + ['api_username', 'api_password'] ], required_one_of=[ - ['access_token', 'private_token'] + ['api_username', 'api_token'] ], supports_check_mode=True, ) - api_url = module.params['api_url'] - access_token = module.params['access_token'] - private_token = module.params['private_token'] - project = module.params['project'] + deprecation_warning(module) + + gitlab_url = re.sub('/api.*', '', module.params['api_url']) + validate_certs = module.params['validate_certs'] + gitlab_user = module.params['api_username'] + gitlab_password = module.params['api_password'] + gitlab_token = module.params['api_token'] + state = module.params['state'] + project_identifier = module.params['project'] + key_title = module.params['title'] + key_keyfile = module.params['key'] + key_can_push = module.params['can_push'] - if not access_token and not private_token: - module.fail_json(msg="need either access_token or private_token") + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) - input = {} + try: + gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password, + private_token=gitlab_token, api_version=4) + gitlab_instance.auth() + except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s" % to_native(e)) + except (gitlab.exceptions.GitlabHttpError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s. \ + Gitlab remove Session API now that private tokens are removed from user API endpoints since version 10.2." % to_native(e)) - for key in ['title', 'key', 'can_push']: - input[key] = module.params[key] + gitlab_deploy_key = GitLabDeployKey(module, gitlab_instance) - success, existing = _find(module, api_url, project, input['key'], access_token, private_token) + project = findProject(gitlab_instance, project_identifier) - if not success: - module.fail_json(msg="failed to list deploy keys", result=existing) + if project is None: + module.fail_json(msg="Failed to create deploy key: project %s doesn't exists" % project_identifier) - if existing: - input['id'] = existing['id'] + deployKey_exists = gitlab_deploy_key.existsDeployKey(project, key_title) - changed = False - success = True - response = None + if state == 'absent': + if deployKey_exists: + gitlab_deploy_key.deleteDeployKey() + module.exit_json(changed=True, msg="Successfully deleted deploy key %s" % key_title) + else: + module.exit_json(changed=False, msg="Deploy key deleted or does not exists") if state == 'present': - if not existing or not _are_equivalent(existing, input): - if not module.check_mode: - success, response = _publish(module, api_url, project, input, access_token, private_token) - changed = True - else: - if existing: - if not module.check_mode: - success, response = _delete(module, api_url, project, existing['id'], access_token, private_token) - changed = True + if gitlab_deploy_key.createOrUpdateDeployKey(project, key_title, key_keyfile, {'can_push': key_can_push}): - if success: - module.exit_json(changed=changed, msg='Success', result=response, previous_version=existing) - else: - module.fail_json(msg='Failure', error=response) + module.exit_json(changed=True, msg="Successfully created or updated the deploy key %s" % key_title, + deploy_key=gitlab_deploy_key.deployKeyObject._attrs) + else: + module.exit_json(changed=False, msg="No need to update the deploy key %s" % key_title, + deploy_key=gitlab_deploy_key.deployKeyObject._attrs) if __name__ == '__main__': diff --git a/lib/ansible/modules/source_control/gitlab_group.py b/lib/ansible/modules/source_control/gitlab_group.py index 87c48785972..72e84021fdf 100644 --- a/lib/ansible/modules/source_control/gitlab_group.py +++ b/lib/ansible/modules/source_control/gitlab_group.py @@ -1,5 +1,7 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) # Copyright: (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -15,203 +17,364 @@ DOCUMENTATION = ''' module: gitlab_group short_description: Creates/updates/deletes Gitlab Groups description: - - When the group does not exist in Gitlab, it will be created. - - When the group does exist and state=absent, the group will be deleted. - - As of Ansible version 2.7, this module make use of a different python module and thus some arguments are deprecated. + - When the group does not exist in Gitlab, it will be created. + - When the group does exist and state=absent, the group will be deleted. version_added: "2.1" -author: "Werner Dijkerman (@dj-wasabi)" +author: + - Werner Dijkerman (@dj-wasabi) + - Guillaume Martinez (@Lunik) requirements: - - python-gitlab python module + - python >= 2.7 + - python-gitlab python module +extends_documentation_fragment: + - auth_basic options: - server_url: - description: - - Url of Gitlab server, with protocol (http or https). - required: true - validate_certs: - description: - - When using https if SSL certificate needs to be verified. - type: bool - default: 'yes' - aliases: - - verify_ssl - login_user: - description: - - Gitlab user name. - login_password: - description: - - Gitlab password for login_user - login_token: - description: - - Gitlab token for logging in. - name: - description: - - Name of the group you want to create. - required: true - path: - description: - - The path of the group you want to create, this will be server_url/group_path - - If not supplied, the group_name will be used. + server_url: description: - description: - - A description for the group. - version_added: "2.7" - state: - description: - - create or delete group. - - Possible values are present and absent. - default: "present" - choices: ["present", "absent"] + - The URL of the Gitlab server, with protocol (i.e. http or https). + required: true + type: str + login_user: + description: + - Gitlab user name. + type: str + login_password: + description: + - Gitlab password for login_user + type: str + api_token: + description: + - Gitlab token for logging in. + type: str + aliases: + - login_token + name: + description: + - Name of the group you want to create. + required: true + type: str + path: + description: + - The path of the group you want to create, this will be server_url/group_path + - If not supplied, the group_name will be used. + type: str + description: + description: + - A description for the group. + version_added: "2.7" + type: str + state: + description: + - create or delete group. + - Possible values are present and absent. + default: present + type: str + choices: ["present", "absent"] + parent: + description: + - Allow to create subgroups + - Id or Full path of parent group in the form of group/name + version_added: "2.8" + type: str + visibility: + description: + - Default visibility of the group + version_added: "2.8" + choices: ["private", "internal", "public"] + default: private + type: str ''' EXAMPLES = ''' - name: "Delete Gitlab Group" - local_action: - gitlab_group: - server_url: http://gitlab.dj-wasabi.local - validate_certs: False - login_token: WnUzDsxjy8230-Dy_k - name: my_first_group - state: absent + gitlab_group: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" + validate_certs: False + name: my_first_group + state: absent - name: "Create Gitlab Group" - local_action: - gitlab_group: - server_url: https://gitlab.dj-wasabi.local" - validate_certs: True - login_user: dj-wasabi - login_password: "MySecretPassword" - name: my_first_group - path: my_first_group - state: present + gitlab_group: + api_url: https://gitlab.example.com/ + validate_certs: True + api_usersername: dj-wasabi + api_password: "MySecretPassword" + name: my_first_group + path: my_first_group + state: present + +# The group will by created at https://gitlab.dj-wasabi.local/super_parent/parent/my_first_group +- name: "Create Gitlab SubGroup" + gitlab_group: + api_url: https://gitlab.example.com/ + validate_certs: True + api_usersername: dj-wasabi + api_password: "MySecretPassword" + name: my_first_group + path: my_first_group + state: present + parent_path: "super_parent/parent" +''' + +RETURN = ''' +msg: + description: Success or failure message + returned: always + type: str + sample: "Success" + +result: + description: json parsed response from the server + returned: always + type: dict + +error: + description: the error message returned by the Gitlab API + returned: failed + type: str + sample: "400: path is already in use" + +group: + description: API object + returned: always + type: dict ''' -RETURN = '''# ''' +import os +import traceback +GITLAB_IMP_ERR = None try: import gitlab HAS_GITLAB_PACKAGE = True except Exception: + GITLAB_IMP_ERR = traceback.format_exc() HAS_GITLAB_PACKAGE = False -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native +from ansible.module_utils.gitlab import findGroup + class GitLabGroup(object): - def __init__(self, module, git): + def __init__(self, module, gitlab_instance): self._module = module - self._gitlab = git + self._gitlab = gitlab_instance self.groupObject = None - def createOrUpdateGroup(self, name, path, description): + ''' + @param group Group object + ''' + def getGroupId(self, group): + if group is not None: + return group.id + return None + + ''' + @param name Name of the group + @param parent Parent group full path + @param options Group options + ''' + def createOrUpdateGroup(self, name, parent, options): changed = False + + # Because we have already call userExists in main() if self.groupObject is None: - group = self._gitlab.groups.create({'name': name, 'path': path}) + parent_id = self.getGroupId(parent) + + group = self.createGroup({ + 'name': name, + 'path': options['path'], + 'parent_id': parent_id}) changed = True else: - group = self.groupObject - - if description is not None: - if group.description != description: - group.description = description - changed = True + changed, group = self.updateGroup(self.groupObject, { + 'name': name, + 'description': options['description'], + 'visibility': options['visibility']}) + self.groupObject = group if changed: if self._module.check_mode: - self._module.exit_json(changed=True, result="Group should have updated.") + self._module.exit_json(changed=True, msg="Successfully created or updated the group %s" % name) + try: group.save() except Exception as e: - self._module.fail_json(msg="Failed to create or update a group: %s " % e) + self._module.fail_json(msg="Failed to update group: %s " % e) return True else: return False + ''' + @param arguments Attributs of the group + ''' + def createGroup(self, arguments): + if self._module.check_mode: + return True + + try: + group = self._gitlab.groups.create(arguments) + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create group: %s " % to_native(e)) + + return group + + ''' + @param group Group Object + @param arguments Attributs of the group + ''' + def updateGroup(self, group, arguments): + changed = False + + for arg_key, arg_value in arguments.items(): + if arguments[arg_key] is not None: + if getattr(group, arg_key) != arguments[arg_key]: + setattr(group, arg_key, arguments[arg_key]) + changed = True + + return (changed, group) + def deleteGroup(self): group = self.groupObject + if len(group.projects.list()) >= 1: self._module.fail_json( msg="There are still projects in this group. These needs to be moved or deleted before this group can be removed.") else: if self._module.check_mode: - self._module.exit_json(changed=True) + return True + try: group.delete() except Exception as e: - self._module.fail_json(msg="Failed to delete a group: %s " % e) - return True + self._module.fail_json(msg="Failed to delete group: %s " % to_native(e)) - def existsGroup(self, name): - """When group/user exists, object will be stored in self.groupObject.""" - groups = self._gitlab.groups.list(search=name) - if len(groups) == 1: - self.groupObject = groups[0] + ''' + @param name Name of the groupe + @param full_path Complete path of the Group including parent group path. / + ''' + def existsGroup(self, project_identifier): + # When group/user exists, object will be stored in self.groupObject. + group = findGroup(self._gitlab, project_identifier) + if group: + self.groupObject = group return True + return False + + +def deprecation_warning(module): + deprecated_aliases = ['login_token'] + + module.deprecate("Aliases \'{aliases}\' are deprecated".format(aliases='\', \''.join(deprecated_aliases)), 2.10) def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + server_url=dict(type='str', required=True, removed_in_version=2.10), + login_user=dict(type='str', no_log=True, removed_in_version=2.10), + login_password=dict(type='str', no_log=True, removed_in_version=2.10), + api_token=dict(type='str', no_log=True, aliases=["login_token"]), + name=dict(type='str', required=True), + path=dict(type='str'), + description=dict(type='str'), + state=dict(type='str', default="present", choices=["absent", "present"]), + parent=dict(type='str'), + visibility=dict(type='str', default="private", choices=["internal", "private", "public"]), + )) + module = AnsibleModule( - argument_spec=dict( - server_url=dict(required=True, type='str'), - validate_certs=dict(required=False, default=True, type='bool', aliases=['verify_ssl']), - login_user=dict(required=False, no_log=True, type='str'), - login_password=dict(required=False, no_log=True, type='str'), - login_token=dict(required=False, no_log=True, type='str'), - name=dict(required=True, type='str'), - path=dict(required=False, type='str'), - description=dict(required=False, type='str'), - state=dict(default="present", choices=["present", "absent"]), - ), + argument_spec=argument_spec, mutually_exclusive=[ + ['api_url', 'server_url'], + ['api_username', 'login_user'], + ['api_password', 'login_password'], + ['api_username', 'api_token'], + ['api_password', 'api_token'], ['login_user', 'login_token'], ['login_password', 'login_token'] ], required_together=[ - ['login_user', 'login_password'] + ['api_username', 'api_password'], + ['login_user', 'login_password'], ], required_one_of=[ - ['login_user', 'login_token'] + ['api_username', 'api_token', 'login_user', 'login_token'] ], - supports_check_mode=True + supports_check_mode=True, ) - if not HAS_GITLAB_PACKAGE: - module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install python-gitlab") + deprecation_warning(module) server_url = module.params['server_url'] - validate_certs = module.params['validate_certs'] login_user = module.params['login_user'] login_password = module.params['login_password'] - login_token = module.params['login_token'] + + api_url = module.params['api_url'] + validate_certs = module.params['validate_certs'] + api_user = module.params['api_username'] + api_password = module.params['api_password'] + + gitlab_url = server_url if api_url is None else api_url + gitlab_user = login_user if api_user is None else api_user + gitlab_password = login_password if api_password is None else api_password + gitlab_token = module.params['api_token'] + group_name = module.params['name'] group_path = module.params['path'] description = module.params['description'] state = module.params['state'] + parent_identifier = module.params['parent'] + group_visibility = module.params['visibility'] + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) try: - git = gitlab.Gitlab(url=server_url, ssl_verify=validate_certs, email=login_user, password=login_password, - private_token=login_token, api_version=4) - git.auth() + gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password, + private_token=gitlab_token, api_version=4) + gitlab_instance.auth() except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e: - module.fail_json(msg='Failed to connect to Gitlab server: %s' % to_native(e)) + module.fail_json(msg="Failed to connect to Gitlab server: %s" % to_native(e)) + except (gitlab.exceptions.GitlabHttpError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s. \ + Gitlab remove Session API now that private tokens are removed from user API endpoints since version 10.2" % to_native(e)) + # Define default group_path based on group_name if group_path is None: group_path = group_name.replace(" ", "_") - group = GitLabGroup(module, git) - group_exists = group.existsGroup(group_name) + gitlab_group = GitLabGroup(module, gitlab_instance) + + parent_group = None + if parent_identifier: + parent_group = findGroup(gitlab_instance, parent_identifier) + if not parent_group: + module.fail_json(msg="Failed create Gitlab group: Parent group doesn't exists") - if group_exists and state == "absent": - if group.deleteGroup(): - module.exit_json(changed=True, result="Successfully deleted group %s" % group_name) + group_exists = gitlab_group.existsGroup(parent_group.full_path + '/' + group_path) else: - if state == "absent": - module.exit_json(changed=False, result="Group deleted or does not exists") + group_exists = gitlab_group.existsGroup(group_path) + + if state == 'absent': + if group_exists: + gitlab_group.deleteGroup() + module.exit_json(changed=True, msg="Successfully deleted group %s" % group_name) + else: + module.exit_json(changed=False, msg="Group deleted or does not exists") + + if state == 'present': + if gitlab_group.createOrUpdateGroup(group_name, parent_group, { + "path": group_path, + "description": description, + "visibility": group_visibility}): + module.exit_json(changed=True, msg="Successfully created or updated the group %s" % group_name, group=gitlab_group.groupObject._attrs) else: - if group.createOrUpdateGroup(name=group_name, path=group_path, description=description): - module.exit_json(changed=True, result="Successfully created or updated the group %s" % group_name) - else: - module.exit_json(changed=False, result="No need to update the group %s" % group_name) + module.exit_json(changed=False, msg="No need to update the group %s" % group_name, group=gitlab_group.groupObject._attrs) if __name__ == '__main__': diff --git a/lib/ansible/modules/source_control/gitlab_hooks.py b/lib/ansible/modules/source_control/gitlab_hooks.py index b65d39ec31b..a1a4b527d35 100644 --- a/lib/ansible/modules/source_control/gitlab_hooks.py +++ b/lib/ansible/modules/source_control/gitlab_hooks.py @@ -1,20 +1,19 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# (c) 2018, Marcus Watkins +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) +# Copyright: (c) 2018, Marcus Watkins # Based on code: -# (c) 2013, Phillip Gentry +# Copyright: (c) 2013, Phillip Gentry # 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 - ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - DOCUMENTATION = ''' --- module: gitlab_hooks @@ -22,103 +21,99 @@ short_description: Manages GitLab project hooks. description: - Adds, updates and removes project hooks version_added: "2.6" +author: + - Marcus Watkins (@marwatk) + - Guillaume Martinez (@Lunik) +requirements: + - python >= 2.7 + - python-gitlab python module +extends_documentation_fragment: + - auth_basic options: - api_url: - description: - - GitLab API url, e.g. https://gitlab.example.com/api - required: true - access_token: - description: - - The oauth key provided by GitLab. One of access_token or private_token is required. See https://docs.gitlab.com/ee/api/oauth2.html - required: false - private_token: + api_token: description: - - Personal access token to use. One of private_token or access_token is required. See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html - required: false + - Gitlab token for logging in. + version_added: "2.8" + type: str + aliases: + - private_token + - access_token project: description: - - Numeric project id or name of project in the form of group/name + - Id or Full path of the project in the form of group/name required: true + type: str hook_url: description: - The url that you want GitLab to post to, this is used as the primary key for updates and deletion. required: true + type: str state: description: - When C(present) the hook will be updated to match the input or created if it doesn't exist. When C(absent) it will be deleted if it exists. required: true default: present + type: str choices: [ "present", "absent" ] push_events: description: - Trigger hook on push events type: bool - default: 'yes' + default: yes issues_events: description: - Trigger hook on issues events type: bool - default: 'no' + default: no merge_requests_events: description: - Trigger hook on merge requests events type: bool - default: 'no' + default: no tag_push_events: description: - Trigger hook on tag push events type: bool - default: 'no' + default: no note_events: description: - Trigger hook on note events type: bool - default: 'no' + default: no job_events: description: - Trigger hook on job events type: bool - default: 'no' + default: no pipeline_events: description: - Trigger hook on pipeline events type: bool - default: 'no' + default: no wiki_page_events: description: - Trigger hook on wiki events type: bool - default: 'no' + default: no enable_ssl_verification: description: - Whether GitLab will do SSL verification when triggering the hook type: bool - default: 'no' + default: no token: description: - Secret token to validate hook messages at the receiver. - If this is present it will always result in a change as it cannot be retrieved from GitLab. - Will show up in the X-Gitlab-Token HTTP request header required: false -author: "Marcus Watkins (@marwatk)" + type: str ''' EXAMPLES = ''' -# Example creating a new project hook -- gitlab_hooks: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" - project: "my_group/my_project" - hook_url: "https://my-ci-server.example.com/gitlab-hook" - state: present - push_events: yes - enable_ssl_verification: no - token: "my-super-secret-token-that-my-ci-server-will-check" - -# Update the above hook to add tag pushes -- gitlab_hooks: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" +- name: "Adding a project hook" + gitlab_hooks: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" project: "my_group/my_project" hook_url: "https://my-ci-server.example.com/gitlab-hook" state: present @@ -127,18 +122,18 @@ EXAMPLES = ''' enable_ssl_verification: no token: "my-super-secret-token-that-my-ci-server-will-check" -# Delete the previous hook -- gitlab_hooks: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" +- name: "Delete the previous hook" + gitlab_hooks: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" project: "my_group/my_project" hook_url: "https://my-ci-server.example.com/gitlab-hook" state: absent -# Delete a hook by numeric project id -- gitlab_hooks: - api_url: https://gitlab.example.com/api - access_token: "{{ access_token }}" +- name: "Delete a hook by numeric project id" + gitlab_hooks: + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" project: 10 hook_url: "https://my-ci-server.example.com/gitlab-hook" state: absent @@ -146,155 +141,266 @@ EXAMPLES = ''' RETURN = ''' msg: - description: Success or failure message - returned: always - type: str - sample: "Success" + description: Success or failure message + returned: always + type: str + sample: "Success" result: - description: json parsed response from the server - returned: always - type: dict + description: json parsed response from the server + returned: always + type: dict error: - description: the error message returned by the Gitlab API - returned: failed - type: str - sample: "400: key is already in use" - -previous_version: - description: object describing the state prior to this task - returned: changed - type: dict + description: the error message returned by the Gitlab API + returned: failed + type: str + sample: "400: path is already in use" + +hook: + description: API object + returned: always + type: dict ''' +import os +import re +import traceback + +GITLAB_IMP_ERR = None +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except Exception: + GITLAB_IMP_ERR = traceback.format_exc() + HAS_GITLAB_PACKAGE = False + +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native + +from ansible.module_utils.gitlab import findProject + + +class GitLabHook(object): + def __init__(self, module, gitlab_instance): + self._module = module + self._gitlab = gitlab_instance + self.hookObject = None + + ''' + @param prokect Project Object + @param hook_url Url to call on event + @param description Description of the group + @param parent Parent group full path + ''' + def createOrUpdateHook(self, project, hook_url, options): + changed = False + + # Because we have already call userExists in main() + if self.hookObject is None: + hook = self.createHook(project, { + 'url': hook_url, + 'push_events': options['push_events'], + 'issues_events': options['issues_events'], + 'merge_requests_events': options['merge_requests_events'], + 'tag_push_events': options['tag_push_events'], + 'note_events': options['note_events'], + 'job_events': options['job_events'], + 'pipeline_events': options['pipeline_events'], + 'wiki_page_events': options['wiki_page_events'], + 'enable_ssl_verification': options['enable_ssl_verification'], + 'token': options['token']}) + changed = True + else: + changed, hook = self.updateHook(self.hookObject, { + 'push_events': options['push_events'], + 'issues_events': options['issues_events'], + 'merge_requests_events': options['merge_requests_events'], + 'tag_push_events': options['tag_push_events'], + 'note_events': options['note_events'], + 'job_events': options['job_events'], + 'pipeline_events': options['pipeline_events'], + 'wiki_page_events': options['wiki_page_events'], + 'enable_ssl_verification': options['enable_ssl_verification'], + 'token': options['token']}) + + self.hookObject = hook + if changed: + if self._module.check_mode: + self._module.exit_json(changed=True, msg="Successfully created or updated the hook %s" % hook_url) + + try: + hook.save() + except Exception as e: + self._module.fail_json(msg="Failed to update hook: %s " % e) + return True + else: + return False -import json + ''' + @param project Project Object + @param arguments Attributs of the hook + ''' + def createHook(self, project, arguments): + if self._module.check_mode: + return True -from ansible.module_utils.basic import AnsibleModule -from copy import deepcopy + hook = project.hooks.create(arguments) -from ansible.module_utils.gitlab import request + return hook + ''' + @param hook Hook Object + @param arguments Attributs of the hook + ''' + def updateHook(self, hook, arguments): + changed = False -def _list(module, api_url, project, access_token, private_token): - path = "/hooks" - return request(module, api_url, project, path, access_token, private_token) + for arg_key, arg_value in arguments.items(): + if arguments[arg_key] is not None: + if getattr(hook, arg_key) != arguments[arg_key]: + setattr(hook, arg_key, arguments[arg_key]) + changed = True + return (changed, hook) -def _find(module, api_url, project, hook_url, access_token, private_token): - success, data = _list(module, api_url, project, access_token, private_token) - if success: - for i in data: - if i["url"] == hook_url: - return success, i - return success, None - return success, data + ''' + @param project Project object + @param hook_url Url to call on event + ''' + def findHook(self, project, hook_url): + hooks = project.hooks.list() + for hook in hooks: + if (hook.url == hook_url): + return hook + ''' + @param project Project object + @param hook_url Url to call on event + ''' + def existsHooks(self, project, hook_url): + # When project exists, object will be stored in self.projectObject. + hook = self.findHook(project, hook_url) + if hook: + self.hookObject = hook + return True + return False -def _publish(module, api_url, project, data, access_token, private_token): - path = "/hooks" - method = "POST" - if 'id' in data: - path += "/%s" % str(data["id"]) - method = "PUT" - data = deepcopy(data) - data.pop('id', None) - return request(module, api_url, project, path, access_token, private_token, json.dumps(data, sort_keys=True), method) + def deleteHook(self): + if self._module.check_mode: + return True + return self.hookObject.delete() -def _delete(module, api_url, project, hook_id, access_token, private_token): - path = "/hooks/%s" % str(hook_id) - return request(module, api_url, project, path, access_token, private_token, method='DELETE') +def deprecation_warning(module): + deprecated_aliases = ['private_token', 'access_token'] -def _are_equivalent(input, existing): - for key in [ - 'url', 'push_events', 'issues_events', 'merge_requests_events', - 'tag_push_events', 'note_events', 'job_events', 'pipeline_events', 'wiki_page_events', - 'enable_ssl_verification']: - if key in input and key not in existing: - return False - if key not in input and key in existing: - return False - if not input[key] == existing[key]: - return False - return True + module.deprecate("Aliases \'{aliases}\' are deprecated".format(aliases='\', \''.join(deprecated_aliases)), 2.10) def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + api_token=dict(type='str', no_log=True, aliases=["private_token", "access_token"]), + state=dict(type='str', default="present", choices=["absent", "present"]), + project=dict(type='str', required=True), + hook_url=dict(type='str', required=True), + push_events=dict(type='bool', default=True), + issues_events=dict(type='bool', default=False), + merge_requests_events=dict(type='bool', default=False), + tag_push_events=dict(type='bool', default=False), + note_events=dict(type='bool', default=False), + job_events=dict(type='bool', default=False), + pipeline_events=dict(type='bool', default=False), + wiki_page_events=dict(type='bool', default=False), + enable_ssl_verification=dict(type='bool', default=False), + token=dict(type='str', no_log=True), + )) + module = AnsibleModule( - argument_spec=dict( - api_url=dict(required=True), - access_token=dict(required=False, no_log=True), - private_token=dict(required=False, no_log=True), - project=dict(required=True), - hook_url=dict(required=True), - state=dict(default='present', choices=['present', 'absent']), - push_events=dict(default='yes', type='bool'), - issues_events=dict(default='no', type='bool'), - merge_requests_events=dict(default='no', type='bool'), - tag_push_events=dict(default='no', type='bool'), - note_events=dict(default='no', type='bool'), - job_events=dict(default='no', type='bool'), - pipeline_events=dict(default='no', type='bool'), - wiki_page_events=dict(default='no', type='bool'), - enable_ssl_verification=dict(default='no', type='bool'), - token=dict(required=False, no_log=True), - ), + argument_spec=argument_spec, mutually_exclusive=[ - ['access_token', 'private_token'] + ['api_username', 'api_token'], + ['api_password', 'api_token'] + ], + required_together=[ + ['api_username', 'api_password'] ], required_one_of=[ - ['access_token', 'private_token'] + ['api_username', 'api_token'] ], supports_check_mode=True, ) - api_url = module.params['api_url'] - access_token = module.params['access_token'] - private_token = module.params['private_token'] - project = module.params['project'] - state = module.params['state'] + deprecation_warning(module) - if not access_token and not private_token: - module.fail_json(msg="need either access_token or private_token") + gitlab_url = re.sub('/api.*', '', module.params['api_url']) + validate_certs = module.params['validate_certs'] + gitlab_user = module.params['api_username'] + gitlab_password = module.params['api_password'] + gitlab_token = module.params['api_token'] - input = {'url': module.params['hook_url']} - - for key in [ - 'push_events', 'issues_events', 'merge_requests_events', - 'tag_push_events', 'note_events', 'job_events', 'pipeline_events', 'wiki_page_events', - 'enable_ssl_verification', 'token']: - input[key] = module.params[key] - - success, existing = _find(module, api_url, project, input['url'], access_token, private_token) - - if not success: - module.fail_json(msg="failed to list hooks", result=existing) - - if existing: - input['id'] = existing['id'] - - changed = False - success = True - response = None + state = module.params['state'] + project_identifier = module.params['project'] + hook_url = module.params['hook_url'] + push_events = module.params['push_events'] + issues_events = module.params['issues_events'] + merge_requests_events = module.params['merge_requests_events'] + tag_push_events = module.params['tag_push_events'] + note_events = module.params['note_events'] + job_events = module.params['job_events'] + pipeline_events = module.params['pipeline_events'] + wiki_page_events = module.params['wiki_page_events'] + enable_ssl_verification = module.params['enable_ssl_verification'] + hook_token = module.params['token'] + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) + + try: + gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password, + private_token=gitlab_token, api_version=4) + gitlab_instance.auth() + except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s" % to_native(e)) + except (gitlab.exceptions.GitlabHttpError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s. \ + Gitlab remove Session API now that private tokens are removed from user API endpoints since version 10.2." % to_native(e)) + + gitlab_hook = GitLabHook(module, gitlab_instance) + + project = findProject(gitlab_instance, project_identifier) + + if project is None: + module.fail_json(msg="Failed to create hook: project %s doesn't exists" % project_identifier) + + hook_exists = gitlab_hook.existsHooks(project, hook_url) + + if state == 'absent': + if hook_exists: + gitlab_hook.deleteHook() + module.exit_json(changed=True, msg="Successfully deleted hook %s" % hook_url) + else: + module.exit_json(changed=False, msg="Hook deleted or does not exists") if state == 'present': - if not existing or input['token'] or not _are_equivalent(existing, input): - if not module.check_mode: - success, response = _publish(module, api_url, project, input, access_token, private_token) - changed = True - else: - if existing: - if not module.check_mode: - success, response = _delete(module, api_url, project, existing['id'], access_token, private_token) - changed = True - - if success: - module.exit_json(changed=changed, msg='Success', result=response, previous_version=existing) - else: - module.fail_json(msg='Failure', error=response) + if gitlab_hook.createOrUpdateHook(project, hook_url, { + "push_events": push_events, + "issues_events": issues_events, + "merge_requests_events": merge_requests_events, + "tag_push_events": tag_push_events, + "note_events": note_events, + "job_events": job_events, + "pipeline_events": pipeline_events, + "wiki_page_events": wiki_page_events, + "enable_ssl_verification": enable_ssl_verification, + "token": hook_token}): + + module.exit_json(changed=True, msg="Successfully created or updated the hook %s" % hook_url, hook=gitlab_hook.hookObject._attrs) + else: + module.exit_json(changed=False, msg="No need to update the hook %s" % hook_url, hook=gitlab_hook.hookObject._attrs) if __name__ == '__main__': diff --git a/lib/ansible/modules/source_control/gitlab_project.py b/lib/ansible/modules/source_control/gitlab_project.py index bff74eb2ae7..eb00312d111 100644 --- a/lib/ansible/modules/source_control/gitlab_project.py +++ b/lib/ansible/modules/source_control/gitlab_project.py @@ -1,133 +1,137 @@ #!/usr/bin/python -# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) +# Copyright: (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) # 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 - ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - DOCUMENTATION = ''' --- module: gitlab_project short_description: Creates/updates/deletes Gitlab Projects description: - - When the project does not exist in Gitlab, it will be created. - - When the project does exists and state=absent, the project will be deleted. - - When changes are made to the project, the project will be updated. + - When the project does not exist in Gitlab, it will be created. + - When the project does exists and state=absent, the project will be deleted. + - When changes are made to the project, the project will be updated. version_added: "2.1" -author: "Werner Dijkerman (@dj-wasabi)" +author: + - Werner Dijkerman (@dj-wasabi) + - Guillaume Martinez (@Lunik) requirements: - - pyapi-gitlab python module + - python >= 2.7 + - python-gitlab python module +extends_documentation_fragment: + - auth_basic options: - server_url: - description: - - Url of Gitlab server, with protocol (http or https). - required: true - validate_certs: - description: - - When using https if SSL certificate needs to be verified. - type: bool - default: 'yes' - aliases: - - verify_ssl - login_user: - description: - - Gitlab user name. - login_password: - description: - - Gitlab password for login_user - login_token: - description: - - Gitlab token for logging in. - group: - description: - - The name of the group of which this projects belongs to. - - When not provided, project will belong to user which is configured in 'login_user' or 'login_token' - - When provided with username, project will be created for this user. 'login_user' or 'login_token' needs admin rights. - name: - description: - - The name of the project - required: true - path: - description: - - The path of the project you want to create, this will be server_url//path - - If not supplied, name will be used. + server_url: + description: + - The URL of the Gitlab server, with protocol (i.e. http or https). + required: true + type: str + login_user: + description: + - Gitlab user name. + type: str + login_password: + description: + - Gitlab password for login_user + type: str + api_token: + description: + - Gitlab token for logging in. + type: str + aliases: + - login_token + group: + description: + - Id or The full path of the group of which this projects belongs to. + type: str + name: + description: + - The name of the project + required: true + type: str + path: + description: + - The path of the project you want to create, this will be server_url//path + - If not supplied, name will be used. + type: str + description: + description: + - An description for the project. + type: str + issues_enabled: + description: + - Whether you want to create issues or not. + - Possible values are true and false. + type: bool + default: yes + merge_requests_enabled: + description: + - If merge requests can be made or not. + - Possible values are true and false. + type: bool + default: yes + wiki_enabled: + description: + - If an wiki for this project should be available or not. + - Possible values are true and false. + type: bool + default: yes + snippets_enabled: + description: + - If creating snippets should be available or not. + - Possible values are true and false. + type: bool + default: yes + visibility: description: - description: - - An description for the project. - issues_enabled: - description: - - Whether you want to create issues or not. - - Possible values are true and false. - type: bool - default: 'yes' - merge_requests_enabled: - description: - - If merge requests can be made or not. - - Possible values are true and false. - type: bool - default: 'yes' - wiki_enabled: - description: - - If an wiki for this project should be available or not. - - Possible values are true and false. - type: bool - default: 'yes' - snippets_enabled: - description: - - If creating snippets should be available or not. - - Possible values are true and false. - type: bool - default: 'yes' - public: - description: - - If the project is public available or not. - - Setting this to true is same as setting visibility_level to 20. - - Possible values are true and false. - type: bool - default: 'no' - visibility_level: - description: - - Private. visibility_level is 0. Project access must be granted explicitly for each user. - - Internal. visibility_level is 10. The project can be cloned by any logged in user. - - Public. visibility_level is 20. The project can be cloned without any authentication. - - Possible values are 0, 10 and 20. - default: 0 - import_url: - description: - - Git repository which will be imported into gitlab. - - Gitlab server needs read access to this git repository. - type: bool - default: 'no' - state: - description: - - create or delete project. - - Possible values are present and absent. - default: "present" - choices: ["present", "absent"] + - Private. Project access must be granted explicitly for each user. + - Internal. The project can be cloned by any logged in user. + - Public. The project can be cloned without any authentication. + default: private + type: str + choices: ["private", "internal", "public"] + aliases: + - visibility_level + import_url: + description: + - Git repository which will be imported into gitlab. + - Gitlab server needs read access to this git repository. + required: false + type: str + state: + description: + - create or delete project. + - Possible values are present and absent. + default: present + type: str + choices: ["present", "absent"] ''' EXAMPLES = ''' - name: Delete Gitlab Project gitlab_project: - server_url: http://gitlab.example.com + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" validate_certs: False - login_token: WnUzDsxjy8230-Dy_k name: my_first_project state: absent delegate_to: localhost - name: Create Gitlab Project in group Ansible gitlab_project: - server_url: https://gitlab.example.com + api_url: https://gitlab.example.com/ validate_certs: True - login_user: dj-wasabi - login_password: MySecretPassword + api_username: dj-wasabi + api_password: "MySecretPassword" name: my_first_project group: ansible issues_enabled: False @@ -138,257 +142,277 @@ EXAMPLES = ''' delegate_to: localhost ''' -RETURN = '''# ''' +RETURN = ''' +msg: + description: Success or failure message + returned: always + type: str + sample: "Success" + +result: + description: json parsed response from the server + returned: always + type: dict + +error: + description: the error message returned by the Gitlab API + returned: failed + type: str + sample: "400: path is already in use" + +project: + description: API object + returned: always + type: dict +''' + +import os +import traceback +GITLAB_IMP_ERR = None try: import gitlab HAS_GITLAB_PACKAGE = True except Exception: + GITLAB_IMP_ERR = traceback.format_exc() HAS_GITLAB_PACKAGE = False -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native +from ansible.module_utils.gitlab import findGroup, findProject + class GitLabProject(object): - def __init__(self, module, git): + def __init__(self, module, gitlab_instance): self._module = module - self._gitlab = git - - def createOrUpdateProject(self, project_exists, group_name, import_url, arguments): - is_user = False - group_id = self.getGroupId(group_name) - if not group_id: - group_id = self.getUserId(group_name) - is_user = True - - if project_exists: - # Edit project - return self.updateProject(group_name, arguments) + self._gitlab = gitlab_instance + self.projectObject = None + + ''' + @param project_name Name of the project + @param namespace Namespace Object (User or Group) + @param options Options of the project + ''' + def createOrUpdateProject(self, project_name, namespace, options): + changed = False + + # Because we have already call userExists in main() + if self.projectObject is None: + project = self.createProject(namespace, { + 'name': project_name, + 'path': options['path'], + 'description': options['description'], + 'issues_enabled': options['issues_enabled'], + 'merge_requests_enabled': options['merge_requests_enabled'], + 'wiki_enabled': options['wiki_enabled'], + 'snippets_enabled': options['snippets_enabled'], + 'visibility': options['visibility'], + 'import_url': options['import_url']}) + changed = True else: - # Create project + changed, project = self.updateProject(self.projectObject, { + 'name': project_name, + 'description': options['description'], + 'issues_enabled': options['issues_enabled'], + 'merge_requests_enabled': options['merge_requests_enabled'], + 'wiki_enabled': options['wiki_enabled'], + 'snippets_enabled': options['snippets_enabled'], + 'visibility': options['visibility']}) + + self.projectObject = project + if changed: if self._module.check_mode: - self._module.exit_json(changed=True) - return self.createProject(is_user, group_id, import_url, arguments) - - def createProject(self, is_user, user_id, import_url, arguments): - if is_user: - result = self._gitlab.createprojectuser(user_id=user_id, import_url=import_url, **arguments) - else: - group_id = user_id - result = self._gitlab.createproject(namespace_id=group_id, import_url=import_url, **arguments) - - if not result: - self._module.fail_json(msg="Failed to create project %r" % arguments['name']) - - return result - - def deleteProject(self, group_name, project_name): - if self.existsGroup(group_name): - project_owner = group_name - else: - project_owner = self._gitlab.currentuser()['username'] - - search_results = self._gitlab.searchproject(search=project_name) - for result in search_results: - owner = result['namespace']['name'] - if owner == project_owner: - return self._gitlab.deleteproject(result['id']) + self._module.exit_json(changed=True, msg="Successfully created or updated the project %s" % project_name) - def existsProject(self, group_name, project_name): - if self.existsGroup(group_name): - project_owner = group_name + try: + project.save() + except Exception as e: + self._module.fail_json(msg="Failed update project: %s " % e) + return True else: - project_owner = self._gitlab.currentuser()['username'] + return False - search_results = self._gitlab.searchproject(search=project_name) - for result in search_results: - owner = result['namespace']['name'] - if owner == project_owner: - return True - return False + ''' + @param namespace Namespace Object (User or Group) + @param arguments Attributs of the project + ''' + def createProject(self, namespace, arguments): + if self._module.check_mode: + return True + + arguments['namespace_id'] = namespace.id + try: + project = self._gitlab.projects.create(arguments) + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create project: %s " % to_native(e)) + + return project + + ''' + @param project Project Object + @param arguments Attributs of the project + ''' + def updateProject(self, project, arguments): + changed = False - def existsGroup(self, group_name): - if group_name is not None: - # Find the group, if group not exists we try for user - for group in self._gitlab.getall(self._gitlab.getgroups): - if group['name'] == group_name: - return True - - user_name = group_name - user_data = self._gitlab.getusers(search=user_name) - for data in user_data: - if 'id' in user_data: - return True + for arg_key, arg_value in arguments.items(): + if arguments[arg_key] is not None: + if getattr(project, arg_key) != arguments[arg_key]: + setattr(project, arg_key, arguments[arg_key]) + changed = True + + return (changed, project) + + def deleteProject(self): + if self._module.check_mode: + return True + + project = self.projectObject + + return project.delete() + + ''' + @param namespace User/Group object + @param name Name of the project + ''' + def existsProject(self, namespace, path): + # When project exists, object will be stored in self.projectObject. + project = findProject(self._gitlab, namespace.full_path + '/' + path) + if project: + self.projectObject = project + return True return False - def getGroupId(self, group_name): - if group_name is not None: - # Find the group, if group not exists we try for user - for group in self._gitlab.getall(self._gitlab.getgroups): - if group['name'] == group_name: - return group['id'] - - def getProjectId(self, group_name, project_name): - if self.existsGroup(group_name): - project_owner = group_name - else: - project_owner = self._gitlab.currentuser()['username'] - - search_results = self._gitlab.searchproject(search=project_name) - for result in search_results: - owner = result['namespace']['name'] - if owner == project_owner: - return result['id'] - def getUserId(self, user_name): - user_data = self._gitlab.getusers(search=user_name) +def deprecation_warning(module): + deprecated_aliases = ['login_token'] - for data in user_data: - if 'id' in data: - return data['id'] - return self._gitlab.currentuser()['id'] - - def to_bool(self, value): - if value: - return 1 - else: - return 0 - - def updateProject(self, group_name, arguments): - project_changed = False - project_name = arguments['name'] - project_id = self.getProjectId(group_name, project_name) - project_data = self._gitlab.getproject(project_id=project_id) - - for arg_key, arg_value in arguments.items(): - project_data_value = project_data[arg_key] - - if isinstance(project_data_value, bool) or project_data_value is None: - to_bool = self.to_bool(project_data_value) - if to_bool != arg_value: - project_changed = True - continue - else: - if project_data_value != arg_value: - project_changed = True - - if project_changed: - if self._module.check_mode: - self._module.exit_json(changed=True) - return self._gitlab.editproject(project_id=project_id, **arguments) - else: - return False + module.deprecate("Aliases \'{aliases}\' are deprecated".format(aliases='\', \''.join(deprecated_aliases)), 2.10) def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + server_url=dict(type='str', required=True, removed_in_version=2.10), + login_user=dict(type='str', no_log=True, removed_in_version=2.10), + login_password=dict(type='str', no_log=True, removed_in_version=2.10), + api_token=dict(type='str', no_log=True, aliases=["login_token"]), + group=dict(type='str'), + name=dict(type='str', required=True), + path=dict(type='str'), + description=dict(type='str'), + issues_enabled=dict(type='bool', default=True), + merge_requests_enabled=dict(type='bool', default=True), + wiki_enabled=dict(type='bool', default=True), + snippets_enabled=dict(default=True, type='bool'), + visibility=dict(type='str', default="private", choices=["internal", "private", "public"], aliases=["visibility_level"]), + import_url=dict(type='str'), + state=dict(type='str', default="present", choices=["absent", "present"]), + )) + module = AnsibleModule( - argument_spec=dict( - server_url=dict(required=True), - validate_certs=dict(required=False, default=True, type='bool', aliases=['verify_ssl']), - login_user=dict(required=False, no_log=True), - login_password=dict(required=False, no_log=True), - login_token=dict(required=False, no_log=True), - group=dict(required=False), - name=dict(required=True), - path=dict(required=False), - description=dict(required=False), - issues_enabled=dict(default=True, type='bool'), - merge_requests_enabled=dict(default=True, type='bool'), - wiki_enabled=dict(default=True, type='bool'), - snippets_enabled=dict(default=True, type='bool'), - public=dict(default=False, type='bool'), - visibility_level=dict(default="0", choices=["0", "10", "20"]), - import_url=dict(required=False), - state=dict(default="present", choices=["present", 'absent']), - ), - supports_check_mode=True + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_url', 'server_url'], + ['api_username', 'login_user'], + ['api_password', 'login_password'], + ['api_username', 'api_token'], + ['api_password', 'api_token'], + ['login_user', 'login_token'], + ['login_password', 'login_token'] + ], + required_together=[ + ['api_username', 'api_password'], + ['login_user', 'login_password'], + ], + required_one_of=[ + ['api_username', 'api_token', 'login_user', 'login_token'] + ], + supports_check_mode=True, ) - if not HAS_GITLAB_PACKAGE: - module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install pyapi-gitlab") + deprecation_warning(module) server_url = module.params['server_url'] - verify_ssl = module.params['validate_certs'] login_user = module.params['login_user'] login_password = module.params['login_password'] - login_token = module.params['login_token'] - group_name = module.params['group'] + + api_url = module.params['api_url'] + validate_certs = module.params['validate_certs'] + api_user = module.params['api_username'] + api_password = module.params['api_password'] + + gitlab_url = server_url if api_url is None else api_url + gitlab_user = login_user if api_user is None else api_user + gitlab_password = login_password if api_password is None else api_password + gitlab_token = module.params['api_token'] + + group_identifier = module.params['group'] project_name = module.params['name'] project_path = module.params['path'] - description = module.params['description'] + project_description = module.params['description'] issues_enabled = module.params['issues_enabled'] merge_requests_enabled = module.params['merge_requests_enabled'] wiki_enabled = module.params['wiki_enabled'] snippets_enabled = module.params['snippets_enabled'] - public = module.params['public'] - visibility_level = module.params['visibility_level'] + visibility = module.params['visibility'] import_url = module.params['import_url'] state = module.params['state'] - # We need both login_user and login_password or login_token, otherwise we fail. - if login_user is not None and login_password is not None: - use_credentials = True - elif login_token is not None: - use_credentials = False - else: - module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) + + try: + gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password, + private_token=gitlab_token, api_version=4) + gitlab_instance.auth() + except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s" % to_native(e)) + except (gitlab.exceptions.GitlabHttpError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s. \ + Gitlab remove Session API now that private tokens are removed from user API endpoints since version 10.2." % to_native(e)) # Set project_path to project_name if it is empty. if project_path is None: project_path = project_name.replace(" ", "_") - # Gitlab API makes no difference between upper and lower cases, so we lower them. - project_name = project_name.lower() - project_path = project_path.lower() - if group_name is not None: - group_name = group_name.lower() + gitlab_project = GitLabProject(module, gitlab_instance) - # Lets make an connection to the Gitlab server_url, with either login_user and login_password - # or with login_token - try: - if use_credentials: - git = gitlab.Gitlab(host=server_url, verify_ssl=verify_ssl) - git.login(user=login_user, password=login_password) - else: - git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) - except Exception as e: - module.fail_json(msg="Failed to connect to Gitlab server: %s " % to_native(e)) - - # Check if user is authorized or not before proceeding to any operations - # if not, exit from here - auth_msg = git.currentuser().get('message', None) - if auth_msg is not None and auth_msg == '401 Unauthorized': - module.fail_json(msg='User unauthorized', - details="User is not allowed to access Gitlab server " - "using login_token. Please check login_token") - - # Validate if project exists and take action based on "state" - project = GitLabProject(module, git) - project_exists = project.existsProject(group_name, project_name) - - # Creating the project dict - arguments = {"name": project_name, - "path": project_path, - "description": description, - "issues_enabled": project.to_bool(issues_enabled), - "merge_requests_enabled": project.to_bool(merge_requests_enabled), - "wiki_enabled": project.to_bool(wiki_enabled), - "snippets_enabled": project.to_bool(snippets_enabled), - "public": project.to_bool(public), - "visibility_level": int(visibility_level)} - - if project_exists and state == "absent": - project.deleteProject(group_name, project_name) - module.exit_json(changed=True, result="Successfully deleted project %s" % project_name) + if group_identifier: + group = findGroup(gitlab_instance, group_identifier) + if group is None: + module.fail_json(msg="Failed to create project: group %s doesn't exists" % group_identifier) + + namespace = gitlab_instance.namespaces.get(group.id) + project_exists = gitlab_project.existsProject(namespace, project_path) else: - if state == "absent": - module.exit_json(changed=False, result="Project deleted or does not exist") + user = gitlab_instance.users.list(username=gitlab_instance.user.username)[0] + namespace = gitlab_instance.namespaces.get(user.id) + project_exists = gitlab_project.existsProject(namespace, project_path) + + if state == 'absent': + if project_exists: + gitlab_project.deleteProject() + module.exit_json(changed=True, msg="Successfully deleted project %s" % project_name) + else: + module.exit_json(changed=False, msg="Project deleted or does not exists") + + if state == 'present': + if gitlab_project.createOrUpdateProject(project_name, namespace, { + "path": project_path, + "description": project_description, + "issues_enabled": issues_enabled, + "merge_requests_enabled": merge_requests_enabled, + "wiki_enabled": wiki_enabled, + "snippets_enabled": snippets_enabled, + "visibility": visibility, + "import_url": import_url}): + + module.exit_json(changed=True, msg="Successfully created or updated the project %s" % project_name, project=gitlab_project.projectObject._attrs) else: - if project.createOrUpdateProject(project_exists, group_name, import_url, arguments): - module.exit_json(changed=True, result="Successfully created or updated the project %s" % project_name) - else: - module.exit_json(changed=False) + module.exit_json(changed=False, msg="No need to update the project %s" % project_name, project=gitlab_project.projectObject._attrs) if __name__ == '__main__': diff --git a/lib/ansible/modules/source_control/gitlab_runner.py b/lib/ansible/modules/source_control/gitlab_runner.py index 68a30ea4a9d..41b1d77ac01 100644 --- a/lib/ansible/modules/source_control/gitlab_runner.py +++ b/lib/ansible/modules/source_control/gitlab_runner.py @@ -1,16 +1,16 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) # Copyright: (c) 2018, Samy Coenen # 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 -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} DOCUMENTATION = ''' --- @@ -30,414 +30,360 @@ notes: - To create a new runner at least the C(private_token), C(registration_token), C(name) and C(url) options are required. - Runners need to have unique names. version_added: 2.8 -author: "Samy Coenen (@SamyCoenen)" +author: + - Samy Coenen (@SamyCoenen) + - Guillaume Martinez (@Lunik) +requirements: + - python >= 2.7 + - python-gitlab python module +extends_documentation_fragment: + - auth_basic options: - private_token: - description: - - Your private token to interact with the GitLab API. - required: True - type: str - name: - description: - - The unique name of the runner. - required: True - type: str - api_timeout: - description: - - The maximum time that a request will be attempted to the GitLab API. - required: False - default: 30 - type: int - state: - description: - - Make sure that the runner with the same name exists with the same configuration or delete the runner with the same name. - required: False - default: "present" - choices: ["present", "absent"] - type: str - registration_token: - description: - - The registration token is used to register new runners. - required: True - type: str - url: - description: - - The GitLab URL including the API v4 path and http or https. - required: False - default: "https://gitlab.com/api/v4/" - type: str - active: - description: - - Define if the runners is immediately active after creation. - required: False - default: True - choices: [True, False] - type: bool - locked: - description: - - Determines if the runner is locked or not. - required: False - default: False - choices: [true, false] - type: bool - access_level: - description: - - Determines if a runner can pick up jobs from protected branches. - required: False - default: "ref_protected" - choices: ["ref_protected", "not_protected"] - type: str - maximum_timeout: - description: - - The maximum timeout that a runner has to pick up a specific job. - required: False - default: 3600 - type: int - run_untagged: - description: - - Run untagged jobs or not. - required: False - default: True - type: bool - tag_list: - description: The tags that apply to the runner. - required: False - default: ["docker"] - type: list + url: + description: + - The URL of the Gitlab server, with protocol (i.e. http or https). + required: true + type: str + api_token: + description: + - Your private token to interact with the GitLab API. + required: True + type: str + aliases: + - private_token + description: + description: + - The unique name of the runner. + required: True + type: str + aliases: + - name + state: + description: + - Make sure that the runner with the same name exists with the same configuration or delete the runner with the same name. + required: False + default: present + choices: ["present", "absent"] + type: str + registration_token: + description: + - The registration token is used to register new runners. + required: True + type: str + active: + description: + - Define if the runners is immediately active after creation. + required: False + default: yes + type: bool + locked: + description: + - Determines if the runner is locked or not. + required: False + default: False + type: bool + access_level: + description: + - Determines if a runner can pick up jobs from protected branches. + required: False + default: ref_protected + choices: ["ref_protected", "not_protected"] + type: str + maximum_timeout: + description: + - The maximum timeout that a runner has to pick up a specific job. + required: False + default: 3600 + type: int + run_untagged: + description: + - Run untagged jobs or not. + required: False + default: yes + type: bool + tag_list: + description: The tags that apply to the runner. + required: False + default: [] + type: list ''' EXAMPLES = ''' -# Register a new runner (if it does not exist) - -- name: Register runner +- name: "Register runner" gitlab_runner: - url: https://gitlab.com/api/v4/ - private_token: ...5432632464326432632463246... - registration_token: ...4gfdsg345... - name: Docker Machine t1 + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" + registration_token: 4gfdsg345 + description: Docker Machine t1 state: present active: True tag_list: ['docker'] run_untagged: False locked: False - access_level: ref_protected - register: api -# Delete a runner -- name: Delete runner +- name: "Delete runner" gitlab_runner: - url: https://gitlab.com/api/v4/ - private_token: ...5432632464326432632463246... - registration_token: ...4gfdsg345... - name: Docker Machine t1 + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" + description: Docker Machine t1 state: absent - register: api ''' RETURN = ''' -changed: - description: Values changed on the API - returned: changed - type: bool - sample: false msg: - description: Information returned from the API when updating a runner, a create only returns the id and token. - returned: always - type: dict - sample: { - "id": 31, - "description": "Docker Machine t2", - "active": true, - "is_shared": true, - "name": null, - "online": null, - "status": "not_connected", - "tag_list": [ - "docker" - ], - "run_untagged": true, - "locked": false, - "maximum_timeout": null, - "access_level": "ref_protected", - "version": null, - "revision": null, - "platform":null, - "architecture": null, - "contacted_at": null, - "token": "ba4be.....e6a3b8", - "projects": [], - "groups": [] - } -token: - description: Runner token of affected runner - returned: when registered or updated runner - type: str - sample: ["2a5aeecc61dc98c4d780b14b330e3282"] + description: Success or failure message + returned: always + type: str + sample: "Success" + +result: + description: json parsed response from the server + returned: always + type: dict + +error: + description: the error message returned by the Gitlab API + returned: failed + type: str + sample: "400: path is already in use" + +runner: + description: API object + returned: always + type: dict ''' -from ansible.module_utils.basic import env_fallback -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.urls import fetch_url -import json +import os +import re +import traceback + +GITLAB_IMP_ERR = None +try: + import gitlab + HAS_GITLAB_PACKAGE = True +except Exception: + GITLAB_IMP_ERR = traceback.format_exc() + HAS_GITLAB_PACKAGE = False +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native -class AnsibleGitlabAPI(AnsibleModule): - def __init__(self, module, url, private_token): +try: + cmp +except NameError: + def cmp(a, b): + return (a > b) - (a < b) + + +class GitLabRunner(object): + def __init__(self, module, gitlab_instance): self._module = module - self._auth_header = {'PRIVATE-TOKEN': private_token} - self.url = url - self.timeout = module.params['api_timeout'] - - def check_response(self, info, response, api_call): - """ - Checks response code. - Returns: response in JSON. - """ - if info['status'] in (200, 201): - return json.loads(response.read()) - elif info['status'] == 204: - return json.loads('{"msg":"Request completed"}') - elif info['status'] in (403, 404): - return None + self._gitlab = gitlab_instance + self.runnerObject = None + + def createOrUpdateRunner(self, description, options): + changed = False + + # Because we have already call userExists in main() + if self.runnerObject is None: + runner = self.createRunner({ + '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']}) + changed = True else: - self._module.fail_json(msg='Failure while calling the GitLab API for ' - '"%s".' % api_call, fetch_url_info=info) - - def _get(self, api_call): - resp, info = fetch_url(self._module, self.url + api_call, - headers=self._auth_header, - timeout=self.timeout) - return self.check_response(info, resp, api_call) - - def _post(self, api_call, data=None): - """ - Sends POST request. - Returns: response. - """ - headers = self._auth_header.copy() - if data is not None: - data = self._module.jsonify(data) - headers['Content-type'] = 'application/json' - - resp, info = fetch_url(self._module, - self.url + api_call, - headers=headers, - method='POST', - data=data, - timeout=self.timeout) - return self.check_response(info, resp, api_call) - - def _put(self, api_call, data=None): - """ - Sends PUT request. - Returns: response. - """ - headers = self._auth_header.copy() - if data is not None: - data = self._module.jsonify(data) - headers['Content-type'] = 'application/json' - - resp, info = fetch_url(self._module, - self.url + api_call, - headers=headers, - method='PUT', - data=data, - timeout=self.timeout) - return self.check_response(info, resp, api_call) - - def _delete(self, api_call): - """ - Sends DELETE request. - Returns: response. - """ - resp, info = fetch_url(self._module, - self.url + api_call, - headers=self._auth_header, - method='DELETE', - timeout=self.timeout) - return self.check_response(info, resp, api_call) - - def get_runner_id(self, description): - """ - Gets the ID for a given description. - Returns: ID as int. - """ - r = self._get('runners/all') - if r is None: - return None - for runner in r: - if runner['description'] == description: - return runner.get('id') - return None - - def delete_runner(self, id): - """ - Sends DELETE request for runner with the same ID. - Returns: response. - """ - return self._delete('runners/' + str(id)) - - def update_runner(self, id, runner): - """ - Sends UPDATE request for runner to change all the fields. - Returns: response. - """ - form_data = runner.get_dict() - return self._put('runners/' + str(id), form_data) - - def register_runner(self, runner, registration_token): - """ - Sends POST request to register runner. - Returns: response. - """ - form_data = runner.get_dict() - form_data["token"] = registration_token - return self._post('runners/', form_data) - - def get_runner_list_short(self): - """ - Sends GET request to get a global list of all runners. - Returns: JSON list. - """ - return self._get('runners/all') - - def get_runner_list(self, ids, primary_key): - """ - Sends GET requests to get details of all runners. - Returns: JSON list. - """ - d = {} - for i in ids: - runner_details = self.get_runner_details(i) - d[runner_details.get(primary_key)] = runner_details - return d - - def get_runner_details(self, id): - """ - Sends GET request to get the details of a specific runner . - Returns: JSON list. - """ - return self._get('runners/' + str(id)) - - -class Runner(): - def __init__(self, id, description, active, tag_list, run_untagged, locked, access_level, maximum_timeout): - self.id = id - self.description = description - self.active = active - self.tag_list = tag_list - self.run_untagged = run_untagged - self.locked = locked - self.access_level = access_level - self.maximum_timeout = maximum_timeout - - def __eq__(self, other): - """ - Compare every field and its value with the fields and values of another instance of this class. - Returns: boolean. - """ - return self.__dict__ == other.__dict__ - - def get_dict(self): - """ - Returns every field and its value of this class. - Returns: dict. - """ - return self.__dict__ - - -def get_gitlab_argument_spec(): - """ - Returns argument spec with all optional or required Ansible arguments. - Returns: dict. - """ - return dict( - private_token=dict( - fallback=(env_fallback, ['GITLAB_PRIVATE_TOKEN']), - no_log=True, - required=True), - name=dict(required=True, type='str'), - active=dict(required=False, type='bool', default=True, choices=[True, False]), - tag_list=dict(required=False, type='list', - default=["docker"]), - run_untagged=dict( - required=False, type='bool', default=True), - locked=dict(required=False, type='bool', default=False, choices=[True, False]), - access_level=dict(required=False, type='str', - default='ref_protected', choices=["ref_protected", "not_protected"]), - maximum_timeout=dict( - required=False, type='int', default=3600), - api_timeout=dict(default=30, type='int'), - url=dict(required=False, type='str', - default="https://gitlab.com/api/v4/"), - registration_token=dict(required=True, type='str'), - state=dict(required=False, type='str', default="present", choices=["present", "absent"]), - ) + changed, runner = self.updateRunner(self.runnerObject, { + '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']}) + + self.runnerObject = 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 + + ''' + @param arguments Attributs of the runner + ''' + def createRunner(self, arguments): + if self._module.check_mode: + return True + + try: + runner = self._gitlab.runners.create(arguments) + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create runner: %s " % to_native(e)) + + return runner + + ''' + @param runner Runner object + @param arguments Attributs of the runner + ''' + def updateRunner(self, runner, arguments): + changed = False + + for arg_key, arg_value in arguments.items(): + if arguments[arg_key] is not None: + if isinstance(arguments[arg_key], list): + list1 = getattr(runner, arg_key) + list1.sort() + list2 = arguments[arg_key] + list2.sort() + if cmp(list1, list2): + setattr(runner, arg_key, arguments[arg_key]) + changed = True + else: + if getattr(runner, arg_key) != arguments[arg_key]: + setattr(runner, arg_key, arguments[arg_key]) + changed = True + + return (changed, runner) + + ''' + @param description Description of the runner + ''' + def findRunner(self, description): + runners = self._gitlab.runners.all() + for runner in runners: + if (runner['description'] == description): + return self._gitlab.runners.get(runner['id']) + + ''' + @param description Description of the runner + ''' + def existsRunner(self, description): + # When runner exists, object will be stored in self.runnerObject. + runner = self.findRunner(description) + + if runner: + self.runnerObject = runner + return True + return False + + def deleteRunner(self): + if self._module.check_mode: + return True + + runner = self.runnerObject + + return runner.delete() + + +def deprecation_warning(module): + deprecated_aliases = ['login_token'] + + module.deprecate("Aliases \'{aliases}\' are deprecated".format(aliases='\', \''.join(deprecated_aliases)), 2.10) def main(): - argument_spec = get_gitlab_argument_spec() + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + url=dict(type='str', required=True, removed_in_version=2.10), + api_token=dict(type='str', no_log=True, aliases=["private_token"]), + description=dict(type='str', required=True, aliases=["name"]), + active=dict(type='bool', default=True), + tag_list=dict(type='list', default=[]), + 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"]), + maximum_timeout=dict(type='int', default=3600), + registration_token=dict(type='str', required=True), + state=dict(type='str', default="present", choices=["absent", "present"]), + )) + module = AnsibleModule( argument_spec=argument_spec, + mutually_exclusive=[ + ['api_url', 'url'], + ['api_username', 'api_token'], + ['api_password', 'api_token'], + ], + required_together=[ + ['api_username', 'api_password'], + ['login_user', 'login_password'], + ], + required_one_of=[ + ['api_username', 'api_token'] + ], supports_check_mode=True, ) - target_state = module.params['state'] - description = module.params['name'] - private_token = module.params['private_token'] - url = module.params['url'] - active = module.params['active'] + deprecation_warning(module) + + url = re.sub('/api.*', '', module.params['url']) + + api_url = module.params['api_url'] + validate_certs = module.params['validate_certs'] + + gitlab_url = url if api_url is None else api_url + gitlab_user = module.params['api_username'] + gitlab_password = module.params['api_password'] + gitlab_token = module.params['api_token'] + + state = module.params['state'] + runner_description = module.params['description'] + runner_active = module.params['active'] tag_list = module.params['tag_list'] run_untagged = module.params['run_untagged'] - locked = module.params['locked'] + runner_locked = module.params['locked'] access_level = module.params['access_level'] maximum_timeout = module.params['maximum_timeout'] - - api = AnsibleGitlabAPI(module, url, private_token) - id = api.get_runner_id(description) - target_runner = Runner(id, description, active, tag_list, - run_untagged, locked, access_level, maximum_timeout) - token = None - api_runner = None - response = None - changed = False - - # Check if runner needs to be registered, updated or deleted - # Don't actually change anything if module is in check_mode (dry run) - if target_state == 'present': - if id is None: - if not module.check_mode: - response = api.register_runner( - target_runner, module.params["registration_token"]) - token = response['token'] - changed = True + registration_token = module.params['registration_token'] + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) + + try: + gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password, + private_token=gitlab_token, api_version=4) + gitlab_instance.auth() + except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s" % to_native(e)) + except (gitlab.exceptions.GitlabHttpError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s. \ + Gitlab remove Session API now that private tokens are removed from user API endpoints since version 10.2" % to_native(e)) + + gitlab_runner = GitLabRunner(module, gitlab_instance) + runner_exists = gitlab_runner.existsRunner(runner_description) + + if state == 'absent': + if runner_exists: + gitlab_runner.deleteRunner() + module.exit_json(changed=True, msg="Successfully deleted runner %s" % runner_description) else: - api_runner_details = api.get_runner_details(id) - response = api_runner_details - token = api_runner_details['token'] - id = api_runner_details['id'] - description = api_runner_details['description'] - active = api_runner_details['active'] - tag_list = api_runner_details['tag_list'] - locked = api_runner_details['locked'] - access_level = api_runner_details['access_level'] - maximum_timeout = api_runner_details['maximum_timeout'] - api_runner = Runner(id, description, active, tag_list, - run_untagged, locked, access_level, maximum_timeout) - if api_runner == target_runner: - changed = False - else: - if not module.check_mode: - response = api.update_runner(id, target_runner) - changed = True - elif target_state == 'absent': - if id is None: - changed = False + module.exit_json(changed=False, msg="Runner deleted or does not exists") + + if state == 'present': + if gitlab_runner.createOrUpdateRunner(runner_description, { + "active": runner_active, + "tag_list": tag_list, + "run_untagged": run_untagged, + "locked": runner_locked, + "access_level": access_level, + "maximum_timeout": maximum_timeout, + "registration_token": registration_token}): + module.exit_json(changed=True, runner=gitlab_runner.runnerObject._attrs, + msg="Successfully created or updated the runner %s" % runner_description) else: - if not module.check_mode: - response = api.delete_runner(id) - changed = True - module.exit_json(changed=changed, msg=response, token=token) + module.exit_json(changed=False, runner=gitlab_runner.runnerObject._attrs, + msg="No need to update the runner %s" % runner_description) if __name__ == '__main__': diff --git a/lib/ansible/modules/source_control/gitlab_user.py b/lib/ansible/modules/source_control/gitlab_user.py index 9430daec7a1..1ad2d7ef7e3 100644 --- a/lib/ansible/modules/source_control/gitlab_user.py +++ b/lib/ansible/modules/source_control/gitlab_user.py @@ -1,114 +1,144 @@ #!/usr/bin/python -# (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Guillaume Martinez (guillaume.lunik@gmail.com) +# Copyright: (c) 2015, Werner Dijkerman (ikben@werner-dijkerman.nl) # 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 - ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} - DOCUMENTATION = ''' --- module: gitlab_user short_description: Creates/updates/deletes Gitlab Users description: - - When the user does not exist in Gitlab, it will be created. - - When the user does exists and state=absent, the user will be deleted. - - When changes are made to user, the user will be updated. + - When the user does not exist in Gitlab, it will be created. + - When the user does exists and state=absent, the user will be deleted. + - When changes are made to user, the user will be updated. version_added: "2.1" -author: "Werner Dijkerman (@dj-wasabi)" +author: + - Werner Dijkerman (@dj-wasabi) + - Guillaume Martinez (@Lunik) requirements: - - pyapi-gitlab python module - - administrator rights on the Gitlab server + - python >= 2.7 + - python-gitlab python module + - administrator rights on the Gitlab server +extends_documentation_fragment: + - auth_basic options: - server_url: - description: - - Url of Gitlab server, with protocol (http or https). - required: true - validate_certs: - description: - - When using https if SSL certificate needs to be verified. - type: bool - default: 'yes' - aliases: - - verify_ssl - login_user: - description: - - Gitlab user name. - login_password: - description: - - Gitlab password for login_user - login_token: - description: - - Gitlab token for logging in. - name: - description: - - Name of the user you want to create - required: true - username: - description: - - The username of the user. - required: true - password: - description: - - The password of the user. - - GitLab server enforces minimum password length to 8, set this value with 8 or more characters. - required: true - email: - description: - - The email that belongs to the user. - required: true - sshkey_name: - description: - - The name of the sshkey - sshkey_file: - description: - - The ssh key itself. - group: - description: - - Add user as an member to this group. - access_level: - description: - - The access level to the group. One of the following can be used. - - guest - - reporter - - developer - - master - - owner - state: - description: - - create or delete group. - - Possible values are present and absent. - default: present - choices: ["present", "absent"] - confirm: - description: - - Require confirmation. - type: bool - default: 'yes' - version_added: "2.4" + server_url: + description: + - The URL of the Gitlab server, with protocol (i.e. http or https). + required: true + type: str + login_user: + description: + - Gitlab user name. + type: str + login_password: + description: + - Gitlab password for login_user + type: str + api_token: + description: + - Gitlab token for logging in. + type: str + aliases: + - login_token + name: + description: + - Name of the user you want to create + required: true + type: str + username: + description: + - The username of the user. + required: true + type: str + password: + description: + - The password of the user. + - GitLab server enforces minimum password length to 8, set this value with 8 or more characters. + required: true + type: str + email: + description: + - The email that belongs to the user. + required: true + type: str + sshkey_name: + description: + - The name of the sshkey + type: str + sshkey_file: + description: + - The ssh key itself. + type: str + group: + description: + - Id or Full path of parent group in the form of group/name + - Add user as an member to this group. + type: str + access_level: + description: + - The access level to the group. One of the following can be used. + - guest + - reporter + - developer + - master (alias for maintainer) + - maintainer + - owner + default: guest + type: str + choices: ["guest", "reporter", "developer", "master", "maintainer", "owner"] + state: + description: + - create or delete group. + - Possible values are present and absent. + default: present + type: str + choices: ["present", "absent"] + confirm: + description: + - Require confirmation. + type: bool + default: yes + version_added: "2.4" + isadmin: + description: + - Grant admin privilieges to the user + type: bool + default: no + version_added: "2.8" + external: + description: + - Define external parameter for this user + type: bool + default: no + version_added: "2.8" ''' EXAMPLES = ''' -- name: Delete Gitlab User +- name: "Delete Gitlab User" gitlab_user: - server_url: http://gitlab.example.com + api_url: https://gitlab.example.com/ + api_token: "{{ access_token }}" validate_certs: False - login_token: WnUzDsxjy8230-Dy_k username: myusername state: absent delegate_to: localhost -- name: Create Gitlab User +- name: "Create Gitlab User" gitlab_user: - server_url: https://gitlab.dj-wasabi.local + api_url: https://gitlab.example.com/ validate_certs: True - login_user: dj-wasabi - login_password: MySecretPassword + api_username: dj-wasabi + api_password: "MySecretPassword" name: My Name username: myusername password: mysecretpassword @@ -116,233 +146,383 @@ EXAMPLES = ''' sshkey_name: MySSH sshkey_file: ssh-rsa AAAAB3NzaC1yc... state: present + group: super_group/mon_group + access_level: owner delegate_to: localhost ''' -RETURN = '''# ''' +RETURN = ''' +msg: + description: Success or failure message + returned: always + type: str + sample: "Success" + +result: + description: json parsed response from the server + returned: always + type: dict + +error: + description: the error message returned by the Gitlab API + returned: failed + type: str + sample: "400: path is already in use" + +user: + description: API object + returned: always + type: dict +''' + +import os +import re +import traceback +GITLAB_IMP_ERR = None try: import gitlab HAS_GITLAB_PACKAGE = True except Exception: + GITLAB_IMP_ERR = traceback.format_exc() HAS_GITLAB_PACKAGE = False -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.api import basic_auth_argument_spec +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native +from ansible.module_utils.gitlab import findGroup + class GitLabUser(object): - def __init__(self, module, git): + def __init__(self, module, gitlab_instance): self._module = module - self._gitlab = git - - def addToGroup(self, group_id, user_id, access_level): - if access_level == "guest": - level = 10 - elif access_level == "reporter": - level = 20 - elif access_level == "developer": - level = 30 - elif access_level == "master": - level = 40 - elif access_level == "owner": - level = 50 - return self._gitlab.addgroupmember(group_id, user_id, level) - - def createOrUpdateUser(self, user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level, confirm): - group_id = '' - arguments = {"name": user_name, - "username": user_username, - "email": user_email} - - if group_name is not None: - if self.existsGroup(group_name): - group_id = self.getGroupId(group_name) - - if self.existsUser(user_username): - self.updateUser(group_id, user_sshkey_name, user_sshkey_file, access_level, arguments) + self._gitlab = gitlab_instance + self.userObject = None + self.ACCESS_LEVEL = { + 'guest': gitlab.GUEST_ACCESS, + 'reporter': gitlab.REPORTER_ACCESS, + 'developer': gitlab.DEVELOPER_ACCESS, + 'master': gitlab.MAINTAINER_ACCESS, + 'maintainer': gitlab.MAINTAINER_ACCESS, + 'owner': gitlab.OWNER_ACCESS} + + ''' + @param username Username of the user + @param options User options + ''' + def createOrUpdateUser(self, username, options): + changed = False + + # Because we have already call userExists in main() + if self.userObject is None: + user = self.createUser({ + 'name': options['name'], + 'username': username, + 'password': options['password'], + 'email': options['email'], + 'skip_confirmation': not options['confirm'], + 'admin': options['isadmin'], + 'external': options['external']}) + changed = True else: + changed, user = self.updateUser(self.userObject, { + 'name': options['name'], + 'email': options['email'], + 'is_admin': options['isadmin'], + 'external': options['external']}) + + # Assign ssh keys + if options['sshkey_name'] and options['sshkey_file']: + changed = changed or self.addSshKeyToUser(user, { + 'name': options['sshkey_name'], + 'file': options['sshkey_file']}) + + # Assign group + if options['group_path']: + changed = changed or self.assignUserToGroup(user, options['group_path'], options['access_level']) + + self.userObject = user + if changed: if self._module.check_mode: - self._module.exit_json(changed=True) - self.createUser(group_id, user_password, user_sshkey_name, user_sshkey_file, access_level, confirm, arguments) - - def createUser(self, group_id, user_password, user_sshkey_name, user_sshkey_file, access_level, confirm, arguments): - user_changed = False - - # Create the user - user_username = arguments['username'] - if self._gitlab.createuser(password=user_password, confirm=confirm, **arguments): - user_id = self.getUserId(user_username) - if self._gitlab.addsshkeyuser(user_id=user_id, title=user_sshkey_name, key=user_sshkey_file): - user_changed = True - # Add the user to the group if group_id is not empty - if group_id != '': - if self.addToGroup(group_id, user_id, access_level): - user_changed = True - user_changed = True - - # Exit with change to true or false - if user_changed: - self._module.exit_json(changed=True, result="Created the user") - else: - self._module.exit_json(changed=False) - - def deleteUser(self, user_username): - user_id = self.getUserId(user_username) + self._module.exit_json(changed=True, msg="Successfully created or updated the user %s" % username) - if self._gitlab.deleteuser(user_id): - self._module.exit_json(changed=True, result="Successfully deleted user %s" % user_username) + try: + user.save() + except Exception as e: + self._module.fail_json(msg="Failed to update user: %s " % to_native(e)) + return True else: - self._module.exit_json(changed=False, result="User %s already deleted or something went wrong" % user_username) + return False + + ''' + @param group User object + ''' + def getUserId(self, user): + if user is not None: + return user.id + return None + + ''' + @param user User object + @param sshkey_name Name of the ssh key + ''' + def sshKeyExists(self, user, sshkey_name): + keyList = map(lambda k: k.title, user.keys.list()) + + return sshkey_name in keyList + + ''' + @param user User object + @param sshkey Dict containing sshkey infos {"name": "", "file": ""} + ''' + def addSshKeyToUser(self, user, sshkey): + if not self.sshKeyExists(user, sshkey['name']): + if self._module.check_mode: + return True + + try: + user.keys.create({ + 'title': sshkey['name'], + 'key': sshkey['file']}) + except gitlab.exceptions.GitlabCreateError as e: + self._module.fail_json(msg="Failed to assign sshkey to user: %s" % to_native(e)) + return True + return False - def existsGroup(self, group_name): - for group in self._gitlab.getall(self._gitlab.getgroups): - if group['name'] == group_name: + ''' + @param group Group object + @param user_id Id of the user to find + ''' + def findMember(self, group, user_id): + try: + member = group.members.get(user_id) + except gitlab.exceptions.GitlabGetError as e: + return None + return member + + ''' + @param group Group object + @param user_id Id of the user to check + ''' + def memberExists(self, group, user_id): + member = self.findMember(group, user_id) + + return member is not None + + ''' + @param group Group object + @param user_id Id of the user to check + @param access_level Gitlab access_level to check + ''' + def memberAsGoodAccessLevel(self, group, user_id, access_level): + member = self.findMember(group, user_id) + + return member.access_level == access_level + + ''' + @param user User object + @param group_path Complete path of the Group including parent group path. / + @param access_level Gitlab access_level to assign + ''' + def assignUserToGroup(self, user, group_identifier, access_level): + group = findGroup(self._gitlab, group_identifier) + + if self._module.check_mode: + return True + + if group is None: + return False + + if self.memberExists(group, self.getUserId(user)): + member = self.findMember(group, self.getUserId(user)) + if not self.memberAsGoodAccessLevel(group, member.id, self.ACCESS_LEVEL[access_level]): + member.access_level = self.ACCESS_LEVEL[access_level] + member.save() return True + else: + try: + group.members.create({ + 'user_id': self.getUserId(user), + 'access_level': self.ACCESS_LEVEL[access_level]}) + except gitlab.exceptions.GitlabCreateError as e: + self._module.fail_json(msg="Failed to assign user to group: %s" % to_native(e)) + return True return False + ''' + @param user User object + @param arguments User attributes + ''' + def updateUser(self, user, arguments): + changed = False + + for arg_key, arg_value in arguments.items(): + if arguments[arg_key] is not None: + if getattr(user, arg_key) != arguments[arg_key]: + setattr(user, arg_key, arguments[arg_key]) + changed = True + + return (changed, user) + + ''' + @param arguments User attributes + ''' + def createUser(self, arguments): + if self._module.check_mode: + return True + + try: + user = self._gitlab.users.create(arguments) + except (gitlab.exceptions.GitlabCreateError) as e: + self._module.fail_json(msg="Failed to create user: %s " % to_native(e)) + + return user + + ''' + @param username Username of the user + ''' + def findUser(self, username): + users = self._gitlab.users.list(search=username) + for user in users: + if (user.username == username): + return user + + ''' + @param username Username of the user + ''' def existsUser(self, username): - found_user = self._gitlab.getusers(search=username) - for user in found_user: - if user['id'] != '': - return True + # When user exists, object will be stored in self.userObject. + user = self.findUser(username) + if user: + self.userObject = user + return True return False - def getGroupId(self, group_name): - for group in self._gitlab.getall(self._gitlab.getgroups): - if group['name'] == group_name: - return group['id'] + def deleteUser(self): + if self._module.check_mode: + return True - def getUserId(self, username): - found_user = self._gitlab.getusers(search=username) - for user in found_user: - if user['id'] != '': - return user['id'] + user = self.userObject - def updateUser(self, group_id, user_sshkey_name, user_sshkey_file, access_level, arguments): - user_changed = False - user_username = arguments['username'] - user_id = self.getUserId(user_username) - user_data = self._gitlab.getuser(user_id=user_id) + return user.delete() - # Lets check if we need to update the user - for arg_key, arg_value in arguments.items(): - if user_data[arg_key] != arg_value: - user_changed = True - if user_changed: - if self._module.check_mode: - self._module.exit_json(changed=True) - self._gitlab.edituser(user_id=user_id, **arguments) - user_changed = True - if self._module.check_mode or self._gitlab.addsshkeyuser(user_id=user_id, title=user_sshkey_name, key=user_sshkey_file): - user_changed = True - if group_id != '': - if self._module.check_mode or self.addToGroup(group_id, user_id, access_level): - user_changed = True - if user_changed: - self._module.exit_json(changed=True, result="The user %s is updated" % user_username) - else: - self._module.exit_json(changed=False, result="The user %s is already up2date" % user_username) +def deprecation_warning(module): + deprecated_aliases = ['login_token'] + + module.deprecate("Aliases \'{aliases}\' are deprecated".format(aliases='\', \''.join(deprecated_aliases)), 2.10) def main(): + argument_spec = basic_auth_argument_spec() + argument_spec.update(dict( + server_url=dict(type='str', required=True, removed_in_version=2.10), + login_user=dict(type='str', no_log=True, removed_in_version=2.10), + login_password=dict(type='str', no_log=True, removed_in_version=2.10), + api_token=dict(type='str', no_log=True, aliases=["login_token"]), + name=dict(type='str', required=True), + state=dict(type='str', default="present", choices=["absent", "present"]), + username=dict(type='str', required=True), + password=dict(type='str', required=True, no_log=True), + email=dict(type='str', required=True), + sshkey_name=dict(type='str'), + sshkey_file=dict(type='str'), + group=dict(type='str'), + access_level=dict(type='str', default="guest", choices=["developer", "guest", "maintainer", "master", "owner", "reporter"]), + confirm=dict(type='bool', default=True), + isadmin=dict(type='bool', default=False), + external=dict(type='bool', default=False), + )) + module = AnsibleModule( - argument_spec=dict( - server_url=dict(required=True), - validate_certs=dict(required=False, default=True, type='bool', aliases=['verify_ssl']), - login_user=dict(required=False, no_log=True), - login_password=dict(required=False, no_log=True), - login_token=dict(required=False, no_log=True), - name=dict(required=True), - username=dict(required=True), - password=dict(required=True, no_log=True), - email=dict(required=True), - sshkey_name=dict(required=False), - sshkey_file=dict(required=False), - group=dict(required=False), - access_level=dict(required=False, choices=["guest", "reporter", "developer", "master", "owner"]), - state=dict(default="present", choices=["present", "absent"]), - confirm=dict(required=False, default=True, type='bool') - ), - supports_check_mode=True + argument_spec=argument_spec, + mutually_exclusive=[ + ['api_url', 'server_url'], + ['api_username', 'login_user'], + ['api_password', 'login_password'], + ['api_username', 'api_token'], + ['api_password', 'api_token'], + ['login_user', 'login_token'], + ['login_password', 'login_token'] + ], + required_together=[ + ['api_username', 'api_password'], + ['login_user', 'login_password'], + ], + required_one_of=[ + ['api_username', 'api_token', 'login_user', 'login_token'] + ], + supports_check_mode=True, ) - if not HAS_GITLAB_PACKAGE: - module.fail_json(msg="Missing required gitlab module (check docs or install with: pip install pyapi-gitlab") + deprecation_warning(module) server_url = module.params['server_url'] - verify_ssl = module.params['validate_certs'] login_user = module.params['login_user'] login_password = module.params['login_password'] - login_token = module.params['login_token'] + + api_url = module.params['api_url'] + validate_certs = module.params['validate_certs'] + api_user = module.params['api_username'] + api_password = module.params['api_password'] + + gitlab_url = server_url if api_url is None else api_url + gitlab_user = login_user if api_user is None else api_user + gitlab_password = login_password if api_password is None else api_password + gitlab_token = module.params['api_token'] + user_name = module.params['name'] - user_username = module.params['username'] + state = module.params['state'] + user_username = module.params['username'].lower() user_password = module.params['password'] user_email = module.params['email'] user_sshkey_name = module.params['sshkey_name'] user_sshkey_file = module.params['sshkey_file'] - group_name = module.params['group'] + group_path = module.params['group'] access_level = module.params['access_level'] - state = module.params['state'] confirm = module.params['confirm'] + user_isadmin = module.params['isadmin'] + user_external = module.params['external'] + + if not HAS_GITLAB_PACKAGE: + module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR) - if len(user_password) < 8: - module.fail_json(msg="New user's 'password' should contain more than 8 characters.") - - # We need both login_user and login_password or login_token, otherwise we fail. - if login_user is not None and login_password is not None: - use_credentials = True - elif login_token is not None: - use_credentials = False - else: - module.fail_json(msg="No login credentials are given. Use login_user with login_password, or login_token") - - # Check if vars are none - if user_sshkey_file is not None and user_sshkey_name is not None: - use_sshkey = True - else: - use_sshkey = False - - if group_name is not None and access_level is not None: - add_to_group = True - group_name = group_name.lower() - else: - add_to_group = False - - user_username = user_username.lower() - - # Lets make an connection to the Gitlab server_url, with either login_user and login_password - # or with login_token try: - if use_credentials: - git = gitlab.Gitlab(host=server_url, verify_ssl=verify_ssl) - git.login(user=login_user, password=login_password) + gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password, + private_token=gitlab_token, api_version=4) + gitlab_instance.auth() + except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s" % to_native(e)) + except (gitlab.exceptions.GitlabHttpError) as e: + module.fail_json(msg="Failed to connect to Gitlab server: %s. \ + Gitlab remove Session API now that private tokens are removed from user API endpoints since version 10.2." % to_native(e)) + + gitlab_user = GitLabUser(module, gitlab_instance) + user_exists = gitlab_user.existsUser(user_username) + + if state == 'absent': + if user_exists: + gitlab_user.deleteUser() + module.exit_json(changed=True, msg="Successfully deleted user %s" % user_username) else: - git = gitlab.Gitlab(server_url, token=login_token, verify_ssl=verify_ssl) - except Exception as e: - module.fail_json(msg="Failed to connect to Gitlab server: %s " % to_native(e)) - - # Check if user is authorized or not before proceeding to any operations - # if not, exit from here - auth_msg = git.currentuser().get('message', None) - if auth_msg is not None and auth_msg == '401 Unauthorized': - module.fail_json(msg='User unauthorized', - details="User is not allowed to access Gitlab server " - "using login_token. Please check login_token") - - # Validate if group exists and take action based on "state" - user = GitLabUser(module, git) - - # Check if user exists, if not exists and state = absent, we exit nicely. - if not user.existsUser(user_username) and state == "absent": - module.exit_json(changed=False, result="User already deleted or does not exist") - else: - # User exists, - if state == "absent": - user.deleteUser(user_username) + module.exit_json(changed=False, msg="User deleted or does not exists") + + if state == 'present': + if gitlab_user.createOrUpdateUser(user_username, { + "name": user_name, + "password": user_password, + "email": user_email, + "sshkey_name": user_sshkey_name, + "sshkey_file": user_sshkey_file, + "group_path": group_path, + "access_level": access_level, + "confirm": confirm, + "isadmin": user_isadmin, + "external": user_external}): + module.exit_json(changed=True, msg="Successfully created or updated the user %s" % user_username, user=gitlab_user.userObject._attrs) else: - user.createOrUpdateUser(user_name, user_username, user_password, user_email, user_sshkey_name, user_sshkey_file, group_name, access_level, confirm) + module.exit_json(changed=False, msg="No need to update the user %s" % user_username, user=gitlab_user.userObject._attrs) if __name__ == '__main__': diff --git a/test/runner/requirements/units.txt b/test/runner/requirements/units.txt index 1e0c6b46736..b899a4ed893 100644 --- a/test/runner/requirements/units.txt +++ b/test/runner/requirements/units.txt @@ -38,3 +38,7 @@ pexpect # requirement for the linode module linode-python # APIv3 linode_api4 ; python_version > '2.6' # APIv4 + +# requirement for the gitlab module +python-gitlab +httmock diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index 036d7795ce4..26e36e460cf 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -822,9 +822,6 @@ lib/ansible/modules/remote_management/ucs/ucs_wwn_pool.py E322 lib/ansible/modules/remote_management/ucs/ucs_wwn_pool.py E323 lib/ansible/modules/source_control/github_issue.py E324 lib/ansible/modules/source_control/github_issue.py E326 -lib/ansible/modules/source_control/gitlab_project.py E324 -lib/ansible/modules/source_control/gitlab_project.py E326 -lib/ansible/modules/source_control/gitlab_user.py E326 lib/ansible/modules/source_control/subversion.py E322 lib/ansible/modules/storage/infinidat/infini_export.py E323 lib/ansible/modules/storage/infinidat/infini_export.py E324 diff --git a/test/units/modules/source_control/gitlab.py b/test/units/modules/source_control/gitlab.py new file mode 100644 index 00000000000..74ee5ffdc6d --- /dev/null +++ b/test/units/modules/source_control/gitlab.py @@ -0,0 +1,563 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import + +import sys + +from httmock import response # noqa +from httmock import urlmatch # noqa + +from units.compat import unittest + +from gitlab import Gitlab + + +class FakeAnsibleModule(object): + def __init__(self): + self.check_mode = False + + def fail_json(self, **args): + pass + + def exit_json(self, **args): + pass + + +class GitlabModuleTestCase(unittest.TestCase): + def setUp(self): + unitest_python_version_check_requirement(self) + + self.mock_module = FakeAnsibleModule() + + self.gitlab_instance = Gitlab("http://localhost", private_token="private_token", api_version=4) + + +# Python 2.7+ is needed for python-gitlab +GITLAB_MINIMUM_PYTHON_VERSION = (2, 7) + + +# Verify if the current Python version is higher than GITLAB_MINIMUM_PYTHON_VERSION +def python_version_match_requirement(): + return sys.version_info >= GITLAB_MINIMUM_PYTHON_VERSION + + +# Skip unittest test case if python version don't match requirement +def unitest_python_version_check_requirement(unittest_testcase): + if not python_version_match_requirement(): + unittest_testcase.skipTest("Python %s+ is needed for python-gitlab" % ",".join(map(str, GITLAB_MINIMUM_PYTHON_VERSION))) + + +''' +USER API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users", method="get") +def resp_find_user(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"id": 1, "username": "john_smith", "name": "John Smith", "state": "active",' + '"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",' + '"web_url": "http://localhost:3000/john_smith"}, {"id": 2,' + '"username": "jack_smith", "name": "Jack Smith", "state": "blocked",' + '"avatar_url": "http://gravatar.com/../e32131cd8.jpeg",' + '"web_url": "http://localhost:3000/jack_smith"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users", method="post") +def resp_create_user(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "username": "john_smith", "name": "John Smith", "state": "active",' + '"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",' + '"web_url": "http://localhost:3000/john_smith","created_at": "2012-05-23T08:00:58Z",' + '"bio": null, "location": null, "public_email": "john@example.com", "skype": "",' + '"linkedin": "", "twitter": "", "website_url": "", "organization": ""}') + content = content.encode("utf-8") + return response(201, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="get") +def resp_get_user(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "username": "john_smith", "name": "John Smith",' + '"state": "active",' + '"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",' + '"web_url": "http://localhost:3000/john_smith",' + '"created_at": "2012-05-23T08:00:58Z", "bio": null, "location": null,' + '"public_email": "john@example.com", "skype": "", "linkedin": "",' + '"twitter": "", "website_url": "", "organization": "", "is_admin": false}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="get") +def resp_get_missing_user(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(404, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="delete") +def resp_delete_user(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(204, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="delete") +def resp_delete_missing_user(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(404, content, headers, None, 5, request) + + +''' +USER SSHKEY API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1/keys", method="get") +def resp_get_user_keys(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"id": 1, "title": "Public key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596' + 'k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQa' + 'SeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",' + '"created_at": "2014-08-01T14:47:39.080Z"},{"id": 3,' + '"title": "Another Public key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596' + 'k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaS' + 'eP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",' + '"created_at": "2014-08-01T14:47:39.080Z"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1/keys", method="post") +def resp_create_user_keys(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "title": "Private key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDA1YotVDm2mAyk2tPt4E7AHm01sS6JZmcUdRuSuA5z' + 'szUJzYPPUSRAX3BCgTqLqYx//UuVncK7YqLVSbbwjKR2Ez5lISgCnVfLVEXzwhv+xawxKWmI7hJ5S0tOv6MJ+Ixy' + 'Ta4xcKwJTwB86z22n9fVOQeJTR2dSOH1WJrf0PvRk+KVNY2jTiGHTi9AIjLnyD/jWRpOgtdfkLRc8EzAWrWlgNmH' + '2WOKBw6za0az6XoG75obUdFVdW3qcD0xc809OHLi7FDf+E7U4wiZJCFuUizMeXyuK/SkaE1aee4Qp5R4dxTR4TP9' + 'M1XAYkf+kF0W9srZ+mhF069XD/zhUPJsvwEF",' + '"created_at": "2014-08-01T14:47:39.080Z"}') + content = content.encode("utf-8") + return response(201, content, headers, None, 5, request) + + +''' +GROUP API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups", method="get") +def resp_find_group(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",' + '"file_template_project_id": 1, "parent_id": null, "projects": []}, {"id": 2, "name": "BarFoo Group", "path": "bar-foor",' + '"description": "An interesting group", "visibility": "public",' + '"lfs_enabled": true, "avatar_url": "http://localhost:3000/uploads/group/avatar/2/bar.jpg",' + '"web_url": "http://localhost:3000/groups/bar-foo", "request_access_enabled": false,' + '"full_name": "BarFoo Group", "full_path": "bar-foo",' + '"file_template_project_id": 1, "parent_id": null, "projects": []}]') + 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_group(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",' + '"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'} + content = ('{}') + content = content.encode("utf-8") + return response(404, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups", method="post") +def resp_create_group(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",' + '"file_template_project_id": 1, "parent_id": null}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups", method="post") +def resp_create_subgroup(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 2, "name": "BarFoo Group", "path": "bar-foor",' + '"description": "An interesting group", "visibility": "public",' + '"lfs_enabled": true, "avatar_url": "http://localhost:3000/uploads/group/avatar/2/bar.jpg",' + '"web_url": "http://localhost:3000/groups/foo-bar/bar-foo", "request_access_enabled": false,' + '"full_name": "BarFoo Group", "full_path": "foo-bar/bar-foo",' + '"file_template_project_id": 1, "parent_id": 1}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/users/1", method="delete") +def resp_delete_group(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(204, content, headers, None, 5, request) + + +''' +GROUP MEMBER API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1/members/1", method="get") +def resp_get_member(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "username": "raymond_smith", "name": "Raymond Smith", "state": "active",' + '"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",' + '"web_url": "http://192.168.1.8:3000/root", "expires_at": "2012-10-22T14:13:35Z", "access_level": 30}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1/members", method="get") +def resp_find_member(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"id": 1, "username": "raymond_smith", "name": "Raymond Smith", "state": "active",' + '"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",' + '"web_url": "http://192.168.1.8:3000/root", "expires_at": "2012-10-22T14:13:35Z", "access_level": 30},{' + '"id": 2, "username": "john_doe", "name": "John Doe","state": "active",' + '"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",' + '"web_url": "http://192.168.1.8:3000/root","expires_at": "2012-10-22T14:13:35Z",' + '"access_level": 30}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1/members", method="post") +def resp_add_member(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "username": "raymond_smith", "name": "Raymond Smith",' + '"state": "active",' + '"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",' + '"web_url": "http://192.168.1.8:3000/root", "expires_at": "2012-10-22T14:13:35Z",' + '"access_level": 30}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/groups/1/members/1", method="put") +def resp_update_member(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1, "username": "raymond_smith", "name": "Raymond Smith",' + '"state": "active",' + '"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",' + '"web_url": "http://192.168.1.8:3000/root", "expires_at": "2012-10-22T14:13:35Z",' + '"access_level": 10}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +''' +DEPLOY KEY API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/deploy_keys", method="get") +def resp_find_project_deploy_key(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"id": 1,"title": "Public key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxc' + 'KDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",' + '"created_at": "2013-10-02T10:12:29Z"},{"id": 3,"title": "Another Public key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxc' + 'KDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",' + '"created_at": "2013-10-02T11:12:29Z"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/deploy_keys/1", method="get") +def resp_get_project_deploy_key(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1,"title": "Public key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxc' + 'KDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",' + '"created_at": "2013-10-02T10:12:29Z"}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/deploy_keys", method="post") +def resp_create_project_deploy_key(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1,"title": "Public key",' + '"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxc' + 'KDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",' + '"created_at": "2013-10-02T10:12:29Z"}') + content = content.encode("utf-8") + return response(201, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/deploy_keys/1", method="delete") +def resp_delete_project_deploy_key(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(204, content, headers, None, 5, request) + + +''' +PROJECT API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects", method="get") +def resp_find_project(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"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/projects/1", method="get") +def resp_get_project(url, request): + headers = {'content-type': 'application/json'} + content = ('{"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/projects/foo-bar%2Fdiaspora-client", method="get") +def resp_get_project_by_name(url, request): + headers = {'content-type': 'application/json'} + content = ('{"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/projects", method="get") +def resp_find_group_project(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"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/projects/1", method="get") +def resp_get_group_project(url, request): + headers = {'content-type': 'application/json'} + content = ('{"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/projects", method="post") +def resp_create_project(url, request): + headers = {'content-type': 'application/json'} + content = ('{"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(201, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1", method="delete") +def resp_delete_project(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + + return response(204, content, headers, None, 5, request) + + +''' +HOOK API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/hooks", method="get") +def resp_find_project_hook(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"id": 1,"url": "http://example.com/hook","project_id": 3,' + '"push_events": true,"push_events_branch_filter": "","issues_events": true,' + '"confidential_issues_events": true,"merge_requests_events": true,' + '"tag_push_events": true,"note_events": true,"job_events": true,' + '"pipeline_events": true,"wiki_page_events": true,"enable_ssl_verification": true,' + '"created_at": "2012-10-12T17:04:47Z"}]') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/hooks/1", method="get") +def resp_get_project_hook(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1,"url": "http://example.com/hook","project_id": 3,' + '"push_events": true,"push_events_branch_filter": "","issues_events": true,' + '"confidential_issues_events": true,"merge_requests_events": true,' + '"tag_push_events": true,"note_events": true,"job_events": true,' + '"pipeline_events": true,"wiki_page_events": true,"enable_ssl_verification": true,' + '"created_at": "2012-10-12T17:04:47Z"}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/hooks", method="post") +def resp_create_project_hook(url, request): + headers = {'content-type': 'application/json'} + content = ('{"id": 1,"url": "http://example.com/hook","project_id": 3,' + '"push_events": true,"push_events_branch_filter": "","issues_events": true,' + '"confidential_issues_events": true,"merge_requests_events": true,' + '"tag_push_events": true,"note_events": true,"job_events": true,' + '"pipeline_events": true,"wiki_page_events": true,"enable_ssl_verification": true,' + '"created_at": "2012-10-12T17:04:47Z"}') + content = content.encode("utf-8") + return response(201, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/projects/1/hooks/1", method="delete") +def resp_delete_project_hook(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(204, content, headers, None, 5, request) + + +''' +HOOK API +''' + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/runners/all", method="get") +def resp_find_runners(url, request): + headers = {'content-type': 'application/json'} + content = ('[{"active": true,"description": "test-1-20150125","id": 1,' + '"is_shared": false,"ip_address": "127.0.0.1","name": null,' + '"online": true,"status": "online"},{"active": true,' + '"description": "test-2-20150125","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="/api/v4/runners/1", method="get") +def resp_get_runner(url, request): + headers = {'content-type': 'application/json'} + content = ('{"active": true,"description": "test-1-20150125","id": 1,' + '"is_shared": false,"ip_address": "127.0.0.1","name": null,' + '"online": true,"status": "online"}') + content = content.encode("utf-8") + return response(200, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/runners", method="post") +def resp_create_runner(url, request): + headers = {'content-type': 'application/json'} + content = ('{"active": true,"description": "test-1-20150125","id": 1,' + '"is_shared": false,"ip_address": "127.0.0.1","name": null,' + '"online": true,"status": "online"}') + content = content.encode("utf-8") + return response(201, content, headers, None, 5, request) + + +@urlmatch(scheme="http", netloc="localhost", path="/api/v4/runners/1", method="delete") +def resp_delete_runner(url, request): + headers = {'content-type': 'application/json'} + content = ('{}') + content = content.encode("utf-8") + return response(204, content, headers, None, 5, request) diff --git a/test/units/modules/source_control/test_gitlab_deploy_key.py b/test/units/modules/source_control/test_gitlab_deploy_key.py index c5da89a31e1..2d5d6b55cd1 100644 --- a/test/units/modules/source_control/test_gitlab_deploy_key.py +++ b/test/units/modules/source_control/test_gitlab_deploy_key.py @@ -1,233 +1,84 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018 Marcus Watkins + +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from units.compat.mock import patch -from ansible.modules.source_control import gitlab_deploy_key -from ansible.module_utils._text import to_bytes -from ansible.module_utils import basic +from __future__ import absolute_import + +from ansible.modules.source_control.gitlab_deploy_key import GitLabDeployKey + +from .gitlab import (GitlabModuleTestCase, + python_version_match_requirement, + resp_get_project, resp_find_project_deploy_key, + resp_create_project_deploy_key, resp_delete_project_deploy_key) + +# Gitlab module requirements +if python_version_match_requirement(): + from gitlab.v4.objects import ProjectKey + +# Unit tests requirements +from httmock import with_httmock # noqa + + +class TestGitlabDeployKey(GitlabModuleTestCase): + def setUp(self): + super(TestGitlabDeployKey, self).setUp() + + self.moduleUtil = GitLabDeployKey(module=self.mock_module, gitlab_instance=self.gitlab_instance) + + @with_httmock(resp_get_project) + @with_httmock(resp_find_project_deploy_key) + def test_deploy_key_exist(self): + project = self.gitlab_instance.projects.get(1) + + rvalue = self.moduleUtil.existsDeployKey(project, "Public key") + + self.assertEqual(rvalue, True) + + rvalue = self.moduleUtil.existsDeployKey(project, "Private key") + + self.assertEqual(rvalue, False) + + @with_httmock(resp_get_project) + @with_httmock(resp_create_project_deploy_key) + def test_create_deploy_key(self): + project = self.gitlab_instance.projects.get(1) + + deploy_key = self.moduleUtil.createDeployKey(project, {"title": "Public key", + "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM" + "4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxc" + "KDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfD" + "zpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="}) + + self.assertEqual(type(deploy_key), ProjectKey) + self.assertEqual(deploy_key.title, "Public key") + + @with_httmock(resp_get_project) + @with_httmock(resp_find_project_deploy_key) + @with_httmock(resp_create_project_deploy_key) + def test_update_deploy_key(self): + project = self.gitlab_instance.projects.get(1) + deployKey = self.moduleUtil.findDeployKey(project, "Public key") + + changed, newDeploy_key = self.moduleUtil.updateDeployKey(deployKey, {"title": "Private key"}) + + self.assertEqual(changed, True) + self.assertEqual(type(newDeploy_key), ProjectKey) + self.assertEqual(newDeploy_key.title, "Private key") -import pytest -import json + changed, newDeploy_key = self.moduleUtil.updateDeployKey(deployKey, {"title": "Private key"}) -from units.modules.utils import set_module_args + self.assertEqual(changed, False) + self.assertEqual(newDeploy_key.title, "Private key") + @with_httmock(resp_get_project) + @with_httmock(resp_find_project_deploy_key) + @with_httmock(resp_delete_project_deploy_key) + def test_delete_deploy_key(self): + project = self.gitlab_instance.projects.get(1) -fake_server_state = [ - { - "id": 1, - "title": "Public key", - "key": 'ssh-rsa long/+base64//+string==', - "created_at": "2013-10-02T10:12:29Z", - "can_push": False - }, -] + self.moduleUtil.existsDeployKey(project, "Public key") + rvalue = self.moduleUtil.deleteDeployKey() -class FakeReader: - def __init__(self, object): - self.content = json.dumps(object, sort_keys=True) - - def read(self): - return self.content - - -class AnsibleExitJson(Exception): - """Exception class to be raised by module.exit_json and caught by the test case""" - pass - - -class AnsibleFailJson(Exception): - """Exception class to be raised by module.fail_json and caught by the test case""" - pass - - -def exit_json(*args, **kwargs): - """function to patch over exit_json; package return data into an exception""" - if 'changed' not in kwargs: - kwargs['changed'] = False - raise AnsibleExitJson(kwargs) - - -def fail_json(*args, **kwargs): - """function to patch over fail_json; package return data into an exception""" - kwargs['failed'] = True - raise AnsibleFailJson(kwargs) - - -@pytest.fixture -def fetch_url_mock(mocker): - return mocker.patch('ansible.module_utils.gitlab.fetch_url') - - -@pytest.fixture -def module_mock(mocker): - return mocker.patch.multiple(basic.AnsibleModule, - exit_json=exit_json, - fail_json=fail_json) - - -def test_access_token_output(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'key': 'ssh-key foobar', - 'title': 'a title', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - first_call = fetch_url_mock.call_args_list[0][1] - assert first_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/deploy_keys' - assert first_call['headers']['Authorization'] == 'Bearer test-access-token' - assert 'Private-Token' not in first_call['headers'] - assert first_call['method'] == 'GET' - - -def test_private_token_output(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'private_token': 'test-private-token', - 'project': 'foo/bar', - 'key': 'ssh-key foobar', - 'title': 'a title', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - first_call = fetch_url_mock.call_args_list[0][1] - assert first_call['url'] == 'https://gitlab.example.com/api/v4/projects/foo%2Fbar/deploy_keys' - assert first_call['headers']['Private-Token'] == 'test-private-token' - assert 'Authorization' not in first_call['headers'] - assert first_call['method'] == 'GET' - - -def test_bad_http_first_response(capfd, fetch_url_mock, module_mock): - fetch_url_mock.side_effect = [[FakeReader("Permission denied"), {'status': 403}], [FakeReader("Permission denied"), {'status': 403}]] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'key': 'ssh-key foobar', - 'title': 'a title', - 'state': 'absent' - }) - with pytest.raises(AnsibleFailJson): - gitlab_deploy_key.main() - - -def test_bad_http_second_response(capfd, fetch_url_mock, module_mock): - fetch_url_mock.side_effect = [[FakeReader(fake_server_state), {'status': 200}], [FakeReader("Permission denied"), {'status': 403}]] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'key': 'ssh-key foobar', - 'title': 'a title', - 'state': 'present' - }) - with pytest.raises(AnsibleFailJson): - gitlab_deploy_key.main() - - -def test_delete_non_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'key': 'ssh-key foobar', - 'title': 'a title', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - assert result.value.args[0]['changed'] is False - - -def test_delete_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'key': 'ssh-rsa long/+base64//+string==', - 'title': 'a title', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - second_call = fetch_url_mock.call_args_list[1][1] - - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/deploy_keys/1' - assert second_call['method'] == 'DELETE' - - assert result.value.args[0]['changed'] is True - - -def test_add_new(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'key': 'ssh-key foobar', - 'title': 'a title', - 'state': 'present' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - second_call = fetch_url_mock.call_args_list[1][1] - - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/deploy_keys' - assert second_call['method'] == 'POST' - assert second_call['data'] == '{"can_push": false, "key": "ssh-key foobar", "title": "a title"}' - assert result.value.args[0]['changed'] is True - - -def test_update_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'title': 'Public key', - 'key': 'ssh-rsa long/+base64//+string==', - 'can_push': 'yes', - 'state': 'present' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - second_call = fetch_url_mock.call_args_list[1][1] - - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/deploy_keys/1' - assert second_call['method'] == 'PUT' - assert second_call['data'] == ('{"can_push": true, "key": "ssh-rsa long/+base64//+string==", "title": "Public key"}') - assert result.value.args[0]['changed'] is True - - -def test_unchanged_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'title': 'Public key', - 'key': 'ssh-rsa long/+base64//+string==', - 'can_push': 'no', - 'state': 'present' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_deploy_key.main() - - assert result.value.args[0]['changed'] is False - assert fetch_url_mock.call_count == 1 + self.assertEqual(rvalue, None) diff --git a/test/units/modules/source_control/test_gitlab_group.py b/test/units/modules/source_control/test_gitlab_group.py new file mode 100644 index 00000000000..b1eda8707f2 --- /dev/null +++ b/test/units/modules/source_control/test_gitlab_group.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import + +from ansible.modules.source_control.gitlab_group import GitLabGroup + +from .gitlab import (GitlabModuleTestCase, + python_version_match_requirement, + resp_get_group, resp_get_missing_group, resp_create_group, + resp_create_subgroup, resp_delete_group, resp_find_group_project) + +# Gitlab module requirements +if python_version_match_requirement(): + from gitlab.v4.objects import Group + +# Unit tests requirements +from httmock import with_httmock # noqa + + +class TestGitlabGroup(GitlabModuleTestCase): + def setUp(self): + super(TestGitlabGroup, self).setUp() + + self.moduleUtil = GitLabGroup(module=self.mock_module, gitlab_instance=self.gitlab_instance) + + @with_httmock(resp_get_group) + def test_exist_group(self): + rvalue = self.moduleUtil.existsGroup(1) + + self.assertEqual(rvalue, True) + + @with_httmock(resp_get_missing_group) + def test_exist_group(self): + rvalue = self.moduleUtil.existsGroup(1) + + self.assertEqual(rvalue, False) + + @with_httmock(resp_create_group) + def test_create_group(self): + group = self.moduleUtil.createGroup({'name': "Foobar Group", 'path': "foo-bar"}) + + self.assertEqual(type(group), Group) + self.assertEqual(group.name, "Foobar Group") + self.assertEqual(group.path, "foo-bar") + self.assertEqual(group.id, 1) + + @with_httmock(resp_create_subgroup) + def test_create_subgroup(self): + group = self.moduleUtil.createGroup({'name': "BarFoo Group", 'path': "bar-foo", "parent_id": 1}) + + self.assertEqual(type(group), Group) + self.assertEqual(group.name, "BarFoo Group") + self.assertEqual(group.full_path, "foo-bar/bar-foo") + self.assertEqual(group.id, 2) + self.assertEqual(group.parent_id, 1) + + @with_httmock(resp_get_group) + def test_update_group(self): + group = self.gitlab_instance.groups.get(1) + changed, newGroup = self.moduleUtil.updateGroup(group, {'name': "BarFoo Group", "visibility": "private"}) + + self.assertEqual(changed, True) + self.assertEqual(newGroup.name, "BarFoo Group") + self.assertEqual(newGroup.visibility, "private") + + changed, newGroup = self.moduleUtil.updateGroup(group, {'name': "BarFoo Group"}) + + self.assertEqual(changed, False) + + @with_httmock(resp_get_group) + @with_httmock(resp_find_group_project) + @with_httmock(resp_delete_group) + def test_delete_group(self): + self.moduleUtil.existsGroup(1) + + print(self.moduleUtil.groupObject.projects) + + rvalue = self.moduleUtil.deleteGroup() + + self.assertEqual(rvalue, None) diff --git a/test/units/modules/source_control/test_gitlab_hooks.py b/test/units/modules/source_control/test_gitlab_hooks.py index 7b1230d4276..c559147392a 100644 --- a/test/units/modules/source_control/test_gitlab_hooks.py +++ b/test/units/modules/source_control/test_gitlab_hooks.py @@ -1,285 +1,79 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018 Marcus Watkins -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from units.compat.mock import patch -from ansible.modules.source_control import gitlab_hooks -from ansible.module_utils._text import to_bytes -from ansible.module_utils import basic - -import pytest -import json - -from units.modules.utils import set_module_args - - -fake_server_state = [ - { - "id": 1, - "url": "https://notification-server.example.com/gitlab-hook", - "project_id": 10, - "push_events": True, - "issues_events": True, - "merge_requests_events": True, - "tag_push_events": True, - "note_events": True, - "job_events": True, - "pipeline_events": True, - "wiki_page_events": True, - "enable_ssl_verification": True, - "created_at": "2012-10-12T17:04:47Z" - }, -] - - -class FakeReader: - def __init__(self, object): - self.content = json.dumps(object, sort_keys=True) - - def read(self): - return self.content - - -class AnsibleExitJson(Exception): - """Exception class to be raised by module.exit_json and caught by the test case""" - pass - - -class AnsibleFailJson(Exception): - """Exception class to be raised by module.fail_json and caught by the test case""" - pass - - -def exit_json(*args, **kwargs): - """function to patch over exit_json; package return data into an exception""" - if 'changed' not in kwargs: - kwargs['changed'] = False - raise AnsibleExitJson(kwargs) +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -def fail_json(*args, **kwargs): - """function to patch over fail_json; package return data into an exception""" - kwargs['failed'] = True - raise AnsibleFailJson(kwargs) - - -@pytest.fixture -def fetch_url_mock(mocker): - return mocker.patch('ansible.module_utils.gitlab.fetch_url') - - -@pytest.fixture -def module_mock(mocker): - return mocker.patch.multiple(basic.AnsibleModule, - exit_json=exit_json, - fail_json=fail_json) - - -def test_access_token_output(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://my-ci-server.example.com/gitlab-hook', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() - - first_call = fetch_url_mock.call_args_list[0][1] - assert first_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/hooks' - assert first_call['headers']['Authorization'] == 'Bearer test-access-token' - assert 'Private-Token' not in first_call['headers'] - assert first_call['method'] == 'GET' - - -def test_private_token_output(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'private_token': 'test-private-token', - 'project': 'foo/bar', - 'hook_url': 'https://my-ci-server.example.com/gitlab-hook', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() - - first_call = fetch_url_mock.call_args_list[0][1] - assert first_call['url'] == 'https://gitlab.example.com/api/v4/projects/foo%2Fbar/hooks' - assert first_call['headers']['Private-Token'] == 'test-private-token' - assert 'Authorization' not in first_call['headers'] - assert first_call['method'] == 'GET' - - -def test_bad_http_first_response(capfd, fetch_url_mock, module_mock): - fetch_url_mock.side_effect = [[FakeReader("Permission denied"), {'status': 403}], [FakeReader("Permission denied"), {'status': 403}]] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://my-ci-server.example.com/gitlab-hook', - 'state': 'absent' - }) - with pytest.raises(AnsibleFailJson): - gitlab_hooks.main() - +from __future__ import absolute_import -def test_bad_http_second_response(capfd, fetch_url_mock, module_mock): - fetch_url_mock.side_effect = [[FakeReader(fake_server_state), {'status': 200}], [FakeReader("Permission denied"), {'status': 403}]] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://my-ci-server.example.com/gitlab-hook', - 'state': 'present' - }) - with pytest.raises(AnsibleFailJson): - gitlab_hooks.main() +from ansible.modules.source_control.gitlab_hooks import GitLabHook +from .gitlab import (GitlabModuleTestCase, + python_version_match_requirement, + resp_get_project, resp_find_project_hook, + resp_create_project_hook, resp_delete_project_hook) -def test_delete_non_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://my-ci-server.example.com/gitlab-hook', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() +# Gitlab module requirements +if python_version_match_requirement(): + from gitlab.v4.objects import ProjectHook - assert result.value.args[0]['changed'] is False +# Unit tests requirements +from httmock import with_httmock # noqa -def test_delete_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://notification-server.example.com/gitlab-hook', - 'state': 'absent' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() +class TestGitlabHook(GitlabModuleTestCase): + def setUp(self): + super(TestGitlabHook, self).setUp() - second_call = fetch_url_mock.call_args_list[1][1] + self.moduleUtil = GitLabHook(module=self.mock_module, gitlab_instance=self.gitlab_instance) - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/hooks/1' - assert second_call['method'] == 'DELETE' + @with_httmock(resp_get_project) + @with_httmock(resp_find_project_hook) + def test_hook_exist(self): + project = self.gitlab_instance.projects.get(1) - assert result.value.args[0]['changed'] is True + rvalue = self.moduleUtil.existsHooks(project, "http://example.com/hook") + self.assertEqual(rvalue, True) -def test_add_new(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://my-ci-server.example.com/gitlab-hook', - 'state': 'present' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() + rvalue = self.moduleUtil.existsHooks(project, "http://gitlab.com/hook") - second_call = fetch_url_mock.call_args_list[1][1] + self.assertEqual(rvalue, False) - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/hooks' - assert second_call['method'] == 'POST' - assert second_call['data'] == ('{"enable_ssl_verification": false, "issues_events": false, "job_events": false, ' - '"merge_requests_events": false, "note_events": false, "pipeline_events": false, "push_events": true, "tag_push_events": ' - 'false, "token": null, "url": "https://my-ci-server.example.com/gitlab-hook", "wiki_page_events": false}') - assert result.value.args[0]['changed'] is True + @with_httmock(resp_get_project) + @with_httmock(resp_create_project_hook) + def test_create_hook(self): + project = self.gitlab_instance.projects.get(1) + hook = self.moduleUtil.createHook(project, {"url": "http://example.com/hook"}) -def test_update_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://notification-server.example.com/gitlab-hook', - 'push_events': 'yes', - 'issues_events': 'yes', - 'merge_requests_events': 'yes', - 'tag_push_events': 'yes', - 'note_events': 'yes', - 'job_events': 'yes', - 'pipeline_events': 'yes', - 'wiki_page_events': 'no', - 'enable_ssl_verification': 'yes', - 'state': 'present' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() + self.assertEqual(type(hook), ProjectHook) + self.assertEqual(hook.url, "http://example.com/hook") - second_call = fetch_url_mock.call_args_list[1][1] + @with_httmock(resp_get_project) + @with_httmock(resp_find_project_hook) + def test_update_hook(self): + project = self.gitlab_instance.projects.get(1) + hook = self.moduleUtil.findHook(project, "http://example.com/hook") - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/hooks/1' - assert second_call['method'] == 'PUT' - assert second_call['data'] == ('{"enable_ssl_verification": true, "issues_events": true, "job_events": true, ' - '"merge_requests_events": true, "note_events": true, "pipeline_events": true, "push_events": true, "tag_push_events": ' - 'true, "token": null, "url": "https://notification-server.example.com/gitlab-hook", "wiki_page_events": false}') - assert result.value.args[0]['changed'] is True + changed, newHook = self.moduleUtil.updateHook(hook, {"url": "http://gitlab.com/hook"}) + self.assertEqual(changed, True) + self.assertEqual(type(newHook), ProjectHook) + self.assertEqual(newHook.url, "http://gitlab.com/hook") -def test_unchanged_existing(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://notification-server.example.com/gitlab-hook', - 'push_events': 'yes', - 'issues_events': 'yes', - 'merge_requests_events': 'yes', - 'tag_push_events': 'yes', - 'note_events': 'yes', - 'job_events': 'yes', - 'pipeline_events': 'yes', - 'wiki_page_events': 'yes', - 'enable_ssl_verification': 'yes', - 'state': 'present' - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() + changed, newHook = self.moduleUtil.updateHook(hook, {"url": "http://gitlab.com/hook"}) - assert result.value.args[0]['changed'] is False - assert fetch_url_mock.call_count == 1 + self.assertEqual(changed, False) + self.assertEqual(newHook.url, "http://gitlab.com/hook") + @with_httmock(resp_get_project) + @with_httmock(resp_find_project_hook) + @with_httmock(resp_delete_project_hook) + def test_delete_hook(self): + project = self.gitlab_instance.projects.get(1) -def test_unchanged_existing_with_token(capfd, fetch_url_mock, module_mock): - fetch_url_mock.return_value = [FakeReader(fake_server_state), {'status': 200}] - set_module_args({ - 'api_url': 'https://gitlab.example.com/api', - 'access_token': 'test-access-token', - 'project': '10', - 'hook_url': 'https://notification-server.example.com/gitlab-hook', - 'push_events': 'yes', - 'issues_events': 'yes', - 'merge_requests_events': 'yes', - 'tag_push_events': 'yes', - 'note_events': 'yes', - 'job_events': 'yes', - 'pipeline_events': 'yes', - 'wiki_page_events': 'yes', - 'enable_ssl_verification': 'yes', - 'state': 'present', - 'token': 'secret-token', - }) - with pytest.raises(AnsibleExitJson) as result: - gitlab_hooks.main() + self.moduleUtil.existsHooks(project, "http://example.com/hook") - second_call = fetch_url_mock.call_args_list[1][1] + rvalue = self.moduleUtil.deleteHook() - assert second_call['url'] == 'https://gitlab.example.com/api/v4/projects/10/hooks/1' - assert second_call['method'] == 'PUT' - assert second_call['data'] == ('{"enable_ssl_verification": true, "issues_events": true, "job_events": true, ' - '"merge_requests_events": true, "note_events": true, "pipeline_events": true, "push_events": true, ' - '"tag_push_events": true, "token": "secret-token", "url": "https://notification-server.example.com/gitlab-hook", ' - '"wiki_page_events": true}') - assert result.value.args[0]['changed'] is True + self.assertEqual(rvalue, None) diff --git a/test/units/modules/source_control/test_gitlab_project.py b/test/units/modules/source_control/test_gitlab_project.py index 1d93aa619ba..896817bc91c 100644 --- a/test/units/modules/source_control/test_gitlab_project.py +++ b/test/units/modules/source_control/test_gitlab_project.py @@ -1,82 +1,78 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018 Pierre-Louis Bonicoli + +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -import json - -from units.compat.mock import MagicMock -from ansible.modules.source_control import gitlab_project - -import pytest - - -@pytest.fixture -def patch_gitlab_project(mocker): - mocker.patch.object(gitlab_project, 'HAS_GITLAB_PACKAGE', mocker.PropertyMock(return_value=True)) - - -@pytest.mark.parametrize('patch_ansible_module', [{}], indirect=['patch_ansible_module']) -@pytest.mark.usefixtures('patch_ansible_module') -def test_without_required_parameters(capfd): - """Failure must occurs when all parameters are missing""" - - with pytest.raises(SystemExit): - gitlab_project.main() - out, err = capfd.readouterr() - results = json.loads(out) - assert results['failed'] - assert 'missing required arguments' in results['msg'] - - -TEST_CASES = [ - [ - { - 'server_url': 'http://gitlab.test/gitlab', - 'validate_certs': True, - 'login_user': 'john', - 'login_token': 'TOKEN', - 'name': 'new_test_repo', - 'group': 'my_repo_group', - 'public': True, - 'visibility_level': 20, - 'issues_enabled': False, - 'wiki_enabled': True, - 'snippets_enabled': True, - 'import_url': 'http://gitlab.test/gitlab/gitrepothatdoesnotexist.git', - 'state': 'present' - }, - { - 'msg': "Failed to create project 'new_test_repo'", - 'failed': True, - } - ], -] - - -@pytest.mark.parametrize('patch_ansible_module, testcase', TEST_CASES, indirect=['patch_ansible_module']) -@pytest.mark.usefixtures('patch_ansible_module') -def test_fail_if_url_import_doesnt_exist(mocker, capfd, patch_gitlab_project, testcase): - """ Test for #36495 - - Ensure errors are reported (meaning task report a failure), - for example when url_import doesn't exist, an error must occur. - """ - - git = MagicMock() - git.createprojectuser.return_value = False - - gitlab = MagicMock() - gitlab.Gitlab.return_value = git - gitlab_project.gitlab = gitlab - - with pytest.raises(SystemExit): - gitlab_project.main() - - # Check that 1. createprojectuser method has been called 2. with expected parameter - assert git.createprojectuser.call_count == 1 - assert git.createprojectuser.call_args[1]['import_url'] == 'http://gitlab.test/gitlab/gitrepothatdoesnotexist.git' - - out, err = capfd.readouterr() - results = json.loads(out) - assert results.get('failed') == testcase.get('failed') - assert results['msg'] == testcase['msg'] +from __future__ import absolute_import + +from ansible.modules.source_control.gitlab_project import GitLabProject + +from .gitlab import (GitlabModuleTestCase, + python_version_match_requirement, + resp_get_group, resp_get_project_by_name, resp_create_project, + resp_get_project, resp_delete_project, resp_get_user) + +# Gitlab module requirements +if python_version_match_requirement(): + from gitlab.v4.objects import Project + +# Unit tests requirements +from httmock import with_httmock # noqa + + +class TestGitlabProject(GitlabModuleTestCase): + @with_httmock(resp_get_user) + def setUp(self): + super(TestGitlabProject, self).setUp() + + self.gitlab_instance.user = self.gitlab_instance.users.get(1) + self.moduleUtil = GitLabProject(module=self.mock_module, gitlab_instance=self.gitlab_instance) + + @with_httmock(resp_get_group) + @with_httmock(resp_get_project_by_name) + def test_project_exist(self): + group = self.gitlab_instance.groups.get(1) + + rvalue = self.moduleUtil.existsProject(group, "diaspora-client") + + self.assertEqual(rvalue, True) + + rvalue = self.moduleUtil.existsProject(group, "missing-project") + + self.assertEqual(rvalue, False) + + @with_httmock(resp_get_group) + @with_httmock(resp_create_project) + def test_create_project(self): + group = self.gitlab_instance.groups.get(1) + project = self.moduleUtil.createProject(group, {"name": "Diaspora Client", "path": "diaspora-client", "namespace_id": group.id}) + + self.assertEqual(type(project), Project) + self.assertEqual(project.name, "Diaspora Client") + + @with_httmock(resp_get_project) + def test_update_project(self): + project = self.gitlab_instance.projects.get(1) + + changed, newProject = self.moduleUtil.updateProject(project, {"name": "New Name"}) + + self.assertEqual(changed, True) + self.assertEqual(type(newProject), Project) + self.assertEqual(newProject.name, "New Name") + + changed, newProject = self.moduleUtil.updateProject(project, {"name": "New Name"}) + + self.assertEqual(changed, False) + self.assertEqual(newProject.name, "New Name") + + @with_httmock(resp_get_group) + @with_httmock(resp_get_project_by_name) + @with_httmock(resp_delete_project) + def test_delete_project(self): + group = self.gitlab_instance.groups.get(1) + + self.moduleUtil.existsProject(group, "diaspora-client") + + rvalue = self.moduleUtil.deleteProject() + + self.assertEqual(rvalue, None) diff --git a/test/units/modules/source_control/test_gitlab_runner.py b/test/units/modules/source_control/test_gitlab_runner.py new file mode 100644 index 00000000000..b694504dcc9 --- /dev/null +++ b/test/units/modules/source_control/test_gitlab_runner.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import + +from ansible.modules.source_control.gitlab_runner import GitLabRunner + +from .gitlab import (GitlabModuleTestCase, + python_version_match_requirement, + resp_find_runners, resp_get_runner, + resp_create_runner, resp_delete_runner) + +# Gitlab module requirements +if python_version_match_requirement(): + from gitlab.v4.objects import Runner + +# Unit tests requirements +from httmock import with_httmock # noqa + + +class TestGitlabRunner(GitlabModuleTestCase): + def setUp(self): + super(TestGitlabRunner, self).setUp() + + self.moduleUtil = GitLabRunner(module=self.mock_module, gitlab_instance=self.gitlab_instance) + + @with_httmock(resp_find_runners) + @with_httmock(resp_get_runner) + def test_runner_exist(self): + rvalue = self.moduleUtil.existsRunner("test-1-20150125") + + self.assertEqual(rvalue, True) + + rvalue = self.moduleUtil.existsRunner("test-3-00000000") + + self.assertEqual(rvalue, False) + + @with_httmock(resp_create_runner) + def test_create_runner(self): + runner = self.moduleUtil.createRunner({"token": "token", "description": "test-1-20150125"}) + + self.assertEqual(type(runner), Runner) + self.assertEqual(runner.description, "test-1-20150125") + + @with_httmock(resp_find_runners) + @with_httmock(resp_get_runner) + def test_update_runner(self): + runner = self.moduleUtil.findRunner("test-1-20150125") + + changed, newRunner = self.moduleUtil.updateRunner(runner, {"description": "Runner description"}) + + self.assertEqual(changed, True) + self.assertEqual(type(newRunner), Runner) + self.assertEqual(newRunner.description, "Runner description") + + changed, newRunner = self.moduleUtil.updateRunner(runner, {"description": "Runner description"}) + + self.assertEqual(changed, False) + self.assertEqual(newRunner.description, "Runner description") + + @with_httmock(resp_find_runners) + @with_httmock(resp_get_runner) + @with_httmock(resp_delete_runner) + def test_delete_runner(self): + self.moduleUtil.existsRunner("test-1-20150125") + + rvalue = self.moduleUtil.deleteRunner() + + self.assertEqual(rvalue, None) diff --git a/test/units/modules/source_control/test_gitlab_user.py b/test/units/modules/source_control/test_gitlab_user.py new file mode 100644 index 00000000000..f2614e2b06d --- /dev/null +++ b/test/units/modules/source_control/test_gitlab_user.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2019, Guillaume Martinez (lunik@tiwabbit.fr) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import + +from ansible.modules.source_control.gitlab_user import GitLabUser + +from .gitlab import (GitlabModuleTestCase, + python_version_match_requirement, + resp_find_user, resp_get_user, resp_get_user_keys, + resp_create_user_keys, resp_create_user, resp_delete_user, + resp_get_member, resp_get_group, resp_add_member, + resp_update_member, resp_get_member) + +# Gitlab module requirements +if python_version_match_requirement(): + from gitlab.v4.objects import User + +# Unit tests requirements +from httmock import with_httmock # noqa + + +class TestGitlabUser(GitlabModuleTestCase): + def setUp(self): + super(TestGitlabUser, self).setUp() + + self.moduleUtil = GitLabUser(module=self.mock_module, gitlab_instance=self.gitlab_instance) + + @with_httmock(resp_find_user) + def test_exist_user(self): + rvalue = self.moduleUtil.existsUser("john_smith") + + self.assertEqual(rvalue, True) + + rvalue = self.moduleUtil.existsUser("paul_smith") + + self.assertEqual(rvalue, False) + + @with_httmock(resp_find_user) + def test_find_user(self): + user = self.moduleUtil.findUser("john_smith") + + self.assertEqual(type(user), User) + self.assertEqual(user.name, "John Smith") + self.assertEqual(user.id, 1) + + @with_httmock(resp_create_user) + def test_create_user(self): + user = self.moduleUtil.createUser({'email': 'john@example.com', 'password': 's3cur3s3cr3T', + 'username': 'john_smith', 'name': 'John Smith'}) + self.assertEqual(type(user), User) + self.assertEqual(user.name, "John Smith") + self.assertEqual(user.id, 1) + + @with_httmock(resp_get_user) + def test_update_user(self): + user = self.gitlab_instance.users.get(1) + changed, newUser = self.moduleUtil.updateUser(user, {'name': "Jack Smith", "is_admin": "true"}) + + self.assertEqual(changed, True) + self.assertEqual(newUser.name, "Jack Smith") + self.assertEqual(newUser.is_admin, "true") + + changed, newUser = self.moduleUtil.updateUser(user, {'name': "Jack Smith"}) + + self.assertEqual(changed, False) + + @with_httmock(resp_find_user) + @with_httmock(resp_delete_user) + def test_delete_user(self): + self.moduleUtil.existsUser("john_smith") + rvalue = self.moduleUtil.deleteUser() + + self.assertEqual(rvalue, None) + + @with_httmock(resp_get_user) + @with_httmock(resp_get_user_keys) + def test_sshkey_exist(self): + user = self.gitlab_instance.users.get(1) + + exist = self.moduleUtil.sshKeyExists(user, "Public key") + self.assertEqual(exist, True) + + notExist = self.moduleUtil.sshKeyExists(user, "Private key") + self.assertEqual(notExist, False) + + @with_httmock(resp_get_user) + @with_httmock(resp_create_user_keys) + @with_httmock(resp_get_user_keys) + def test_create_sshkey(self): + user = self.gitlab_instance.users.get(1) + + rvalue = self.moduleUtil.addSshKeyToUser(user, { + 'name': "Public key", + 'file': "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJe" + "jgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4" + "soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="}) + self.assertEqual(rvalue, False) + + rvalue = self.moduleUtil.addSshKeyToUser(user, { + 'name': "Private key", + 'file': "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDA1YotVDm2mAyk2tPt4E7AHm01sS6JZmcU" + "dRuSuA5zszUJzYPPUSRAX3BCgTqLqYx//UuVncK7YqLVSbbwjKR2Ez5lISgCnVfLVEXzwhv+" + "xawxKWmI7hJ5S0tOv6MJ+IxyTa4xcKwJTwB86z22n9fVOQeJTR2dSOH1WJrf0PvRk+KVNY2j" + "TiGHTi9AIjLnyD/jWRpOgtdfkLRc8EzAWrWlgNmH2WOKBw6za0az6XoG75obUdFVdW3qcD0x" + "c809OHLi7FDf+E7U4wiZJCFuUizMeXyuK/SkaE1aee4Qp5R4dxTR4TP9M1XAYkf+kF0W9srZ+mhF069XD/zhUPJsvwEF"}) + self.assertEqual(rvalue, True) + + @with_httmock(resp_get_group) + @with_httmock(resp_get_member) + def test_find_member(self): + group = self.gitlab_instance.groups.get(1) + + user = self.moduleUtil.findMember(group, 1) + self.assertEqual(user.username, "raymond_smith") + + @with_httmock(resp_get_user) + @with_httmock(resp_get_group) + @with_httmock(resp_get_group) + @with_httmock(resp_get_member) + @with_httmock(resp_add_member) + @with_httmock(resp_update_member) + def test_assign_user_to_group(self): + group = self.gitlab_instance.groups.get(1) + user = self.gitlab_instance.users.get(1) + + rvalue = self.moduleUtil.assignUserToGroup(user, group.id, "developer") + self.assertEqual(rvalue, False) + + rvalue = self.moduleUtil.assignUserToGroup(user, group.id, "guest") + self.assertEqual(rvalue, True)