Add persistent read-only reconciliation orchestration

This commit is contained in:
Lyncoln
2026-09-19 18:52:29 -04:00
parent 5a61b6e409
commit 5cd1bc5ba3
2 changed files with 577 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
from collections.abc import Iterable
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Protocol
from .adapters import (
DNS_RECORDS,
PROXY_HOSTS,
Capability,
CapabilityOperation,
DiscoveredObject,
Endpoint,
EndpointPlan,
NormalizedObject,
Observation,
)
from .engine import hostname_matches_domain
from .object_normalizer import normalize_observations
from .persistence import SQLiteStateStore
from .projections import project_proxy_hosts_to_dns
from .reconciliation import (
build_endpoint_plans,
reconcile_observations,
)
class ReconciliationAdapter(Protocol):
endpoint: Endpoint
capabilities: frozenset[Capability]
def connect(self) -> None: ...
def discover(
self,
object_type: str,
) -> list[DiscoveredObject]: ...
@dataclass(frozen=True)
class DiscoveryFailure:
endpoint_id: str
object_type: str
error: str
@dataclass(frozen=True)
class ReconciliationRun:
observations: tuple[Observation, ...]
desired: tuple[NormalizedObject, ...]
plans: tuple[EndpointPlan, ...]
failures: tuple[DiscoveryFailure, ...]
def _discovery_types(
adapter: ReconciliationAdapter,
) -> tuple[str, ...]:
return tuple(
sorted(
capability.object_type
for capability in adapter.capabilities
if capability.operation == CapabilityOperation.DISCOVER
)
)
def _configured_observations(
store: SQLiteStateStore,
endpoint_ids: set[str],
) -> list[Observation]:
return [
observation
for observation in store.load_observations()
if observation.endpoint_id in endpoint_ids
]
def _filter_projected_dns(
observations: Iterable[Observation],
*,
domain_matching: bool,
domain: str | None,
) -> list[Observation]:
rows = list(observations)
if not domain_matching:
return rows
if not domain:
raise RuntimeError(
"Domain filtering enabled without a configured DOMAIN"
)
return [
observation
for observation in rows
if hostname_matches_domain(
observation.object.logical_key,
domain,
)
]
def discover_reconcile_plan(
adapters: Iterable[ReconciliationAdapter],
store: SQLiteStateStore,
*,
target_ip: str,
include_disabled: bool = False,
domain_matching: bool = False,
domain: str | None = None,
run_at: datetime | None = None,
) -> ReconciliationRun:
timestamp = run_at or datetime.now(timezone.utc)
if timestamp.tzinfo is None:
raise RuntimeError(
"Reconciliation run timestamp must be timezone-aware"
)
timestamp = timestamp.astimezone(timezone.utc)
adapter_list = list(adapters)
adapters_by_endpoint: dict[str, ReconciliationAdapter] = {}
for adapter in adapter_list:
endpoint_id = adapter.endpoint.identity
if endpoint_id in adapters_by_endpoint:
raise RuntimeError(
f"Duplicate adapter endpoint: {endpoint_id}"
)
adapters_by_endpoint[endpoint_id] = adapter
failures: list[DiscoveryFailure] = []
for endpoint_id, adapter in sorted(
adapters_by_endpoint.items()
):
object_types = _discovery_types(adapter)
if not object_types:
continue
try:
adapter.connect()
except Exception as exc:
for object_type in object_types:
error = str(exc)
store.record_discovery_failure(
endpoint_id,
object_type,
error,
timestamp,
)
failures.append(
DiscoveryFailure(
endpoint_id,
object_type,
error,
)
)
continue
for object_type in object_types:
try:
discovered = adapter.discover(object_type)
observations = normalize_observations(
endpoint_id,
discovered,
timestamp,
)
store.replace_observations(
endpoint_id,
object_type,
observations,
timestamp,
)
except Exception as exc:
error = str(exc)
store.record_discovery_failure(
endpoint_id,
object_type,
error,
timestamp,
)
failures.append(
DiscoveryFailure(
endpoint_id,
object_type,
error,
)
)
observations = _configured_observations(
store,
set(adapters_by_endpoint),
)
proxy_observations = [
observation
for observation in observations
if observation.object.object_type == PROXY_HOSTS
]
projected_dns = project_proxy_hosts_to_dns(
proxy_observations,
target_ip,
include_disabled=include_disabled,
)
projected_dns = _filter_projected_dns(
projected_dns,
domain_matching=domain_matching,
domain=domain,
)
desired = reconcile_observations(projected_dns)
store.replace_desired_state(
DNS_RECORDS,
desired,
timestamp,
)
plans = build_endpoint_plans(
adapter_list,
desired,
observations,
)
return ReconciliationRun(
observations=tuple(observations),
desired=tuple(desired),
plans=plans,
failures=tuple(failures),
)
+353
View File
@@ -0,0 +1,353 @@
import unittest
from dataclasses import dataclass, field
from datetime import datetime, timezone
from dns_sync.adapters import (
DNS_RECORDS,
PROXY_HOSTS,
Capability,
CapabilityOperation,
DiscoveredObject,
Endpoint,
PlanOperation,
)
from dns_sync.object_normalizer import normalize_observations
from dns_sync.orchestrator import discover_reconcile_plan
from dns_sync.persistence import SQLiteStateStore
T1 = datetime(2026, 9, 19, 22, 0, tzinfo=timezone.utc)
T2 = datetime(2026, 9, 19, 22, 5, tzinfo=timezone.utc)
def capabilities(
object_type: str,
*operations: CapabilityOperation,
) -> frozenset[Capability]:
return frozenset(
Capability(object_type, operation)
for operation in operations
)
def proxy_host(
local_key: str,
domains: list[str],
backend_ip: str = "192.168.1.50",
enabled: bool = True,
) -> DiscoveredObject:
return DiscoveredObject(
object_type=PROXY_HOSTS,
local_key=local_key,
payload={
"domain_names": domains,
"forward_scheme": "http",
"forward_host": backend_ip,
"forward_port": 8080,
"enabled": enabled,
},
)
def dns_record(
local_key: str,
hostname: str,
ip: str,
) -> DiscoveredObject:
return DiscoveredObject(
object_type=DNS_RECORDS,
local_key=local_key,
payload={
"hostname": hostname,
"ip": ip,
"record_type": "A",
},
)
@dataclass
class FakeAdapter:
endpoint: Endpoint
capabilities: frozenset[Capability]
discovered: dict[str, list[DiscoveredObject]] = field(
default_factory=dict
)
connect_error: Exception | None = None
connect_count: int = 0
discover_calls: list[str] = field(default_factory=list)
apply_count: int = 0
verify_count: int = 0
def connect(self) -> None:
self.connect_count += 1
if self.connect_error is not None:
raise self.connect_error
def discover(
self,
object_type: str,
) -> list[DiscoveredObject]:
self.discover_calls.append(object_type)
return list(self.discovered.get(object_type, []))
def apply(self, action):
self.apply_count += 1
raise AssertionError("read-only orchestration called apply")
def verify(self, object):
self.verify_count += 1
raise AssertionError("read-only orchestration called verify")
class ReadOnlyOrchestrationTests(unittest.TestCase):
def test_discovers_persists_reconciles_and_plans_vip_update(self):
npm = FakeAdapter(
endpoint=Endpoint("npm", "192.168.1.3:81"),
capabilities=capabilities(
PROXY_HOSTS,
CapabilityOperation.DISCOVER,
),
discovered={
PROXY_HOSTS: [
proxy_host(
"12",
["app.example.com"],
backend_ip="192.168.1.50",
)
]
},
)
verizon = FakeAdapter(
endpoint=Endpoint("verizon", "192.168.1.1:443"),
capabilities=capabilities(
DNS_RECORDS,
CapabilityOperation.DISCOVER,
CapabilityOperation.APPLY,
CapabilityOperation.VERIFY,
),
discovered={
DNS_RECORDS: [
dns_record(
"41",
"app.example.com",
"192.168.1.50",
)
]
},
)
with SQLiteStateStore(":memory:") as store:
result = discover_reconcile_plan(
[npm, verizon],
store,
target_ip="192.168.1.101",
run_at=T2,
)
persisted_desired = store.load_desired_state(
DNS_RECORDS
)
self.assertEqual(len(result.desired), 1)
self.assertEqual(
result.desired[0].payload["ip"],
"192.168.1.101",
)
self.assertEqual(len(result.plans), 1)
self.assertEqual(
result.plans[0].endpoint_id,
verizon.endpoint.identity,
)
self.assertEqual(
result.plans[0].actions[0].operation,
PlanOperation.UPDATE,
)
self.assertEqual(
result.plans[0].actions[0].desired.payload["ip"],
"192.168.1.101",
)
self.assertEqual(
persisted_desired[0].object.payload["ip"],
"192.168.1.101",
)
self.assertEqual(npm.connect_count, 1)
self.assertEqual(verizon.connect_count, 1)
self.assertEqual(npm.apply_count, 0)
self.assertEqual(verizon.apply_count, 0)
self.assertEqual(npm.verify_count, 0)
self.assertEqual(verizon.verify_count, 0)
def test_failed_node_remains_represented_by_cached_observation(self):
npm_endpoint = Endpoint("npm", "192.168.1.3:81")
cached = normalize_observations(
npm_endpoint.identity,
[proxy_host("12", ["cached.example.com"])],
T1,
)
npm = FakeAdapter(
endpoint=npm_endpoint,
capabilities=capabilities(
PROXY_HOSTS,
CapabilityOperation.DISCOVER,
),
connect_error=RuntimeError("temporarily unavailable"),
)
verizon = FakeAdapter(
endpoint=Endpoint("verizon", "192.168.1.1:443"),
capabilities=capabilities(
DNS_RECORDS,
CapabilityOperation.DISCOVER,
CapabilityOperation.APPLY,
),
discovered={DNS_RECORDS: []},
)
with SQLiteStateStore(":memory:") as store:
store.replace_observations(
npm_endpoint.identity,
PROXY_HOSTS,
cached,
T1,
)
result = discover_reconcile_plan(
[npm, verizon],
store,
target_ip="192.168.1.101",
run_at=T2,
)
status = store.observation_status(
npm_endpoint.identity,
PROXY_HOSTS,
)
self.assertEqual(
tuple(item.logical_key for item in result.desired),
("cached.example.com",),
)
self.assertEqual(
result.plans[0].actions[0].operation,
PlanOperation.CREATE,
)
self.assertFalse(status.is_live)
self.assertEqual(status.last_success_at, T1)
self.assertEqual(status.last_attempt_at, T2)
self.assertEqual(
status.last_error,
"temporarily unavailable",
)
self.assertEqual(len(result.failures), 1)
def test_live_empty_snapshot_supersedes_cached_state(self):
npm_endpoint = Endpoint("npm", "192.168.1.3:81")
npm = FakeAdapter(
endpoint=npm_endpoint,
capabilities=capabilities(
PROXY_HOSTS,
CapabilityOperation.DISCOVER,
),
discovered={PROXY_HOSTS: []},
)
cached = normalize_observations(
npm_endpoint.identity,
[proxy_host("12", ["removed.example.com"])],
T1,
)
with SQLiteStateStore(":memory:") as store:
store.replace_observations(
npm_endpoint.identity,
PROXY_HOSTS,
cached,
T1,
)
result = discover_reconcile_plan(
[npm],
store,
target_ip="192.168.1.101",
run_at=T2,
)
persisted_desired = store.load_desired_state(
DNS_RECORDS
)
self.assertEqual(result.desired, ())
self.assertEqual(result.plans, ())
self.assertEqual(persisted_desired, [])
def test_domain_filtering_occurs_after_projection(self):
npm = FakeAdapter(
endpoint=Endpoint("npm", "192.168.1.3:81"),
capabilities=capabilities(
PROXY_HOSTS,
CapabilityOperation.DISCOVER,
),
discovered={
PROXY_HOSTS: [
proxy_host(
"12",
[
"app.example.com",
"outside.example.net",
],
)
]
},
)
with SQLiteStateStore(":memory:") as store:
result = discover_reconcile_plan(
[npm],
store,
target_ip="192.168.1.101",
domain_matching=True,
domain="example.com",
run_at=T2,
)
self.assertEqual(
tuple(item.logical_key for item in result.desired),
("app.example.com",),
)
def test_removed_endpoints_do_not_contribute_cached_state(self):
removed_endpoint = Endpoint("npm", "192.168.1.4:81")
active_endpoint = Endpoint("npm", "192.168.1.3:81")
removed_cache = normalize_observations(
removed_endpoint.identity,
[proxy_host("12", ["removed.example.com"])],
T1,
)
active = FakeAdapter(
endpoint=active_endpoint,
capabilities=capabilities(
PROXY_HOSTS,
CapabilityOperation.DISCOVER,
),
discovered={
PROXY_HOSTS: [
proxy_host("13", ["active.example.com"])
]
},
)
with SQLiteStateStore(":memory:") as store:
store.replace_observations(
removed_endpoint.identity,
PROXY_HOSTS,
removed_cache,
T1,
)
result = discover_reconcile_plan(
[active],
store,
target_ip="192.168.1.101",
run_at=T2,
)
self.assertEqual(
tuple(item.logical_key for item in result.desired),
("active.example.com",),
)
if __name__ == "__main__":
unittest.main()