import logging import io import os.path import zipfile from github import Github, UnknownObjectException, GithubException from tempfile import SpooledTemporaryFile from app import app user_files = app.config['USERFILES'] client = app.config['HTTPCLIENT'] logger = logging.getLogger(__name__) ZIPBALL = 'application/zip' CHUNK_SIZE = 512 * 1024 class BuildArchiveException(Exception): pass class InvalidServiceException(Exception): pass class TriggerActivationException(Exception): pass class TriggerDeactivationException(Exception): pass class ValidationRequestException(Exception): pass class EmptyRepositoryException(Exception): pass class BuildTrigger(object): def __init__(self): pass def list_build_sources(self, auth_token): """ Take the auth information for the specific trigger type and load the list of build sources(repositories). """ raise NotImplementedError def list_build_subdirs(self, auth_token, config): """ Take the auth information and the specified config so far and list all of the possible subdirs containing dockerfiles. """ raise NotImplementedError def handle_trigger_request(self, request, auth_token, config): """ Transform the incoming request data into a set of actions. Returns a tuple of usefiles resource id, docker tags, build name, and resource subdir. """ raise NotImplementedError def is_active(self, config): """ Returns True if the current build trigger is active. Inactive means further setup is needed. """ raise NotImplementedError def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): """ Activates the trigger for the service, with the given new configuration. Returns new configuration that should be stored if successful. """ raise NotImplementedError def deactivate(self, auth_token, config): """ Deactivates the trigger for the service, removing any hooks installed in the remote service. Returns the new config that should be stored if this trigger is going to be re-activated. """ raise NotImplementedError def manual_start(self, auth_token, config): """ Manually creates a repository build for this trigger. """ raise NotImplementedError @classmethod def service_name(cls): """ Particular service implemented by subclasses. """ raise NotImplementedError @classmethod def get_trigger_for_service(cls, service): for subc in cls.__subclasses__(): if subc.service_name() == service: return subc() raise InvalidServiceException('Unable to find service: %s' % service) def raise_unsupported(): raise io.UnsupportedOperation class GithubBuildTrigger(BuildTrigger): @staticmethod def _get_client(auth_token): return Github(auth_token, client_id=app.config['GITHUB_CLIENT_ID'], client_secret=app.config['GITHUB_CLIENT_SECRET']) @classmethod def service_name(cls): return 'github' def is_active(self, config): return 'hook_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) try: to_add_webhook = gh_client.get_repo(new_build_source) except UnknownObjectException: msg = 'Unable to find GitHub repository for source: %s' raise TriggerActivationException(msg % new_build_source) webhook_config = { 'url': standard_webhook_url, 'content_type': 'json', } try: hook = to_add_webhook.create_hook('web', webhook_config) config['hook_id'] = hook.id config['master_branch'] = to_add_webhook.master_branch except GithubException: msg = 'Unable to create webhook on repository: %s' raise TriggerActivationException(msg % new_build_source) return config def deactivate(self, auth_token, config): gh_client = self._get_client(auth_token) try: repo = gh_client.get_repo(config['build_source']) to_delete = repo.get_hook(config['hook_id']) to_delete.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, 'avatar_url': org.avatar_url } }) return repos_by_org 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) default_commit = repo.get_branch(repo.master_branch).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: msg = 'Unable to list contents of repository: %s' % source raise EmptyRepositoryException(msg) @staticmethod def _prepare_build(config, repo, commit_sha, build_name, ref): # Prepare the download and upload URLs archive_link = repo.get_archive_link('zipball', commit_sha) download_archive = client.get(archive_link, stream=True) zipball_subdir = '' with SpooledTemporaryFile(CHUNK_SIZE) as zipball: for chunk in download_archive.iter_content(CHUNK_SIZE): zipball.write(chunk) # Pull out the name of the subdir that GitHub generated with zipfile.ZipFile(zipball) as archive: zipball_subdir = archive.namelist()[0] dockerfile_id = user_files.store_file(zipball, ZIPBALL) logger.debug('Successfully prepared job') # compute the tag(s) branch = ref.split('/')[-1] tags = {branch} if branch == repo.master_branch: tags.add('latest') logger.debug('Pushing to tags: %s' % tags) # compute the subdir repo_subdir = config['subdir'] joined_subdir = os.path.join(zipball_subdir, repo_subdir) logger.debug('Final subdir: %s' % joined_subdir) return dockerfile_id, list(tags), build_name, joined_subdir @staticmethod def get_display_name(sha): return sha[0:7] def handle_trigger_request(self, request, auth_token, config): payload = request.get_json() if 'zen' in payload: raise ValidationRequestException() logger.debug('Payload %s', payload) ref = payload['ref'] commit_sha = payload['head_commit']['id'] 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): source = config['build_source'] subdir = config['subdir'] gh_client = self._get_client(auth_token) repo = gh_client.get_repo(source) master = repo.get_branch(repo.master_branch) master_sha = master.commit.sha short_sha = GithubBuildTrigger.get_display_name(master_sha) ref = 'refs/heads/%s' % repo.master_branch return self._prepare_build(config, repo, master_sha, short_sha, ref)