diff --git a/README.md b/README.md index afbf6b1..32f2a9c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ like `cron`, or a Kubernetes CronJob. - [Discord](#discord) - [Slack](#slack) - [Telegram](#telegram) + - [Json](#json) - [Rotation](#rotation) - [Cooldown](#cooldown) @@ -503,8 +504,8 @@ You can also define a custom location (root is App Folder) using the ## Notifiers `blackbox` also implements different _notifiers_, which is how it reports the -result of one of its jobs to you. Right now we only support **Discord** and **Slack** -and **Telegram**, but if you need a specific notifier, feel free to open an issue. +result of one of its jobs to you. Right now we only support the below listed notifiers, +but if you need a specific notifier, feel free to open an issue. To configure notifiers, add a section with this format: @@ -572,6 +573,39 @@ Modern: ![blackbox](https://github.com/lemonsaurus/blackbox/raw/main/img/blackbox_telegram_fail.png) +### Json +- **Notifier Type**: `json` +- **Required fields**: `url` +- YAML will look like this: +```notifiers: + json: + json_1: + url: https://mydomain.com/api/blackbox-notifications +``` + +**Note**: All notifications are sent as a `POST` request because the data will live within the request's body. + +The body of the HTTP request will look like this + +```json +{ + "backup-data": [ + { + "source": "main_postgres", + "success": true, + "output": "", + "backup": [ + { + "name": "main_dropbox", + "success": true + } + ] + } + ] +} +``` + + ## Rotation By default, `blackbox` will automatically remove all backup files older than 7 diff --git a/blackbox/__init__.py b/blackbox/__init__.py index 93274ea..59ef657 100644 --- a/blackbox/__init__.py +++ b/blackbox/__init__.py @@ -1,4 +1,6 @@ from pathlib import Path + from single_source import get_version -__version__ = get_version(__name__, Path(__file__).parent.parent) \ No newline at end of file + +__version__ = get_version(__name__, Path(__file__).parent.parent) diff --git a/blackbox/handlers/notifiers/__init__.py b/blackbox/handlers/notifiers/__init__.py index 0244091..8a5c044 100644 --- a/blackbox/handlers/notifiers/__init__.py +++ b/blackbox/handlers/notifiers/__init__.py @@ -1,4 +1,5 @@ from ._base import BlackboxNotifier from .discord import Discord +from .json import Json from .slack import Slack from .telegram import Telegram diff --git a/blackbox/handlers/notifiers/json.py b/blackbox/handlers/notifiers/json.py new file mode 100644 index 0000000..522e371 --- /dev/null +++ b/blackbox/handlers/notifiers/json.py @@ -0,0 +1,43 @@ +import requests + +from blackbox.handlers.notifiers._base import BlackboxNotifier + + +class Json(BlackboxNotifier): + """A notifier for sending webhooks to a backend.""" + + required_fields = ("url",) + + def _parse_report(self) -> dict: + """Turn the report into something the notify function can use.""" + payload = [] + + # Iterate over each database report we have + # For each report, we will include + # 1. Which database are we backing up ? + # 2. Was the backup successful overall ? + # 3. Any output that we might have gotten back during the backup + for database in self.report.databases: + database_payload = { + "source": database.database_id, + "success": database.success, + "output": database.output or None + } + + storages_payload = [] + # A single database can be backed up in multiple storage points + # For each database, we include the storage provider and + # whether the backup succeeded or not + # for that particular storage point. + for provider in database.storages: + storages_payload.append({"name": provider.storage_id, "success": provider.success}) + + # Aggregate the storage points data with the current database + database_payload['backup'] = storages_payload + payload.append(database_payload) + + return {"backup-data": payload} + + def notify(self): + """Send a webhook to a particular url with a blackbox report.""" + requests.post(self.config["url"], json=self._parse_report()) diff --git a/tests/test_json.py b/tests/test_json.py new file mode 100644 index 0000000..acef05a --- /dev/null +++ b/tests/test_json.py @@ -0,0 +1,80 @@ +import pytest +import requests_mock + +from blackbox.exceptions import MissingFields +from blackbox.handlers.notifiers.json import Json +from blackbox.utils import reports + + +URL = "https://some-domain.com/api/blackbox-notifications" + + +@pytest.fixture +def mock_valid_json_config(): + """Mock valid Json config.""" + return {"url": URL} + + +@pytest.fixture +def mock_invalid_json_config(): + """Mock invalid Json config.""" + return {"key": "value"} + + +def test_json_handler_can_be_instantiated_with_required_fields(mock_valid_json_config): + Json(**mock_valid_json_config) + + +def test_json_handler_fails_without_required_fields(mock_invalid_json_config): + """Test if the json notifier handler cannot be instantiated with missing fields.""" + with pytest.raises(MissingFields): + Json(**mock_invalid_json_config) + + +def test_json_notifier(mock_valid_json_config, report): + """Test report parsing for raw JSON notifications.""" + json_notifier = Json(**mock_valid_json_config) + json_notifier.report = report + + expected_report = { + "backup-data": [ + { + "source": "main_mongo", + "output": "salad", + "backup": [ + { + "name": "main_s3", + "success": True + } + ], + "success": True + }, + { + "source": "secondary_mongo", + "output": "ham-sandwich", + "backup": [ + { + "name": "main_dropbox", + "success": True + }, + { + "name": "secondary_s3", + "success": False + } + ], + "success": False + } + ] + } + + database = reports.DatabaseReport(database_id="secondary_mongo", success=False, output='') + database.report_storage("main_dropbox", True, "ham-") + database.report_storage("secondary_s3", False, "sandwich") + + report.databases.append(database) + + with requests_mock.Mocker() as m: + adapter = m.post(URL) + json_notifier.notify() + assert adapter.call_count == 1 + assert adapter.last_request.json() == expected_report