diff --git a/endpoints/notificationevent.py b/endpoints/notificationevent.py
index f1bf0a09d..dc0491e27 100644
--- a/endpoints/notificationevent.py
+++ b/endpoints/notificationevent.py
@@ -1,6 +1,7 @@
import logging
import time
import json
+import re
from datetime import datetime
from notificationhelper import build_event_data
@@ -138,7 +139,28 @@ class VulnerabilityFoundEvent(NotificationEvent):
', '.join(event_data['tags']))
-class BuildQueueEvent(NotificationEvent):
+class BaseBuildEvent(NotificationEvent):
+ def should_perform(self, event_data, notification_data):
+ event_config = json.loads(notification_data.event_config_json)
+ ref_regex = event_config.get('ref-regex') or None
+ if ref_regex is None:
+ return True
+
+ # Lookup the ref. If none, this is a non-git build and we should not fire the event.
+ ref = event_data.get('trigger_metadata', {}).get('ref', None)
+ if ref is None:
+ return False
+
+ # Try parsing the regex string as a regular expression. If we fail, we fail to fire
+ # the event.
+ try:
+ return bool(re.compile(str(ref_regex)).match(ref))
+ except Exception:
+ logger.warning('Regular expression error for build event filter: %s', ref_regex)
+ return False
+
+
+class BuildQueueEvent(BaseBuildEvent):
@classmethod
def event_name(cls):
return 'build_queued'
@@ -177,7 +199,7 @@ class BuildQueueEvent(NotificationEvent):
return 'Build queued ' + _build_summary(event_data)
-class BuildStartEvent(NotificationEvent):
+class BuildStartEvent(BaseBuildEvent):
@classmethod
def event_name(cls):
return 'build_start'
@@ -205,7 +227,7 @@ class BuildStartEvent(NotificationEvent):
return 'Build started ' + _build_summary(event_data)
-class BuildSuccessEvent(NotificationEvent):
+class BuildSuccessEvent(BaseBuildEvent):
@classmethod
def event_name(cls):
return 'build_success'
@@ -234,7 +256,7 @@ class BuildSuccessEvent(NotificationEvent):
return 'Build succeeded ' + _build_summary(event_data)
-class BuildFailureEvent(NotificationEvent):
+class BuildFailureEvent(BaseBuildEvent):
@classmethod
def event_name(cls):
return 'build_failure'
diff --git a/static/css/directives/ui/create-external-notification-dialog.css b/static/css/directives/ui/create-external-notification-dialog.css
deleted file mode 100644
index 12394955c..000000000
--- a/static/css/directives/ui/create-external-notification-dialog.css
+++ /dev/null
@@ -1,20 +0,0 @@
-#createNotificationModal .dropdown-select {
- margin: 0px;
-}
-
-#createNotificationModal .options-table {
- width: 100%;
- margin-bottom: 10px;
-}
-
-#createNotificationModal .options-table td {
- padding-bottom: 6px;
-}
-
-#createNotificationModal .options-table td.name {
- width: 160px;
-}
-
-#createNotificationModal .options-table-wrapper {
- padding: 10px;
-}
\ No newline at end of file
diff --git a/static/css/directives/ui/create-external-notification.css b/static/css/directives/ui/create-external-notification.css
new file mode 100644
index 000000000..133cced84
--- /dev/null
+++ b/static/css/directives/ui/create-external-notification.css
@@ -0,0 +1,69 @@
+.create-external-notification-element {
+ padding: 10px;
+}
+
+.create-external-notification-element .dropdown-select {
+ margin: 0px;
+}
+
+.create-external-notification-element .button-bar {
+ margin-top: 20px;
+ padding: 20px;
+ border-top: 1px solid #eee;
+ padding-bottom: 8px;
+ padding-left: 6px;
+}
+
+.create-external-notification-element .options-table .section-header {
+ font-size: 18px;
+ padding: 6px;
+ padding-bottom: 2px;
+}
+
+.create-external-notification-element .options-table {
+ width: 100%;
+}
+
+.create-external-notification-element .options-table td {
+ padding: 10px;
+}
+
+.create-external-notification-element .options-table td.name {
+ padding-left: 20px;
+ width: 1px;
+ white-space: nowrap;
+ vertical-align: top;
+ padding-top: 16px;
+}
+
+.create-external-notification-element .options-table td.value {
+ padding-left: 20px;
+}
+
+.create-external-notification-element .help-table {
+ width: 100%;
+}
+
+.create-external-notification-element .help-table > tbody > tr > td {
+ vertical-align: top;
+ max-width: 600px;
+}
+
+.create-external-notification-element .help-table td.help-col {
+ vertical-align: top;
+ padding: 20px;
+ padding-top: 40px;
+ width: 500px;
+ font-size: 15px;
+ color: #aaa;
+}
+
+.create-external-notification-element .config-section {
+ margin-top: 20px;
+}
+
+
+.create-external-notification-element .help-text {
+ margin-top: 10px;
+ color: #aaa;
+}
\ No newline at end of file
diff --git a/static/directives/create-external-notification-dialog.html b/static/directives/create-external-notification-dialog.html
deleted file mode 100644
index f45e7353e..000000000
--- a/static/directives/create-external-notification-dialog.html
+++ /dev/null
@@ -1,174 +0,0 @@
-
-
diff --git a/static/directives/create-external-notification.html b/static/directives/create-external-notification.html
new file mode 100644
index 000000000..7bc684365
--- /dev/null
+++ b/static/directives/create-external-notification.html
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+
+
+
+ ×
+
+
+
+
+ An e-mail has been sent to {{ currentConfig.email }}
. Please click the link contained
+ in the e-mail.
+
+
+
+
+
+
+ The e-mail address {{ currentConfig.email }}
has not been authorized to receive
+ notifications from this repository. Please click "Send Authorization E-mail" below to start
+ the authorization process.
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/static/directives/regex-editor.html b/static/directives/regex-editor.html
new file mode 100644
index 000000000..96201d7d0
--- /dev/null
+++ b/static/directives/regex-editor.html
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html
index 6605fecf7..2a4edf047 100644
--- a/static/directives/repo-view/repo-panel-info.html
+++ b/static/directives/repo-view/repo-panel-info.html
@@ -75,14 +75,6 @@
-
- Automated Security Scanning
-
-
-
diff --git a/static/directives/repository-events-table.html b/static/directives/repository-events-table.html
index 7fae2b4ff..67257080b 100644
--- a/static/directives/repository-events-table.html
+++ b/static/directives/repository-events-table.html
@@ -4,9 +4,9 @@
Events and Notifications
@@ -19,7 +19,7 @@
Click the "Create Notification" button above to add a new notification for a repository event.
-
Click here to add a new notification for a repository event.
+
Click here to add a new notification for a repository event.
@@ -95,11 +95,4 @@
-
-
-
diff --git a/static/js/app.js b/static/js/app.js
index 19e0390d9..a08e89d70 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -120,6 +120,9 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
// Repo Build View
.route('/repository/:namespace/:name/build/:buildid', 'build-view')
+ // Create repository notification
+ .route('/repository/:namespace/:name/create-notification', 'create-repository-notification')
+
// Repo List
.route('/repository/', 'repo-list')
diff --git a/static/js/directives/ui/create-external-notification-dialog.js b/static/js/directives/ui/create-external-notification.js
similarity index 79%
rename from static/js/directives/ui/create-external-notification-dialog.js
rename to static/js/directives/ui/create-external-notification.js
index 874769216..f6f00774f 100644
--- a/static/js/directives/ui/create-external-notification-dialog.js
+++ b/static/js/directives/ui/create-external-notification.js
@@ -1,16 +1,15 @@
/**
- * An element which displays a dialog to register a new external notification on a repository.
+ * An element which displays a form to register a new external notification on a repository.
*/
-angular.module('quay').directive('createExternalNotificationDialog', function () {
+angular.module('quay').directive('createExternalNotification', function () {
var directiveDefinitionObject = {
priority: 0,
- templateUrl: '/static/directives/create-external-notification-dialog.html',
+ templateUrl: '/static/directives/create-external-notification.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repository': '=repository',
- 'counter': '=counter',
'notificationCreated': '¬ificationCreated',
'defaultData': '=defaultData'
},
@@ -27,7 +26,12 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
$scope.methods = ExternalNotificationData.getSupportedMethods();
$scope.getPattern = function(field) {
- return new RegExp(field.regex);
+ if (field._cached_regex) {
+ return field._cached_regex;
+ }
+
+ field._cached_regex = new RegExp(field.pattern);
+ return field._cached_regex;
};
$scope.setEvent = function(event) {
@@ -99,14 +103,6 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
ApiService.createRepoNotification(data, params).then(function(resp) {
$scope.status = '';
$scope.notificationCreated({'notification': resp});
-
- // Used by repository-events-summary.
- if (!$scope.repository._notificationCounter) {
- $scope.repository._notificationCounter = 0;
- }
-
- $scope.repository._notificationCounter++;
- $('#createNotificationModal').modal('hide');
});
};
@@ -123,6 +119,7 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
}
$scope.unauthorizedEmail = true;
+ $('#authorizeEmailModal').modal({});
};
$scope.sendAuthEmail = function() {
@@ -146,6 +143,11 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
}, 1000);
};
+ $scope.cancelEmailAuth = function() {
+ $scope.status = '';
+ $('#authorizeEmailModal').modal('hide');
+ };
+
$scope.getHelpUrl = function(field, config) {
var helpUrl = field['help_url'];
if (!helpUrl) {
@@ -155,21 +157,9 @@ angular.module('quay').directive('createExternalNotificationDialog', function ()
return StringBuilderService.buildUrl(helpUrl, config);
};
- $scope.$watch('counter', function(counter) {
- if (counter) {
- $scope.clearCounter++;
- $scope.status = '';
- $scope.currentEvent = null;
- $scope.currentMethod = null;
- $scope.unauthorizedEmail = false;
-
- $timeout(function() {
- if ($scope.defaultData && $scope.defaultData['currentEvent']) {
- $scope.setEvent($scope.defaultData['currentEvent']);
- }
- }, 100);
-
- $('#createNotificationModal').modal({});
+ $scope.$watch('defaultData', function(counter) {
+ if ($scope.defaultData && $scope.defaultData['currentEvent']) {
+ $scope.setEvent($scope.defaultData['currentEvent']);
}
});
}
diff --git a/static/js/directives/ui/regex-editor.js b/static/js/directives/ui/regex-editor.js
new file mode 100644
index 000000000..4b3358c49
--- /dev/null
+++ b/static/js/directives/ui/regex-editor.js
@@ -0,0 +1,39 @@
+/**
+ * An element which displays an edit box for regular expressions.
+ */
+angular.module('quay').directive('regexEditor', function () {
+ var directiveDefinitionObject = {
+ priority: 0,
+ templateUrl: '/static/directives/regex-editor.html',
+ replace: false,
+ transclude: true,
+ restrict: 'C',
+ scope: {
+ 'placeholder': '@placeholder',
+ 'optional': '=optional',
+ 'binding': '=binding'
+ },
+ controller: function($scope, $element) {
+ }
+ };
+ return directiveDefinitionObject;
+});
+
+angular.module('quay').directive('requireValidRegex', function() {
+ return {
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ function validator(value) {
+ try {
+ new RegExp(value)
+ ctrl.$setValidity('regex', true);
+ } catch (e) {
+ ctrl.$setValidity('regex', false);
+ }
+ return value;
+ }
+
+ ctrl.$parsers.push(validator);
+ }
+ };
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/repository-events-table.js b/static/js/directives/ui/repository-events-table.js
index 8aca804b6..02c94ba73 100644
--- a/static/js/directives/ui/repository-events-table.js
+++ b/static/js/directives/ui/repository-events-table.js
@@ -53,14 +53,6 @@ angular.module('quay').directive('repositoryEventsTable', function () {
loadNotifications();
- $scope.handleNotificationCreated = function(notification) {
- $scope.notifications.push(notification);
- };
-
- $scope.askCreateNotification = function() {
- $scope.showNewNotificationCounter++;
- };
-
$scope.findEnumValue = function(values, index) {
var found = null;
Object.keys(values).forEach(function(key) {
diff --git a/static/js/pages/create-repository-notification.js b/static/js/pages/create-repository-notification.js
new file mode 100644
index 000000000..7a02ca4d6
--- /dev/null
+++ b/static/js/pages/create-repository-notification.js
@@ -0,0 +1,33 @@
+(function() {
+ /**
+ * Create repository notification page.
+ */
+ angular.module('quayPages').config(['pages', function(pages) {
+ pages.create('create-repository-notification', 'create-repository-notification.html', CreateRepoNotificationCtrl, {
+ 'newLayout': true,
+ 'title': 'Create Repo Notification: {{ namespace }}/{{ name }}',
+ 'description': 'Create repository notification for repository {{ namespace }}/{{ name }}'
+ })
+ }]);
+
+ function CreateRepoNotificationCtrl($scope, $routeParams, $location, ApiService) {
+ $scope.namespace = $routeParams.namespace;
+ $scope.name = $routeParams.name;
+
+ var loadRepository = function() {
+ var params = {
+ 'repository': $scope.namespace + '/' + $scope.name
+ };
+
+ $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
+ $scope.repository = repo;
+ });
+ };
+
+ loadRepository();
+
+ $scope.notificationCreated = function() {
+ $location.url('repository/' + $scope.namespace + '/' + $scope.name + '?tab=settings');
+ };
+ }
+})();
\ No newline at end of file
diff --git a/static/js/services/external-notification-data.js b/static/js/services/external-notification-data.js
index 229667e8c..623f1445d 100644
--- a/static/js/services/external-notification-data.js
+++ b/static/js/services/external-notification-data.js
@@ -20,22 +20,66 @@ function(Config, Features, VulnerabilityService) {
{
'id': 'build_queued',
'title': 'Dockerfile Build Queued',
- 'icon': 'fa-tasks'
+ 'icon': 'fa-tasks',
+ '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)'
+ }
+ ]
},
{
'id': 'build_start',
'title': 'Dockerfile Build Started',
- 'icon': 'fa-circle-o-notch'
+ 'icon': 'fa-circle-o-notch',
+ '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)'
+ }
+ ]
},
{
'id': 'build_success',
'title': 'Dockerfile Build Successfully Completed',
- 'icon': 'fa-check-circle-o'
+ 'icon': 'fa-check-circle-o',
+ '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)'
+ }
+ ]
},
{
'id': 'build_failure',
'title': 'Dockerfile Build Failed',
- 'icon': 'fa-times-circle-o'
+ 'icon': 'fa-times-circle-o',
+ '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) {
@@ -52,8 +96,12 @@ function(Config, Features, VulnerabilityService) {
{
'name': 'level',
'type': 'enum',
- 'title': 'Minimum Priority Level',
+ 'title': 'minimum severity level',
'values': VulnerabilityService.LEVELS,
+ 'help_text': 'A vulnerability must have a severity of the chosen level (or higher) ' +
+ 'for this notification to fire. Defcon 1 is a special severity level ' +
+ 'manually tagged by the ' + Config.REGISTRY_TITLE_SHORT + ' team for ' +
+ 'above-critical issues',
}
]
});
@@ -68,7 +116,8 @@ function(Config, Features, VulnerabilityService) {
{
'name': 'target',
'type': 'entity',
- 'title': 'Recipient'
+ 'title': 'Recipient',
+ 'help_text': 'The ' + Config.REGISTRY_TITLE_SHORT + ' user to notify'
}
]
},
@@ -117,11 +166,11 @@ function(Config, Features, VulnerabilityService) {
'fields': [
{
'name': 'room_id',
- 'type': 'regex',
+ 'type': 'pattern',
'title': 'Room ID #',
- 'regex': '^[0-9]+$',
+ 'pattern': '^[0-9]+$',
'help_url': 'https://hipchat.com/admin/rooms',
- 'regex_fail_message': 'We require the HipChat room number , not name.'
+ 'pattern_fail_message': 'We require the HipChat room number , not name.'
},
{
'name': 'notification_token',
@@ -138,9 +187,9 @@ function(Config, Features, VulnerabilityService) {
'fields': [
{
'name': 'url',
- 'type': 'regex',
+ 'type': 'pattern',
'title': 'Webhook URL',
- 'regex': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
+ 'pattern': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
'help_url': 'https://slack.com/services/new/incoming-webhook',
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
}
diff --git a/static/partials/create-repository-notification.html b/static/partials/create-repository-notification.html
new file mode 100644
index 000000000..70ab559cc
--- /dev/null
+++ b/static/partials/create-repository-notification.html
@@ -0,0 +1,21 @@
+
diff --git a/test/test_notifications.py b/test/test_notifications.py
new file mode 100644
index 000000000..3da728912
--- /dev/null
+++ b/test/test_notifications.py
@@ -0,0 +1,135 @@
+import unittest
+
+from endpoints.notificationevent import BuildSuccessEvent
+from util.morecollections import AttrDict
+
+class TestShouldPerform(unittest.TestCase):
+ def test_build_nofilter(self):
+ notification_data = AttrDict({
+ 'event_config_json': '{}',
+ })
+
+ # No build data at all.
+ self.assertTrue(BuildSuccessEvent().should_perform({}, notification_data))
+
+ # With trigger metadata but no ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {},
+ }, notification_data))
+
+ # With trigger metadata and a ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/somebranch',
+ },
+ }, notification_data))
+
+
+ def test_build_emptyfilter(self):
+ notification_data = AttrDict({
+ 'event_config_json': '{"ref-regex": ""}',
+ })
+
+ # No build data at all.
+ self.assertTrue(BuildSuccessEvent().should_perform({}, notification_data))
+
+ # With trigger metadata but no ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {},
+ }, notification_data))
+
+ # With trigger metadata and a ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/somebranch',
+ },
+ }, notification_data))
+
+
+ def test_build_invalidfilter(self):
+ notification_data = AttrDict({
+ 'event_config_json': '{"ref-regex": "]["}',
+ })
+
+ # No build data at all.
+ self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data))
+
+ # With trigger metadata but no ref.
+ self.assertFalse(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {},
+ }, notification_data))
+
+ # With trigger metadata and a ref.
+ self.assertFalse(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/somebranch',
+ },
+ }, notification_data))
+
+
+ def test_build_withfilter(self):
+ notification_data = AttrDict({
+ 'event_config_json': '{"ref-regex": "refs/heads/master"}',
+ })
+
+ # No build data at all.
+ self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data))
+
+ # With trigger metadata but no ref.
+ self.assertFalse(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {},
+ }, notification_data))
+
+ # With trigger metadata and a not-matching ref.
+ self.assertFalse(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/somebranch',
+ },
+ }, notification_data))
+
+ # With trigger metadata and a matching ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/master',
+ },
+ }, notification_data))
+
+
+ def test_build_withwildcardfilter(self):
+ notification_data = AttrDict({
+ 'event_config_json': '{"ref-regex": "refs/heads/.+"}',
+ })
+
+ # No build data at all.
+ self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data))
+
+ # With trigger metadata but no ref.
+ self.assertFalse(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {},
+ }, notification_data))
+
+ # With trigger metadata and a not-matching ref.
+ self.assertFalse(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/tags/sometag',
+ },
+ }, notification_data))
+
+ # With trigger metadata and a matching ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/master',
+ },
+ }, notification_data))
+
+ # With trigger metadata and another matching ref.
+ self.assertTrue(BuildSuccessEvent().should_perform({
+ 'trigger_metadata': {
+ 'ref': 'refs/heads/somebranch',
+ },
+ }, notification_data))
+
+
+if __name__ == '__main__':
+ unittest.main()
+