From fa08055778d7b3900fdbbd346298ed9a6479067e Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Sun, 19 Jul 2026 06:41:30 +0200 Subject: [PATCH] Repair missing Store configuration before scanner setup --- src/apps/config/tests/test_cli.py | 36 ++++++++++++++++++++++ src/libs/python/bongo/configuration/cli.py | 32 ++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/apps/config/tests/test_cli.py b/src/apps/config/tests/test_cli.py index d78b281..1aed54f 100644 --- a/src/apps/config/tests/test_cli.py +++ b/src/apps/config/tests/test_cli.py @@ -40,6 +40,17 @@ from bongo.configuration.storage import ConfigurationError # noqa: E402 class FakeStore: def __init__(self): self.saved = [] + self.load_errors = [] + + def load_live(self): + if self.load_errors: + error = self.load_errors.pop(0) + if error: + raise error + return configurations() + + def validate(self, _configurations): + return [] def save(self, configurations): self.saved.append(copy.deepcopy(configurations)) @@ -83,6 +94,31 @@ class ConfigurationCliTest(unittest.TestCase): self.assertEqual(len(store.saved), 2) self.assertEqual(store.saved[1], original) + def test_missing_new_store_document_is_synchronized_before_save(self): + store = FakeStore() + store.load_errors = [ + ConfigurationError("cannot read configuration acme"), None, + ] + original = configurations() + with mock.patch.object(cli, "_reload_manager") as reload_manager, \ + mock.patch.object(cli.time, "sleep") as sleep: + cli._activate_verified_scanners( + store, original, {"SpamAssassin"}) + self.assertEqual(len(store.saved), 1) + self.assertEqual(reload_manager.call_count, 2) + sleep.assert_not_called() + + def test_incomplete_store_sync_stops_before_scanner_activation(self): + store = FakeStore() + store.load_errors = [ConfigurationError("missing acme")] * 21 + with mock.patch.object(cli, "_reload_manager"), \ + mock.patch.object(cli.time, "sleep"): + with self.assertRaisesRegex( + ConfigurationError, "did not synchronize every"): + cli._activate_verified_scanners( + store, configurations(), {"SpamAssassin"}) + self.assertEqual(store.saved, []) + if __name__ == "__main__": unittest.main() diff --git a/src/libs/python/bongo/configuration/cli.py b/src/libs/python/bongo/configuration/cli.py index b0609fc..87bb7fb 100644 --- a/src/libs/python/bongo/configuration/cli.py +++ b/src/libs/python/bongo/configuration/cli.py @@ -28,6 +28,7 @@ import curses import os import subprocess import sys +import time from pathlib import Path from .acme_dns import provision_acme_dns @@ -35,7 +36,7 @@ from .model import enable_scanner_agents, errors from .scanner_config import ScannerTransaction from .spamassassin_rules import key_change_warnings from .security_material import install_dns_tasks, provision_security_material -from .storage import ConfigurationError, ConfigurationStore +from .storage import ConfigurationError, ConfigurationStore, ValidationError from .tui import AdminEditor, Cancelled, scanner_setup_wizard, setup_wizard @@ -69,12 +70,41 @@ def _reload_manager() -> None: "bongo-manager") +def _ensure_live_configuration( + store: ConfigurationStore, local_configuration: dict) -> None: + """Ask the manager to backfill Store documents added after setup.""" + invalid = errors(store.validate(local_configuration)) + if invalid: + raise ValidationError(invalid) + initial_error = None + try: + store.load_live() + return + except ConfigurationError as error: + initial_error = error + _reload_manager() + + last_error = initial_error + for attempt in range(20): + try: + store.load_live() + return + except ConfigurationError as error: + last_error = error + if attempt != 19: + time.sleep(0.1) + raise ConfigurationError( + "bongo-manager did not synchronize every local configuration " + f"document into the Store: {last_error}") from initial_error + + def _activate_verified_scanners( store: ConfigurationStore, originals: dict, scanners: set[str]) -> None: if not scanners: return + _ensure_live_configuration(store, originals) updated = copy.deepcopy(originals) enable_scanner_agents(updated, scanners) store.save(updated)