refactor: use injected config sources for retention
Allow retention planning and pruning to use the same ConfigSource abstraction as scheduled backups. This removes the remaining SQL-to-YAML export dependency from Django backup runs with pruning, keeping YAML only as a legacy CLI compatibility path.
This commit is contained in:
@@ -145,7 +145,7 @@ Run a backup through Django while still using the existing pobsync engine:
|
|||||||
python3 manage.py run_pobsync_backup <host> --prefix /opt/pobsync --prune
|
python3 manage.py run_pobsync_backup <host> --prefix /opt/pobsync --prune
|
||||||
```
|
```
|
||||||
|
|
||||||
The Django backup command reads config from SQL directly. Runtime YAML export is kept as a compatibility tool for older CLI flows and retention planning during the transition.
|
The Django backup command reads backup and retention config from SQL directly. Runtime YAML export is kept as a compatibility tool for older CLI flows during the transition.
|
||||||
|
|
||||||
Export database configs to the runtime YAML files consumed by the current engine:
|
Export database configs to the runtime YAML files consumed by the current engine:
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import shutil
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from ..config.source import ConfigSource
|
||||||
from ..errors import ConfigError
|
from ..errors import ConfigError
|
||||||
from ..lock import acquire_host_lock
|
from ..lock import acquire_host_lock
|
||||||
from ..paths import PobsyncPaths
|
from ..paths import PobsyncPaths
|
||||||
@@ -19,6 +20,7 @@ def run_retention_apply(
|
|||||||
yes: bool,
|
yes: bool,
|
||||||
max_delete: int,
|
max_delete: int,
|
||||||
acquire_lock: bool = True,
|
acquire_lock: bool = True,
|
||||||
|
config_source: ConfigSource | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
host = sanitize_host(host)
|
host = sanitize_host(host)
|
||||||
|
|
||||||
@@ -34,7 +36,13 @@ def run_retention_apply(
|
|||||||
paths = PobsyncPaths(home=prefix)
|
paths = PobsyncPaths(home=prefix)
|
||||||
|
|
||||||
def _do_apply() -> dict[str, Any]:
|
def _do_apply() -> dict[str, Any]:
|
||||||
plan = run_retention_plan(prefix=prefix, host=host, kind=kind, protect_bases=bool(protect_bases))
|
plan = run_retention_plan(
|
||||||
|
prefix=prefix,
|
||||||
|
host=host,
|
||||||
|
kind=kind,
|
||||||
|
protect_bases=bool(protect_bases),
|
||||||
|
config_source=config_source,
|
||||||
|
)
|
||||||
|
|
||||||
delete_list = plan.get("delete") or []
|
delete_list = plan.get("delete") or []
|
||||||
if not isinstance(delete_list, list):
|
if not isinstance(delete_list, list):
|
||||||
@@ -100,4 +108,3 @@ def run_retention_apply(
|
|||||||
|
|
||||||
# Caller guarantees locking (used by run-scheduled)
|
# Caller guarantees locking (used by run-scheduled)
|
||||||
return _do_apply()
|
return _do_apply()
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from ..config.load import load_global_config, load_host_config
|
from ..config.source import ConfigSource, FileConfigSource
|
||||||
from ..config.merge import build_effective_config
|
|
||||||
from ..errors import ConfigError
|
from ..errors import ConfigError
|
||||||
from ..paths import PobsyncPaths
|
from ..paths import PobsyncPaths
|
||||||
from ..retention import Snapshot, build_retention_plan
|
from ..retention import Snapshot, build_retention_plan
|
||||||
@@ -82,7 +81,13 @@ def _apply_base_protection(
|
|||||||
return keep, reasons
|
return keep, reasons
|
||||||
|
|
||||||
|
|
||||||
def run_retention_plan(prefix: Path, host: str, kind: str, protect_bases: bool) -> dict[str, Any]:
|
def run_retention_plan(
|
||||||
|
prefix: Path,
|
||||||
|
host: str,
|
||||||
|
kind: str,
|
||||||
|
protect_bases: bool,
|
||||||
|
config_source: ConfigSource | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
host = sanitize_host(host)
|
host = sanitize_host(host)
|
||||||
|
|
||||||
if kind not in {"scheduled", "manual", "all"}:
|
if kind not in {"scheduled", "manual", "all"}:
|
||||||
@@ -90,9 +95,8 @@ def run_retention_plan(prefix: Path, host: str, kind: str, protect_bases: bool)
|
|||||||
|
|
||||||
paths = PobsyncPaths(home=prefix)
|
paths = PobsyncPaths(home=prefix)
|
||||||
|
|
||||||
global_cfg = load_global_config(paths.global_config_path)
|
source = config_source or FileConfigSource(prefix=paths.home)
|
||||||
host_cfg = load_host_config(paths.hosts_dir / f"{host}.yaml")
|
cfg = source.effective_config_for_host(host)
|
||||||
cfg = build_effective_config(global_cfg, host_cfg)
|
|
||||||
|
|
||||||
retention = cfg.get("retention")
|
retention = cfg.get("retention")
|
||||||
if not isinstance(retention, dict):
|
if not isinstance(retention, dict):
|
||||||
@@ -161,4 +165,3 @@ def run_retention_plan(prefix: Path, host: str, kind: str, protect_bases: bool)
|
|||||||
],
|
],
|
||||||
"reasons": reasons,
|
"reasons": reasons,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ def run_scheduled(
|
|||||||
yes=True,
|
yes=True,
|
||||||
max_delete=10 if prune_max_delete is None else int(prune_max_delete),
|
max_delete=10 if prune_max_delete is None else int(prune_max_delete),
|
||||||
acquire_lock=False,
|
acquire_lock=False,
|
||||||
|
config_source=source,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from pobsync.commands.run_scheduled import run_scheduled
|
from pobsync.commands.run_scheduled import run_scheduled
|
||||||
from pobsync.paths import PobsyncPaths
|
from pobsync.paths import PobsyncPaths
|
||||||
from pobsync_backend.config_repository import export_runtime_configs
|
|
||||||
from pobsync_backend.config_source import DjangoConfigSource
|
from pobsync_backend.config_source import DjangoConfigSource
|
||||||
from pobsync_backend.models import BackupRun, HostConfig
|
from pobsync_backend.models import BackupRun, HostConfig
|
||||||
|
|
||||||
@@ -33,11 +32,6 @@ class Command(BaseCommand):
|
|||||||
except HostConfig.DoesNotExist as exc:
|
except HostConfig.DoesNotExist as exc:
|
||||||
raise CommandError(f"Missing enabled HostConfig {host_name!r}") from exc
|
raise CommandError(f"Missing enabled HostConfig {host_name!r}") from exc
|
||||||
|
|
||||||
# Compatibility bridge: retention planning still reads runtime YAML.
|
|
||||||
# The backup run itself receives config directly from Django.
|
|
||||||
if options["prune"]:
|
|
||||||
export_runtime_configs(prefix=paths.home, host=host.host)
|
|
||||||
|
|
||||||
run = BackupRun.objects.create(
|
run = BackupRun.objects.create(
|
||||||
host=host,
|
host=host,
|
||||||
run_type=BackupRun.RunType.SCHEDULED,
|
run_type=BackupRun.RunType.SCHEDULED,
|
||||||
|
|||||||
53
src/pobsync_backend/tests/test_retention_config_source.py
Normal file
53
src/pobsync_backend/tests/test_retention_config_source.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from pobsync.commands.retention_plan import run_retention_plan
|
||||||
|
from pobsync.util import write_yaml_atomic
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConfigSource:
|
||||||
|
def __init__(self, backup_root: str) -> None:
|
||||||
|
self.backup_root = backup_root
|
||||||
|
|
||||||
|
def effective_config_for_host(self, host: str) -> dict:
|
||||||
|
return {
|
||||||
|
"backup_root": self.backup_root,
|
||||||
|
"host": host,
|
||||||
|
"address": "example.test",
|
||||||
|
"retention": {"daily": 1, "weekly": 0, "monthly": 0, "yearly": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RetentionConfigSourceTests(SimpleTestCase):
|
||||||
|
def test_retention_plan_uses_injected_config_source(self) -> None:
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
root = Path(tmp) / "backups"
|
||||||
|
snap_dir = root / "web-01" / "scheduled" / "20260519-021500Z__ABCDEFGH"
|
||||||
|
meta_dir = snap_dir / "meta"
|
||||||
|
meta_dir.mkdir(parents=True)
|
||||||
|
write_yaml_atomic(
|
||||||
|
meta_dir / "meta.yaml",
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"started_at": datetime(2026, 5, 19, 2, 15, tzinfo=timezone.utc)
|
||||||
|
.isoformat()
|
||||||
|
.replace("+00:00", "Z"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
plan = run_retention_plan(
|
||||||
|
prefix=Path("/missing-prefix"),
|
||||||
|
host="web-01",
|
||||||
|
kind="scheduled",
|
||||||
|
protect_bases=False,
|
||||||
|
config_source=FakeConfigSource(str(root)),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(plan["ok"])
|
||||||
|
self.assertEqual(plan["keep"], ["20260519-021500Z__ABCDEFGH"])
|
||||||
|
self.assertEqual(plan["delete"], [])
|
||||||
Reference in New Issue
Block a user