From d5c70878c5fcaeb8a0b524fc9be50f65edf5de01 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 29 Apr 2015 17:04:52 -0400 Subject: [PATCH] Get build preparation working for bitbucket and do a lot of code cleanup around this process across all the triggers. Note: tests are not yet updated. --- endpoints/api/build.py | 17 +- endpoints/api/trigger.py | 12 +- endpoints/building.py | 186 ++++++++++++++++++++ endpoints/common.py | 78 +-------- endpoints/trigger.py | 357 +++++++++++++++++++++++++-------------- endpoints/webhooks.py | 8 +- 6 files changed, 432 insertions(+), 226 deletions(-) create mode 100644 endpoints/building.py diff --git a/endpoints/api/build.py b/endpoints/api/build.py index f6a4b05fb..0a5319a5c 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -10,7 +10,7 @@ from endpoints.api import (RepositoryParamResource, parse_args, query_param, nic require_repo_read, require_repo_write, validate_json_request, ApiResource, internal_only, format_date, api, Unauthorized, NotFound, path_param, InvalidRequest, require_repo_admin) -from endpoints.common import start_build +from endpoints.building import start_build, PreparedBuild from endpoints.trigger import BuildTriggerHandler from data import model, database from auth.auth_context import get_authenticated_user @@ -191,8 +191,8 @@ class RepositoryBuildList(RepositoryParamResource): raise Unauthorized() # Check if the dockerfile resource has already been used. If so, then it - # can only be reused if the user has access to the repository for which it - # was used. + # can only be reused if the user has access to the repository in which the + # dockerfile was previously built. associated_repository = model.get_repository_for_resource(dockerfile_id) if associated_repository: if not ModifyRepositoryPermission(associated_repository.namespace_user.username, @@ -201,11 +201,16 @@ class RepositoryBuildList(RepositoryParamResource): # Start the build. repo = model.get_repository(namespace, repository) - display_name = user_files.get_file_checksum(dockerfile_id) - build_request = start_build(repo, dockerfile_id, tags, display_name, subdir, True, - pull_robot_name=pull_robot_name) + prepared = PreparedBuild() + prepared.build_name = user_files.get_file_checksum(dockerfile_id) + prepared.dockerfile_id = dockerfile_id + prepared.tags = tags + prepared.subdirectory = subdir + prepared.is_manual = True + prepared.metadata = {} + build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name) resp = build_status_view(build_request, can_write=True) repo_string = '%s/%s' % (namespace, repository) headers = { diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index dc7ae3bba..805ea08bc 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -12,7 +12,7 @@ from endpoints.api import (RepositoryParamResource, nickname, resource, require_ path_param) from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus, get_trigger_config) -from endpoints.common import start_build +from endpoints.building import start_build from endpoints.trigger import (BuildTriggerHandler, TriggerDeactivationException, TriggerActivationException, EmptyRepositoryException, RepositoryReadException, TriggerStartException) @@ -423,16 +423,12 @@ class ActivateBuildTrigger(RepositoryParamResource): raise InvalidRequest('Trigger is not active.') try: - run_parameters = request.get_json() - specs = handler.manual_start(run_parameters=run_parameters) - dockerfile_id, tags, name, subdir, metadata = specs - repo = model.get_repository(namespace, repository) pull_robot_name = model.get_pull_robot_name(trigger) - build_request = start_build(repo, dockerfile_id, tags, name, subdir, True, - trigger=trigger, pull_robot_name=pull_robot_name, - trigger_metadata=metadata) + 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: raise InvalidRequest(tse.message) diff --git a/endpoints/building.py b/endpoints/building.py new file mode 100644 index 000000000..6dc010cc9 --- /dev/null +++ b/endpoints/building.py @@ -0,0 +1,186 @@ +import logging +import json + +from app import app, dockerfile_build_queue +from data import model +from data.database import db +from auth.auth_context import get_authenticated_user +from endpoints.notificationhelper import spawn_notification +from flask import request + +logger = logging.getLogger(__name__) + +def start_build(repository, prepared_build, pull_robot_name=None): + host = app.config['SERVER_HOSTNAME'] + repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name) + + token = model.create_access_token(repository, 'write', kind='build-worker', + friendly_name='Repository Build Token') + logger.debug('Creating build %s with repo %s tags %s', + prepared_build.build_name, repo_path, prepared_build.tags) + + job_config = { + 'docker_tags': prepared_build.tags, + 'registry': host, + 'build_subdir': prepared_build.subdirectory, + 'trigger_metadata': prepared_build.metadata or {}, + 'is_manual': prepared_build.is_manual, + 'manual_user': get_authenticated_user().username if get_authenticated_user() else None + } + + with app.config['DB_TRANSACTION_FACTORY'](db): + build_request = model.create_repository_build(repository, token, job_config, + prepared_build.dockerfile_id, + prepared_build.build_name, + prepared_build.trigger, + pull_robot_name=pull_robot_name) + + json_data = json.dumps({ + 'build_uuid': build_request.uuid, + 'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None + }) + + queue_id = dockerfile_build_queue.put([repository.namespace_user.username, repository.name], + json_data, + retries_remaining=3) + + build_request.queue_id = queue_id + build_request.save() + + # Add the build to the repo's log and spawn the build_queued notification. + event_log_metadata = { + 'build_uuid': build_request.uuid, + 'docker_tags': prepared_build.tags, + 'repo': repository.name, + 'namespace': repository.namespace_user.username, + 'is_manual': prepared_build.is_manual, + 'manual_user': get_authenticated_user().username if get_authenticated_user() else None + } + + if prepared_build.trigger: + event_log_metadata['trigger_id'] = prepared_build.trigger.uuid + event_log_metadata['trigger_kind'] = prepared_build.trigger.service.name + + model.log_action('build_dockerfile', repository.namespace_user.username, ip=request.remote_addr, + metadata=event_log_metadata, repository=repository) + + spawn_notification(repository, 'build_queued', event_log_metadata, + subpage='build?current=%s' % build_request.uuid, + pathargs=['build', build_request.uuid]) + + return build_request + + +class PreparedBuild(object): + """ Class which holds all the information about a prepared build. The build queuing service + will use this result to actually invoke the build. + """ + def __init__(self, trigger=None): + self._dockerfile_id = None + self._tags = None + self._build_name = None + self._subdirectory = None + self._metadata = None + self._trigger = trigger + self._is_manual = None + + @staticmethod + def get_display_name(sha): + return sha[0:7] + + def tags_from_ref(self, ref, default_branch='master'): + branch = ref.split('/')[-1] + tags = {branch} + + if branch == default_branch: + tags.add('latest') + + self.tags = tags + + def name_from_sha(self, sha): + self.build_name = PreparedBuild.get_display_name(sha) + + @property + def is_manual(self): + if self._is_manual is None: + raise Exception('Property is_manual not set') + + return self._is_manual + + @is_manual.setter + def is_manual(self, value): + if self._is_manual is not None: + raise Exception('Property is_manual already set') + + self._is_manual = value + + @property + def trigger(self): + return self._trigger + + @property + def dockerfile_id(self): + return self._dockerfile_id + + @dockerfile_id.setter + def dockerfile_id(self, value): + if self._dockerfile_id: + raise Exception('Property dockerfile_id already set') + + self._dockerfile_id = value + + @property + def tags(self): + if not self._tags: + raise Exception('Missing property tags') + + return self._tags + + @tags.setter + def tags(self, value): + if self._tags: + raise Exception('Property tags already set') + + self._tags = list(value) + + @property + def build_name(self): + if not self._build_name: + raise Exception('Missing property build_name') + + return self._build_name + + @build_name.setter + def build_name(self, value): + if self._build_name: + raise Exception('Property build_name already set') + + self._build_name = value + + @property + def subdirectory(self): + if self._subdirectory is None: + raise Exception('Missing property subdirectory') + + return self._subdirectory + + @subdirectory.setter + def subdirectory(self, value): + if self._subdirectory: + raise Exception('Property subdirectory already set') + + self._subdirectory = value + + @property + def metadata(self): + if self._metadata is None: + raise Exception('Missing property metadata') + + return self._metadata + + @metadata.setter + def metadata(self, value): + if self._metadata: + raise Exception('Property metadata already set') + + self._metadata = value diff --git a/endpoints/common.py b/endpoints/common.py index 5f40e48ed..faeccee41 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -1,5 +1,4 @@ import logging -import urlparse import json import string import datetime @@ -14,18 +13,15 @@ from flask.ext.principal import identity_changed from random import SystemRandom from data import model -from data.database import db -from app import app, oauth_apps, dockerfile_build_queue, LoginWrappedDBUser +from app import app, oauth_apps, LoginWrappedDBUser from auth.permissions import QuayDeferredPermissionUser from auth import scopes -from auth.auth_context import get_authenticated_user from endpoints.api.discovery import swagger_route_data from werkzeug.routing import BaseConverter from functools import wraps from config import getFrontendVisibleConfig from external_libraries import get_external_javascript, get_external_css -from endpoints.notificationhelper import spawn_notification import features @@ -210,75 +206,3 @@ def check_repository_usage(user_or_org, plan_found): else: model.delete_notifications_by_kind(user_or_org, 'over_private_usage') - -def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, - trigger=None, pull_robot_name=None, trigger_metadata=None): - host = urlparse.urlparse(request.url).netloc - repo_path = '%s/%s/%s' % (host, repository.namespace_user.username, repository.name) - - token = model.create_access_token(repository, 'write', kind='build-worker', - friendly_name='Repository Build Token') - logger.debug('Creating build %s with repo %s tags %s and dockerfile_id %s', - build_name, repo_path, tags, dockerfile_id) - - job_config = { - 'docker_tags': tags, - 'registry': host, - 'build_subdir': subdir, - 'trigger_metadata': trigger_metadata or {}, - 'is_manual': manual, - 'manual_user': get_authenticated_user().username if get_authenticated_user() else None - } - - with app.config['DB_TRANSACTION_FACTORY'](db): - build_request = model.create_repository_build(repository, token, job_config, - dockerfile_id, build_name, - trigger, pull_robot_name=pull_robot_name) - - json_data = json.dumps({ - 'build_uuid': build_request.uuid, - 'pull_credentials': model.get_pull_credentials(pull_robot_name) if pull_robot_name else None - }) - - queue_id = dockerfile_build_queue.put([repository.namespace_user.username, repository.name], - json_data, - retries_remaining=3) - - build_request.queue_id = queue_id - build_request.save() - - # Add the build to the repo's log. - metadata = { - 'repo': repository.name, - 'namespace': repository.namespace_user.username, - 'fileid': dockerfile_id, - 'is_manual': manual, - 'manual_user': get_authenticated_user().username if get_authenticated_user() else None - } - - if trigger: - metadata['trigger_id'] = trigger.uuid - metadata['config'] = json.loads(trigger.config) - metadata['service'] = trigger.service.name - - model.log_action('build_dockerfile', repository.namespace_user.username, ip=request.remote_addr, - metadata=metadata, repository=repository) - - # Add notifications for the build queue. - logger.debug('Adding notifications for repository') - event_data = { - 'build_id': build_request.uuid, - 'build_name': build_name, - 'docker_tags': tags, - 'is_manual': manual, - 'manual_user': get_authenticated_user().username if get_authenticated_user() else None - } - - if trigger: - event_data['trigger_id'] = trigger.uuid - event_data['trigger_kind'] = trigger.service.name - - spawn_notification(repository, 'build_queued', event_data, - subpage='build?current=%s' % build_request.uuid, - pathargs=['build', build_request.uuid]) - return build_request diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 2882c494f..cb8d7622f 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -6,6 +6,7 @@ import base64 import re import json +from endpoints.building import PreparedBuild from github import Github, UnknownObjectException, GithubException from bitbucket import BitBucket from tempfile import SpooledTemporaryFile @@ -27,9 +28,6 @@ TARBALL_MIME = 'application/gzip' CHUNK_SIZE = 512 * 1024 -def should_skip_commit(message): - return '[skip build]' in message or '[build skip]' in message - class InvalidPayloadException(Exception): pass @@ -64,6 +62,42 @@ class TriggerProviderException(Exception): pass +def find_matching_branches(config, branches): + if 'branchtag_regex' in config: + try: + regex = re.compile(config['branchtag_regex']) + return [branch for branch in branches + if matches_ref('refs/heads/' + branch, regex)] + except: + pass + + return branches + +def raise_if_skipped(config, ref): + """ Raises a SkipRequestException if the given ref should be skipped. """ + if 'branchtag_regex' in config: + try: + regex = re.compile(config['branchtag_regex']) + except: + regex = re.compile('.*') + + if not matches_ref(ref, regex): + raise SkipRequestException() + +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 should_skip_commit(message): + return '[skip build]' in message or '[build skip]' in message + def raise_unsupported(): raise io.UnsupportedOperation @@ -113,8 +147,7 @@ class BuildTriggerHandler(object): def handle_trigger_request(self): """ - Transform the incoming request data into a set of actions. Returns a tuple - of usefiles resource id, docker tags, build name, and resource subdir. + Transform the incoming request data into a set of actions. Returns a PreparedBuild. """ raise NotImplementedError @@ -142,7 +175,7 @@ class BuildTriggerHandler(object): def manual_start(self, run_parameters=None): """ - Manually creates a repository build for this trigger. + Manually creates a repository build for this trigger. Returns a PreparedBuild. """ raise NotImplementedError @@ -166,7 +199,7 @@ class BuildTriggerHandler(object): if subc.service_name() == trigger.service.name: return subc(trigger, override_config) - raise InvalidServiceException('Unable to find service: %s' % service) + raise InvalidServiceException('Unable to find service: %s' % trigger.service.name) def put_config_key(self, key, value): """ Updates a config key in the trigger, saving it to the DB. """ @@ -272,7 +305,6 @@ class BitbucketBuildTrigger(BuildTriggerHandler): config['hook_id'] = data['id'] return config, {'private_key': private_key} - def deactivate(self): config = self.config repository = self._get_repository_client() @@ -294,7 +326,6 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return config - def list_build_sources(self): bitbucket_client = self._get_authorized_client() (result, data, err_msg) = bitbucket_client.get_visible_repositories() @@ -321,12 +352,16 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return namespaces.values() def list_build_subdirs(self): + config = self.config repository = self._get_repository_client() - (result, data, err_msg) = repository.get_path_contents('', revision='master') + + # Find the first matching branch. + repo_branches = self.list_field_values('branch_name') or [] + branches = find_matching_branches(config, repo_branches) + (result, data, err_msg) = repository.get_path_contents('', revision=branches[0]) if not result: raise RepositoryReadException(err_msg) - files = set([f['path'] for f in data['files']]) if 'Dockerfile' in files: return ['/'] @@ -392,29 +427,114 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return None - - def handle_trigger_request(self, request): - return - - - def manual_start(self, run_parameters=None): + def _prepare_build(self, commit_sha, ref, is_manual): config = self.config repository = self._get_repository_client() - source = config['build_source'] - run_parameters = run_parameters or {} - - # Lookup the branch to build. - master_branch = 'master' + # Lookup the default branch associated with the repository. We use this when building + # the tags. + default_branch = '' (result, data, _) = repository.get_main_branch() if result: - master_branch = data['name'] + default_branch = data['name'] - branch_name = run_parameters.get('branch_name') or master_branch + # Lookup the commit sha. + (result, data, _) = repository.changesets().get(commit_sha) + if not result: + raise TriggerStartException('Could not lookup commit SHA') - # Find the SHA for the branch. - # TODO - return None + namespace = repository.namespace + name = repository.repository_name + + commit_info = { + 'url': 'https://bitbucket.org/%s/%s/commits/%s' % (namespace, name, commit_sha), + 'message': data['message'], + 'date': data['timestamp'] + } + + # Try to lookup the author by email address. The raw_author field (if it exists) is returned + # in the form: "Joseph Schorr " + if data.get('raw_author'): + match = re.compile(r'.*<(.+)>').match(data['raw_author']) + if match: + email_address = match.group(1) + bitbucket_client = self._get_authorized_client() + (result, data, _) = bitbucket_client.accounts().get_profile(email_address) + if result: + commit_info['author'] = { + 'username': data['user']['username'], + 'url': 'https://bitbucket.org/%s/' % data['user']['username'], + 'avatar_url': data['user']['avatar'] + } + + metadata = { + 'commit_sha': commit_sha, + 'ref': ref, + 'default_branch': default_branch, + 'git_url': 'git@bitbucket.org:%s/%s.git' % (namespace, name), + 'commit_info': commit_info + } + + prepared = PreparedBuild(self.trigger) + prepared.tags_from_ref(ref, default_branch) + prepared.name_from_sha(commit_sha) + prepared.subdirectory = config['subdir'] + prepared.metadata = metadata + prepared.is_manual = is_manual + + return prepared + + + def handle_trigger_request(self, request): + # Parse the JSON payload. + payload_json = request.form.get('payload') + if not payload_json: + raise SkipRequestException() + + try: + payload = json.loads(payload_json) + except ValueError: + raise SkipRequestException() + + logger.debug('BitBucket trigger payload %s', payload) + + # Make sure we have a commit in the payload. + if not payload.get('commits'): + raise SkipRequestException() + + # Check if this build should be skipped by commit message. + commit = payload['commits'][0] + commit_message = commit['message'] + if should_skip_commit(commit_message): + raise SkipRequestException() + + # Check to see if this build should be skipped by ref. + ref = 'refs/heads/' + commit['branch'] if commit.get('branch') else 'refs/tags/' + commit['tag'] + raise_if_skipped(self.config, ref) + + commit_sha = commit['node'] + return self._prepare_build(commit_sha, ref, False) + + + def manual_start(self, run_parameters=None): + run_parameters = run_parameters or {} + repository = self._get_repository_client() + + # Find the branch to build. + branch_name = run_parameters.get('branch_name') + (result, data, _) = repository.get_main_branch() + if result: + branch_name = data['name'] or branch_name + + # Lookup the commit SHA for the branch. + (result, data, _) = repository.get_branches() + if not result or not branch_name in data: + raise TriggerStartException('Could not find branch commit SHA') + + commit_sha = data[branch_name]['node'] + ref = 'refs/heads/%s' % (branch_name) + + return self._prepare_build(commit_sha, ref, True) class GithubBuildTrigger(BuildTriggerHandler): @@ -543,18 +663,6 @@ class GithubBuildTrigger(BuildTriggerHandler): 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): config = self.config gh_client = self._get_client() @@ -564,15 +672,8 @@ class GithubBuildTrigger(BuildTriggerHandler): 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 - + repo_branches = self.list_field_values('branch_name') or [] + branches = find_matching_branches(config, repo_branches) 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) @@ -691,37 +792,31 @@ class GithubBuildTrigger(BuildTriggerHandler): return tarball_subdir, dockerfile_id - @staticmethod - def _prepare_build(trigger, config, repo, commit_sha, build_name, ref, git_url): - repo_subdir = config['subdir'] - joined_subdir = repo_subdir - dockerfile_id = None + def _prepare_build(self, repo, ref, commit_sha, is_manual): + config = self.config + prepared = PreparedBuild(self.trigger) - if trigger.private_key is None: - # If the trigger isn't using git, prepare the buildpack. + # If the trigger isn't using git, prepare the buildpack. + if self.trigger.private_key is None: tarball_subdir, dockerfile_id = GithubBuildTrigger._prepare_tarball(repo, commit_sha) - logger.debug('Successfully prepared job') - # Join provided subdir with the tarball subdir. - joined_subdir = os.path.join(tarball_subdir, repo_subdir) + prepared.subdirectory = os.path.join(tarball_subdir, config['subdir']) + prepared.dockerfile_id = dockerfile_id + else: + prepared.subdirectory = config['subdir'] - logger.debug('Final subdir: %s', joined_subdir) + # Set the name. + prepared.name_from_sha(commit_sha) - # compute the tag(s) - branch = ref.split('/')[-1] - tags = {branch} + # Set the tag(s). + prepared.tags_from_ref(ref, repo.default_branch) - if branch == repo.default_branch: - tags.add('latest') - - logger.debug('Pushing to tags: %s', tags) - - # compute the metadata + # Build and set the metadata. metadata = { 'commit_sha': commit_sha, 'ref': ref, 'default_branch': repo.default_branch, - 'git_url': git_url, + 'git_url': repo.git_url, } # add the commit info. @@ -729,71 +824,63 @@ class GithubBuildTrigger(BuildTriggerHandler): if commit_info is not None: metadata['commit_info'] = commit_info - return dockerfile_id, list(tags), build_name, joined_subdir, metadata + prepared.metadata = metadata + prepared.is_manual = is_manual + return prepared - @staticmethod - def get_display_name(sha): - return sha[0:7] def handle_trigger_request(self, request): + # Check the payload to see if we should skip it based on the lack of a head_commit. payload = request.get_json() if not payload or payload.get('head_commit') is None: raise SkipRequestException() + # This is for GitHub's probing/testing. if 'zen' in payload: raise ValidationRequestException() - logger.debug('Payload %s', payload) + logger.debug('GitHub trigger payload %s', payload) + ref = payload['ref'] commit_sha = payload['head_commit']['id'] commit_message = payload['head_commit'].get('message', '') - git_url = payload['repository']['git_url'] - - config = self.config - 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() + # Check if this build should be skipped by commit message. if should_skip_commit(commit_message): raise SkipRequestException() - short_sha = GithubBuildTrigger.get_display_name(commit_sha) + # Check to see if this build should be skipped by ref. + raise_if_skipped(self.config, ref) - gh_client = self._get_client() - - 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(self.trigger, config, repo, commit_sha, short_sha, - ref, git_url) - - def manual_start(self, run_parameters=None): - config = self.config try: - source = config['build_source'] - run_parameters = run_parameters or {} + repo_full_name = '%s/%s' % (payload['repository']['owner']['name'], + payload['repository']['name']) gh_client = self._get_client() - 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) - git_url = repo.git_url + repo = gh_client.get_repo(repo_full_name) - return self._prepare_build(self.trigger, config, repo, branch_sha, short_sha, ref, git_url) + return self._prepare_build(repo, ref, commit_sha, False) except GithubException as ghe: raise TriggerStartException(ghe.data['message']) + def manual_start(self, run_parameters=None): + config = self.config + source = config['build_source'] + run_parameters = run_parameters or {} + + try: + gh_client = self._get_client() + + # Lookup the branch and its associated current SHA. + repo = gh_client.get_repo(source) + branch_name = run_parameters.get('branch_name') or repo.default_branch + branch = repo.get_branch(branch_name) + commit_sha = branch.commit.sha + ref = 'refs/heads/%s' % (branch_name) + + return self._prepare_build(repo, ref, commit_sha, True) + except GithubException as ghe: + raise TriggerStartException(ghe.data['message']) def list_field_values(self, field_name): if field_name == 'refs': @@ -922,24 +1009,50 @@ class CustomBuildTrigger(BuildTriggerHandler): return metadata def handle_trigger_request(self, request): + # Skip if there is no payload. payload = request.get_json() if not payload: raise SkipRequestException() logger.debug('Payload %s', payload) + + # Skip if the commit message matches. metadata = self._metadata_from_payload(payload) + if should_skip_commit(metadata.get('commit_info', {}).get('message', '')): + raise SkipRequestException() # The build source is the canonical git URL used to clone. config = self.config metadata['git_url'] = config['build_source'] - branch = metadata['ref'].split('/')[-1] - tags = {branch} + prepared = PreparedBuild(self.trigger) + prepared.tags_from_ref(metadata['ref']) + prepared.name_from_sha(metadata['commit_sha']) + prepared.subdirectory = config['subdir'] + prepared.metadata = metadata - build_name = metadata['commit_sha'][:6] - dockerfile_id = None + return prepared - return dockerfile_id, tags, build_name, config['subdir'], metadata + def manual_start(self, run_parameters=None): + # commit_sha is the only required parameter + commit_sha = run_parameters.get('commit_sha') + if commit_sha is None: + raise TriggerStartException('missing required parameter') + + config = self.config + metadata = { + 'commit_sha': commit_sha, + 'git_url': config['build_source'], + } + + prepared = PreparedBuild(self.trigger) + prepared.tags = [commit_sha] + prepared.name_from_sha(commit_sha) + prepared.subdirectory = config['subdir'] + prepared.metadata = metadata + prepared.is_manual = True + + return prepared def activate(self, standard_webhook_url): config = self.config @@ -962,19 +1075,3 @@ class CustomBuildTrigger(BuildTriggerHandler): config.pop('credentials', None) self.config = config return config - - def manual_start(self, run_parameters=None): - # commit_sha is the only required parameter - if 'commit_sha' not in run_parameters: - raise TriggerStartException('missing required parameter') - - config = self.config - dockerfile_id = None - tags = {run_parameters['commit_sha']} - build_name = run_parameters['commit_sha'] - metadata = { - 'commit_sha': run_parameters['commit_sha'], - 'git_url': config['build_source'], - } - - return dockerfile_id, list(tags), build_name, config['subdir'], metadata diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index 8804e8ef1..913b00fb0 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -11,7 +11,7 @@ from util.useremails import send_invoice_email, send_subscription_change, send_p from util.http import abort from endpoints.trigger import (BuildTriggerHandler, ValidationRequestException, SkipRequestException, InvalidPayloadException) -from endpoints.common import start_build +from endpoints.building import start_build logger = logging.getLogger(__name__) @@ -87,8 +87,7 @@ def build_trigger_webhook(trigger_uuid, **kwargs): logger.debug('Passing webhook request to handler %s', handler) try: - specs = handler.handle_trigger_request(request) - dockerfile_id, tags, name, subdir, metadata = specs + prepared = handler.handle_trigger_request(request) except ValidationRequestException: # This was just a validation request, we don't need to build anything return make_response('Okay') @@ -101,8 +100,7 @@ def build_trigger_webhook(trigger_uuid, **kwargs): pull_robot_name = model.get_pull_robot_name(trigger) repo = model.get_repository(namespace, repository) - start_build(repo, dockerfile_id, tags, name, subdir, False, trigger, - pull_robot_name=pull_robot_name, trigger_metadata=metadata) + start_build(repo, prepared, pull_robot_name=pull_robot_name) return make_response('Okay')