diff --git a/endpoints/trigger.py b/endpoints/trigger.py index cb8d7622f..120fea9be 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -6,6 +6,8 @@ import base64 import re import json +import gitlab + from endpoints.building import PreparedBuild from github import Github, UnknownObjectException, GithubException from bitbucket import BitBucket @@ -303,6 +305,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): raise TriggerActivationException(msg) config['hook_id'] = data['id'] + self.config = config return config, {'private_key': private_key} def deactivate(self): @@ -636,9 +639,14 @@ class GithubBuildTrigger(BuildTriggerHandler): gh_client = self._get_client() usr = gh_client.get_user() + try: + repos = usr.get_repos() + except GithubException: + raise RepositoryReadException('Unable to list user repositories') + personal = { 'personal': True, - 'repos': [repo.full_name for repo in usr.get_repos()], + 'repos': [repo.full_name for repo in repos], 'info': { 'name': usr.login, 'avatar_url': usr.avatar_url, @@ -1075,3 +1083,284 @@ class CustomBuildTrigger(BuildTriggerHandler): config.pop('credentials', None) self.config = config return config + + +class GitLabBuildTrigger(BuildTriggerHandler): + """ + BuildTrigger for GitLab. + """ + @classmethod + def service_name(cls): + return 'gitlab' + + def _get_authorized_client(self): + host = app.config.get('GITLAB_TRIGGER_CONFIG', {}).get('HOSTNAME', '') + auth_token = self.auth_token or 'invalid' + return gitlab.Gitlab(host, oauth_token=auth_token) + + def is_active(self): + return 'hook_id' in self.config + + def activate(self, standard_webhook_url): + config = self.config + new_build_source = config['build_source'] + gl_client = self._get_authorized_client() + + # Find the GitLab repository. + repository = gl_client.getproject(new_build_source) + if repository is False: + msg = 'Unable to find GitLab repository for source: %s' % new_build_source + raise TriggerActivationException(msg) + + # Add a deploy key to the repository. + public_key, private_key = generate_ssh_keypair() + config['credentials'] = [ + { + 'name': 'SSH Public Key', + 'value': public_key, + }, + ] + success = gl_client.adddeploykey(repository['id'], '%s Builder' % app.config['REGISTRY_TITLE'], + public_key) + if success is False: + msg = 'Unable to add deploy key to repository: %s' % new_build_source + raise TriggerActivationException(msg) + + # Add the webhook to the GitLab repository. + hook = gl_client.addprojecthook(repository['id'], standard_webhook_url, push=True) + if hook is False: + msg = 'Unable to create webhook on repository: %s' % new_build_source + raise TriggerActivationException(msg) + + config['hook_id'] = hook['id'] + self.config = config + return config, {'private_key': private_key} + + def deactivate(self): + config = self.config + gl_client = self._get_authorized_client() + + # Find the GitLab repository. + repository = gl_client.getproject(config['build_source']) + if repository is False: + msg = 'Unable to find GitLab repository for source: %s' % config['build_source'] + raise TriggerDeactivationException(msg) + + # Remove the webhook. + success = gl_client.deleteprojecthook(repository['id'], config['hook_id']) + if success is False: + msg = 'Unable to remove hook: %s' % config['hook_id'] + raise TriggerDeactivationException(msg) + + config.pop('hook_id', None) + self.config = config + + return config + + def list_build_sources(self): + gl_client = self._get_authorized_client() + current_user = gl_client.currentuser() + + repositories = gl_client.getprojects() + if repositories is False: + raise RepositoryReadException('Unable to list user repositories') + + namespaces = {} + for repo in repositories: + owner = repo['namespace']['name'] + if not owner in namespaces: + namespaces[owner] = { + 'personal': owner == current_user.username, + 'repos': [], + 'info': { + 'name': owner, + } + } + + namespaces[owner]['repos'].append(repo['path_with_namespace']) + + return namespaces.values() + + def list_build_subdirs(self): + config = self.config + gl_client = self._get_authorized_client() + new_build_source = config['build_source'] + + repository = gl_client.getproject(new_build_source) + if repository is False: + msg = 'Unable to find GitLab repository for source: %s' % new_build_source + raise RepositoryReadException(msg) + + repo_branches = gl_client.getbranches(repository['id']) + if repo_branches is False: + msg = 'Unable to find GitLab branches for source: %s' % new_build_source + raise RepositoryReadException(msg) + + branches = [branch['name'] for branch in repo_branches] + branches = find_matching_branches(config, branches) + branches = branches or [repository['default_branch'] or 'master'] + + repo_tree = gl_client.getrepositorytree(repository['id'], ref_name=branches[0]) + if repo_tree is False: + msg = 'Unable to find GitLab repository tree for source: %s' % new_build_source + raise RepositoryReadException(msg) + + for node in repo_tree: + if node['name'] == 'Dockerfile': + return ['/'] + + return [] + + def dockerfile_url(self): + gl_client = self._get_authorized_client() + subdir = self.config.get('subdir', '') + path = subdir + '/Dockerfile' if subdir else 'Dockerfile' + + repository = gl_client.getproject(self.config['build_source']) + if repository is False: + return None + branch = repository['default_branch'] + + return '%s/%s/blob/%s/%s' % (gl_client.host, + repository['path_with_namespace'], + branch, + path) + + def load_dockerfile_contents(self): + gl_client = self._get_authorized_client() + subdir = self.config.get('subdir', '') + path = subdir + '/Dockerfile' if subdir else 'Dockerfile' + + repository = gl_client.getproject(self.config['build_source']) + if repository is False: + return None + + contents = gl_client.getrawfile(repository['id'], repository['default_branch'], path) + if contents is False: + return None + return contents + + def list_field_values(self, field_name): + if field_name == 'refs': + branches = self.list_field_values('branch_name') + tags = self.list_field_values('tag_name') + + return ([{'kind': 'branch', 'name': b} for b in branches] + + [{'kind': 'tag', 'name': t} for t in tags]) + + gl_client = self._get_authorized_client() + repo = gl_client.getproject(self.config['build_source']) + if repo is False: + return [] + + if field_name == 'tag_name': + tags = gl_client.getall(gl_client.getrepositorytags(repo['id'])) + if tags is False: + return [] + return [tag.name for tag in tags] + + if field_name == 'branch_name': + branches = gl_client.getbranches(repo['id']) + if branches is False: + return [] + return [branch.name for branch in branches] + + return None + + def _prepare_build(self, commit, ref, is_manual): + config = self.config + gl_client = self._get_authorized_client() + + repo = gl_client.getproject(self.config['build_source']) + if repo is False: + raise TriggerStartException('Could not find repository') + + try: + [committer] = gl_client.getusers(search=commit['committer_email']) + except ValueError: + committer = None + + try: + [author] = gl_client.getusers(search=commit['author_email']) + except ValueError: + author = None + + metadata = { + 'commit_sha': commit['id'], + 'ref': ref, + 'default_branch': repo['default_branch'], + 'git_url': repo['ssh_url_to_repo'], + 'commit_info': { + 'url': '', + 'message': commit['message'], + 'date': commit['committed_date'], + }, + } + + if committer is not None: + metadata['commit_info']['committer'] = { + 'username': committer['username'], + 'avatar_url': committer['avatar_url'], + 'url': client.host + '/' + committer['username'], + } + + if author is not None: + metadata['commit_info']['author'] = { + 'username': author['username'], + 'avatar_url': author['avatar_url'], + 'url': client.host + '/' + author['username'] + } + + prepared = PreparedBuild(self.trigger) + prepared.tags_from_ref(ref, repo['default_branch']) + prepared.name_from_sha(commit['id']) + prepared.subdirectory = config['subdir'] + prepared.metadata = metadata + prepared.is_manual = is_manual + + return prepared + + def handle_trigger_request(self, request): + payload = request.get_json() + if not payload: + raise SkipRequestException() + + logger.debug('GitLab trigger payload %s', payload) + + if not payload.gets('commits'): + raise SkipRequestException() + + commit = payload['commits'][0] + commit_message = commit['message'] + if should_skip_commit(commit_message): + raise SkipRequestException() + + ref = payload['ref'] + raise_if_skipped(self.config, ref) + + return self._prepare_build(commit, ref, False) + + def manual_start(self, run_parameters=None): + run_parameters = run_parameters or {} + gl_client = self._get_authorized_client() + + repo = gl_client.getproject(self.config['build_source']) + if repo is False: + raise TriggerStartException('Could not find repository') + + branch_name = run_parameters.get('branch_name') or repo['default_branch'] + + branches = gl_client.getbranches(repo['id']) + if branches is False: + raise TriggerStartException('Could not find branches') + + commit = None + for branch in branches: + if branch['name'] is branch_name: + commit = branch['commit'] + if commit is None: + raise TriggerStartException('Could not find commit') + + ref = 'refs/heads/%s' % branch_name + + return self._prepare_build(commit, ref, True)