the-custodian/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_rollout.py
codex 26d4efd280
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 14s
Prepare exact guarded JSON-digest scanner rollout from founder preflight
2026-11-02 01:25:37 +01:00

97 lines
5.3 KiB
Python

"""Guard semantics and recovery tests; no Kubernetes access."""
from pathlib import Path
import copy
import importlib.util
import json
import unittest
from unittest.mock import patch
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('scanner_rollout', HERE / 'rollout.py')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
C = json.loads((HERE / 'inputs.json').read_bytes())
def deployment():
init = [{'name': 'unrelated'}] * (C['guard_index'] + 1)
init[C['guard_index']] = {'name': 'verify-secret-scanner', 'command': C['guard_command']}
volumes = [{'name': 'unrelated'}] * (C['volume_index'] + 1)
volumes[C['volume_index']] = C['volume']
return copy.deepcopy({'metadata': {'uid': C['deployment_uid'], 'resourceVersion': '123'},
'spec': {'strategy': C['strategy'], 'template': {'spec': {
'initContainers': init, 'volumes': volumes,
'containers': [{'name': 'gitea', 'image': 'existing-image', 'env': [{'name': 'public', 'value': 'preserved'}]}]}}}})
def apply_patch(d, operations):
value = copy.deepcopy(d)
for op in operations:
parts = op['path'].strip('/').split('/'); obj = value
for key in parts[:-1]: obj = obj[int(key)] if isinstance(obj, list) else obj[key]
key = int(parts[-1]) if isinstance(obj, list) else parts[-1]
if op['op'] == 'test':
if obj[key] != op['value']: raise ValueError('test-failed')
else: obj[key] = op['value']
return value
class Tests(unittest.TestCase):
def test_forward_and_reverse_change_only_two_selected_fields(self):
before = deployment(); ops = m.patch_for(before, C)
self.assertEqual(sum(o['op'] == 'replace' for o in ops), 2)
after = apply_patch(before, ops)
self.assertEqual(after['spec']['template']['spec']['containers'], before['spec']['template']['spec']['containers'])
self.assertEqual(apply_patch(after, m.patch_for(after, C, True)), before)
def test_identity_guard_volume_strategy_and_resource_version_drift_refused(self):
for key in ('uid', 'guard', 'volume', 'strategy'):
d = deployment(); s = d['spec']['template']['spec']
if key == 'uid': d['metadata']['uid'] = 'other'
elif key == 'guard': s['initContainers'][C['guard_index']]['command'] = ['other']
elif key == 'volume': s['volumes'][C['volume_index']]['configMap']['name'] = 'other'
else: d['spec']['strategy'] = {'type': 'RollingUpdate'}
with self.subTest(key=key), self.assertRaises(RuntimeError): m.patch_for(d, C)
d = deployment(); ops = m.patch_for(d, C); d['metadata']['resourceVersion'] = '124'
with self.assertRaises(ValueError): apply_patch(d, ops)
def test_inventory_requires_all_scanner_hooks_and_preserves_protected_set(self):
repo = '/data/git/gitea-repositories/coulomb/example.git'
raw = ('775 1000 1000 ' + C['old_hook_sha256'] + ' ' + repo + '/hooks/' + m.HOOK + '\n' +
'755 1000 1000 ' + 'a'*64 + ' ' + repo + '/hooks/pre-receive\n').encode()
before = m.parse_inventory(raw, C, {C['old_hook_sha256']})
m.protected_equal(before, copy.deepcopy(before))
changed = copy.deepcopy(before); changed[repo]['pre-receive']['sha256'] = 'b'*64
with self.assertRaises(RuntimeError): m.protected_equal(before, changed)
with self.assertRaises(RuntimeError): m.parse_inventory(raw.splitlines(keepends=True)[1], C, {C['old_hook_sha256']})
with self.assertRaises(RuntimeError): m.parse_inventory(raw + raw, C, {C['old_hook_sha256']})
def test_lost_patch_response_attempts_guarded_recovery_and_retains_failure(self):
class Simulated(m.Rollout):
def __init__(self): super().__init__(C); self.recovered = False
def preflight(self): return deployment(), 'pod', {}
def event(self, name, **kw): self.receipt['events'].append({'event': name, **kw})
def shell(self, *a, **kw): return b'{}'
def probe(self, *a): pass
def migrate(self, *a): return {}
def deployment(self): return deployment()
def k(self, *a, **kw):
if 'patch' in a: raise RuntimeError('lost-response-after-patch')
return b''
def rollback(self, before=None): self.recovered = True
x = Simulated()
with self.assertRaisesRegex(RuntimeError, 'lost-response'): x.apply(Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'))
self.assertTrue(x.recovered)
self.assertIn('activation-failed', [e['event'] for e in x.receipt['events']])
self.assertNotIn('activation-verified', [e['event'] for e in x.receipt['events']])
def test_rollback_refuses_unknown_deployment_instead_of_overwriting(self):
class Simulated(m.Rollout):
def target(self): pass
def deployment(self):
d = deployment(); d['spec']['template']['spec']['initContainers'][C['guard_index']]['command'] = ['unknown']
return d
def k(self, *a, **kw): raise AssertionError('must not mutate')
with self.assertRaisesRegex(RuntimeError, 'unknown-guard'): Simulated(C).rollback()
if __name__ == '__main__': unittest.main()