add data interface and pre oci impelementation for repo notifications

This commit is contained in:
Evan Cordell 2017-07-17 17:53:08 -04:00
parent 0cbe3bdf73
commit 047722b295
4 changed files with 183 additions and 94 deletions

View file

@ -1,19 +1,15 @@
import logging import logging
import datetime import datetime
import json
from calendar import timegm from calendar import timegm
from email.utils import formatdate from email.utils import formatdate
from functools import partial, wraps from functools import partial, wraps
from enum import Enum from flask import Blueprint, request, session
from flask import Blueprint, Response, request, make_response, jsonify, session, url_for
from flask_restful import Resource, abort, Api, reqparse from flask_restful import Resource, abort, Api, reqparse
from flask_restful.utils.cors import crossdomain from flask_restful.utils.cors import crossdomain
from jsonschema import validate, ValidationError from jsonschema import validate, ValidationError
import features
from app import app, metric_queue from app import app, metric_queue
from data import model from data import model
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission,
@ -23,16 +19,13 @@ from auth import scopes
from auth.auth_context import get_authenticated_user, get_validated_oauth_token from auth.auth_context import get_authenticated_user, get_validated_oauth_token
from auth.decorators import process_oauth from auth.decorators import process_oauth
from endpoints.csrf import csrf_protect from endpoints.csrf import csrf_protect
from endpoints.exception import (ApiException, Unauthorized, InvalidRequest, InvalidResponse, from endpoints.exception import (Unauthorized, InvalidRequest, InvalidResponse,
FreshLoginRequired, NotFound) FreshLoginRequired, NotFound)
from endpoints.decorators import check_anon_protection from endpoints.decorators import check_anon_protection
from endpoints.decorated import (handle_dme, handle_emailexception, handle_configexception,
handle_too_many_login_attempts)
from util.config.provider.baseprovider import CannotWriteConfigException
from util.metrics.metricqueue import time_decorator from util.metrics.metricqueue import time_decorator
from util.names import parse_namespace_repository from util.names import parse_namespace_repository
from util.pagination import encrypt_page_token, decrypt_page_token from util.pagination import encrypt_page_token, decrypt_page_token
from util.useremails import CannotSendEmailException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
api_bp = Blueprint('api', __name__) api_bp = Blueprint('api', __name__)

View file

