Add certificate replacement lifecycle

This commit is contained in:
Lyncoln
2026-09-20 01:44:56 -04:00
parent 4b094820b2
commit 2668651a98
7 changed files with 582 additions and 21 deletions
+198
View File
@@ -16,6 +16,7 @@ from dns_sync.adapters import (
Observation,
PlanAction,
PlanOperation,
RetireRequest,
)
from dns_sync.executor import execute_endpoint_plans
from dns_sync.persistence import SQLiteStateStore
@@ -117,6 +118,9 @@ class FakeAdapter:
events: list[tuple[str, str, str]] = field(
default_factory=list
)
retired: list[RetireRequest] = field(
default_factory=list
)
def apply(self, plan_action):
desired = plan_action.desired
@@ -163,6 +167,16 @@ class FakeAdapter:
raise self.verify_error
return self.verify_result
def retire(self, request):
self.events.append(
(
"retire",
request.object.object_type,
request.object.logical_key,
)
)
self.retired.append(request)
class ExecutionTests(unittest.TestCase):
def test_successful_actions_are_applied_verified_and_persisted(self):
@@ -216,6 +230,190 @@ class ExecutionTests(unittest.TestCase):
("apply", "verify", "apply", "verify"),
)
def test_replace_rebinds_dependent_before_retiring_old_object(self):
endpoint = Endpoint(
"npm",
"192.168.1.4:81",
)
class ReplacementAdapter(FakeAdapter):
def apply(self, plan_action):
result = super().apply(plan_action)
if (
plan_action.operation
== PlanOperation.REPLACE
):
return ApplyResult(
object=result.object,
detail="replacement",
retirements=(
RetireRequest(
object=plan_action.current,
),
),
)
return result
adapter = ReplacementAdapter(
endpoint,
capabilities(
CERTIFICATES,
PROXY_HOSTS,
),
apply_local_keys={
(
CERTIFICATES,
"app.example.com",
): "84",
(
PROXY_HOSTS,
"app.example.com",
): "12",
},
)
old_certificate = NormalizedObject(
object_type=CERTIFICATES,
logical_key="app.example.com",
local_key="67",
payload={
"domain_names": ["app.example.com"],
"valid_from": "2026-08-01T00:00:00+00:00",
"expires_on": "2026-11-01T00:00:00+00:00",
},
)
desired_certificate = NormalizedObject(
object_type=CERTIFICATES,
logical_key="app.example.com",
payload={
"domain_names": ["app.example.com"],
"valid_from": "2026-09-01T00:00:00+00:00",
"expires_on": "2026-12-01T00:00:00+00:00",
},
)
old_proxy = NormalizedObject(
object_type=PROXY_HOSTS,
logical_key="app.example.com",
local_key="12",
payload={
"domain_names": ["app.example.com"],
"forward_scheme": "https",
"forward_host": "backend.internal",
"forward_port": 443,
"enabled": True,
},
references=(
ObjectReference(
relation="certificate",
object_type=CERTIFICATES,
local_key="67",
),
),
)
desired_proxy = NormalizedObject(
object_type=PROXY_HOSTS,
logical_key="app.example.com",
payload=dict(old_proxy.payload),
references=(
ObjectReference(
relation="certificate",
object_type=CERTIFICATES,
logical_key="app.example.com",
),
),
)
plan = EndpointPlan(
endpoint.identity,
(
PlanAction(
endpoint_id=endpoint.identity,
operation=PlanOperation.REPLACE,
desired=desired_certificate,
current=old_certificate,
),
PlanAction(
endpoint_id=endpoint.identity,
operation=PlanOperation.UPDATE,
desired=desired_proxy,
current=old_proxy,
),
),
)
with SQLiteStateStore(":memory:") as store:
store.replace_observations(
endpoint.identity,
CERTIFICATES,
[
Observation(
endpoint.identity,
old_certificate,
NOW,
)
],
NOW,
)
store.replace_observations(
endpoint.identity,
PROXY_HOSTS,
[
Observation(
endpoint.identity,
old_proxy,
NOW,
)
],
NOW,
)
execute_endpoint_plans(
[adapter],
[plan],
store,
executed_at=NOW,
)
self.assertEqual(
tuple(event[0] for event in adapter.events),
(
"apply",
"verify",
"apply",
"verify",
"retire",
),
)
proxy_apply = adapter.applied_actions[1]
self.assertEqual(
proxy_apply.desired.references,
(
ObjectReference(
relation="certificate",
object_type=CERTIFICATES,
local_key="84",
),
),
)
self.assertEqual(
adapter.retired,
[
RetireRequest(
object=old_certificate,
)
],
)
def test_failed_verification_is_persisted_and_raises(self):
endpoint = Endpoint("verizon", "192.168.1.1:443")
adapter = FakeAdapter(