summaryrefslogtreecommitdiff
path: root/lorrycontroller
diff options
context:
space:
mode:
authorBen Hutchings <ben.hutchings@codethink.co.uk>2020-05-14 13:41:20 +0100
committerBen Hutchings <ben.hutchings@codethink.co.uk>2020-06-01 17:17:59 +0100
commite68d17adf03b7a203f4b5d75da175ee37e97ce45 (patch)
tree7d4c050a78e4883a4e99f66164b9fe3a091118f9 /lorrycontroller
parent36a0a30c13e669514ba2bce7a7f77d53f7275a11 (diff)
downloadlorry-controller-e68d17adf03b7a203f4b5d75da175ee37e97ce45.tar.gz
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.
Diffstat (limited to 'lorrycontroller')
-rw-r--r--lorrycontroller/__init__.py2
-rw-r--r--lorrycontroller/gitea.py138
2 files changed, 140 insertions, 0 deletions
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)