From e68d17adf03b7a203f4b5d75da175ee37e97ce45 Mon Sep 17 00:00:00 2001 From: Ben Hutchings Date: Thu, 14 May 2020 13:41:20 +0100 Subject: Add gitea Downstream Host connector Implement organisation and repository creation using the Gitea REST API v1. Add a 'gitea-access-token' application setting for this. Closes #9. --- lorrycontroller/__init__.py | 2 + lorrycontroller/gitea.py | 138 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 lorrycontroller/gitea.py (limited to 'lorrycontroller') diff --git a/lorrycontroller/__init__.py b/lorrycontroller/__init__.py index ddc2f74..05c6f8a 100644 --- a/lorrycontroller/__init__.py +++ b/lorrycontroller/__init__.py @@ -41,6 +41,7 @@ from .static import StaticFile from .proxy import setup_proxy from . import gerrit from . import gitano +from . import gitea from . import gitlab from . import local @@ -48,6 +49,7 @@ from . import local downstream_types = { 'gerrit': gerrit.GerritDownstream, 'gitano': gitano.GitanoDownstream, + 'gitea': gitea.GiteaDownstream, 'gitlab': gitlab.GitlabDownstream, 'local': local.LocalDownstream, } diff --git a/lorrycontroller/gitea.py b/lorrycontroller/gitea.py new file mode 100644 index 0000000..56aedb9 --- /dev/null +++ b/lorrycontroller/gitea.py @@ -0,0 +1,138 @@ +# Copyright 2020 Codethink Limited +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import json +import urllib.error +import urllib.parse +import urllib.request + +from . import hosts + + +class _Gitea: + '''Run commands on a Gitea instance.''' + + def __init__(self, url, token): + self._api_url = urllib.parse.urljoin(url, 'api/v1') + self._api_token = token + + def request(self, method, *path, headers={}, data=None): + url = self._api_url + for part in path: + url = url + '/' + urllib.parse.quote(part, safe='') + + headers = headers.copy() + headers['Authorization'] = 'token ' + self._api_token + + if data is not None: + if method == 'GET': + data = urllib.parse.urlencode(data).encode('ascii') + elif method in ['POST', 'PATCH']: + data = json.dumps(data).encode('utf-8') + headers['Content-Type'] = 'application/json; charset=utf-8' + else: + assert isinstance(data, bytes) + + request = urllib.request.Request(url, data=data, headers=headers, + method=method) + + with urllib.request.urlopen(request) as response: + if response.getcode() == 204: + return None + return json.loads(response.read().decode('utf-8')) + + +class GiteaDownstream(hosts.DownstreamHost): + @staticmethod + def add_app_settings(app_settings): + app_settings.string( + ['gitea-access-token'], + 'access token for Gitea API access') + + @staticmethod + def check_app_settings(app_settings): + app_settings.require('downstream-http-url') + app_settings.require('gitea-access-token') + + def __init__(self, app_settings): + self._gitea = _Gitea(app_settings['downstream-http-url'], + app_settings['gitea-access-token']) + + # Gitea supports all visibilities for organisations, but + # repositories can only be private or inherit the visibility + # of the organisation. If repository visibility is supposed + # to be 'internal', assume that the group will be 'internal' + # rather than making the repository less visible. + visibility = app_settings['downstream-visibility'] + self._group_vis = 'limited' if visibility == 'internal' else visibility + self._repo_private = visibility == 'private' + + def prepare_repo(self, repo_path, metadata): + path_comps = repo_path.split('/') + + # As of Gitea 1.11.5, a 2-level hierarchy must be used: + # user/organisation and repository. Deeper hiearchies must be + # collapsed using a prefixmap. + if len(path_comps) < 2: + raise ValueError( + 'cannot create Gitea repository outside an organisation') + if len(path_comps) > 2: + raise ValueError('cannot create nested Gitea organisations') + + org_name, repo_name = path_comps + repo_edit = {} + + try: + repo = self._gitea.request('GET', 'repos', org_name, repo_name) + except urllib.error.HTTPError as e: + if e.code != 404: + raise + + # Get or create organisation + try: + self._gitea.request('GET', 'orgs', org_name) + except urllib.error.HTTPError as e: + if e.code != 404: + raise + org_data = { + 'username': org_name, + 'visibility': self._group_vis, + } + self._gitea.request('POST', 'orgs', data=org_data) + + # Create repository. This was under /org, not /orgs, + # before Gitea 1.11.5 (which supports both). + repo_create = { + 'auto_init': False, + 'name': repo_name, + 'private': self._repo_private, + } + repo = self._gitea.request('POST', 'org', org_name, 'repos', + data=repo_create) + + # These can only be turned off after creating the repo + for feature in ['has_issues', 'has_wiki', 'has_pull_requests']: + if repo[feature] != False: + repo_edit[feature] = False + + # Update repository metadata + if 'head' in metadata and repo['default_branch'] != metadata['head']: + repo_edit['default_branch'] = metadata['head'] + if 'description' in metadata \ + and repo['description'] != metadata['description']: + repo_edit['description'] = metadata['description'] + if repo_edit: + self._gitea.request('PATCH', 'repos', org_name, repo_name, + data=repo_edit) -- cgit v1.2.1