Protect manifest artifacts in RC10 audit
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-18 07:15:40 +02:00
parent 6438bd418b
commit bb38d8a6a3
3 changed files with 75 additions and 5 deletions
@@ -146,6 +146,43 @@ def test_storage_audit_only_selects_old_unreferenced_allowlisted_files(
assert "release-evidence" in report["cleanup"]["protected_prefixes"] assert "release-evidence" in report["cleanup"]["protected_prefixes"]
def test_referenced_tile_manifest_protects_its_tiles(tmp_path: Path) -> None:
audit = load_script("audit_data_operations.py")
storage = tmp_path / "storage"
manifest = storage / "tiles" / "dataset" / "set" / "manifest.json"
tile = manifest.parent / "tile_0000.tif"
tile.parent.mkdir(parents=True)
tile.write_bytes(b"tile")
manifest.write_text(json.dumps({"tiles": [{"path": "tile_0000.tif"}]}), encoding="utf-8")
expanded = audit.expand_manifest_references({manifest.resolve()}, storage)
assert manifest.resolve() in expanded
assert tile.resolve() in expanded
def test_disk_pressure_uses_absolute_headroom_for_large_arrays(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
audit = load_script("audit_data_operations.py")
monkeypatch.setattr(
audit.shutil,
"disk_usage",
lambda _path: SimpleNamespace(
total=56 * 1024**4,
used=(56 * 1024**4) - (700 * 1024**3),
free=700 * 1024**3,
),
)
pressure = audit.disk_pressure(tmp_path)
assert pressure["free_percent"] < 2
assert pressure["status"] == "ok"
assert pressure["acquisition_allowed"] is True
def test_source_family_report_covers_national_regional_and_maritime( def test_source_family_report_covers_national_regional_and_maritime(
tmp_path: Path, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
+4 -2
View File
@@ -48,8 +48,10 @@ The command:
Use `--fail-on-pressure warning` for a stricter acquisition preflight and Use `--fail-on-pressure warning` for a stricter acquisition preflight and
`--fail-on-missing-reference` for release gates. `critical` disk pressure `--fail-on-missing-reference` for release gates. `critical` disk pressure
blocks acquisition by default. The thresholds are 10 GiB or 5% free for blocks acquisition by default. Critical means less than 10 GiB free. Warning
critical and 25 GiB or 10% free for warning. means less than 50 GiB free, or less than 2% and less than 250 GiB free. The
combined percentage/absolute rule avoids falsely blocking a large Unraid
array that still has hundreds of GiB available.
The existing project-level freshness policy remains authoritative: The existing project-level freshness policy remains authoritative:
+34 -3
View File
@@ -165,6 +165,33 @@ def normalize_storage_reference(value: str | None, storage_root: Path) -> Path |
return resolved return resolved
def expand_manifest_references(references: set[Path], storage_root: Path) -> set[Path]:
"""Protect files named by referenced, bounded JSON manifests."""
root = storage_root.resolve()
expanded = set(references)
for manifest in list(references):
if manifest.suffix.lower() != ".json" or not manifest.is_file():
continue
try:
if manifest.stat().st_size > 16 * 1024 * 1024:
continue
payload = json.loads(manifest.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
continue
for value in _iter_strings(payload):
normalized = normalize_storage_reference(value, root)
if normalized is None and "://" not in value:
try:
relative = (manifest.parent / value).resolve()
relative.relative_to(root)
except (OSError, ValueError):
continue
normalized = relative
if normalized is not None:
expanded.add(normalized)
return expanded
def _add_row_references(target: set[Path], row: Any, storage_root: Path, direct_fields: Iterable[str]) -> None: def _add_row_references(target: set[Path], row: Any, storage_root: Path, direct_fields: Iterable[str]) -> None:
for field in direct_fields: for field in direct_fields:
normalized = normalize_storage_reference(getattr(row, field, None), storage_root) normalized = normalize_storage_reference(getattr(row, field, None), storage_root)
@@ -327,9 +354,11 @@ def collect_database_state(db: Any, storage_root: Path) -> dict[str, Any]:
def disk_pressure(storage_root: Path) -> dict[str, Any]: def disk_pressure(storage_root: Path) -> dict[str, Any]:
usage = shutil.disk_usage(storage_root) usage = shutil.disk_usage(storage_root)
free_percent = (usage.free / usage.total * 100) if usage.total else 0.0 free_percent = (usage.free / usage.total * 100) if usage.total else 0.0
if usage.free < 10 * 1024**3 or free_percent < 5: # Absolute headroom governs very large Unraid arrays: a low percentage of
# tens of terabytes can still leave hundreds of GiB safely available.
if usage.free < 10 * 1024**3:
status = "critical" status = "critical"
elif usage.free < 25 * 1024**3 or free_percent < 10: elif usage.free < 50 * 1024**3 or (free_percent < 2 and usage.free < 250 * 1024**3):
status = "warning" status = "warning"
else: else:
status = "ok" status = "ok"
@@ -357,7 +386,7 @@ def build_report(
raise RuntimeError(f"Storage root is not a directory: {root}") raise RuntimeError(f"Storage root is not a directory: {root}")
records, skipped_symlinks = inventory_storage(root) records, skipped_symlinks = inventory_storage(root)
database = collect_database_state(db, root) database = collect_database_state(db, root)
references: set[Path] = database.pop("references") references: set[Path] = expand_manifest_references(database.pop("references"), root)
cutoff = utc_now() - timedelta(days=minimum_age_days) cutoff = utc_now() - timedelta(days=minimum_age_days)
categories: dict[str, dict[str, Any]] = defaultdict( categories: dict[str, dict[str, Any]] = defaultdict(
@@ -376,12 +405,14 @@ def build_report(
for path in references for path in references
if path not in existing_paths and not path.is_dir() if path not in existing_paths and not path.is_dir()
) )
referenced_directories = tuple(path for path in references if path.is_dir())
candidates = sorted( candidates = sorted(
( (
record record
for record in records for record in records
if record.cleanup_eligible if record.cleanup_eligible
and record.path not in references and record.path not in references
and not any(record.path.is_relative_to(reference) for reference in referenced_directories)
and record.modified_at <= cutoff and record.modified_at <= cutoff
and record.path.name not in IGNORED_FILENAMES and record.path.name not in IGNORED_FILENAMES
), ),