# Copyright (C) 2016-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. ''' Run commands on a GitLab instance. This uses the python wrapper around the GitLab API. Use of the API requires the private token of a user with master access to the targetted group. ''' import logging import urllib.parse try: import gitlab except ImportError: gitlab = None from . import hosts class MissingGitlabModuleError(Exception): pass def _init_gitlab(url, token): if gitlab: return gitlab.Gitlab(url, token) else: raise MissingGitlabModuleError('gitlab module missing\n' '\tpython-gitlab is required with GitLab as the git server') class GitlabDownstream(hosts.DownstreamHost): @staticmethod def add_app_settings(app_settings): app_settings.string( ['gitlab-private-token'], 'private token for GitLab API access') @staticmethod def check_app_settings(app_settings): if not app_settings['gitlab-private-token']: logging.error('A private token must be provided to create ' 'repositories on a GitLab instance.') app_settings.require('gitlab-private-token') def __init__(self, app_settings): url = app_settings['downstream-http-url'] if url is None: url = 'http://localhost/' self.gl = _init_gitlab(url, app_settings['gitlab-private-token']) self._visibility = app_settings['downstream-visibility'] def prepare_repo(self, repo_path, metadata): try: project = self.gl.projects.get(repo_path) except gitlab.GitlabGetError: pass else: logging.info('Project %s exists in local GitLab already.', repo_path) if 'description' in metadata \ and project.description != metadata['description']: project.description = metadata['description'] project.save() # This will fail if we haven't created the branch yet. # We'll fix it next time round. try: if 'head' in metadata \ and project.default_branch != metadata['head']: project.default_branch = metadata['head'] project.save() except gitlab.GitlabUpdateError: pass return path_comps = repo_path.split('/') if len(path_comps) < 2: raise ValueError('cannot create GitLab project outside a group') # Create hierarchy of groups as necessary parent_group = None for group_name in path_comps[:-1]: if parent_group is None: group_path = group_name else: group_path = parent_group.full_path + '/' + group_name try: group = self.gl.groups.get(group_path) except gitlab.GitlabGetError as e: if e.response_code != 404: raise data = { 'name': group_name, 'path': group_name, 'visibility': self._visibility, } if parent_group is not None: data['parent_id'] = parent_group.id group = self.gl.groups.create(data) parent_group = group proj_create = { 'name': path_comps[-1], 'visibility': self._visibility, 'namespace_id': group.id, 'default_branch': metadata.get('head'), 'description': metadata.get('description'), 'pages_access_level': 'disabled', 'container_registry_enabled': False, 'autoclose_referenced_issues': False, 'lfs_enabled': True, 'auto_devops_enabled': False, } project = self.gl.projects.create(proj_create) # Disabling these during creation doesn't work (as of GitLab # 12.10.1) so do it immediately after for attr_name in ['issues_access_level', 'merge_requests_access_level', 'builds_access_level', 'wiki_access_level', 'snippets_access_level']: setattr(project, attr_name, 'disabled') project.save() logging.info('Created %s project in local GitLab.', repo_path) class GitlabUpstream(hosts.UpstreamHost): @staticmethod def check_host_type_params(validator, section): validator.check_has_required_fields(section, ['private-token']) @staticmethod def get_host_type_params(section): return {'private-token': section['private-token']} def __init__(self, host_info): self._protocol = host_info['protocol'] if self._protocol == 'ssh': url = 'https://%(host)s/' % host_info else: url = '%(protocol)s://%(host)s/' % host_info self.gl = _init_gitlab(url, host_info['type_params']['private-token']) def list_repos(self): '''List projects on a GitLab instance.''' return [x.path_with_namespace for x in self.gl.projects.list()] def get_repo_url(self, repo_path): '''Return the clone url for a GitLab project. Depending on the protocol specified, will return a suitable clone url. If 'ssh', a url in the format 'git@host:group/project.git' will be returned. If 'http' or 'https', the http_url_to_repo from the GitLab API is split with urlparse into its constituent parts: the protocol (http by default), the host, and the path ('group/project.git'). This is then rejoined, replacing the protocol with what is specified. The resulting format matching 'http(s)://host/group/project.git'. ''' project = self.gl.projects.get(repo_path) if self._protocol == 'ssh': return project.ssh_url_to_repo elif self._protocol in ('http', 'https'): split = urllib.parse.urlsplit(project.http_url_to_repo) return urllib.parse.urlunsplit(( self._protocol, split.netloc, split.path, '', '')) def get_repo_metadata(self, repo_path): project = self.gl.projects.get(repo_path) metadata = {} if project.default_branch is not None: metadata['head'] = project.default_branch if project.description is not None: metadata['description'] = project.description return metadata