Enable safe NPM replica convergence

This commit is contained in:
Lyncoln
2026-09-20 10:37:11 -04:00
parent 291f4f492e
commit e4ca80d4d5
7 changed files with 351 additions and 16 deletions
+142
View File
@@ -655,6 +655,148 @@ class ExecutionTests(unittest.TestCase):
)
self.assertEqual(store.load_applied_state(), [])
def test_preflight_requires_retire_for_planned_retirement_before_mutation(
self,
):
endpoint = Endpoint("npm", "192.168.1.4:81")
inner = FakeAdapter(
endpoint,
capabilities(CERTIFICATES),
)
class NoRetireAdapter:
def __init__(self, wrapped):
self.endpoint = wrapped.endpoint
self.capabilities = wrapped.capabilities
self.wrapped = wrapped
def apply(self, plan_action):
return self.wrapped.apply(plan_action)
def verify(self, object):
return self.wrapped.verify(object)
adapter = NoRetireAdapter(inner)
old_certificate = NormalizedObject(
object_type=CERTIFICATES,
logical_key="app.example.com",
local_key="67",
payload={
"domain_names": ["app.example.com"],
},
)
plan = EndpointPlan(
endpoint.identity,
(
action(
endpoint.identity,
PlanOperation.CREATE,
object_type=CERTIFICATES,
logical_key="other.example.com",
),
),
retirements=(
RetireRequest(
object=old_certificate,
),
),
)
with SQLiteStateStore(":memory:") as store:
with self.assertRaisesRegex(
RuntimeError,
"does not support deferred retirement",
):
execute_endpoint_plans(
[adapter],
[plan],
store,
executed_at=NOW,
)
self.assertEqual(
store.load_applied_state(),
[],
)
self.assertEqual(inner.events, [])
self.assertEqual(inner.applied_actions, [])
def test_preflight_requires_retire_for_replace_before_mutation(
self,
):
endpoint = Endpoint("npm", "192.168.1.4:81")
inner = FakeAdapter(
endpoint,
capabilities(CERTIFICATES),
)
class NoRetireAdapter:
def __init__(self, wrapped):
self.endpoint = wrapped.endpoint
self.capabilities = wrapped.capabilities
self.wrapped = wrapped
def apply(self, plan_action):
return self.wrapped.apply(plan_action)
def verify(self, object):
return self.wrapped.verify(object)
adapter = NoRetireAdapter(inner)
current = NormalizedObject(
object_type=CERTIFICATES,
logical_key="app.example.com",
local_key="67",
payload={
"domain_names": ["app.example.com"],
"expires_on": "2026-11-01T00:00:00+00:00",
},
)
desired = NormalizedObject(
object_type=CERTIFICATES,
logical_key="app.example.com",
payload={
"domain_names": ["app.example.com"],
"expires_on": "2026-12-01T00:00:00+00:00",
},
)
plan = EndpointPlan(
endpoint.identity,
(
PlanAction(
endpoint_id=endpoint.identity,
operation=PlanOperation.REPLACE,
desired=desired,
current=current,
),
),
)
with SQLiteStateStore(":memory:") as store:
with self.assertRaisesRegex(
RuntimeError,
"does not support deferred retirement",
):
execute_endpoint_plans(
[adapter],
[plan],
store,
executed_at=NOW,
)
self.assertEqual(
store.load_applied_state(),
[],
)
self.assertEqual(inner.events, [])
self.assertEqual(inner.applied_actions, [])
def test_preflight_requires_verify_before_any_mutation(self):
endpoint = Endpoint("verizon", "192.168.1.1:443")
adapter = FakeAdapter(
+9 -16
View File
@@ -94,7 +94,7 @@ class NpmAdapterTests(unittest.TestCase):
self.assertIs(adapters[0].client, first_client)
self.assertIs(adapters[1].client, second_client)
def test_capabilities_cover_discovery_and_verify_only(self):
def test_capabilities_cover_discovery_apply_and_verify(self):
adapter = self.adapter(Mock())
self.assertEqual(
@@ -105,6 +105,10 @@ class NpmAdapterTests(unittest.TestCase):
PROXY_HOSTS,
CapabilityOperation.DISCOVER,
),
Capability(
PROXY_HOSTS,
CapabilityOperation.APPLY,
),
Capability(
PROXY_HOSTS,
CapabilityOperation.VERIFY,
@@ -113,6 +117,10 @@ class NpmAdapterTests(unittest.TestCase):
CERTIFICATES,
CapabilityOperation.DISCOVER,
),
Capability(
CERTIFICATES,
CapabilityOperation.APPLY,
),
Capability(
CERTIFICATES,
CapabilityOperation.VERIFY,
@@ -121,21 +129,6 @@ class NpmAdapterTests(unittest.TestCase):
),
)
self.assertNotIn(
Capability(
PROXY_HOSTS,
CapabilityOperation.APPLY,
),
adapter.capabilities,
)
self.assertNotIn(
Capability(
CERTIFICATES,
CapabilityOperation.APPLY,
),
adapter.capabilities,
)
def test_discovers_proxy_hosts_with_endpoint_local_key(self):
client = Mock()
client.proxy_hosts.return_value = [
+103
View File
@@ -240,6 +240,109 @@ class ObservationAuthorityTests(unittest.TestCase):
)
)
def test_persisted_newer_certificate_cannot_regress_to_older_live_certificate(
self,
):
endpoint = Endpoint(
"npm",
"192.168.1.3:81",
)
live = FakeAdapter(
endpoint=endpoint,
capabilities=capability_set(
CERTIFICATES,
apply=True,
),
discovered={
CERTIFICATES: [
discovered_certificate(
"20",
valid_from=(
"2026-09-15T00:00:00+00:00"
),
expires_on=(
"2026-12-15T00:00:00+00:00"
),
)
]
},
)
persisted = normalized_certificate(
"99",
valid_from=(
"2026-09-19T00:00:00+00:00"
),
expires_on=(
"2026-12-19T00:00:00+00:00"
),
)
persisted = NormalizedObject(
object_type=persisted.object_type,
logical_key=persisted.logical_key,
payload=dict(persisted.payload),
)
with SQLiteStateStore(":memory:") as store:
store.replace_desired_state(
CERTIFICATES,
[persisted],
T1,
)
result = discover_reconcile_plan(
[live],
store,
target_ip="192.168.1.101",
run_at=T2,
)
stored = store.load_desired_state(
CERTIFICATES
)
certificates = [
object
for object in result.desired
if object.object_type == CERTIFICATES
]
self.assertEqual(len(certificates), 1)
self.assertEqual(
certificates[0].payload["valid_from"],
"2026-09-19T00:00:00+00:00",
)
self.assertEqual(
certificates[0].payload["expires_on"],
"2026-12-19T00:00:00+00:00",
)
self.assertEqual(len(stored), 1)
self.assertEqual(
stored[0].object,
persisted,
)
self.assertEqual(len(result.plans), 1)
self.assertEqual(
result.plans[0].endpoint_id,
endpoint.identity,
)
self.assertEqual(
len(result.plans[0].actions),
1,
)
self.assertEqual(
result.plans[0].actions[0].operation.value,
"replace",
)
self.assertEqual(
result.plans[0].actions[0].desired,
persisted,
)
def test_all_failed_sources_preserve_persisted_desired_without_planning(self):
npm_endpoint = Endpoint(
"npm",