import json from peewee import JOIN_LEFT_OUTER from datetime import timedelta, datetime from data.database import (BuildTriggerService, RepositoryBuildTrigger, Repository, Namespace, User, RepositoryBuild, BUILD_PHASE, db_for_update, db_random_func) from data.model import (InvalidBuildTriggerException, InvalidRepositoryBuildException, db_transaction, user as user_model) PRESUMED_DEAD_BUILD_AGE = timedelta(days=15) PHASES_NOT_ALLOWED_TO_CANCEL_FROM = (BUILD_PHASE.PUSHING, BUILD_PHASE.COMPLETE, BUILD_PHASE.ERROR, BUILD_PHASE.INTERNAL_ERROR) ARCHIVABLE_BUILD_PHASES = [BUILD_PHASE.COMPLETE, BUILD_PHASE.ERROR, BUILD_PHASE.CANCELLED] def update_build_trigger(trigger, config, auth_token=None): trigger.config = json.dumps(config or {}) if auth_token is not None: trigger.auth_token = auth_token trigger.save() def create_build_trigger(repo, service_name, auth_token, user, pull_robot=None, config=None): config = config or {} service = BuildTriggerService.get(name=service_name) trigger = RepositoryBuildTrigger.create(repository=repo, service=service, auth_token=auth_token, connected_user=user, pull_robot=pull_robot, config=json.dumps(config)) return trigger def get_build_trigger(trigger_uuid): try: return (RepositoryBuildTrigger .select(RepositoryBuildTrigger, BuildTriggerService, Repository, Namespace) .join(BuildTriggerService) .switch(RepositoryBuildTrigger) .join(Repository) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .switch(RepositoryBuildTrigger) .join(User) .where(RepositoryBuildTrigger.uuid == trigger_uuid) .get()) except RepositoryBuildTrigger.DoesNotExist: msg = 'No build trigger with uuid: %s' % trigger_uuid raise InvalidBuildTriggerException(msg) def list_build_triggers(namespace_name, repository_name): return (RepositoryBuildTrigger .select(RepositoryBuildTrigger, BuildTriggerService, Repository) .join(BuildTriggerService) .switch(RepositoryBuildTrigger) .join(Repository) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .where(Namespace.username == namespace_name, Repository.name == repository_name)) def list_trigger_builds(namespace_name, repository_name, trigger_uuid, limit): return (list_repository_builds(namespace_name, repository_name, limit) .where(RepositoryBuildTrigger.uuid == trigger_uuid)) def get_repository_for_resource(resource_key): try: return (Repository .select(Repository, Namespace) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .switch(Repository) .join(RepositoryBuild) .where(RepositoryBuild.resource_key == resource_key) .get()) except Repository.DoesNotExist: return None def _get_build_base_query(): return (RepositoryBuild .select(RepositoryBuild, RepositoryBuildTrigger, BuildTriggerService, Repository, Namespace, User) .join(Repository) .join(Namespace, on=(Repository.namespace_user == Namespace.id)) .switch(RepositoryBuild) .join(User, JOIN_LEFT_OUTER) .switch(RepositoryBuild) .join(RepositoryBuildTrigger, JOIN_LEFT_OUTER) .join(BuildTriggerService, JOIN_LEFT_OUTER) .order_by(RepositoryBuild.started.desc())) def get_repository_build(build_uuid): try: return _get_build_base_query().where(RepositoryBuild.uuid == build_uuid).get() except RepositoryBuild.DoesNotExist: msg = 'Unable to locate a build by id: %s' % build_uuid raise InvalidRepositoryBuildException(msg) def list_repository_builds(namespace_name, repository_name, limit, include_inactive=True, since=None): query = (_get_build_base_query() .where(Repository.name == repository_name, Namespace.username == namespace_name) .limit(limit)) if since is not None: query = query.where(RepositoryBuild.started >= since) if not include_inactive: query = query.where(RepositoryBuild.phase != BUILD_PHASE.ERROR, RepositoryBuild.phase != BUILD_PHASE.COMPLETE) return query def get_recent_repository_build(namespace_name, repository_name): query = list_repository_builds(namespace_name, repository_name, 1) try: return query.get() except RepositoryBuild.DoesNotExist: return None def create_repository_build(repo, access_token, job_config_obj, dockerfile_id, display_name, trigger=None, pull_robot_name=None): pull_robot = None if pull_robot_name: pull_robot = user_model.lookup_robot(pull_robot_name) return RepositoryBuild.create(repository=repo, access_token=access_token, job_config=json.dumps(job_config_obj), display_name=display_name, trigger=trigger, resource_key=dockerfile_id, pull_robot=pull_robot) def get_pull_robot_name(trigger): if not trigger.pull_robot: return None return trigger.pull_robot.username def _get_build_row_for_update(build_uuid): return db_for_update(RepositoryBuild.select().where(RepositoryBuild.uuid == build_uuid)).get() def update_phase(build_uuid, phase): """ A function to change the phase of a build """ try: build = _get_build_row_for_update(build_uuid) except RepositoryBuild.DoesNotExist: return False # Can't update a cancelled build if build.phase == BUILD_PHASE.CANCELLED: return False build.phase = phase build.save() return True def create_cancel_build_in_queue(build, build_queue): """ A function to cancel a build before it leaves the queue """ def cancel_build(): cancelled = False if build.queue_id is not None: cancelled = build_queue.cancel(build.queue_id) if build.phase != BUILD_PHASE.WAITING: return False return cancelled return cancel_build def create_cancel_build_in_manager(build, build_canceller): """ A function to cancel the build before it starts to push """ def cancel_build(): if build.phase in PHASES_NOT_ALLOWED_TO_CANCEL_FROM: return False return build_canceller.try_cancel_build(build.uuid) return cancel_build def cancel_repository_build(build, build_queue): """ This tries to cancel the build returns true if request is successful false if it can't be cancelled """ with db_transaction(): from app import build_canceller from buildman.jobutil.buildjob import BuildJobNotifier # Reload the build for update. # We are loading the build for update so checks should be as quick as possible. try: build = _get_build_row_for_update(build.uuid) except RepositoryBuild.DoesNotExist: return False cancel_builds = [create_cancel_build_in_queue(build, build_queue), create_cancel_build_in_manager(build, build_canceller), ] original_phase = build.phase for cancelled in cancel_builds: if cancelled(): build.phase = BUILD_PHASE.CANCELLED BuildJobNotifier(build.uuid).send_notification("build_cancelled") build.save() return True build.phase = original_phase build.save() return False def get_archivable_build(): presumed_dead_date = datetime.utcnow() - PRESUMED_DEAD_BUILD_AGE candidates = (RepositoryBuild .select(RepositoryBuild.id) .where((RepositoryBuild.phase << ARCHIVABLE_BUILD_PHASES) | (RepositoryBuild.started < presumed_dead_date), RepositoryBuild.logs_archived == False) .limit(50) .alias('candidates')) try: found_id = (RepositoryBuild .select(candidates.c.id) .from_(candidates) .order_by(db_random_func()) .get()) return RepositoryBuild.get(id=found_id) except RepositoryBuild.DoesNotExist: return None