Convert over to notifications system. Note this is incomplete

This commit is contained in:
Joseph Schorr 2014-07-17 22:51:58 -04:00
parent de8e898ad0
commit 8d7493cb86
17 changed files with 432 additions and 166 deletions

View file

@ -316,4 +316,3 @@ import endpoints.api.tag
import endpoints.api.team
import endpoints.api.trigger
import endpoints.api.user
import endpoints.api.webhook

View file

@ -2,8 +2,10 @@ import json
from flask import request
from app import notification_queue
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, api, NotFound)
from endpoints.notificationevent import NotificationEvent
from data import model
@ -23,7 +25,7 @@ def notification_view(notification):
@resource('/v1/repository/<repopath:repository>/notification/')
class NotificaitonList(RepositoryParamResource):
class RepositoryNotificationList(RepositoryParamResource):
""" Resource for dealing with listing and creating notifications on a repository. """
schemas = {
'NotificationCreateRequest': {
@ -81,7 +83,7 @@ class NotificaitonList(RepositoryParamResource):
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
class Notification(RepositoryParamResource):
class RepositoryNotification(RepositoryParamResource):
""" Resource for dealing with specific notifications. """
@require_repo_admin
@nickname('getRepoNotification')
@ -105,3 +107,28 @@ class Notification(RepositoryParamResource):
repo=model.get_repository(namespace, repository))
return 'No Content', 204
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
class TestRepositoryNotification(RepositoryParamResource):
""" Resource for queuing a test of a notification. """
@require_repo_admin
@nickname('testRepoNotification')
def post(self, namespace, repository, uuid):
""" Queues a test notification for this repository. """
try:
notification = model.get_repo_notification(namespace, repository, uuid)
except model.InvalidNotificationException:
raise NotFound()
event_info = NotificationEvent.get_event(notification.event.name)
sample_data = event_info.get_sample_data(repository=notification.repository)
notification_data = {
'notification_id': notification.id,
'repository_id': notification.repository.id,
'event_data': sample_data
}
notification_queue.put([namespace, repository, notification.event.name],
json.dumps(notification_data))
return {}

View file

@ -1,77 +0,0 @@
import json
from flask import request
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, api, NotFound)
from data import model
def webhook_view(webhook):
return {
'public_id': webhook.public_id,
'parameters': json.loads(webhook.parameters),
}
@resource('/v1/repository/<repopath:repository>/webhook/')
class WebhookList(RepositoryParamResource):
""" Resource for dealing with listing and creating webhooks. """
schemas = {
'WebhookCreateRequest': {
'id': 'WebhookCreateRequest',
'type': 'object',
'description': 'Arbitrary json.',
},
}
@require_repo_admin
@nickname('createWebhook')
@validate_json_request('WebhookCreateRequest')
def post(self, namespace, repository):
""" Create a new webhook for the specified repository. """
repo = model.get_repository(namespace, repository)
webhook = model.create_webhook(repo, request.get_json())
resp = webhook_view(webhook)
repo_string = '%s/%s' % (namespace, repository)
headers = {
'Location': api.url_for(Webhook, repository=repo_string, public_id=webhook.public_id),
}
log_action('add_repo_webhook', namespace,
{'repo': repository, 'webhook_id': webhook.public_id},
repo=repo)
return resp, 201, headers
@require_repo_admin
@nickname('listWebhooks')
def get(self, namespace, repository):
""" List the webhooks for the specified repository. """
webhooks = model.list_webhooks(namespace, repository)
return {
'webhooks': [webhook_view(webhook) for webhook in webhooks]
}
@resource('/v1/repository/<repopath:repository>/webhook/<public_id>')
class Webhook(RepositoryParamResource):
""" Resource for dealing with specific webhooks. """
@require_repo_admin
@nickname('getWebhook')
def get(self, namespace, repository, public_id):
""" Get information for the specified webhook. """
try:
webhook = model.get_webhook(namespace, repository, public_id)
except model.InvalidWebhookException:
raise NotFound()
return webhook_view(webhook)
@require_repo_admin
@nickname('deleteWebhook')
def delete(self, namespace, repository, public_id):
""" Delete the specified webhook. """
model.delete_webhook(namespace, repository, public_id)
log_action('delete_repo_webhook', namespace,
{'repo': repository, 'webhook_id': public_id},
repo=model.get_repository(namespace, repository))
return 'No Content', 204

View file

