diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 655c9f6e3..8da2d4f58 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -1,19 +1,15 @@ import logging import datetime -import json from calendar import timegm from email.utils import formatdate from functools import partial, wraps -from enum import Enum -from flask import Blueprint, Response, request, make_response, jsonify, session, url_for +from flask import Blueprint, request, session from flask_restful import Resource, abort, Api, reqparse from flask_restful.utils.cors import crossdomain from jsonschema import validate, ValidationError -import features - from app import app, metric_queue from data import model 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.decorators import process_oauth from endpoints.csrf import csrf_protect -from endpoints.exception import (ApiException, Unauthorized, InvalidRequest, InvalidResponse, +from endpoints.exception import (Unauthorized, InvalidRequest, InvalidResponse, FreshLoginRequired, NotFound) 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.names import parse_namespace_repository from util.pagination import encrypt_page_token, decrypt_page_token -from util.useremails import CannotSendEmailException + logger = logging.getLogger(__name__) api_bp = Blueprint('api', __name__) diff --git a/endpoints/api/repositorynotification.py b/endpoints/api/repositorynotification.py index 6ff5bb7f2..767a7c8c5 100644 --- a/endpoints/api/repositorynotification.py +++ b/endpoints/api/repositorynotification.py @@ -1,47 +1,20 @@ """ List, create and manage repository events/notifications. """ -import json - import logging from flask import request -from app import notification_queue from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, log_action, validate_json_request, request_error, path_param, disallow_for_app_repositories) from endpoints.exception import NotFound -from endpoints.notificationevent import NotificationEvent from endpoints.notificationmethod import (NotificationMethod, CannotValidateNotificationMethodException) from endpoints.notificationhelper import build_notification_data -from data import model 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__) -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//notification/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @@ -86,41 +59,39 @@ class RepositoryNotificationList(RepositoryParamResource): @nickname('createRepoNotification') @disallow_for_app_repositories @validate_json_request('NotificationCreateRequest') - def post(self, namespace, repository): - """ Create a new notification for the specified repository. """ - repo = model.repository.get_repository(namespace, repository) + def post(self, namespace_name, repository_name): parsed = request.get_json() - + repository = model.get_repository(namespace_name, repository_name) + method_handler = NotificationMethod.get_method(parsed['method']) - if not method_handler: - raise request_error(message='Unknown method') - try: - method_handler.validate(repo, parsed['config']) + method_handler.validate(repository, parsed['config']) except CannotValidateNotificationMethodException as ex: 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'], - parsed['method'], parsed['config'], - parsed['eventConfig'], - parsed.get('title', None)) - - resp = notification_view(new_notification) - log_action('add_repo_notification', namespace, - {'repo': repository, 'namespace': namespace, + log_action('add_repo_notification', namespace_name, + {'repo': repository_name, 'namespace': namespace_name, 'notification_id': new_notification.uuid, 'event': parsed['event'], 'method': parsed['method']}, - repo=repo) - return resp, 201 + repo=repository) + return new_notification.to_dict(), 201 @require_repo_admin @nickname('listRepoNotifications') @disallow_for_app_repositories - def get(self, namespace, repository): + def get(self, namespace_name, repository_name): """ 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 { - '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 @nickname('getRepoNotification') @disallow_for_app_repositories - def get(self, namespace, repository, uuid): + def get(self, namespace_name, repository_name, uuid): """ Get information for the specified notification. """ try: - found = model.notification.get_repo_notification(uuid) + found = model.get_repo_notification(uuid) except model.InvalidNotificationException: raise NotFound() - if (found.repository.namespace_user.username != namespace or - found.repository.name != repository): - raise NotFound() - - return notification_view(found) + return found.to_dict() @require_repo_admin @nickname('deleteRepoNotification') @disallow_for_app_repositories - def delete(self, namespace, repository, uuid): + def delete(self, namespace_name, repository_name, uuid): """ Deletes the specified notification. """ - deleted = model.notification.delete_repo_notification(namespace, repository, uuid) - log_action('delete_repo_notification', namespace, - {'repo': repository, 'namespace': namespace, 'notification_id': uuid, + repository = model.get_repository(namespace_name, repository_name) + deleted = model.delete_repo_notification(repository, 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}, - repo=model.repository.get_repository(namespace, repository)) + repo=repository) return 'No Content', 204 @require_repo_admin @nickname('resetRepositoryNotificationFailures') @disallow_for_app_repositories - def post(self, namespace, repository, uuid): + def post(self, namespace_name, repository_name, uuid): """ 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: - log_action('reset_repo_notification', namespace, - {'repo': repository, 'namespace': namespace, 'notification_id': uuid, + log_action('reset_repo_notification', namespace_name, + {'repo': repository_name, 'namespace': namespace_name, 'notification_id': uuid, 'event': reset.event.name, 'method': reset.method.name}, - repo=model.repository.get_repository(namespace, repository)) + repo=repository) return 'No Content', 204 @@ -183,23 +152,8 @@ class TestRepositoryNotification(RepositoryParamResource): @disallow_for_app_repositories def post(self, namespace, repository, uuid): """ Queues a test notification for this repository. """ - try: - test_note = model.notification.get_repo_notification(uuid) - except model.InvalidNotificationException: + test_note = model.get_repo_notification(uuid) + if not test_note: raise NotFound() - - 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)) - + model.queue_test_notification(test_note) return {} diff --git a/endpoints/api/repositorynotification_models_interface.py b/endpoints/api/repositorynotification_models_interface.py new file mode 100644 index 000000000..fc4e3ee6f --- /dev/null +++ b/endpoints/api/repositorynotification_models_interface.py @@ -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 diff --git a/endpoints/api/repositorynotification_models_pre_oci.py b/endpoints/api/repositorynotification_models_pre_oci.py new file mode 100644 index 000000000..2dc578fa9 --- /dev/null +++ b/endpoints/api/repositorynotification_models_pre_oci.py @@ -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() \ No newline at end of file