Merge pull request #3110 from quay/joseph.schorr/QUAY-966/gitlab-v4

Reimplement GitLab trigger handler using the V4 API library
This commit is contained in:
Joseph Schorr 2018-06-12 17:03:31 -04:00 committed by GitHub
commit 1be22a9a56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 912 additions and 393 deletions

View file

@ -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)

View file

@ -1,10 +1,588 @@
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=''):
@urlmatch(netloc=r'fakegitlab')
def catchall_handler(url, request):
return {'status_code': 404}
@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}
if url.query.find('knownuser') < 0:
return {
'status_code': 200,
'headers': {
'Content-Type': 'application/json',
},
'content': json.dumps([]),
}
return {
'status_code': 200,
'headers': {
'Content-Type': 'application/json',
},
'content': json.dumps([
{
"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",
}
]),
}
@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]
with HTTMock(*handlers):
trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger'))
trigger = GitLabBuildTrigger(trigger_obj, {
'build_source': 'foo/bar',
@ -12,208 +590,8 @@ def get_gitlab_trigger(dockerfile_path=''):
'username': 'knownuser'
})
trigger._get_authorized_client = get_mock_gitlab(with_nulls=False)
return trigger
client = gitlab.Gitlab('http://fakegitlab', oauth_token='foobar', timeout=20, api_version=4)
client.auth()
def adddeploykey_mock(project_id, name, public_key):
return {'id': 'foo'}
def addprojecthook_mock(project_id, webhook_url, push=False):
return {'id': 'foo'}
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 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 [
{
'username': 'knownuser',
'avatar_url': 'avatarurl',
}
]
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',
}
}
def getrawfile_mock(repo_id, branch_name, path):
if path == 'Dockerfile':
return 'hello world'
if path == 'somesubdir/Dockerfile':
return 'hi universe'
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

View file

@ -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

View file

@ -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,7 +25,7 @@ def test_list_build_subdirs(gitlab_trigger):
('unknownpath', None),
])
def test_load_dockerfile_contents(dockerfile_path, contents):
trigger = get_gitlab_trigger(dockerfile_path)
with get_gitlab_trigger(dockerfile_path=dockerfile_path) as trigger:
assert trigger.load_dockerfile_contents() == contents
@ -37,23 +38,47 @@ 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')
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()
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': '',
}]
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': 'someorg',
'url': 'https://bitbucket.org/someorg',
'title': u'someorg',
'url': u'http://gitlab.com/groups/someorg',
'score': 1,
'id': 'someorg',
'id': '2',
}
assert namespace_data == [expected]
@ -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

View file

@ -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__)

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -35,7 +35,7 @@
<img class="namespace-avatar" ng-src="{{ ::item.avatar_url }}" ng-if="::item.avatar_url">
<span class="anchor"
href="{{ ::item.url }}"
is-text-only="::!item.url">{{ ::item.id }}</span>
is-text-only="::!item.url">{{ ::item.title }}</span>
</script>
</cor-table-col>
<cor-table-col title="Importance"