1491 lines
42 KiB
Python
1491 lines
42 KiB
Python
import unittest
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
|
|
from dns_sync.adapters import (
|
|
CERTIFICATES,
|
|
ApplyResult,
|
|
DNS_RECORDS,
|
|
PROXY_HOSTS,
|
|
Capability,
|
|
CapabilityOperation,
|
|
Endpoint,
|
|
EndpointPlan,
|
|
NormalizedObject,
|
|
ObjectReference,
|
|
Observation,
|
|
PlanAction,
|
|
PlanOperation,
|
|
RetireRequest,
|
|
)
|
|
from dns_sync.executor import execute_endpoint_plans
|
|
from dns_sync.persistence import SQLiteStateStore
|
|
|
|
|
|
NOW = datetime(2026, 9, 19, 23, 15, tzinfo=timezone.utc)
|
|
|
|
|
|
def object_for(
|
|
object_type: str,
|
|
logical_key: str,
|
|
*,
|
|
references=(),
|
|
) -> NormalizedObject:
|
|
if object_type == DNS_RECORDS:
|
|
payload = {
|
|
"hostname": logical_key,
|
|
"ip": "192.168.1.101",
|
|
"record_type": "A",
|
|
"wildcard": False,
|
|
}
|
|
elif object_type == CERTIFICATES:
|
|
payload = {
|
|
"domain_names": [logical_key],
|
|
"expires_on": "2026-12-15T00:00:00+00:00",
|
|
}
|
|
else:
|
|
payload = {
|
|
"domain_names": [logical_key],
|
|
"forward_scheme": "http",
|
|
"forward_host": "192.168.1.50",
|
|
"forward_port": 8080,
|
|
"enabled": True,
|
|
}
|
|
|
|
return NormalizedObject(
|
|
object_type=object_type,
|
|
logical_key=logical_key,
|
|
payload=payload,
|
|
references=references,
|
|
)
|
|
|
|
|
|
def action(
|
|
endpoint_id: str,
|
|
operation: PlanOperation,
|
|
object_type: str = DNS_RECORDS,
|
|
logical_key: str = "app.example.com",
|
|
*,
|
|
references=(),
|
|
) -> PlanAction:
|
|
return PlanAction(
|
|
endpoint_id=endpoint_id,
|
|
operation=operation,
|
|
desired=object_for(
|
|
object_type,
|
|
logical_key,
|
|
references=references,
|
|
),
|
|
)
|
|
|
|
|
|
def capabilities(
|
|
*object_types: str,
|
|
verify: bool = True,
|
|
) -> frozenset[Capability]:
|
|
result = {
|
|
Capability(
|
|
object_type,
|
|
CapabilityOperation.APPLY,
|
|
)
|
|
for object_type in object_types
|
|
}
|
|
if verify:
|
|
result.update(
|
|
Capability(
|
|
object_type,
|
|
CapabilityOperation.VERIFY,
|
|
)
|
|
for object_type in object_types
|
|
)
|
|
return frozenset(result)
|
|
|
|
|
|
@dataclass
|
|
class FakeAdapter:
|
|
endpoint: Endpoint
|
|
capabilities: frozenset[Capability]
|
|
verify_result: bool = True
|
|
apply_error: Exception | None = None
|
|
verify_error: Exception | None = None
|
|
apply_local_keys: dict[
|
|
tuple[str, str],
|
|
str,
|
|
] = field(default_factory=dict)
|
|
applied_actions: list[PlanAction] = field(
|
|
default_factory=list
|
|
)
|
|
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
|
|
self.events.append(
|
|
(
|
|
"apply",
|
|
desired.object_type,
|
|
desired.logical_key,
|
|
)
|
|
)
|
|
if self.apply_error is not None:
|
|
raise self.apply_error
|
|
|
|
self.applied_actions.append(plan_action)
|
|
|
|
local_key = self.apply_local_keys.get(
|
|
(
|
|
desired.object_type,
|
|
desired.logical_key,
|
|
),
|
|
desired.local_key or "test-local",
|
|
)
|
|
|
|
return ApplyResult(
|
|
object=NormalizedObject(
|
|
object_type=desired.object_type,
|
|
logical_key=desired.logical_key,
|
|
local_key=local_key,
|
|
payload=dict(desired.payload),
|
|
references=desired.references,
|
|
),
|
|
detail="test detail",
|
|
)
|
|
|
|
def verify(self, object):
|
|
self.events.append(
|
|
(
|
|
"verify",
|
|
object.object_type,
|
|
object.logical_key,
|
|
)
|
|
)
|
|
if self.verify_error is not None:
|
|
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):
|
|
endpoint = Endpoint("verizon", "192.168.1.1:443")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(DNS_RECORDS),
|
|
)
|
|
plan = EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
logical_key="add.example.com",
|
|
),
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.NOOP,
|
|
logical_key="keep.example.com",
|
|
),
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.UPDATE,
|
|
logical_key="update.example.com",
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
result = execute_endpoint_plans(
|
|
[adapter],
|
|
[plan],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
persisted = store.load_applied_state(
|
|
endpoint.identity,
|
|
DNS_RECORDS,
|
|
)
|
|
|
|
self.assertEqual(len(result.applied), 2)
|
|
self.assertEqual(len(result.skipped), 1)
|
|
self.assertTrue(all(row.is_verified for row in persisted))
|
|
self.assertEqual(
|
|
{row.object.logical_key for row in persisted},
|
|
{"add.example.com", "update.example.com"},
|
|
)
|
|
self.assertEqual(
|
|
tuple(event[0] for event in adapter.events),
|
|
("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_restart_recovery_rebinds_before_planned_retirement(self):
|
|
endpoint = Endpoint(
|
|
"npm",
|
|
"192.168.1.4:81",
|
|
)
|
|
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(
|
|
CERTIFICATES,
|
|
PROXY_HOSTS,
|
|
),
|
|
apply_local_keys={
|
|
(
|
|
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",
|
|
},
|
|
)
|
|
|
|
new_certificate = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
local_key="84",
|
|
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_certificate = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
payload=dict(new_certificate.payload),
|
|
)
|
|
|
|
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.NOOP,
|
|
desired=desired_certificate,
|
|
current=new_certificate,
|
|
),
|
|
PlanAction(
|
|
endpoint_id=endpoint.identity,
|
|
operation=PlanOperation.UPDATE,
|
|
desired=desired_proxy,
|
|
current=old_proxy,
|
|
reason=(
|
|
"referenced local object is being retired"
|
|
),
|
|
),
|
|
),
|
|
retirements=(
|
|
RetireRequest(
|
|
object=old_certificate,
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
store.replace_observations(
|
|
endpoint.identity,
|
|
CERTIFICATES,
|
|
[
|
|
Observation(
|
|
endpoint.identity,
|
|
old_certificate,
|
|
NOW,
|
|
),
|
|
Observation(
|
|
endpoint.identity,
|
|
new_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",
|
|
"retire",
|
|
),
|
|
)
|
|
|
|
proxy_action = adapter.applied_actions[0]
|
|
|
|
self.assertEqual(
|
|
proxy_action.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(
|
|
endpoint,
|
|
capabilities(DNS_RECORDS),
|
|
verify_result=False,
|
|
)
|
|
plan = EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"verification failed",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[plan],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
persisted = store.load_applied_state()
|
|
|
|
self.assertEqual(len(persisted), 1)
|
|
self.assertFalse(persisted[0].is_verified)
|
|
self.assertEqual(
|
|
persisted[0].verification_error,
|
|
"adapter returned false",
|
|
)
|
|
|
|
def test_apply_failure_does_not_record_applied_state(self):
|
|
endpoint = Endpoint("verizon", "192.168.1.1:443")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(DNS_RECORDS),
|
|
apply_error=RuntimeError("write failed"),
|
|
)
|
|
plan = EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(RuntimeError, "write failed"):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[plan],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
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(
|
|
endpoint,
|
|
capabilities(DNS_RECORDS, verify=False),
|
|
)
|
|
plan = EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"lacks verify capability",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[plan],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(adapter.events, [])
|
|
|
|
def test_certificates_execute_before_proxy_hosts(self):
|
|
events: list[tuple[str, str, str]] = []
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(CERTIFICATES, PROXY_HOSTS),
|
|
events=events,
|
|
)
|
|
plan = EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(
|
|
ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
),
|
|
),
|
|
),
|
|
action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
CERTIFICATES,
|
|
"app.example.com",
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[plan],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(
|
|
tuple((event[0], event[1]) for event in events),
|
|
(
|
|
("apply", CERTIFICATES),
|
|
("verify", CERTIFICATES),
|
|
("apply", PROXY_HOSTS),
|
|
("verify", PROXY_HOSTS),
|
|
),
|
|
)
|
|
|
|
def test_existing_reference_resolves_from_observation(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
reference = ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
)
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(reference,),
|
|
)
|
|
|
|
observed_certificate = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
local_key="67",
|
|
payload={
|
|
"domain_names": ["app.example.com"],
|
|
"expires_on": "2026-12-15T00:00:00+00:00",
|
|
},
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
store.replace_observations(
|
|
endpoint.identity,
|
|
CERTIFICATES,
|
|
[
|
|
Observation(
|
|
endpoint.identity,
|
|
observed_certificate,
|
|
NOW,
|
|
),
|
|
],
|
|
NOW,
|
|
)
|
|
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
applied_reference = (
|
|
adapter.applied_actions[0]
|
|
.desired
|
|
.references[0]
|
|
)
|
|
self.assertEqual(applied_reference.local_key, "67")
|
|
self.assertIsNone(applied_reference.logical_key)
|
|
|
|
|
|
def test_created_reference_uses_apply_result_local_key(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(CERTIFICATES, PROXY_HOSTS),
|
|
apply_local_keys={
|
|
(
|
|
CERTIFICATES,
|
|
"app.example.com",
|
|
): "68",
|
|
(
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
): "12",
|
|
},
|
|
)
|
|
|
|
certificate_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
CERTIFICATES,
|
|
"app.example.com",
|
|
)
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(
|
|
ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
proxy_action,
|
|
certificate_action,
|
|
),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
persisted = store.load_applied_state(
|
|
endpoint.identity,
|
|
)
|
|
|
|
self.assertEqual(
|
|
tuple(
|
|
item.desired.object_type
|
|
for item in adapter.applied_actions
|
|
),
|
|
(CERTIFICATES, PROXY_HOSTS),
|
|
)
|
|
|
|
resolved = (
|
|
adapter.applied_actions[1]
|
|
.desired
|
|
.references[0]
|
|
)
|
|
self.assertEqual(resolved.local_key, "68")
|
|
self.assertIsNone(resolved.logical_key)
|
|
|
|
persisted_by_type = {
|
|
row.object.object_type: row.object
|
|
for row in persisted
|
|
}
|
|
self.assertEqual(
|
|
persisted_by_type[CERTIFICATES].local_key,
|
|
"68",
|
|
)
|
|
self.assertEqual(
|
|
persisted_by_type[PROXY_HOSTS]
|
|
.references[0]
|
|
.local_key,
|
|
"68",
|
|
)
|
|
|
|
|
|
def test_unresolved_reference_fails_before_dependent_apply(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(
|
|
ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
logical_key="missing.example.com",
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"Unresolved endpoint-local reference",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(
|
|
store.load_applied_state(),
|
|
[],
|
|
)
|
|
|
|
self.assertEqual(adapter.events, [])
|
|
self.assertEqual(adapter.applied_actions, [])
|
|
|
|
|
|
def test_existing_reference_falls_back_to_verified_applied_state(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
|
|
certificate = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
local_key="71",
|
|
payload={
|
|
"domain_names": ["app.example.com"],
|
|
},
|
|
)
|
|
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(
|
|
ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
store.record_applied(
|
|
endpoint.identity,
|
|
certificate,
|
|
NOW,
|
|
)
|
|
store.record_verification_success(
|
|
endpoint.identity,
|
|
certificate,
|
|
NOW,
|
|
)
|
|
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
resolved = (
|
|
adapter.applied_actions[0]
|
|
.desired
|
|
.references[0]
|
|
)
|
|
self.assertEqual(resolved.local_key, "71")
|
|
|
|
|
|
def test_ambiguous_logical_reference_fails_closed(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(
|
|
ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
),
|
|
),
|
|
)
|
|
|
|
first = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
local_key="63",
|
|
payload={
|
|
"domain_names": ["app.example.com"],
|
|
},
|
|
)
|
|
second = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
local_key="67",
|
|
payload={
|
|
"domain_names": ["app.example.com"],
|
|
},
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
store.replace_observations(
|
|
endpoint.identity,
|
|
CERTIFICATES,
|
|
[
|
|
Observation(
|
|
endpoint.identity,
|
|
first,
|
|
NOW,
|
|
),
|
|
Observation(
|
|
endpoint.identity,
|
|
second,
|
|
NOW,
|
|
),
|
|
],
|
|
NOW,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"Ambiguous endpoint-local reference",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(adapter.events, [])
|
|
self.assertEqual(adapter.applied_actions, [])
|
|
|
|
|
|
def test_unrelated_duplicate_identity_does_not_block_apply(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"unrelated.example.com",
|
|
)
|
|
|
|
first = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="duplicate.example.com",
|
|
local_key="63",
|
|
payload={
|
|
"domain_names": ["duplicate.example.com"],
|
|
},
|
|
)
|
|
second = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="duplicate.example.com",
|
|
local_key="67",
|
|
payload={
|
|
"domain_names": ["duplicate.example.com"],
|
|
},
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
store.replace_observations(
|
|
endpoint.identity,
|
|
CERTIFICATES,
|
|
[
|
|
Observation(
|
|
endpoint.identity,
|
|
first,
|
|
NOW,
|
|
),
|
|
Observation(
|
|
endpoint.identity,
|
|
second,
|
|
NOW,
|
|
),
|
|
],
|
|
NOW,
|
|
)
|
|
|
|
result = execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(len(result.applied), 1)
|
|
self.assertEqual(len(adapter.applied_actions), 1)
|
|
|
|
|
|
def test_unknown_local_reference_is_not_trusted(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(
|
|
ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
local_key="999",
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"local key '999'",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(adapter.events, [])
|
|
self.assertEqual(adapter.applied_actions, [])
|
|
|
|
|
|
def test_known_local_reference_is_preserved(self):
|
|
endpoint = Endpoint("npm", "192.168.1.4:81")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(PROXY_HOSTS),
|
|
)
|
|
|
|
certificate = NormalizedObject(
|
|
object_type=CERTIFICATES,
|
|
logical_key="app.example.com",
|
|
local_key="67",
|
|
payload={
|
|
"domain_names": ["app.example.com"],
|
|
},
|
|
)
|
|
local_reference = ObjectReference(
|
|
relation="certificate",
|
|
object_type=CERTIFICATES,
|
|
local_key="67",
|
|
)
|
|
proxy_action = action(
|
|
endpoint.identity,
|
|
PlanOperation.CREATE,
|
|
PROXY_HOSTS,
|
|
"app.example.com",
|
|
references=(local_reference,),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
store.replace_observations(
|
|
endpoint.identity,
|
|
CERTIFICATES,
|
|
[
|
|
Observation(
|
|
endpoint.identity,
|
|
certificate,
|
|
NOW,
|
|
),
|
|
],
|
|
NOW,
|
|
)
|
|
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(proxy_action,),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(
|
|
adapter.applied_actions[0].desired.references,
|
|
(local_reference,),
|
|
)
|
|
|
|
|
|
def test_dependency_cycle_fails_before_any_mutation(self):
|
|
endpoint = Endpoint("generic", "node.example:443")
|
|
object_a = "object_a"
|
|
object_b = "object_b"
|
|
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(object_a, object_b),
|
|
)
|
|
|
|
action_a = PlanAction(
|
|
endpoint_id=endpoint.identity,
|
|
operation=PlanOperation.CREATE,
|
|
desired=NormalizedObject(
|
|
object_type=object_a,
|
|
logical_key="a",
|
|
payload={},
|
|
references=(
|
|
ObjectReference(
|
|
relation="b",
|
|
object_type=object_b,
|
|
logical_key="b",
|
|
),
|
|
),
|
|
),
|
|
)
|
|
action_b = PlanAction(
|
|
endpoint_id=endpoint.identity,
|
|
operation=PlanOperation.CREATE,
|
|
desired=NormalizedObject(
|
|
object_type=object_b,
|
|
logical_key="b",
|
|
payload={},
|
|
references=(
|
|
ObjectReference(
|
|
relation="a",
|
|
object_type=object_a,
|
|
logical_key="a",
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"dependency cycle",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[
|
|
EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action_a,
|
|
action_b,
|
|
),
|
|
)
|
|
],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(
|
|
store.load_applied_state(),
|
|
[],
|
|
)
|
|
|
|
self.assertEqual(adapter.events, [])
|
|
self.assertEqual(adapter.applied_actions, [])
|
|
|
|
|
|
def test_invalid_plan_fails_preflight_without_mutation(self):
|
|
endpoint = Endpoint("verizon", "192.168.1.1:443")
|
|
adapter = FakeAdapter(
|
|
endpoint,
|
|
capabilities(DNS_RECORDS),
|
|
)
|
|
invalid = EndpointPlan(
|
|
endpoint.identity,
|
|
(
|
|
action(
|
|
"verizon://different:443",
|
|
PlanOperation.CREATE,
|
|
),
|
|
),
|
|
)
|
|
|
|
with SQLiteStateStore(":memory:") as store:
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"does not match",
|
|
):
|
|
execute_endpoint_plans(
|
|
[adapter],
|
|
[invalid],
|
|
store,
|
|
executed_at=NOW,
|
|
)
|
|
|
|
self.assertEqual(adapter.events, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|