From f6f93e9079ddd32b18e2c0b3059ba6cc2c085afa Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 19 Mar 2015 17:12:27 -0400 Subject: [PATCH] consolidate everything into one GitHub trigger --- endpoints/callbacks.py | 5 +- endpoints/trigger.py | 415 +++----------------------- initdb.py | 1 - static/js/services/trigger-service.js | 45 +-- test/data/test.db | Bin 294912 -> 294912 bytes util/ssh.py | 2 +- 6 files changed, 42 insertions(+), 426 deletions(-) diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index 506443028..cbe96f6e7 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -248,8 +248,6 @@ def github_oauth_attach(): @callback.route('/github/callback/trigger/', methods=['GET']) @callback.route('/github/callback/trigger//__new', methods=['GET']) -@callback.route('/github/callback/trigger//__git', methods=['GET']) -@callback.route('/github/callback/trigger//__git/__new', methods=['GET']) @route_show_if(features.GITHUB_BUILD) @require_session_login @parse_repository_name @@ -262,8 +260,7 @@ def attach_github_build_trigger(namespace, repository): msg = 'Invalid repository: %s/%s' % (namespace, repository) abort(404, message=msg) - service_name = 'github-git' if '__git' in request.url else 'github' - trigger = model.create_build_trigger(repo, service_name, token, current_user.db_user()) + trigger = model.create_build_trigger(repo, 'github', token, current_user.db_user()) # TODO(jschorr): Remove once the new layout is in place. admin_path = '%s/%s/%s' % (namespace, repository, 'admin') diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 1a36ef568..deba3fe56 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -169,34 +169,61 @@ class GithubBuildTrigger(BuildTrigger): new_build_source = config['build_source'] gh_client = self._get_client(auth_token) + # Find the GitHub repository. try: - to_add_webhook = gh_client.get_repo(new_build_source) + gh_repo = gh_client.get_repo(new_build_source) except UnknownObjectException: - msg = 'Unable to find GitHub repository for source: %s' - raise TriggerActivationException(msg % new_build_source) + msg = 'Unable to find GitHub repository for source: %s' % new_build_source + raise TriggerActivationException(msg) + # Add a deploy key to the GitHub repository. + try: + deploy_key = gh_repo.create_key('Quay.io Builder', config['public_key']) + config['deploy_key_id'] = deploy_key.id + except GithubException: + msg = 'Unable to add deploy key to repository: %s' % new_build_source + raise TriggerActivationException(msg) + + # Create a webhook config. webhook_config = { 'url': standard_webhook_url, 'content_type': 'json', } + # Add the webhook to the GitHub repository. try: - hook = to_add_webhook.create_hook('web', webhook_config) + hook = gh_repo.create_hook('web', webhook_config) config['hook_id'] = hook.id - config['master_branch'] = to_add_webhook.default_branch + config['master_branch'] = gh_repo.default_branch except GithubException: - msg = 'Unable to create webhook on repository: %s' - raise TriggerActivationException(msg % new_build_source) + msg = 'Unable to create webhook on repository: %s' % new_build_source + raise TriggerActivationException(msg) return config def deactivate(self, auth_token, config): gh_client = self._get_client(auth_token) + # Find the GitHub repository. try: repo = gh_client.get_repo(config['build_source']) - to_delete = repo.get_hook(config['hook_id']) - to_delete.delete() + except UnknownObjectException: + msg = 'Unable to find GitHub repository for source: %s' % config['build_source'] + raise TriggerDeactivationException(msg) + + # If the trigger uses a deploy key, remove it. + if config['deploy_key_id']: + try: + deploy_key = repo.get_key(config['deploy_key_id']) + deploy_key.delete() + except GithubException: + msg = 'Unable to remove deploy key: %s' % config['deploy_key_id'] + raise TriggerDeactivationException(msg) + + # Remove the webhook. + try: + hook = repo.get_hook(config['hook_id']) + hook.delete() except GithubException: msg = 'Unable to remove hook: %s' % config['hook_id'] raise TriggerDeactivationException(msg) @@ -283,8 +310,8 @@ class GithubBuildTrigger(BuildTrigger): source = config['build_source'] subdirectory = config.get('subdir', '') path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile' - gh_client = self._get_client(auth_token) + try: repo = gh_client.get_repo(source) master_branch = repo.default_branch or 'master' @@ -464,374 +491,6 @@ class GithubBuildTrigger(BuildTrigger): raise TriggerStartException(ghe.data['message']) - def list_field_values(self, auth_token, config, field_name): - if field_name == 'refs': - branches = self.list_field_values(auth_token, config, 'branch_name') - tags = self.list_field_values(auth_token, config, 'tag_name') - - return ([{'kind': 'branch', 'name': b} for b in branches] + - [{'kind': 'tag', 'name': tag} for tag in tags]) - - if field_name == 'tag_name': - gh_client = self._get_client(auth_token) - source = config['build_source'] - repo = gh_client.get_repo(source) - return [tag.name for tag in repo.get_tags()] - - if field_name == 'branch_name': - gh_client = self._get_client(auth_token) - source = config['build_source'] - repo = gh_client.get_repo(source) - branches = [branch.name for branch in repo.get_branches()] - - if not repo.default_branch in branches: - branches.insert(0, repo.default_branch) - - if branches[0] != repo.default_branch: - branches.remove(repo.default_branch) - branches.insert(0, repo.default_branch) - - return branches - - return None - -class GitHubBuildTrigger(BuildTrigger): - """ - BuildTrigger for GitHub that uses deploy keys and git. - """ - @staticmethod - def _get_client(auth_token): - return Github(auth_token, - base_url=github_trigger.api_endpoint(), - client_id=github_trigger.client_id(), - client_secret=github_trigger.client_secret()) - - @classmethod - def service_name(cls): - return 'github-git' - - def is_active(self, config): - return 'hook_id' in config and 'deploy_key_id' in config - - def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): - new_build_source = config['build_source'] - gh_client = self._get_client(auth_token) - - # Find the GitHub repository. - try: - gh_repo = gh_client.get_repo(new_build_source) - except UnknownObjectException: - msg = 'Unable to find GitHub repository for source: %s' % new_build_source - raise TriggerActivationException(msg) - - # Add a deploy key to the GitHub repository. - try: - deploy_key = gh_repo.create_key('Quay.io Builder', config['public_key']) - config['deploy_key_id'] = deploy_key.id - except GithubException: - msg = 'Unable to add deploy key to repository: %s' % new_build_source - raise TriggerActivationException(msg) - - # Create a webhook config. - webhook_config = { - 'url': standard_webhook_url, - 'content_type': 'json', - } - - # Add the webhook to the GitHub repository. - try: - hook = gh_repo.create_hook('web', webhook_config) - config['hook_id'] = hook.id - config['master_branch'] = gh_repo.default_branch - except GithubException: - msg = 'Unable to create webhook on repository: %s' % new_build_source - raise TriggerActivationException(msg) - - return config - - def deactivate(self, auth_token, config): - gh_client = self._get_client(auth_token) - - # Find the GitHub repository. - try: - repo = gh_client.get_repo(config['build_source']) - except UnknownObjectException: - msg = 'Unable to find GitHub repository for source: %s' % config['build_source'] - raise TriggerDeactivationException(msg) - - # Remove the deploy key. - try: - deploy_key = repo.get_key(config['deploy_key_id']) - deploy_key.delete() - except GithubException: - msg = 'Unable to remove deploy key to repository: %s' % config['build_source'] - raise TriggerActivationException(msg) - config.pop('deploy_key_id', None) - - # Remove the webhook. - try: - hook = repo.get_hook(config['hook_id']) - hook.delete() - except GithubException: - msg = 'Unable to remove hook: %s' % config['hook_id'] - raise TriggerDeactivationException(msg) - config.pop('hook_id', None) - - return config - - def list_build_sources(self, auth_token): - gh_client = self._get_client(auth_token) - usr = gh_client.get_user() - - personal = { - 'personal': True, - 'repos': [repo.full_name for repo in usr.get_repos()], - 'info': { - 'name': usr.login, - 'avatar_url': usr.avatar_url, - } - } - - repos_by_org = [personal] - - for org in usr.get_orgs(): - repo_list = [] - for repo in org.get_repos(type='member'): - repo_list.append(repo.full_name) - - repos_by_org.append({ - 'personal': False, - 'repos': repo_list, - 'info': { - 'name': org.name or org.login, - 'avatar_url': org.avatar_url - } - }) - - return repos_by_org - - @staticmethod - def matches_ref(ref, regex): - match_string = ref.split('/', 1)[1] - if not regex: - return False - - m = regex.match(match_string) - if not m: - return False - - return len(m.group(0)) == len(match_string) - - def list_build_subdirs(self, auth_token, config): - gh_client = self._get_client(auth_token) - source = config['build_source'] - - try: - repo = gh_client.get_repo(source) - - # Find the first matching branch. - branches = None - if 'branchtag_regex' in config: - try: - regex = re.compile(config['branchtag_regex']) - branches = [branch.name for branch in repo.get_branches() - if GitHubBuildTrigger.matches_ref('refs/heads/' + branch.name, regex)] - except: - pass - - branches = branches or [repo.default_branch or 'master'] - default_commit = repo.get_branch(branches[0]).commit - commit_tree = repo.get_git_tree(default_commit.sha, recursive=True) - - return [os.path.dirname(elem.path) for elem in commit_tree.tree - if (elem.type == u'blob' and - os.path.basename(elem.path) == u'Dockerfile')] - except GithubException as ge: - message = ge.data.get('message', 'Unable to list contents of repository: %s' % source) - if message == 'Branch not found': - raise EmptyRepositoryException() - - raise RepositoryReadException(message) - - def dockerfile_url(self, auth_token, config): - source = config['build_source'] - subdirectory = config.get('subdir', '') - path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile' - - gh_client = self._get_client(auth_token) - try: - repo = gh_client.get_repo(source) - master_branch = repo.default_branch or 'master' - return 'https://github.com/%s/blob/%s/%s' % (source, master_branch, path) - except GithubException: - return None - - def load_dockerfile_contents(self, auth_token, config): - gh_client = self._get_client(auth_token) - - source = config['build_source'] - subdirectory = config.get('subdir', '') - path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile' - - try: - repo = gh_client.get_repo(source) - file_info = repo.get_file_contents(path) - if file_info is None: - return None - - content = file_info.content - if file_info.encoding == 'base64': - content = base64.b64decode(content) - return content - - except GithubException as ge: - message = ge.data.get('message', 'Unable to read Dockerfile: %s' % source) - raise RepositoryReadException(message) - - @staticmethod - def _build_commit_info(repo, commit_sha): - try: - commit = repo.get_commit(commit_sha) - except GithubException: - logger.exception('Could not load data for commit') - return - - return { - 'url': commit.html_url, - 'message': commit.commit.message, - 'author': { - 'username': commit.author.login, - 'avatar_url': commit.author.avatar_url, - 'url': commit.author.html_url - }, - 'committer': { - 'username': commit.committer.login, - 'avatar_url': commit.committer.avatar_url, - 'url': commit.committer.html_url - }, - 'date': commit.last_modified - } - - @staticmethod - def _prepare_build(config, repo, commit_sha, build_name, ref): - # Prepare the download and upload URLs - archive_link = repo.get_archive_link('tarball', commit_sha) - download_archive = client.get(archive_link, stream=True) - - tarball_subdir = '' - with SpooledTemporaryFile(CHUNK_SIZE) as tarball: - for chunk in download_archive.iter_content(CHUNK_SIZE): - tarball.write(chunk) - - # Seek to position 0 to make tarfile happy - tarball.seek(0) - - # Pull out the name of the subdir that GitHub generated - with tarfile.open(fileobj=tarball) as archive: - tarball_subdir = archive.getnames()[0] - - # Seek to position 0 to make tarfile happy. - tarball.seek(0) - - entries = { - tarball_subdir + '/.git/HEAD': commit_sha, - tarball_subdir + '/.git/objects/': None, - tarball_subdir + '/.git/refs/': None - } - - appender = TarfileAppender(tarball, entries).get_stream() - dockerfile_id = user_files.store_file(appender, TARBALL_MIME) - - logger.debug('Successfully prepared job') - - # compute the tag(s) - branch = ref.split('/')[-1] - tags = {branch} - - if branch == repo.default_branch: - tags.add('latest') - - logger.debug('Pushing to tags: %s', tags) - - # compute the subdir - repo_subdir = config['subdir'] - joined_subdir = os.path.join(tarball_subdir, repo_subdir) - logger.debug('Final subdir: %s', joined_subdir) - - # compute the metadata - metadata = { - 'commit_sha': commit_sha, - 'ref': ref, - 'default_branch': repo.default_branch, - } - - # add the commit info. - commit_info = GitHubBuildTrigger._build_commit_info(repo, commit_sha) - if commit_info is not None: - metadata['commit_info'] = commit_info - - return dockerfile_id, list(tags), build_name, joined_subdir, metadata - - @staticmethod - def get_display_name(sha): - return sha[0:7] - - def handle_trigger_request(self, request, auth_token, config): - payload = request.get_json() - if not payload or payload.get('head_commit') is None: - raise SkipRequestException() - - if 'zen' in payload: - raise ValidationRequestException() - - logger.debug('Payload %s', payload) - ref = payload['ref'] - commit_sha = payload['head_commit']['id'] - commit_message = payload['head_commit'].get('message', '') - - if 'branchtag_regex' in config: - try: - regex = re.compile(config['branchtag_regex']) - except: - regex = re.compile('.*') - - if not GitHubBuildTrigger.matches_ref(ref, regex): - raise SkipRequestException() - - if should_skip_commit(commit_message): - raise SkipRequestException() - - short_sha = GitHubBuildTrigger.get_display_name(commit_sha) - - gh_client = self._get_client(auth_token) - - repo_full_name = '%s/%s' % (payload['repository']['owner']['name'], - payload['repository']['name']) - repo = gh_client.get_repo(repo_full_name) - - logger.debug('Github repo: %s', repo) - - return GitHubBuildTrigger._prepare_build(config, repo, commit_sha, - short_sha, ref) - - def manual_start(self, auth_token, config, run_parameters=None): - try: - source = config['build_source'] - run_parameters = run_parameters or {} - - gh_client = self._get_client(auth_token) - repo = gh_client.get_repo(source) - branch_name = run_parameters.get('branch_name') or repo.default_branch - branch = repo.get_branch(branch_name) - branch_sha = branch.commit.sha - short_sha = GitHubBuildTrigger.get_display_name(branch_sha) - ref = 'refs/heads/%s' % (branch_name) - - return self._prepare_build(config, repo, branch_sha, short_sha, ref) - except GithubException as ghe: - raise TriggerStartException(ghe.data['message']) - - def list_field_values(self, auth_token, config, field_name): if field_name == 'refs': branches = self.list_field_values(auth_token, config, 'branch_name') diff --git a/initdb.py b/initdb.py index 6f7ed7d20..f35b77111 100644 --- a/initdb.py +++ b/initdb.py @@ -195,7 +195,6 @@ def initialize_database(): LoginService.create(name='ldap') BuildTriggerService.create(name='github') - BuildTriggerService.create(name='github-git') AccessTokenKind.create(name='build-worker') AccessTokenKind.create(name='pushpull-token') diff --git a/static/js/services/trigger-service.js b/static/js/services/trigger-service.js index 426d5b4b6..169b8766b 100644 --- a/static/js/services/trigger-service.js +++ b/static/js/services/trigger-service.js @@ -8,45 +8,6 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K var triggerTypes = { 'github': { - 'description': function(config) { - var source = UtilService.textToSafeHtml(config['build_source']); - var desc = ' Push to Github Repository (Deprecated) '; - desc += '' + source + ''; - desc += '
Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']); - return desc; - }, - 'run_parameters': [ - { - 'title': 'Branch', - 'type': 'option', - 'name': 'branch_name' - } - ], - 'get_redirect_url': function(namespace, repository) { - var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' + - namespace + '/' + repository; - - var authorize_url = KeyService['githubTriggerAuthorizeUrl']; - var client_id = KeyService['githubTriggerClientId']; - - return authorize_url + 'client_id=' + client_id + - '&scope=repo,user:email&redirect_uri=' + redirect_uri; - }, - 'is_enabled': function() { - return Features.GITHUB_BUILD; - }, - 'icon': 'fa-github', - 'title': function() { - var isEnterprise = KeyService.isEnterprise('github-trigger'); - if (isEnterprise) { - return 'GitHub Enterprise Repository Push (API)'; - } - - return 'GitHub Repository Push (API)'; - } - }, - - 'github-git': { 'description': function(config) { var source = UtilService.textToSafeHtml(config['build_source']); var desc = ' Push to Github Repository '; @@ -63,7 +24,7 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K ], 'get_redirect_url': function(namespace, repository) { var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' + - namespace + '/' + repository + '/__git'; + namespace + '/' + repository; // TODO(jschorr): Remove once the new layout is in place. if (CookieService.get('quay.exp-new-layout') == 'true') { @@ -83,10 +44,10 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K 'title': function() { var isEnterprise = KeyService.isEnterprise('github-trigger'); if (isEnterprise) { - return 'GitHub Enterprise Repository Push (Deploy Key)'; + return 'GitHub Enterprise Repository Push'; } - return 'GitHub Repository Push (Deploy Key)'; + return 'GitHub Repository Push'; } } } diff --git a/test/data/test.db b/test/data/test.db index 832456ba0993591972fe69297a7befe70f24aeba..d673e22915673a5e98fa4a9459582cb544394243 100644 GIT binary patch delta 12695 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 diff --git a/util/ssh.py b/util/ssh.py index e4274a622..3641f4ec9 100644 --- a/util/ssh.py +++ b/util/ssh.py @@ -5,6 +5,6 @@ def generate_ssh_keypair(): Generates a new 2048 bit RSA public key in OpenSSH format and private key in PEM format. """ key = RSA.generate(2048) - public_key = key.publicKey().exportKey('OpenSSH') + public_key = key.publickey().exportKey('OpenSSH') private_key = key.exportKey('PEM') return (public_key, private_key)