import json import requests ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], } DOCUMENTATION = ''' --- module: gitea_repository short_description: Manage Gitea repositories description: - "Manage repositories (and deployment keys) in Gitea" options: state: description: - The desired state of the repository. choices: ['present', 'absent'] default: present required: no name: description: - The name for your repository. required: true private: description: - Wether the repository is publically accessible choices: ['yes', 'no'] default: yes required: false organization: description: - The organization account the repository belongs to. One of organization and user must be set. Only one of organization and user must be set. required: false user: description: - The user account the repository belongs to. One of organization and user must be set. Only one of organization and user must be set. required: false auto_init: description: - Wether to initialize the repository with default files. Can only be given on creation. required: false choices: ['yes', 'no'] description: description: - The repositories description. required: false gitignores: description: - .gitignore file for the repository. Can only be given on creation. required: false issue_labels: description: - Issue labels for the repository. Can only be given on creation. required: false license: description: - license file for the repository. Can only be given on creation. required: false readme: description: - README (in Markdown) for the repository. Can only be given on creation. required: false deploy_key: description: - SSH deployment key that is to be set up for the repository suboptions: state: description: - The desired state of the deploy key. choices: ['present', 'absent'] default: present required: false key: description: - The public key in SSH format (e.g. "ssh-rsa 12345....12345 identifier") required: true read_only: description: - Wether the deploy key should have read-only access to the repository. choices: ['yes', 'no'] default: yes title: description: - A title for identification of the supplied key required: false required: false auth_token: description: - Authentification token for your gitea account required: true gitea_url: description: - Base URL of your gitea API instance (e.g. "https://git.zknt.org") required: true author: - Chris Gebhardt (@hnrd) ''' EXAMPLES = ''' gitea_repository: auth_token: 1234...6789 gitea_url: https://git.example.com name: 'testrepo' organization: test123 state: present deploy_key: key: ssh-ed25519 AAAA...1234 jenkins title: CI key read_only: False ''' RETURN = ''' return_code: description: The HTTP return code from the Gitea API type: int returned: always gitea_respone: description: The JSON output message that Gitea returns type: dict ''' from ansible.module_utils.basic import AnsibleModule def run_module(): deploy_key_spec = dict( state=dict(type='str', default='present', choices=['present', 'absent']), key=dict(type='str', required=True), read_only=dict(type='bool', default=True), title=dict(type='str', default=""), ) module_args = dict( state=dict(type='str', default='present', choices=['present', 'absent']), auth_token=dict(type='str', required=True), gitea_url=dict(type='str', required=True), organization=dict(type="str", default=None), user=dict(type="str", default=None), auto_init=dict(type="bool", default=False), description=dict(type="str", default=""), gitignores=dict(type="str", default=""), issue_labels=dict(type="str", default=""), license=dict(type="str", default=""), name=dict(type="str", default=""), private=dict(type="bool", default=True), readme=dict(type="str", default=""), ### TODO support multiple keys deploy_key=dict(type="dict", default=None, options=deploy_key_spec), ) result = dict( changed=False, return_code=0, gitea_response={}, ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True ) if ( ('organization' in module.params and module.params['organization']) and ('user' in module.params and module.params['user']) ): module.fail_json(msg="Either organization *or*" \ "user must be set", **result) gitea_url = module.params['gitea_url'] reponame = module.params['name'] headers = { "Content-Type": "application/json", "Authorization": "token {}".format(module.params['auth_token']), } user_req = requests.get(gitea_url + '/api/v1/user', headers=headers) logged_in = user_req.json().get('username') if not logged_in == module.params['user']: headers['Sudo'] = module.params['user'] if module.check_mode: module.exit_json(**result) # Prepare repo data new_data = { "auto_init": module.params['auto_init'], "description": module.params['description'], "gitignores": module.params['gitignores'], "issue_labels": module.params['issue_labels'], "license": module.params['license'], "name": reponame, "private": module.params['private'], "readme": module.params['readme'], } if 'organization' in module.params and module.params['organization']: owner = module.params['organization'] else: owner = module.params['user'] original_repo = requests.get( gitea_url + '/api/v1/repos/{owner}/{repo}'.format( owner=owner, repo=reponame, ), headers=headers, ) if module.params['state'] == 'present': if original_repo.status_code == 200: result['return_code'] = 200 result['gitea_response'] = original_repo.json() old_data = original_repo.json() # check if repo needs to be patched if ( new_data['description'] != old_data['description'] or new_data['private'] != old_data['private'] ): new_data.pop('name') new_data.pop('auto_init') new_data.pop('gitignores') new_data.pop('issue_labels') new_data.pop('readme') req_patch = requests.patch( gitea_url + '/api/v1/repos/{owner}/{repo}'.format( owner=module.params['organization'], repo=reponame, ), headers=headers, data=json.dumps(new_data), ) result['return_code'] = req_patch.status_code result['gitea_response'] = req_patch.json() result['changed'] = True # all is fine result['state'] = 'present' elif original_repo.status_code == 404: if ( 'organization' in module.params and module.params['organization'] ): create_path = '/api/v1/org/{org}/repos'.format( org=owner, ) else: create_path = '/api/v1/user/repos' create_req = requests.post( gitea_url + create_path, headers=headers, data=json.dumps(new_data), ) result['return_code'] = create_req.status_code result['gitea_response'] = create_req.json() if create_req.status_code != 201: module.fail_json(msg="Creation failed", **result) result['changed'] = True result['state'] = 'present' if 'deploy_key' in module.params: deploy_key = module.params['deploy_key'] repokeys_req = requests.get( gitea_url + '/api/v1/repos/{owner}/{repo}/keys'.format( owner=owner, repo=reponame, ), headers=headers, ) repokeys = repokeys_req.json() key_id = None repokey = None for repokey in repokeys: if ( ' '.join(repokey.get('key', '').split()[:-1]) == ' '.join(deploy_key.get('key', '').split()[:-1]) ): key_id = repokey.get('key_id') break if deploy_key['state'] == "present": if key_id: if ( repokey.get('title', "") != deploy_key['title'] or repokey.get('read_only') != deploy_key['read_only'] or repokey.get('key', "") != deploy_key['key'] ): # there is no PATCH for keys, so delete and re-create keydelete_req = requests.delete( gitea_url + '/api/v1/repos/{owner}/{repo}/keys/{id}'.format( owner=owner, repo=reponame, id=key_id, ), headers=headers, ) requests.post( gitea_url + '/api/v1/repos/{owner}/{repo}/keys'.format( owner=owner, repo=reponame, ), headers=headers, data=json.dumps({ "key": deploy_key['key'], "read_only": deploy_key['read_only'], "title": deploy_key['title'], }), ) result['changed'] = True else: requests.post( gitea_url + '/api/v1/repos/{owner}/{repo}/keys'.format( owner=owner, repo=reponame, ), headers=headers, data=json.dumps({ "key": deploy_key['key'], "read_only": deploy_key['read_only'], "title": deploy_key['title'], }), ) result['changed'] = True elif deploy_key['state'] == "absent": if key_id: keydelete_req = requests.delete( gitea_url + '/api/v1/repos/{owner}/{repo}/keys/{id}'.format( owner=owner, repo=reponame, id=key_id, ), headers=headers, ) if keydelete_req.status_code != 204: # deletion failed result['gitea_response'] = keydelete_req.json() module.fail_json(msg="Deletion failed", **result) result['changed'] = True if module.params['state'] == 'absent': if original_repo.status_code == 404: result['return_code'] = 404 result['gitea_response'] = original_repo.json() result['state'] = 'absent' elif original_repo.status_code == 200: # repo should be abenst and needs to be deleted delete_req = requests.delete( gitea_url + '/api/v1/repos/{owner}/{repo}'.format( owner=owner, repo=reponame, ), headers=headers, ) result['return_code'] = delete_req.status_code if delete_req.status_code != 204: # deletion failed result['gitea_response'] = delete_req.json() module.fail_json(msg="Deletion failed", **result) result['changed'] = True result['state'] = 'absent' module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()