diff --git a/buildman/jobutil/buildjob.py b/buildman/jobutil/buildjob.py index 6a2127c2d..02199898f 100644 --- a/buildman/jobutil/buildjob.py +++ b/buildman/jobutil/buildjob.py @@ -14,6 +14,7 @@ class BuildJobLoadException(Exception): """ Exception raised if a build job could not be instantiated for some reason. """ pass + class BuildJob(object): """ Represents a single in-progress build job. """ def __init__(self, job_item): @@ -21,6 +22,7 @@ class BuildJob(object): try: self.job_details = json.loads(job_item.body) + self.build_notifier = BuildJobNotifier(self.build_uuid) except ValueError: raise BuildJobLoadException( 'Could not parse build queue item config with ID %s' % self.job_details['build_uuid'] @@ -34,35 +36,7 @@ class BuildJob(object): return self.job_item.retries_remaining > 0 def send_notification(self, kind, error_message=None, image_id=None, manifest_digests=None): - tags = self.build_config.get('docker_tags', ['latest']) - event_data = { - 'build_id': self.repo_build.uuid, - 'build_name': self.repo_build.display_name, - 'docker_tags': tags, - 'trigger_id': self.repo_build.trigger.uuid, - 'trigger_kind': self.repo_build.trigger.service.name, - 'trigger_metadata': self.build_config.get('trigger_metadata', {}) - } - - if image_id is not None: - event_data['image_id'] = image_id - - if manifest_digests: - event_data['manifest_digests'] = manifest_digests - - if error_message is not None: - event_data['error_message'] = error_message - - # TODO(jzelinskie): remove when more endpoints have been converted to using - # interfaces - repo = AttrDict({ - 'namespace_name': self.repo_build.repository.namespace_user.username, - 'name': self.repo_build.repository.name, - }) - spawn_notification(repo, kind, event_data, - subpage='build/%s' % self.repo_build.uuid, - pathargs=['build', self.repo_build.uuid]) - + self.build_notifier.send_notification(kind, error_message, image_id, manifest_digests) @lru_cache(maxsize=1) def _load_repo_build(self): @@ -182,3 +156,61 @@ class BuildJob(object): return list(cached_tags)[0] return None + + +class BuildJobNotifier(object): + """ A class for sending notifications to a job that only relies on the build_uuid """ + + def __init__(self, build_uuid): + self.build_uuid = build_uuid + + @property + def repo_build(self): + return self._load_repo_build() + + @lru_cache(maxsize=1) + def _load_repo_build(self): + try: + return model.build.get_repository_build(self.build_uuid) + except model.InvalidRepositoryBuildException: + raise BuildJobLoadException( + 'Could not load repository build with ID %s' % self.build_uuid) + + @property + def build_config(self): + try: + return json.loads(self.repo_build.job_config) + except ValueError: + raise BuildJobLoadException( + 'Could not parse repository build job config with ID %s' % self.repo_build.uuid + ) + + def send_notification(self, kind, error_message=None, image_id=None, manifest_digests=None): + tags = self.build_config.get('docker_tags', ['latest']) + event_data = { + 'build_id': self.repo_build.uuid, + 'build_name': self.repo_build.display_name, + 'docker_tags': tags, + 'trigger_id': self.repo_build.trigger.uuid, + 'trigger_kind': self.repo_build.trigger.service.name, + 'trigger_metadata': self.build_config.get('trigger_metadata', {}) + } + + if image_id is not None: + event_data['image_id'] = image_id + + if manifest_digests: + event_data['manifest_digests'] = manifest_digests + + if error_message is not None: + event_data['error_message'] = error_message + + # TODO(jzelinskie): remove when more endpoints have been converted to using + # interfaces + repo = AttrDict({ + 'namespace_name': self.repo_build.repository.namespace_user.username, + 'name': self.repo_build.repository.name, + }) + spawn_notification(repo, kind, event_data, + subpage='build/%s' % self.repo_build.uuid, + pathargs=['build', self.repo_build.uuid]) \ No newline at end of file diff --git a/data/migrations/versions/94836b099894_create_new_notification_type.py b/data/migrations/versions/94836b099894_create_new_notification_type.py new file mode 100644 index 000000000..ad8f147e9 --- /dev/null +++ b/data/migrations/versions/94836b099894_create_new_notification_type.py @@ -0,0 +1,28 @@ +"""Create new notification type + +Revision ID: 94836b099894 +Revises: faf752bd2e0a +Create Date: 2016-11-30 10:29:51.519278 + +""" + +# revision identifiers, used by Alembic. +revision = '94836b099894' +down_revision = 'faf752bd2e0a' + +from alembic import op + + +def upgrade(tables): + op.bulk_insert(tables.externalnotificationevent, + [ + {'name': 'build_cancelled'}, + ]) + + +def downgrade(tables): + op.execute(tables + .externalnotificationevent + .delete() + .where(tables. + externalnotificationevent.c.name == op.inline_literal('build_cancelled'))) diff --git a/data/model/build.py b/data/model/build.py index 90b3d8bb1..1b7355af7 100644 --- a/data/model/build.py +++ b/data/model/build.py @@ -197,6 +197,7 @@ 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: @@ -210,6 +211,7 @@ def cancel_repository_build(build, build_queue): 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 diff --git a/endpoints/notificationevent.py b/endpoints/notificationevent.py index 036fb010f..a662522b1 100644 --- a/endpoints/notificationevent.py +++ b/endpoints/notificationevent.py @@ -358,3 +358,38 @@ class BuildFailureEvent(BaseBuildEvent): def get_summary(self, event_data, notification_data): return 'Build failure ' + _build_summary(event_data) + +class BuildCancelledEvent(BaseBuildEvent): + @classmethod + def event_name(cls): + return 'build_cancelled' + + def get_level(self, event_data, notification_data): + return 'info' + + def get_sample_data(self, notification): + build_uuid = 'fake-build-id' + + # TODO(jzelinskie): remove when more endpoints have been converted to using + # interfaces + repo = AttrDict({ + 'namespace_name': notification.repository.namespace_user.username, + 'name': notification.repository.name, + }) + return build_event_data(repo, { + 'build_id': build_uuid, + 'build_name': 'some-fake-build', + 'docker_tags': ['latest', 'foo', 'bar'], + 'trigger_id': '1245634', + 'trigger_kind': 'GitHub', + 'trigger_metadata': { + "default_branch": "master", + "ref": "refs/heads/somebranch", + "commit": "42d4a62c53350993ea41069e9f2cfdefb0df097d" + }, + 'image_id': '1245657346' + }, subpage='/build/%s' % build_uuid) + + def get_summary(self, event_data, notification_data): + return 'Build cancelled ' + _build_summary(event_data) + diff --git a/initdb.py b/initdb.py index 7c2e49bc7..447274082 100644 --- a/initdb.py +++ b/initdb.py @@ -359,6 +359,7 @@ def initialize_database(): ExternalNotificationEvent.create(name='build_queued') ExternalNotificationEvent.create(name='build_start') ExternalNotificationEvent.create(name='build_success') + ExternalNotificationEvent.create(name='build_cancelled') ExternalNotificationEvent.create(name='build_failure') ExternalNotificationEvent.create(name='vulnerability_found') @@ -374,6 +375,7 @@ def initialize_database(): NotificationKind.create(name='build_queued') NotificationKind.create(name='build_start') NotificationKind.create(name='build_success') + NotificationKind.create(name='build_cancelled') NotificationKind.create(name='build_failure') NotificationKind.create(name='vulnerability_found') NotificationKind.create(name='service_key_submitted') diff --git a/static/js/services/external-notification-data.js b/static/js/services/external-notification-data.js index 623f1445d..0c9fe0653 100644 --- a/static/js/services/external-notification-data.js +++ b/static/js/services/external-notification-data.js @@ -80,6 +80,22 @@ function(Config, Features, VulnerabilityService) { 'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)' } ] + }, + { + 'id': 'build_cancelled', + 'title': 'Docker Build Cancelled', + 'icon': 'fa-minus-circle', + 'fields': [ + { + 'name': 'ref-regex', + 'type': 'regex', + 'title': 'matching ref(s)', + 'help_text': 'An optional regular expression for matching the git branch or tag ' + + 'git ref. If left blank, the notification will fire for all builds.', + 'optional': true, + 'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)' + } + ] }]; for (var i = 0; i < buildEvents.length; ++i) { diff --git a/static/js/services/notification-service.js b/static/js/services/notification-service.js index 02526699d..546700ad4 100644 --- a/static/js/services/notification-service.js +++ b/static/js/services/notification-service.js @@ -123,6 +123,14 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P }, 'dismissable': true }, + 'build_cancelled': { + 'level': 'info', + 'message': 'A build was cancelled for repository {repository}', + 'page': function(metadata) { + return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id; + }, + 'dismissable': true + }, 'vulnerability_found': { 'level': function(metadata) { var priority = metadata['vulnerability']['priority']; diff --git a/test/data/test.db b/test/data/test.db index 13703972b..1d66476c3 100644 Binary files a/test/data/test.db and b/test/data/test.db differ diff --git a/test/test_notifications.py b/test/test_notifications.py index bf490c77b..329ed6d63 100644 --- a/test/test_notifications.py +++ b/test/test_notifications.py @@ -11,6 +11,7 @@ class TestCreate(unittest.TestCase): self.assertIsNotNone(NotificationEvent.get_event('build_success')) self.assertIsNotNone(NotificationEvent.get_event('build_failure')) self.assertIsNotNone(NotificationEvent.get_event('build_start')) + self.assertIsNotNone(NotificationEvent.get_event('build_cancelled')) self.assertIsNotNone(NotificationEvent.get_event('vulnerability_found'))