@ -8,7 +8,7 @@ from collections import OrderedDict
from data import model
from data.model import oauth
from app import analytics, app, webhook_queue, authentication, userevents, storage
from app import analytics, app, notification_queue, authentication, userevents, storage
from auth.auth import process_auth
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
from util.names import parse_repository_name
@ -315,27 +315,30 @@ def update_images(namespace, repository):
profile.debug('GCing repository')
num_removed = model.garbage_collect_repository(namespace, repository)
# Generate a job for each webhook that has been added to this repo
profile.debug('Adding webhooks for repository')
# Generate a job for each notification that has been added to this repo
profile.debug('Adding notifications for repository')
webhooks = model.list_webhooks(namespace, repository)
for webhook in webhooks:
webhook_data = json.loads(webhook.parameters)
repo_string = '%s/%s' % (namespace, repository)
profile.debug('Creating webhook for repository \'%s\' for url \'%s\'',
repo_string, webhook_data['url'])
webhook_data['payload'] = {
'repository': repo_string,
'namespace': namespace,
'name': repository,
'docker_url': 'quay.io/%s' % repo_string,
'homepage': 'https://quay.io/repository/%s' % repo_string,
'visibility': repo.visibility.name,
'updated_tags': updated_tags,
'pushed_image_count': len(image_with_checksums),
'pruned_image_count': num_removed,
repo_string = '%s/%s' % (namespace, repository)
event_data = {
'repository': repo_string,
'namespace': namespace,
'name': repository,
'docker_url': 'quay.io/%s' % repo_string,
'homepage': 'https://quay.io/repository/%s' % repo_string,
'visibility': repo.visibility.name,
'updated_tags': updated_tags,
'pushed_image_count': len(image_with_checksums),
'pruned_image_count': num_removed
}
notifications = model.list_repo_notifications(namespace, repository, event_name='repo_push')
for notification in notifications:
notification_data = {
'notification_id': notification.id,
'repository_id': repository.id,
'event_data': event_data
}
webhook_queue.put([namespace, repository], json.dumps(webhook_data))
notification_queue.put([namespace, repository, 'repo_push'], json.dumps(notification_data))
return make_response('Updated', 204)

View file

@ -0,0 +1,108 @@
import logging
import io
import os.path
import tarfile
import base64
logger = logging.getLogger(__name__)
class InvalidNotificationEventException(Exception):
pass
class NotificationEvent(object):
def __init__(self):
pass
def get_summary(self, notification_data):
"""
Returns a human readable one-line summary for the given notification data.
"""
raise NotImplementedError
def get_message(self, notification_data):
"""
Returns a human readable HTML message for the given notification data.
"""
raise NotImplementedError
def get_sample_data(self, repository=None):
"""
Returns sample data for testing the raising of this notification, with an optional
repository.
"""
raise NotImplementedError
@classmethod
def event_name(cls):
"""
Particular event implemented by subclasses.
"""
raise NotImplementedError
@classmethod
def get_event(cls, eventname):
for subc in cls.__subclasses__():
if subc.event_name() == eventname:
return subc()
raise InvalidNotificationEventException('Unable to find event: %s' % eventname)
class RepoPushEvent(NotificationEvent):
@classmethod
def event_name(cls):
return 'repo_push'
def get_summary(self, notification_data):
return 'Repository %s updated' % (event_data['repository'])
def get_message(self, notification_data):
event_data = notification_data['event_data']
if not event_data['tags']:
return '%s images pushed for repository %s (%s)' % (event_data['pushed_image_count'],
event_data['repository'], event_data['homepage'])
return 'Tags %s updated for repository %s (%s)' % (event_data['updated_tags'],
event_data['repository'], event_data['homepage'])
def get_sample_data(self, repository=None):
repo_string = '%s/%s' % (repository.namespace, repository.name)
event_data = {
'repository': repo_string,
'namespace': repository.namespace,
'name': repository.name,
'docker_url': 'quay.io/%s' % repo_string,
'homepage': 'https://quay.io/repository/%s' % repo_string,
'visibility': repository.visibility.name,
'updated_tags': ['latest', 'foo', 'bar'],
'pushed_image_count': 10,
'pruned_image_count': 3
}
return event_data
class BuildStartEvent(NotificationEvent):
@classmethod
def event_name(cls):
return 'build_start'
def get_sample_data(repository=None):
pass
class BuildSuccessEvent(NotificationEvent):
@classmethod
def event_name(cls):
return 'build_success'
def get_sample_data(repository=None):
pass
class BuildFailureEvent(NotificationEvent):
@classmethod
def event_name(cls):
return 'build_failure'
def get_sample_data(repository=None):
pass

View file

@ -0,0 +1,114 @@
import logging
import io
import os.path
import tarfile
import base64
import json
from flask.ext.mail import Message
from app import mail, app
from data import model
logger = logging.getLogger(__name__)
class InvalidNotificationMethodException(Exception):
pass
class NotificationMethod(object):
def __init__(self):
pass
@classmethod
def method_name(cls):
"""
Particular method implemented by subclasses.
"""
raise NotImplementedError
def perform(self, notification, event_handler, notification_data):
"""
Performs the notification method.
notification: The noticication record itself.
event_handler: The NotificationEvent handler.
notification_data: The dict of notification data placed in the queue.
"""
raise NotImplementedError
@classmethod
def get_method(cls, methodname):
for subc in cls.__subclasses__():
if subc.method_name() == methodname:
return subc()
raise InvalidNotificationMethodException('Unable to find method: %s' % methodname)
class QuayNotificationMethod(NotificationMethod):
@classmethod
def method_name(cls):
return 'quay_notification'
def perform(self, notification, event_handler, notification_data):
repository_id = notification_data['repository_id']
repository = model.lookup_repository(repository_id)
if not repository:
# Probably deleted.
return True
model.create_notification(event_handler.event_name(),
repository.namespace, metadata=notification_data['event_data'])
return True
class EmailMethod(NotificationMethod):
@classmethod
def method_name(cls):
return 'email'
def perform(self, notification, event_handler, notification_data):
config_data = json.loads(notification.config_json)
email = config_data.get('email', '')
if not email:
return False
msg = Message(event_handler.get_summary(notification_data),
sender='support@quay.io',
recipients=[email])
msg.html = event_handler.get_message(notification_data)
try:
mail.send(msg)
except Exception as ex:
logger.exception('Email was unable to be sent: %s' % ex.message)
return False
return True
class WebhookMethod(NotificationMethod):
@classmethod
def method_name(cls):
return 'webhook'
def perform(self, notification, event_handler, notification_data):
config_data = json.loads(notification.config_json)
url = config_data.get('url', '')
if not url:
return False
payload = notification_data['event_data']
headers = {'Content-type': 'application/json'}
try:
resp = requests.post(url, data=json.dumps(payload), headers=headers)
if resp.status_code/100 != 2:
logger.error('%s response for webhook to url: %s' % (resp.status_code,
url))
return False
except requests.exceptions.RequestException as ex:
logger.exception('Webhook was unable to be sent: %s' % ex.message)
return False
return True