atlas-iac/testing/tests/test_node_pod_log_cleanup.py

135 lines
5.0 KiB
Python

"""Exercise orphan cleanup against temporary kubelet and log inventories."""
import importlib.util
import json
import os
from pathlib import Path
import time
import pytest
SOURCE = Path(__file__).resolve().parents[2] / 'services/maintenance/node-ops/scripts/node_pod_log_cleanup.py'
spec = importlib.util.spec_from_file_location('node_pod_log_cleanup', SOURCE)
cleaner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cleaner)
ACTIVE = '11111111-1111-1111-1111-111111111111'
ORPHAN = '22222222-2222-2222-2222-222222222222'
def inventory(root):
"""Create one current pod with a UID-only kubelet directory."""
(root / 'var/lib/kubelet/pods' / ACTIVE).mkdir(parents=True)
def logs(root, uid, relative='var/log/pods', old=True):
"""Create the real namespace_name_UID directory layout with a synthetic log."""
path = root / relative / ('namespace_name_' + uid)
(path / 'container').mkdir(parents=True)
(path / 'container/0.log').write_text('SYNTHETIC LOG CONTENT')
if old:
stamp = time.time() - 10 * 86400
for entry in [*path.rglob('*'), path]:
os.utime(entry, (stamp, stamp))
return path
def test_active_uid_preserves_old_current_and_hdd_logs(tmp_path):
"""Old quiet containers remain protected in both log locations."""
inventory(tmp_path)
paths = [logs(tmp_path, ACTIVE, relative=r) for r in ('var/log/pods', 'var/log.hdd/pods')]
assert cleaner.cleanup(tmp_path, 3)['removed'] == 0
assert all((p / 'container/0.log').exists() for p in paths)
def test_only_expired_orphans_are_removed(tmp_path):
"""Dry-run reports the same eligible directory without deleting it."""
inventory(tmp_path)
path = logs(tmp_path, ORPHAN)
assert cleaner.cleanup(tmp_path, 3, dry_run=True)['eligible'] == 1
assert path.exists()
assert cleaner.cleanup(tmp_path, 3)['removed'] == 1
assert not path.exists()
def test_recent_log_preserves_old_parent_directory(tmp_path):
"""Writing a log does not update the pod directory's own mtime."""
inventory(tmp_path)
path = logs(tmp_path, ORPHAN)
os.utime(path / 'container/0.log', None)
assert cleaner.cleanup(tmp_path, 3)['skipped'] == 1
assert path.exists()
@pytest.mark.parametrize('empty', [False, True])
def test_unavailable_inventory_cannot_authorize_deletion(tmp_path, empty):
"""Missing and empty inventories both preserve all candidate logs."""
if empty:
(tmp_path / 'var/lib/kubelet/pods').mkdir(parents=True)
path = logs(tmp_path, ORPHAN)
assert cleaner.cleanup(tmp_path, 3)['inventory_unavailable'] == 1
assert path.exists()
def test_symlinks_unknown_names_and_nondirectories_are_preserved(tmp_path):
"""Only recognized pod directories enter deletion; links never escape root."""
inventory(tmp_path)
parent = tmp_path / 'var/log/pods'
parent.mkdir(parents=True)
outside = tmp_path / 'outside'
outside.mkdir()
(parent / ('ns_link_' + ORPHAN)).symlink_to(outside)
(parent / 'unrecognized').mkdir()
(parent / ('ns_file_' + ORPHAN)).write_text('metadata')
assert cleaner.cleanup(tmp_path, 3)['removed'] == 0
assert outside.exists()
assert (parent / 'unrecognized').exists()
def test_scan_failure_preserves_candidate(tmp_path, monkeypatch):
"""An unreadable log subtree must not look like an old empty directory."""
inventory(tmp_path)
path = logs(tmp_path, ORPHAN)
def fail_scan(*args, **kwargs):
"""Simulate the walker reporting a storage error."""
kwargs['onerror'](OSError('synthetic scan failure'))
monkeypatch.setattr(cleaner.os, 'walk', fail_scan)
assert cleaner.cleanup(tmp_path, 3)['skipped'] == 1
assert path.exists()
@pytest.mark.parametrize('new_active', [True, False])
def test_inventory_change_before_deletion_is_rechecked(tmp_path, monkeypatch, new_active):
"""A newly active UID or vanished inventory invalidates the initial decision."""
inventory(tmp_path)
path = logs(tmp_path, ORPHAN)
original = cleaner.newest_mtime
def change_inventory(candidate):
"""Alter only inventory metadata after the age scan."""
newest = original(candidate)
pods = tmp_path / 'var/lib/kubelet/pods'
if new_active:
(pods / ORPHAN).mkdir()
else:
(pods / ACTIVE).rmdir()
return newest
monkeypatch.setattr(cleaner, 'newest_mtime', change_inventory)
assert cleaner.cleanup(tmp_path, 3)['removed'] == 0
assert path.exists()
def test_cli_reports_counts_and_rejects_zero_retention(tmp_path, monkeypatch, capsys):
"""No content or candidate path enters the operator's cleanup summary."""
inventory(tmp_path)
path = logs(tmp_path, ORPHAN)
monkeypatch.setattr('sys.argv', ['clean', '--host-root', str(tmp_path), '--dry-run'])
cleaner.main()
result = json.loads(capsys.readouterr().out)
assert result['eligible'] == 1 and result['removed'] == 0
assert path.exists()
with pytest.raises(ValueError, match='at least one day'):
cleaner.cleanup(tmp_path, 0)