From bf966545baf9c0ee3ebcc0e367bfc48a47e7f029 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 11 Jun 2018 15:22:02 -0400 Subject: [PATCH] Reimplement GitLab trigger handler using the V4 API library GitLab has deprecated and removed the V3 API entirely Fixes https://jira.coreos.com/browse/QUAY-966 --- buildtrigger/gitlabhandler.py | 307 +++---- buildtrigger/test/gitlabmock.py | 778 +++++++++++++----- buildtrigger/test/test_githosthandler.py | 3 +- buildtrigger/test/test_gitlabhandler.py | 165 +++- buildtrigger/triggerutil.py | 28 +- endpoints/api/trigger.py | 18 +- requirements-nover.txt | 2 +- requirements.txt | 2 +- .../manage-trigger.component.html | 2 +- 9 files changed, 912 insertions(+), 393 deletions(-) diff --git a/buildtrigger/gitlabhandler.py b/buildtrigger/gitlabhandler.py index 3decdca07..5f31e2252 100644 --- a/buildtrigger/gitlabhandler.py +++ b/buildtrigger/gitlabhandler.py @@ -1,28 +1,26 @@ +import os.path import logging -import os from calendar import timegm from functools import wraps import dateutil.parser - -from app import app, gitlab_trigger +import gitlab +import requests from jsonschema import validate + +from app import app, gitlab_trigger from buildtrigger.triggerutil import (RepositoryReadException, TriggerActivationException, TriggerDeactivationException, TriggerStartException, SkipRequestException, InvalidPayloadException, + TriggerAuthException, determine_build_ref, raise_if_skipped_build, find_matching_branches) - from buildtrigger.basehandler import BuildTriggerHandler - +from endpoints.exception import ExternalServiceError from util.security.ssh import generate_ssh_keypair from util.dict_wrappers import JSONPathDict, SafeDictSetter -from endpoints.exception import ExternalServiceError - -import gitlab -import requests logger = logging.getLogger(__name__) @@ -89,7 +87,7 @@ _ACCESS_LEVEL_MAP = { _PER_PAGE_COUNT = 20 -def _catch_timeouts(func): +def _catch_timeouts_and_errors(func): @wraps(func) def wrapper(*args, **kwargs): try: @@ -98,17 +96,21 @@ def _catch_timeouts(func): msg = 'Request to the GitLab API timed out' logger.exception(msg) raise ExternalServiceError(msg) + except gitlab.GitlabError: + msg = 'GitLab API error. Please contact support.' + logger.exception(msg) + raise ExternalServiceError(msg) return wrapper -def _paginated_iterator(func, exc): +def _paginated_iterator(func, exc, **kwargs): """ Returns an iterator over invocations of the given function, automatically handling pagination. """ - page = 0 + page = 1 while True: - result = func(page=page, per_page=_PER_PAGE_COUNT) - if result is False: + result = func(page=page, per_page=_PER_PAGE_COUNT, **kwargs) + if result is None or result is False: raise exc counter = 0 @@ -196,20 +198,28 @@ class GitLabBuildTrigger(BuildTriggerHandler): def _get_authorized_client(self): auth_token = self.auth_token or 'invalid' - return gitlab.Gitlab(gitlab_trigger.api_endpoint(), oauth_token=auth_token, timeout=5) + api_version = self.config.get('API_VERSION', '4') + client = gitlab.Gitlab(gitlab_trigger.api_endpoint(), oauth_token=auth_token, timeout=20, + api_version=api_version) + try: + client.auth() + except gitlab.GitlabGetError as ex: + raise TriggerAuthException(ex.message) + + return client def is_active(self): return 'hook_id' in self.config - @_catch_timeouts + @_catch_timeouts_and_errors 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: + gl_project = gl_client.projects.get(new_build_source) + if not gl_project: msg = 'Unable to find GitLab repository for source: %s' % new_build_source raise TriggerActivationException(msg) @@ -221,20 +231,28 @@ class GitLabBuildTrigger(BuildTriggerHandler): 'value': public_key, }, ] - key = gl_client.adddeploykey(repository['id'], '%s Builder' % app.config['REGISTRY_TITLE'], - public_key) - if key is False: + + key = gl_project.keys.create({ + 'title': '%s Builder' % app.config['REGISTRY_TITLE'], + 'key': public_key, + }) + + if not key: msg = 'Unable to add deploy key to repository: %s' % new_build_source raise TriggerActivationException(msg) - config['key_id'] = key['id'] + + config['key_id'] = key.get_id() # Add the webhook to the GitLab repository. - hook = gl_client.addprojecthook(repository['id'], standard_webhook_url, push=True) - if hook is False: + hook = gl_project.hooks.create({ + 'url': standard_webhook_url, + 'push': True, + }) + if not hook: msg = 'Unable to create webhook on repository: %s' % new_build_source raise TriggerActivationException(msg) - config['hook_id'] = hook['id'] + config['hook_id'] = hook.get_id() self.config = config return config, {'private_key': private_key} @@ -243,72 +261,71 @@ class GitLabBuildTrigger(BuildTriggerHandler): gl_client = self._get_authorized_client() # Find the GitLab repository. - repository = gl_client.getproject(config['build_source']) - if repository is False: + gl_project = gl_client.projects.get(config['build_source']) + if not gl_project: 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) + gl_project.hooks.delete(config['hook_id']) config.pop('hook_id', None) # Remove the key - success = gl_client.deletedeploykey(repository['id'], config['key_id']) - if success is False: - msg = 'Unable to remove deploy key: %s' % config['key_id'] - raise TriggerDeactivationException(msg) + gl_project.keys.delete(config['key_id']) config.pop('key_id', None) self.config = config return config - @_catch_timeouts + @_catch_timeouts_and_errors def list_build_source_namespaces(self): gl_client = self._get_authorized_client() - current_user = gl_client.currentuser() - if current_user is False: + current_user = gl_client.user + if not current_user: raise RepositoryReadException('Unable to get current user') namespaces = {} - repositories = _paginated_iterator(gl_client.getprojects, RepositoryReadException) - for repo in repositories: - namespace = repo.get('namespace') or {} - if not namespace: + for namespace in _paginated_iterator(gl_client.namespaces.list, RepositoryReadException): + namespace_id = namespace.get_id() + + # Retrieve the namespace as a user or group. + namespace_obj = self._get_namespace(gl_client, namespace) + if namespace_obj is None: + logger.warning('Could not load details for namespace %s', namespace_id) continue - namespace_id = namespace['id'] - - avatar_url = '' - if 'avatar' in namespace: - avatar_data = namespace.get('avatar') or {} - avatar_url = avatar_data.get('url') - elif 'owner' in repo: - owner_data = repo.get('owner') or {} - avatar_url = owner_data.get('avatar_url') - if namespace_id in namespaces: namespaces[namespace_id]['score'] = namespaces[namespace_id]['score'] + 1 else: - owner = namespace['name'] + owner = namespace.attributes['name'] namespaces[namespace_id] = { - 'personal': owner == current_user['username'], - 'id': namespace['path'], - 'title': namespace['name'], - 'avatar_url': avatar_url, + 'personal': owner == current_user.attributes['username'], + 'id': str(namespace_id), + 'title': namespace.attributes['name'], + 'avatar_url': namespace_obj.attributes.get('avatar_url', ''), 'score': 1, - 'url': gl_client.host + '/' + namespace['path'], + 'url': namespace_obj.attributes.get('web_url', ''), } return BuildTriggerHandler.build_namespaces_response(namespaces) - @_catch_timeouts - def list_build_sources_for_namespace(self, namespace): + def _get_namespace(self, gl_client, gl_namespace, lazy=False): + try: + if gl_namespace.attributes['kind'] == 'group': + return gl_client.groups.get(gl_namespace.attributes['name'], lazy=lazy) + + return gl_client.users.get(gl_namespace.attributes['name'], lazy=lazy) + except gitlab.GitlabGetError: + return None + + @_catch_timeouts_and_errors + def list_build_sources_for_namespace(self, namespace_id): + if not namespace_id: + return [] + def repo_view(repo): # Because *anything* can be None in GitLab API! - permissions = repo.get('permissions') or {} + permissions = repo.attributes.get('permissions') or {} group_access = permissions.get('group_access') or {} project_access = permissions.get('project_access') or {} @@ -327,17 +344,17 @@ class GitLabBuildTrigger(BuildTriggerHandler): has_admin_permission = True view = { - 'name': repo['path'], - 'full_name': repo['path_with_namespace'], - 'description': repo.get('description') or '', - 'url': repo.get('web_url'), + 'name': repo.attributes['path'], + 'full_name': repo.attributes['path_with_namespace'], + 'description': repo.attributes.get('description') or '', + 'url': repo.attributes.get('web_url'), 'has_admin_permissions': has_admin_permission, - 'private': repo.get('public', False) is False, + 'private': repo.attributes.get('visibility') == 'private', } - if repo.get('last_activity_at'): + if repo.attributes.get('last_activity_at'): try: - last_modified = dateutil.parser.parse(repo['last_activity_at']) + last_modified = dateutil.parser.parse(repo.attributes['last_activity_at']) view['last_updated'] = timegm(last_modified.utctimetuple()) except ValueError: logger.exception('Gitlab gave us an invalid last_activity_at: %s', last_modified) @@ -345,44 +362,50 @@ class GitLabBuildTrigger(BuildTriggerHandler): return view gl_client = self._get_authorized_client() - repositories = _paginated_iterator(gl_client.getprojects, RepositoryReadException) - repos = [repo_view(repo) for repo in repositories if repo['namespace']['path'] == namespace] - return BuildTriggerHandler.build_sources_response(repos) - @_catch_timeouts + try: + gl_namespace = gl_client.namespaces.get(namespace_id) + except gitlab.GitlabGetError: + return [] + + namespace_obj = self._get_namespace(gl_client, gl_namespace, lazy=True) + repositories = _paginated_iterator(namespace_obj.projects.list, RepositoryReadException) + return BuildTriggerHandler.build_sources_response([repo_view(repo) for repo in repositories]) + + @_catch_timeouts_and_errors 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: + gl_project = gl_client.projects.get(new_build_source) + if not gl_project: 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: + repo_branches = gl_project.branches.list() + if not repo_branches: msg = 'Unable to find GitLab branches for source: %s' % new_build_source raise RepositoryReadException(msg) - branches = [branch['name'] for branch in repo_branches] + branches = [branch.attributes['name'] for branch in repo_branches] branches = find_matching_branches(config, branches) - branches = branches or [repository['default_branch'] or 'master'] + branches = branches or [gl_project.attributes['default_branch'] or 'master'] - repo_tree = gl_client.getrepositorytree(repository['id'], ref_name=branches[0]) - if repo_tree is False: + repo_tree = gl_project.repository_tree(ref=branches[0]) + if not repo_tree: msg = 'Unable to find GitLab repository tree for source: %s' % new_build_source raise RepositoryReadException(msg) - return ["/" + node['name'] for node in repo_tree if self.filename_is_dockerfile(node['name'])] + return [node['name'] for node in repo_tree if self.filename_is_dockerfile(node['name'])] - @_catch_timeouts + @_catch_timeouts_and_errors def load_dockerfile_contents(self): gl_client = self._get_authorized_client() path = self.get_dockerfile_path() - repository = gl_client.getproject(self.config['build_source']) - if repository is False: + gl_project = gl_client.projects.get(self.config['build_source']) + if not gl_project: return None branches = self.list_field_values('branch_name') @@ -391,16 +414,15 @@ class GitLabBuildTrigger(BuildTriggerHandler): return None branch_name = branches[0] - if repository['default_branch'] in branches: - branch_name = repository['default_branch'] + if gl_project.attributes['default_branch'] in branches: + branch_name = gl_project.attributes['default_branch'] - contents = gl_client.getrawfile(repository['id'], branch_name, path) - if contents is False: + try: + return gl_project.files.get(path, branch_name).decode() + except gitlab.GitlabGetError: return None - return contents - - @_catch_timeouts + @_catch_timeouts_and_errors def list_field_values(self, field_name, limit=None): if field_name == 'refs': branches = self.list_field_values('branch_name') @@ -410,139 +432,138 @@ class GitLabBuildTrigger(BuildTriggerHandler): [{'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: + gl_project = gl_client.projects.get(self.config['build_source']) + if not gl_project: return [] if field_name == 'tag_name': - tags = gl_client.getrepositorytags(repo['id']) - if tags is False: + tags = gl_project.tags.list() + if not tags: return [] if limit: tags = tags[0:limit] - return [tag['name'] for tag in tags] + return [tag.attributes['name'] for tag in tags] if field_name == 'branch_name': - branches = gl_client.getbranches(repo['id']) - if branches is False: + branches = gl_project.branches.list() + if not branches: return [] if limit: branches = branches[0:limit] - return [branch['name'] for branch in branches] + return [branch.attributes['name'] for branch in branches] return None def get_repository_url(self): return gitlab_trigger.get_public_url(self.config['build_source']) - @_catch_timeouts + @_catch_timeouts_and_errors def lookup_commit(self, repo_id, commit_sha): if repo_id is None: return None gl_client = self._get_authorized_client() - commit = gl_client.getrepositorycommit(repo_id, commit_sha) - if commit is False: + gl_project = gl_client.projects.get(self.config['build_source'], lazy=True) + commit = gl_project.commits.get(commit_sha) + if not commit: return None return commit - @_catch_timeouts + @_catch_timeouts_and_errors def lookup_user(self, email): gl_client = self._get_authorized_client() try: - result = gl_client.getusers(search=email) - if result is False: + result = gl_client.users.list(search=email) + if not result: return None [user] = result return { - 'username': user['username'], - 'html_url': gl_client.host + '/' + user['username'], - 'avatar_url': user['avatar_url'] + 'username': user.attributes['username'], + 'html_url': user.attributes['web_url'], + 'avatar_url': user.attributes['avatar_url'] } except ValueError: return None - @_catch_timeouts + @_catch_timeouts_and_errors def get_metadata_for_commit(self, commit_sha, ref, repo): - gl_client = self._get_authorized_client() - commit = gl_client.getrepositorycommit(repo['id'], commit_sha) + commit = self.lookup_commit(repo.get_id(), commit_sha) + if commit is None: + return None metadata = { - 'commit': commit['id'], + 'commit': commit.attributes['id'], 'ref': ref, - 'default_branch': repo['default_branch'], - 'git_url': repo['ssh_url_to_repo'], + 'default_branch': repo.attributes['default_branch'], + 'git_url': repo.attributes['ssh_url_to_repo'], 'commit_info': { - 'url': gl_client.host + '/' + repo['path_with_namespace'] + '/commit/' + commit['id'], - 'message': commit['message'], - 'date': commit['committed_date'], + 'url': os.path.join(repo.attributes['web_url'], 'commit', commit.attributes['id']), + 'message': commit.attributes['message'], + 'date': commit.attributes['committed_date'], }, } committer = None - if 'committer_email' in commit: - committer = self.lookup_user(commit['committer_email']) + if 'committer_email' in commit.attributes: + committer = self.lookup_user(commit.attributes['committer_email']) author = None - if 'author_email' in commit: - author = self.lookup_user(commit['author_email']) + if 'author_email' in commit.attributes: + author = self.lookup_user(commit.attributes['author_email']) if committer is not None: metadata['commit_info']['committer'] = { 'username': committer['username'], 'avatar_url': committer['avatar_url'], - 'url': gl_client.host + '/' + committer['username'], + 'url': committer.get('http_url', ''), } if author is not None: metadata['commit_info']['author'] = { 'username': author['username'], 'avatar_url': author['avatar_url'], - 'url': gl_client.host + '/' + author['username'] + 'url': author.get('http_url', ''), } return metadata - @_catch_timeouts + @_catch_timeouts_and_errors def manual_start(self, run_parameters=None): gl_client = self._get_authorized_client() - - repo = gl_client.getproject(self.config['build_source']) - if repo is False: + gl_project = gl_client.projects.get(self.config['build_source']) + if not gl_project: raise TriggerStartException('Could not find repository') def get_tag_sha(tag_name): - tags = gl_client.getrepositorytags(repo['id']) - if tags is False: + try: + tag = gl_project.tags.get(tag_name) + except gitlab.GitlabGetError: raise TriggerStartException('Could not find tag in repository') - for tag in tags: - if tag['name'] == tag_name: - return tag['commit']['id'] - - raise TriggerStartException('Could not find tag in repository') + return tag.attributes['commit']['id'] def get_branch_sha(branch_name): - branch = gl_client.getbranch(repo['id'], branch_name) - if branch is False: + try: + branch = gl_project.branches.get(branch_name) + except gitlab.GitlabGetError: raise TriggerStartException('Could not find branch in repository') - return branch['commit']['id'] + return branch.attributes['commit']['id'] # Find the branch or tag to build. (commit_sha, ref) = determine_build_ref(run_parameters, get_branch_sha, get_tag_sha, - repo['default_branch']) + gl_project.attributes['default_branch']) - metadata = self.get_metadata_for_commit(commit_sha, ref, repo) + metadata = self.get_metadata_for_commit(commit_sha, ref, gl_project) return self.prepare_build(metadata, is_manual=True) - @_catch_timeouts + @_catch_timeouts_and_errors def handle_trigger_request(self, request): payload = request.get_json() if not payload: @@ -552,12 +573,12 @@ class GitLabBuildTrigger(BuildTriggerHandler): # Lookup the default branch. gl_client = self._get_authorized_client() - repo = gl_client.getproject(self.config['build_source']) - if repo is False: + gl_project = gl_client.projects.get(self.config['build_source']) + if not gl_project: logger.debug('Skipping GitLab build; project %s not found', self.config['build_source']) raise InvalidPayloadException() - default_branch = repo['default_branch'] + default_branch = gl_project.attributes['default_branch'] metadata = get_transformed_webhook_payload(payload, default_branch=default_branch, lookup_user=self.lookup_user, lookup_commit=self.lookup_commit) diff --git a/buildtrigger/test/gitlabmock.py b/buildtrigger/test/gitlabmock.py index 9ac201b10..aedc08f5f 100644 --- a/buildtrigger/test/gitlabmock.py +++ b/buildtrigger/test/gitlabmock.py @@ -1,219 +1,597 @@ -from datetime import datetime -from mock import Mock +import base64 +import json + +from contextlib import contextmanager + +import gitlab + +from httmock import urlmatch, HTTMock from buildtrigger.gitlabhandler import GitLabBuildTrigger from util.morecollections import AttrDict -def get_gitlab_trigger(dockerfile_path=''): - trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) - trigger = GitLabBuildTrigger(trigger_obj, { - 'build_source': 'foo/bar', - 'dockerfile_path': dockerfile_path, - 'username': 'knownuser' - }) - trigger._get_authorized_client = get_mock_gitlab(with_nulls=False) - return trigger +@urlmatch(netloc=r'fakegitlab') +def catchall_handler(url, request): + return {'status_code': 404} -def adddeploykey_mock(project_id, name, public_key): - return {'id': 'foo'} -def addprojecthook_mock(project_id, webhook_url, push=False): - return {'id': 'foo'} +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/users$') +def users_handler(url, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} -def get_currentuser_mock(): - return { - 'username': 'knownuser' - } - -def project(namespace, name, is_org=False): - project_access = None - - if name != 'null': - if namespace == 'knownuser': - project_access = { - 'access_level': 50, - } - else: - project_access = { - 'access_level': 0, - } - - data = { - 'id': '%s/%s' % (namespace, name), - 'default_branch': 'master', - 'namespace': { - 'id': namespace, - 'path': namespace, - 'name': namespace, - }, - 'path': name, - 'path_with_namespace': '%s/%s' % (namespace, name), - 'description': 'some %s repo' % name, - 'last_activity_at': str(datetime.utcfromtimestamp(0)), - 'web_url': 'https://bitbucket.org/%s/%s' % (namespace, name), - 'ssh_url_to_repo': 'git://%s/%s' % (namespace, name), - 'public': name != 'somerepo', - 'permissions': { - 'project_access': project_access, - 'group_access': {'access_level': 0}, - }, - 'owner': { - 'avatar_url': 'avatarurl', + if url.query.find('knownuser') < 0: + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([]), } - } - - if name == 'null': - del data['owner']['avatar_url'] - data['namespace']['avatar'] = None - elif is_org: - del data['owner']['avatar_url'] - data['namespace']['avatar'] = {'url': 'avatarurl'} - - return data - -def getprojects_mock(with_nulls=False): - if with_nulls: - def _getprojs(page=1, per_page=100): - return [ - project('someorg', 'null', is_org=True), - ] - return _getprojs - - else: - def _getprojs(page=1, per_page=100): - return [ - project('knownuser', 'somerepo'), - project('someorg', 'somerepo', is_org=True), - project('someorg', 'anotherrepo', is_org=True), - ] - return _getprojs - -def getproject_mock(project_name): - if project_name == 'knownuser/somerepo': - return project('knownuser', 'somerepo') - - if project_name == 'foo/bar': - return project('foo', 'bar', is_org=True) - - return False - - -def getbranches_mock(project_id): - return [ - { - 'name': 'master', - 'commit': { - 'id': 'aaaaaaa', - } - }, - { - 'name': 'otherbranch', - 'commit': { - 'id': 'aaaaaaa', - } - }, - ] - -def getrepositorytags_mock(project_id): - return [ - { - 'name': 'sometag', - 'commit': { - 'id': 'aaaaaaa', - } - }, - { - 'name': 'someothertag', - 'commit': { - 'id': 'aaaaaaa', - } - }, - ] - -def getrepositorytree_mock(project_id, ref_name='master'): - return [ - {'name': 'README'}, - {'name': 'Dockerfile'}, - ] - -def getrepositorycommit_mock(project_id, commit_sha): - if commit_sha != 'aaaaaaa': - return False return { - 'id': 'aaaaaaa', - 'message': 'some message', - 'committed_date': 'now', - } - -def getusers_mock(search=None): - if search == 'knownuser': - return [ + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([ { - 'username': 'knownuser', - 'avatar_url': 'avatarurl', + "id": 1, + "username": "knownuser", + "name": "Known User", + "state": "active", + "avatar_url": "avatarurl", + "web_url": "https://bitbucket.org/knownuser", + }, + ]), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/user$') +def user_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 1, + "username": "john_smith", + "email": "john@example.com", + "name": "John Smith", + "state": "active", + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/foo%2Fbar$') +def project_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 4, + "description": None, + "default_branch": "master", + "visibility": "private", + "path_with_namespace": "someorg/somerepo", + "ssh_url_to_repo": "git@example.com:someorg/somerepo.git", + "web_url": "http://example.com/someorg/somerepo", + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/tree$') +def project_tree_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([ + { + "id": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba", + "name": "Dockerfile", + "type": "tree", + "path": "files/Dockerfile", + "mode": "040000", + }, + ]), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/tags$') +def project_tags_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([ + { + 'name': 'sometag', + 'commit': { + 'id': '60a8ff033665e1207714d6670fcd7b65304ec02f', + }, + }, + { + 'name': 'someothertag', + 'commit': { + 'id': '60a8ff033665e1207714d6670fcd7b65304ec02f', + }, + }, + ]), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/branches$') +def project_branches_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([ + { + 'name': 'master', + 'commit': { + 'id': '60a8ff033665e1207714d6670fcd7b65304ec02f', + }, + }, + { + 'name': 'otherbranch', + 'commit': { + 'id': '60a8ff033665e1207714d6670fcd7b65304ec02f', + }, + }, + ]), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/branches/master$') +def project_branch_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "name": "master", + "merged": True, + "protected": True, + "developers_can_push": False, + "developers_can_merge": False, + "commit": { + "author_email": "john@example.com", + "author_name": "John Smith", + "authored_date": "2012-06-27T05:51:39-07:00", + "committed_date": "2012-06-28T03:44:20-07:00", + "committer_email": "john@example.com", + "committer_name": "John Smith", + "id": "60a8ff033665e1207714d6670fcd7b65304ec02f", + "short_id": "7b5c3cc", + "title": "add projects API", + "message": "add projects API", + "parent_ids": [ + "4ad91d3c1144c406e50c7b33bae684bd6837faf8", + ], + }, + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/namespaces/someorg$') +def namespace_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 2, + "name": "someorg", + "path": "someorg", + "kind": "group", + "full_path": "someorg", + "parent_id": None, + "members_count_with_descendants": 2 + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/namespaces/knownuser$') +def user_namespace_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 2, + "name": "knownuser", + "path": "knownuser", + "kind": "user", + "full_path": "knownuser", + "parent_id": None, + "members_count_with_descendants": 2 + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/namespaces(/)?$') +def namespaces_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([{ + "id": 2, + "name": "someorg", + "path": "someorg", + "kind": "group", + "full_path": "someorg", + "parent_id": None, + "members_count_with_descendants": 2 + }]), + } + + +def get_projects_handler(add_permissions_block): + @urlmatch(netloc=r'fakegitlab', path=r'/api/v4/groups/someorg/projects$') + def projects_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + permissions_block = { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 20, + "notification_level": 3 + }, + } + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([{ + "id": 4, + "name": "Some project", + "description": None, + "default_branch": "master", + "visibility": "private", + "path": "someproject", + "path_with_namespace": "someorg/someproject", + "last_activity_at": "2013-09-30T13:46:02Z", + "web_url": "http://example.com/someorg/someproject", + "permissions": permissions_block if add_permissions_block else None, + }, + { + "id": 5, + "name": "Another project", + "description": None, + "default_branch": "master", + "visibility": "public", + "path": "anotherproject", + "path_with_namespace": "someorg/anotherproject", + "last_activity_at": "2013-09-30T13:46:02Z", + "web_url": "http://example.com/someorg/anotherproject", + }]), + } + return projects_handler + + +def get_group_handler(null_avatar): + @urlmatch(netloc=r'fakegitlab', path=r'/api/v4/groups/someorg$') + def group_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 1, + "name": "SomeOrg Group", + "path": "someorg", + "description": "An interesting group", + "visibility": "public", + "lfs_enabled": True, + "avatar_url": 'avatar_url' if not null_avatar else None, + "web_url": "http://gitlab.com/groups/someorg", + "request_access_enabled": False, + "full_name": "SomeOrg Group", + "full_path": "someorg", + "parent_id": None, + }), + } + return group_handler + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/files/Dockerfile$') +def dockerfile_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "file_name": "Dockerfile", + "file_path": "Dockerfile", + "size": 10, + "encoding": "base64", + "content": base64.b64encode('hello world'), + "ref": "master", + "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", + "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", + "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/files/somesubdir%2FDockerfile$') +def sub_dockerfile_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "file_name": "Dockerfile", + "file_path": "somesubdir/Dockerfile", + "size": 10, + "encoding": "base64", + "content": base64.b64encode('hi universe'), + "ref": "master", + "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", + "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", + "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/repository/tags/sometag$') +def tag_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "name": "sometag", + "message": "some cool message", + "target": "60a8ff033665e1207714d6670fcd7b65304ec02f", + "commit": { + "id": "60a8ff033665e1207714d6670fcd7b65304ec02f", + "short_id": "60a8ff03", + "title": "Initial commit", + "created_at": "2017-07-26T11:08:53.000+02:00", + "parent_ids": [ + "f61c062ff8bcbdb00e0a1b3317a91aed6ceee06b" + ], + "message": "v5.0.0\n", + "author_name": "Arthur Verschaeve", + "author_email": "contact@arthurverschaeve.be", + "authored_date": "2015-02-01T21:56:31.000+01:00", + "committer_name": "Arthur Verschaeve", + "committer_email": "contact@arthurverschaeve.be", + "committed_date": "2015-02-01T21:56:31.000+01:00" + }, + "release": None, + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/foo%2Fbar/repository/commits/60a8ff033665e1207714d6670fcd7b65304ec02f$') +def commit_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": "60a8ff033665e1207714d6670fcd7b65304ec02f", + "short_id": "60a8ff03366", + "title": "Sanitize for network graph", + "author_name": "someguy", + "author_email": "some.guy@gmail.com", + "committer_name": "Some Guy", + "committer_email": "some.guy@gmail.com", + "created_at": "2012-09-20T09:06:12+03:00", + "message": "Sanitize for network graph", + "committed_date": "2012-09-20T09:06:12+03:00", + "authored_date": "2012-09-20T09:06:12+03:00", + "parent_ids": [ + "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" + ], + "last_pipeline" : { + "id": 8, + "ref": "master", + "sha": "2dc6aa325a317eda67812f05600bdf0fcdc70ab0", + "status": "created", + }, + "stats": { + "additions": 15, + "deletions": 10, + "total": 25 + }, + "status": "running" + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/deploy_keys$', method='POST') +def create_deploykey_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 1, + "title": "Public key", + "key": "ssh-rsa some stuff", + "created_at": "2013-10-02T10:12:29Z", + "can_push": False, + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/hooks$', method='POST') +def create_hook_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({ + "id": 1, + "url": "http://example.com/hook", + "project_id": 4, + "push_events": True, + "issues_events": True, + "confidential_issues_events": True, + "merge_requests_events": True, + "tag_push_events": True, + "note_events": True, + "job_events": True, + "pipeline_events": True, + "wiki_page_events": True, + "enable_ssl_verification": True, + "created_at": "2012-10-12T17:04:47Z", + }), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/hooks/1$', method='DELETE') +def delete_hook_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({}), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/projects/4/deploy_keys/1$', method='DELETE') +def delete_deploykey_handker(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps({}), + } + + +@urlmatch(netloc=r'fakegitlab', path=r'/api/v4/users/knownuser/projects$') +def user_projects_list_handler(_, request): + if not request.headers.get('Authorization') == 'Bearer foobar': + return {'status_code': 401} + + return { + 'status_code': 200, + 'headers': { + 'Content-Type': 'application/json', + }, + 'content': json.dumps([ + { + "id": 2, + "name": "Another project", + "description": None, + "default_branch": "master", + "visibility": "public", + "path": "anotherproject", + "path_with_namespace": "knownuser/anotherproject", + "last_activity_at": "2013-09-30T13:46:02Z", + "web_url": "http://example.com/knownuser/anotherproject", } - ] - - return False - -def getbranch_mock(repo_id, branch): - if branch != 'master' and branch != 'otherbranch': - return False - - return { - 'name': branch, - 'commit': { - 'id': 'aaaaaaa', - } + ]), } -def gettag_mock(repo_id, tag): - if tag != 'sometag' and tag != 'someothertag': - return False - return { - 'name': tag, - 'commit': { - 'id': 'aaaaaaa', - } - } +@contextmanager +def get_gitlab_trigger(dockerfile_path='', add_permissions=True, missing_avatar_url=False): + handlers = [user_handler, users_handler, project_branches_handler, project_tree_handler, + project_handler, get_projects_handler(add_permissions), tag_handler, + project_branch_handler, get_group_handler(missing_avatar_url), dockerfile_handler, + sub_dockerfile_handler, namespace_handler, user_namespace_handler, namespaces_handler, + commit_handler, create_deploykey_handler, delete_deploykey_handker, + create_hook_handler, delete_hook_handler, project_tags_handler, + user_projects_list_handler, catchall_handler] -def getrawfile_mock(repo_id, branch_name, path): - if path == 'Dockerfile': - return 'hello world' + with HTTMock(*handlers): + trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) + trigger = GitLabBuildTrigger(trigger_obj, { + 'build_source': 'foo/bar', + 'dockerfile_path': dockerfile_path, + 'username': 'knownuser' + }) - if path == 'somesubdir/Dockerfile': - return 'hi universe' + client = gitlab.Gitlab('http://fakegitlab', oauth_token='foobar', timeout=20, api_version=4) + client.auth() - return False - -def get_mock_gitlab(with_nulls=False): - def _get_mock(): - mock_gitlab = Mock() - mock_gitlab.host = 'https://bitbucket.org' - - mock_gitlab.currentuser = Mock(side_effect=get_currentuser_mock) - mock_gitlab.getusers = Mock(side_effect=getusers_mock) - - mock_gitlab.getprojects = Mock(side_effect=getprojects_mock(with_nulls)) - mock_gitlab.getproject = Mock(side_effect=getproject_mock) - mock_gitlab.getbranches = Mock(side_effect=getbranches_mock) - - mock_gitlab.getbranch = Mock(side_effect=getbranch_mock) - mock_gitlab.gettag = Mock(side_effect=gettag_mock) - - mock_gitlab.getrepositorytags = Mock(side_effect=getrepositorytags_mock) - mock_gitlab.getrepositorytree = Mock(side_effect=getrepositorytree_mock) - mock_gitlab.getrepositorycommit = Mock(side_effect=getrepositorycommit_mock) - - mock_gitlab.getrawfile = Mock(side_effect=getrawfile_mock) - - mock_gitlab.adddeploykey = Mock(side_effect=adddeploykey_mock) - mock_gitlab.addprojecthook = Mock(side_effect=addprojecthook_mock) - mock_gitlab.deletedeploykey = Mock(return_value=True) - mock_gitlab.deleteprojecthook = Mock(return_value=True) - return mock_gitlab - - return _get_mock + trigger._get_authorized_client = lambda: client + yield trigger diff --git a/buildtrigger/test/test_githosthandler.py b/buildtrigger/test/test_githosthandler.py index 53005d4c0..933233e96 100644 --- a/buildtrigger/test/test_githosthandler.py +++ b/buildtrigger/test/test_githosthandler.py @@ -3,12 +3,11 @@ import pytest from buildtrigger.triggerutil import TriggerStartException from buildtrigger.test.bitbucketmock import get_bitbucket_trigger from buildtrigger.test.githubmock import get_github_trigger -from buildtrigger.test.gitlabmock import get_gitlab_trigger from endpoints.building import PreparedBuild # Note: This test suite executes a common set of tests against all the trigger types specified # in this fixture. Each trigger's mock is expected to return the same data for all of these calls. -@pytest.fixture(params=[get_github_trigger(), get_bitbucket_trigger(), get_gitlab_trigger()]) +@pytest.fixture(params=[get_github_trigger(), get_bitbucket_trigger()]) def githost_trigger(request): return request.param diff --git a/buildtrigger/test/test_gitlabhandler.py b/buildtrigger/test/test_gitlabhandler.py index fae6f4989..66a324956 100644 --- a/buildtrigger/test/test_gitlabhandler.py +++ b/buildtrigger/test/test_gitlabhandler.py @@ -3,19 +3,20 @@ import pytest from mock import Mock -from buildtrigger.test.gitlabmock import get_gitlab_trigger, get_mock_gitlab +from buildtrigger.test.gitlabmock import get_gitlab_trigger from buildtrigger.triggerutil import (SkipRequestException, ValidationRequestException, - InvalidPayloadException) + InvalidPayloadException, TriggerStartException) from endpoints.building import PreparedBuild from util.morecollections import AttrDict -@pytest.fixture +@pytest.fixture() def gitlab_trigger(): - return get_gitlab_trigger() + with get_gitlab_trigger() as t: + yield t def test_list_build_subdirs(gitlab_trigger): - assert gitlab_trigger.list_build_subdirs() == ['/Dockerfile'] + assert gitlab_trigger.list_build_subdirs() == ['Dockerfile'] @pytest.mark.parametrize('dockerfile_path, contents', [ @@ -24,8 +25,8 @@ def test_list_build_subdirs(gitlab_trigger): ('unknownpath', None), ]) def test_load_dockerfile_contents(dockerfile_path, contents): - trigger = get_gitlab_trigger(dockerfile_path) - assert trigger.load_dockerfile_contents() == contents + with get_gitlab_trigger(dockerfile_path=dockerfile_path) as trigger: + assert trigger.load_dockerfile_contents() == contents @pytest.mark.parametrize('email, expected_response', [ @@ -37,26 +38,50 @@ def test_lookup_user(email, expected_response, gitlab_trigger): assert gitlab_trigger.lookup_user(email) == expected_response -def test_null_permissions(gitlab_trigger): - gitlab_trigger._get_authorized_client = get_mock_gitlab(with_nulls=True) - sources = gitlab_trigger.list_build_sources_for_namespace('someorg') - source = sources[0] - assert source['has_admin_permissions'] +def test_null_permissions(): + with get_gitlab_trigger(add_permissions=False) as trigger: + sources = trigger.list_build_sources_for_namespace('someorg') + source = sources[0] + assert source['has_admin_permissions'] -def test_null_avatar(gitlab_trigger): - gitlab_trigger._get_authorized_client = get_mock_gitlab(with_nulls=True) - namespace_data = gitlab_trigger.list_build_source_namespaces() - expected = { - 'avatar_url': None, - 'personal': False, - 'title': 'someorg', - 'url': 'https://bitbucket.org/someorg', - 'score': 1, - 'id': 'someorg', - } +def test_list_build_sources(): + with get_gitlab_trigger() as trigger: + sources = trigger.list_build_sources_for_namespace('someorg') + assert sources == [ + { + 'last_updated': 1380548762, + 'name': u'someproject', + 'url': u'http://example.com/someorg/someproject', + 'private': True, + 'full_name': u'someorg/someproject', + 'has_admin_permissions': False, + 'description': '' + }, + { + 'last_updated': 1380548762, + 'name': u'anotherproject', + 'url': u'http://example.com/someorg/anotherproject', + 'private': False, + 'full_name': u'someorg/anotherproject', + 'has_admin_permissions': True, + 'description': '', + }] - assert namespace_data == [expected] + +def test_null_avatar(): + with get_gitlab_trigger(missing_avatar_url=True) as trigger: + namespace_data = trigger.list_build_source_namespaces() + expected = { + 'avatar_url': None, + 'personal': False, + 'title': u'someorg', + 'url': u'http://gitlab.com/groups/someorg', + 'score': 1, + 'id': '2', + } + + assert namespace_data == [expected] @pytest.mark.parametrize('payload, expected_error, expected_message', [ @@ -112,3 +137,95 @@ def test_handle_trigger_request(gitlab_trigger, payload, expected_error, expecte assert isinstance(gitlab_trigger.handle_trigger_request(request), PreparedBuild) +@pytest.mark.parametrize('run_parameters, expected_error, expected_message', [ + # No branch or tag specified: use the commit of the default branch. + ({}, None, None), + + # Invalid branch. + ({'refs': {'kind': 'branch', 'name': 'invalid'}}, TriggerStartException, + 'Could not find branch in repository'), + + # Invalid tag. + ({'refs': {'kind': 'tag', 'name': 'invalid'}}, TriggerStartException, + 'Could not find tag in repository'), + + # Valid branch. + ({'refs': {'kind': 'branch', 'name': 'master'}}, None, None), + + # Valid tag. + ({'refs': {'kind': 'tag', 'name': 'sometag'}}, None, None), +]) +def test_manual_start(run_parameters, expected_error, expected_message, gitlab_trigger): + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + gitlab_trigger.manual_start(run_parameters) + assert ipe.value.message == expected_message + else: + assert isinstance(gitlab_trigger.manual_start(run_parameters), PreparedBuild) + + +def test_activate_and_deactivate(gitlab_trigger): + _, private_key = gitlab_trigger.activate('http://some/url') + assert 'private_key' in private_key + + gitlab_trigger.deactivate() + + +@pytest.mark.parametrize('name, expected', [ + ('refs', [ + {'kind': 'branch', 'name': 'master'}, + {'kind': 'branch', 'name': 'otherbranch'}, + {'kind': 'tag', 'name': 'sometag'}, + {'kind': 'tag', 'name': 'someothertag'}, + ]), + ('tag_name', set(['sometag', 'someothertag'])), + ('branch_name', set(['master', 'otherbranch'])), + ('invalid', None) +]) +def test_list_field_values(name, expected, gitlab_trigger): + if expected is None: + assert gitlab_trigger.list_field_values(name) is None + elif isinstance(expected, set): + assert set(gitlab_trigger.list_field_values(name)) == set(expected) + else: + assert gitlab_trigger.list_field_values(name) == expected + + +@pytest.mark.parametrize('namespace, expected', [ + ('', []), + ('unknown', []), + + ('knownuser', [ + { + 'last_updated': 1380548762, + 'name': u'anotherproject', + 'url': u'http://example.com/knownuser/anotherproject', + 'private': False, + 'full_name': u'knownuser/anotherproject', + 'has_admin_permissions': True, + 'description': '' + }, + ]), + + ('someorg', [ + { + 'last_updated': 1380548762, + 'name': u'someproject', + 'url': u'http://example.com/someorg/someproject', + 'private': True, + 'full_name': u'someorg/someproject', + 'has_admin_permissions': False, + 'description': '' + }, + { + 'last_updated': 1380548762, + 'name': u'anotherproject', + 'url': u'http://example.com/someorg/anotherproject', + 'private': False, + 'full_name': u'someorg/anotherproject', + 'has_admin_permissions': True, + 'description': '', + }]), +]) +def test_list_build_sources_for_namespace(namespace, expected, gitlab_trigger): + assert gitlab_trigger.list_build_sources_for_namespace(namespace) == expected diff --git a/buildtrigger/triggerutil.py b/buildtrigger/triggerutil.py index a3d82cedc..5c459e53e 100644 --- a/buildtrigger/triggerutil.py +++ b/buildtrigger/triggerutil.py @@ -3,37 +3,43 @@ import io import logging import re -class InvalidPayloadException(Exception): +class TriggerException(Exception): pass -class BuildArchiveException(Exception): +class TriggerAuthException(TriggerException): pass -class InvalidServiceException(Exception): +class InvalidPayloadException(TriggerException): pass -class TriggerActivationException(Exception): +class BuildArchiveException(TriggerException): pass -class TriggerDeactivationException(Exception): +class InvalidServiceException(TriggerException): pass -class TriggerStartException(Exception): +class TriggerActivationException(TriggerException): pass -class ValidationRequestException(Exception): +class TriggerDeactivationException(TriggerException): pass -class SkipRequestException(Exception): +class TriggerStartException(TriggerException): pass -class EmptyRepositoryException(Exception): +class ValidationRequestException(TriggerException): pass -class RepositoryReadException(Exception): +class SkipRequestException(TriggerException): pass -class TriggerProviderException(Exception): +class EmptyRepositoryException(TriggerException): + pass + +class RepositoryReadException(TriggerException): + pass + +class TriggerProviderException(TriggerException): pass logger = logging.getLogger(__name__) diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 8fee8505a..4df891635 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -11,9 +11,7 @@ from app import app from auth.permissions import (UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission, AdministerRepositoryPermission) from buildtrigger.basehandler import BuildTriggerHandler -from buildtrigger.triggerutil import (TriggerDeactivationException, - TriggerActivationException, EmptyRepositoryException, - RepositoryReadException, TriggerStartException) +from buildtrigger.triggerutil import TriggerException, EmptyRepositoryException from data import model from data.model.build import update_build_trigger from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, @@ -118,7 +116,7 @@ class BuildTrigger(RepositoryParamResource): if handler.is_active(): try: handler.deactivate() - except TriggerDeactivationException as ex: + except TriggerException as ex: # We are just going to eat this error logger.warning('Trigger deactivation problem: %s', ex) @@ -178,7 +176,7 @@ class BuildTriggerSubdirs(RepositoryParamResource): 'contextMap': {}, 'dockerfile_paths': [], } - except RepositoryReadException as exc: + except TriggerException as exc: return { 'status': 'error', 'message': exc.message, @@ -264,7 +262,7 @@ class BuildTriggerActivate(RepositoryParamResource): if 'private_key' in private_config: trigger.private_key = private_config['private_key'] - except TriggerActivationException as exc: + except TriggerException as exc: write_token.delete_instance() raise request_error(message=exc.message) @@ -332,7 +330,7 @@ class BuildTriggerAnalyze(RepositoryParamResource): new_config_dict, AdministerOrganizationPermission(namespace_name).can()) return trigger_analyzer.analyze_trigger() - except RepositoryReadException as rre: + except TriggerException as rre: return { 'status': 'error', 'message': 'Could not analyze the repository: %s' % rre.message, @@ -391,7 +389,7 @@ class ActivateBuildTrigger(RepositoryParamResource): run_parameters = request.get_json() prepared = handler.manual_start(run_parameters=run_parameters) build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name) - except TriggerStartException as tse: + except TriggerException as tse: raise InvalidRequest(tse.message) except MaximumBuildsQueuedException: abort(429, message='Maximum queued build rate exceeded.') @@ -494,7 +492,7 @@ class BuildTriggerSources(RepositoryParamResource): return { 'sources': handler.list_build_sources_for_namespace(namespace) } - except RepositoryReadException as rre: + except TriggerException as rre: raise InvalidRequest(rre.message) else: raise Unauthorized() @@ -522,7 +520,7 @@ class BuildTriggerSourceNamespaces(RepositoryParamResource): return { 'namespaces': handler.list_build_source_namespaces() } - except RepositoryReadException as rre: + except TriggerException as rre: raise InvalidRequest(rre.message) else: raise Unauthorized() diff --git a/requirements-nover.txt b/requirements-nover.txt index 78260d5a7..24736d085 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -5,7 +5,6 @@ -e git+https://github.com/NateFerrero/oauth2lib.git#egg=oauth2lib -e git+https://github.com/coreos/mockldap.git@v0.1.x#egg=mockldap -e git+https://github.com/coreos/py-bitbucket.git#egg=py-bitbucket --e git+https://github.com/coreos/pyapi-gitlab.git@timeout#egg=pyapi-gitlab -e git+https://github.com/coreos/resumablehashlib.git#egg=resumablehashlib -e git+https://github.com/jepcastelein/marketo-rest-python.git#egg=marketorestpython -e git+https://github.com/app-registry/appr-server.git@c2ef3b88afe926a92ef5f2e11e7d4a259e286a17#egg=cnr_server # naming has changed @@ -58,6 +57,7 @@ pyjwkest pyjwt pymysql==0.6.7 # Remove version when baseimage has Python 2.7.9+ python-dateutil +python-gitlab python-keystoneclient python-ldap python-magic diff --git a/requirements.txt b/requirements.txt index 0372df258..44ccab610 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ aiowsgi==0.6 alembic==0.9.8 -e git+https://github.com/coreos/mockldap.git@59a46efbe8c7cd8146a87a7c4f2b09746b953e11#egg=mockldap -e git+https://github.com/coreos/py-bitbucket.git@55a1ada645f2fb6369147996ec71edd7828d91c8#egg=py_bitbucket --e git+https://github.com/coreos/pyapi-gitlab.git@136c3970d591136a4f766a846c5d22aad52e124f#egg=pyapi_gitlab -e git+https://github.com/coreos/resumablehashlib.git@b1b631249589b07adf40e0ee545b323a501340b4#egg=resumablehashlib -e git+https://github.com/DevTable/aniso8601-fake.git@bd7762c7dea0498706d3f57db60cd8a8af44ba90#egg=aniso8601 -e git+https://github.com/DevTable/anunidecode.git@d59236a822e578ba3a0e5e5abbd3855873fa7a88#egg=anunidecode @@ -113,6 +112,7 @@ pyparsing==2.2.0 PyPDF2==1.26.0 python-dateutil==2.6.1 python-editor==1.0.3 +python-gitlab==1.4.0 python-keystoneclient==3.15.0 python-ldap==2.5.2 python-magic==0.4.15 diff --git a/static/js/directives/ui/manage-trigger/manage-trigger.component.html b/static/js/directives/ui/manage-trigger/manage-trigger.component.html index 8da9e2b56..7eec1ff63 100644 --- a/static/js/directives/ui/manage-trigger/manage-trigger.component.html +++ b/static/js/directives/ui/manage-trigger/manage-trigger.component.html @@ -35,7 +35,7 @@ {{ ::item.id }} + is-text-only="::!item.url">{{ ::item.title }}