@ -1,47 +1,20 @@
""" List, create and manage repository events/notifications. """ """ List, create and manage repository events/notifications. """
import json
import logging import logging
from flask import request from flask import request
from app import notification_queue
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, request_error, log_action, validate_json_request, request_error,
path_param, disallow_for_app_repositories) path_param, disallow_for_app_repositories)
from endpoints.exception import NotFound from endpoints.exception import NotFound
from endpoints.notificationevent import NotificationEvent
from endpoints.notificationmethod import (NotificationMethod, from endpoints.notificationmethod import (NotificationMethod,
CannotValidateNotificationMethodException) CannotValidateNotificationMethodException)
from endpoints.notificationhelper import build_notification_data from endpoints.notificationhelper import build_notification_data
from data import model
from workers.notificationworker.models_pre_oci import notification from workers.notificationworker.models_pre_oci import notification
from endpoints.api.tag_models_pre_oci import pre_oci_model as model
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def notification_view(note):
config = {}
try:
config = json.loads(note.config_json)
except:
config = {}
event_config = {}
try:
event_config = json.loads(note.event_config_json)
except:
event_config = {}
return {
'uuid': note.uuid,
'event': note.event.name,
'method': note.method.name,
'config': config,
'title': note.title,
'event_config': event_config,
'number_of_failures': note.number_of_failures,
}
@resource('/v1/repository/<apirepopath:repository>/notification/') @resource('/v1/repository/<apirepopath:repository>/notification/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('repository', 'The full path of the repository. e.g. namespace/name')
@ -86,41 +59,39 @@ class RepositoryNotificationList(RepositoryParamResource):
@nickname('createRepoNotification') @nickname('createRepoNotification')
@disallow_for_app_repositories @disallow_for_app_repositories
@validate_json_request('NotificationCreateRequest') @validate_json_request('NotificationCreateRequest')
def post(self, namespace, repository): def post(self, namespace_name, repository_name):
""" Create a new notification for the specified repository. """
repo = model.repository.get_repository(namespace, repository)
parsed = request.get_json() parsed = request.get_json()
repository = model.get_repository(namespace_name, repository_name)
method_handler = NotificationMethod.get_method(parsed['method']) method_handler = NotificationMethod.get_method(parsed['method'])
if not method_handler:
raise request_error(message='Unknown method')
try: try:
method_handler.validate(repo, parsed['config']) method_handler.validate(repository, parsed['config'])
except CannotValidateNotificationMethodException as ex: except CannotValidateNotificationMethodException as ex:
raise request_error(message=ex.message) raise request_error(message=ex.message)
new_notification = model.create_repo_notification(repository,
parsed['event'],
parsed['method'],
parsed['config'],
parsed['eventConfig'],
parsed.get('title'))
new_notification = model.notification.create_repo_notification(repo, parsed['event'], log_action('add_repo_notification', namespace_name,
parsed['method'], parsed['config'], {'repo': repository_name, 'namespace': namespace_name,
parsed['eventConfig'],
parsed.get('title', None))
resp = notification_view(new_notification)
log_action('add_repo_notification', namespace,
{'repo': repository, 'namespace': namespace,
'notification_id': new_notification.uuid, 'notification_id': new_notification.uuid,
'event': parsed['event'], 'method': parsed['method']}, 'event': parsed['event'], 'method': parsed['method']},
repo=repo) repo=repository)
return resp, 201 return new_notification.to_dict(), 201
@require_repo_admin @require_repo_admin
@nickname('listRepoNotifications') @nickname('listRepoNotifications')
@disallow_for_app_repositories @disallow_for_app_repositories
def get(self, namespace, repository): def get(self, namespace_name, repository_name):
""" List the notifications for the specified repository. """ """ List the notifications for the specified repository. """
notifications = model.notification.list_repo_notifications(namespace, repository) notifications = model.list_repo_notifications(namespace_name, repository_name)
return { return {
'notifications': [notification_view(n) for n in notifications] 'notifications': [n.to_dict() for n in notifications]
} }
@ -132,43 +103,41 @@ class RepositoryNotification(RepositoryParamResource):
@require_repo_admin @require_repo_admin
@nickname('getRepoNotification') @nickname('getRepoNotification')
@disallow_for_app_repositories @disallow_for_app_repositories
def get(self, namespace, repository, uuid): def get(self, namespace_name, repository_name, uuid):
""" Get information for the specified notification. """ """ Get information for the specified notification. """
try: try:
found = model.notification.get_repo_notification(uuid) found = model.get_repo_notification(uuid)
except model.InvalidNotificationException: except model.InvalidNotificationException:
raise NotFound() raise NotFound()
if (found.repository.namespace_user.username != namespace or return found.to_dict()
found.repository.name != repository):
raise NotFound()
return notification_view(found)
@require_repo_admin @require_repo_admin
@nickname('deleteRepoNotification') @nickname('deleteRepoNotification')
@disallow_for_app_repositories @disallow_for_app_repositories
def delete(self, namespace, repository, uuid): def delete(self, namespace_name, repository_name, uuid):
""" Deletes the specified notification. """ """ Deletes the specified notification. """
deleted = model.notification.delete_repo_notification(namespace, repository, uuid) repository = model.get_repository(namespace_name, repository_name)
log_action('delete_repo_notification', namespace, deleted = model.delete_repo_notification(repository, uuid)
{'repo': repository, 'namespace': namespace, 'notification_id': uuid, log_action('delete_repo_notification', namespace_name,
{'repo': repository_name, 'namespace': namespace_name, 'notification_id': uuid,
'event': deleted.event.name, 'method': deleted.method.name}, 'event': deleted.event.name, 'method': deleted.method.name},
repo=model.repository.get_repository(namespace, repository)) repo=repository)
return 'No Content', 204 return 'No Content', 204
@require_repo_admin @require_repo_admin
@nickname('resetRepositoryNotificationFailures') @nickname('resetRepositoryNotificationFailures')
@disallow_for_app_repositories @disallow_for_app_repositories
def post(self, namespace, repository, uuid): def post(self, namespace_name, repository_name, uuid):
""" Resets repository notification to 0 failures. """ """ Resets repository notification to 0 failures. """
reset = model.notification.reset_notification_number_of_failures(namespace, repository, uuid) repository = model.get_repository(namespace_name, repository_name)
reset = model.reset_notification_number_of_failures(repository, uuid)
if reset is not None: if reset is not None:
log_action('reset_repo_notification', namespace, log_action('reset_repo_notification', namespace_name,
{'repo': repository, 'namespace': namespace, 'notification_id': uuid, {'repo': repository_name, 'namespace': namespace_name, 'notification_id': uuid,
'event': reset.event.name, 'method': reset.method.name}, 'event': reset.event.name, 'method': reset.method.name},
repo=model.repository.get_repository(namespace, repository)) repo=repository)
return 'No Content', 204 return 'No Content', 204
@ -183,23 +152,8 @@ class TestRepositoryNotification(RepositoryParamResource):
@disallow_for_app_repositories @disallow_for_app_repositories
def post(self, namespace, repository, uuid): def post(self, namespace, repository, uuid):
""" Queues a test notification for this repository. """ """ Queues a test notification for this repository. """
try: test_note = model.get_repo_notification(uuid)
test_note = model.notification.get_repo_notification(uuid) if not test_note:
except model.InvalidNotificationException:
raise NotFound() raise NotFound()
model.queue_test_notification(test_note)
if (test_note.repository.namespace_user.username != namespace or
test_note.repository.name != repository):
raise NotFound()
event_info = NotificationEvent.get_event(test_note.event.name)
# TODO(jschorr): Stop depending on the worker module's data interface and instead only depend
# on the notification's data interface (to be added).
sample_data = event_info.get_sample_data(notification(test_note))
notification_data = build_notification_data(test_note, sample_data)
notification_queue.put([test_note.repository.namespace_user.username, repository,
test_note.event.name], json.dumps(notification_data))
return {} return {}

View file

@ -0,0 +1,85 @@
import json
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from six import add_metaclass
class RepositoryNotification(
namedtuple('RepositoryNotification', [
'uuid',
'title',
'event_name',
'method_name',
'config_json',
'event_config_json',
'number_of_failures',
])):
"""
RepositoryNotification represents a notification for a repository.
:type uuid: string
:type event: string
:type method: string
:type config: string
:type title: string
:type event_config: string
:type number_of_failures: int
"""
def to_dict(self):
config = {}
try:
config = json.loads(self.config_json)
except:
config = {}
event_config = {}
try:
event_config = json.loads(self.event_config_json)
except:
event_config = {}
return {
'uuid': self.uuid,
'title': self.title,
'event': self.event_name,
'method': self.method_name,
'config': config,
'event_config': event_config,
'number_of_failures': self.number_of_failures,
}
@add_metaclass(ABCMeta)
class RepoNotificationInterface(object):
"""
Interface that represents all data store interactions required by the RepositoryNotification API
"""
@abstractmethod
def get_repository(self, namespace_name, repository_name):
pass
@abstractmethod
def create_repo_notification(self, repository, event_name, method_name, method_config, event_config, title=None):
pass
@abstractmethod
def list_repo_notifications(self, namespace_name, repository_name, event_name=None):
pass
@abstractmethod
def get_repo_notification(self, uuid):
pass
@abstractmethod
def delete_repo_notification(self, repository, uuid):
pass
@abstractmethod
def reset_notification_number_of_failures(self, repository, uuid):
pass
@abstractmethod
def queue_test_notification(self, notification):
pass

View file

@ -0,0 +1,57 @@
import json
from app import notification_queue
from data import model
from endpoints.api.repositorynotification_models_interface import RepoNotificationInterface, RepositoryNotification
from endpoints.notificationevent import NotificationEvent
from endpoints.notificationhelper import build_notification_data
class PreOCIModel(RepoNotificationInterface):
def get_repository(self, namespace_name, repository_name):
return self._notification(model.repository.get_repository(namespace_name, repository_name))
def create_repo_notification(self, repository, event_name, method_name, method_config, event_config, title=None):
return self._notification(model.notification.create_repo_notification(repository,
event_name,
method_name,
method_config,
event_config,
title))
def list_repo_notifications(self, namespace_name, repository_name, event_name=None):
return [self._notification(n)
for n in model.notification.list_repo_notifications(namespace_name, repository_name, event_name)]
def get_repo_notification(self, uuid):
return self._notification(model.notification.get_repo_notification(uuid))
def delete_repo_notification(self, repository, uuid):
return self._notification(
self.model.notification.delete_repo_notification(repository.namespace_user, repository.name, uuid))
def reset_notification_number_of_failures(self, repository, uuid):
return self._notification(
model.notification.reset_notification_number_of_failures(repository.namespace_user, repository.name, uuid))
def queue_test_notification(self, notification):
event_info = NotificationEvent.get_event(notification.event.name)
sample_data = event_info.get_sample_data(notification)
notification_data = build_notification_data(notification, sample_data)
notification_queue.put([notification.repository.namespace_user.username, notification,
notification.event.name], json.dumps(notification_data))
def _notification(self, notification):
return RepositoryNotification(uuid=notification.uuid,
title=notification.title,
event_name=notification.event.name,
method_name=notification.method.name,
config_json=notification.config_json,
event_config_json=notification.event_config_json,
number_of_failures=notification.number_of_failures)
pre_oci_model = PreOCIModel()