Add certificate replacement lifecycle
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user