Require live observations for convergence
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
import unittest
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from dns_sync.adapters import (
|
||||
CERTIFICATES,
|
||||
DNS_RECORDS,
|
||||
PROXY_HOSTS,
|
||||
Capability,
|
||||
CapabilityOperation,
|
||||
DiscoveredObject,
|
||||
Endpoint,
|
||||
NormalizedObject,
|
||||
Observation,
|
||||
)
|
||||
from dns_sync.orchestrator import discover_reconcile_plan
|
||||
from dns_sync.persistence import SQLiteStateStore
|
||||
|
||||
|
||||
T1 = datetime(
|
||||
2026, 9, 20, 8, 0, tzinfo=timezone.utc
|
||||
)
|
||||
T2 = datetime(
|
||||
2026, 9, 20, 9, 0, tzinfo=timezone.utc
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeAdapter:
|
||||
endpoint: Endpoint
|
||||
capabilities: frozenset[Capability]
|
||||
discovered: dict[str, list[DiscoveredObject]] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
failures: dict[str, str] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
|
||||
def connect(self) -> None:
|
||||
return None
|
||||
|
||||
def discover(
|
||||
self,
|
||||
object_type: str,
|
||||
) -> list[DiscoveredObject]:
|
||||
if object_type in self.failures:
|
||||
raise RuntimeError(
|
||||
self.failures[object_type]
|
||||
)
|
||||
return list(
|
||||
self.discovered.get(
|
||||
object_type,
|
||||
[],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def capability_set(
|
||||
*object_types: str,
|
||||
apply: bool = False,
|
||||
) -> frozenset[Capability]:
|
||||
rows = {
|
||||
Capability(
|
||||
object_type,
|
||||
CapabilityOperation.DISCOVER,
|
||||
)
|
||||
for object_type in object_types
|
||||
}
|
||||
|
||||
if apply:
|
||||
rows.update(
|
||||
Capability(
|
||||
object_type,
|
||||
CapabilityOperation.APPLY,
|
||||
)
|
||||
for object_type in object_types
|
||||
)
|
||||
|
||||
return frozenset(rows)
|
||||
|
||||
|
||||
def normalized_certificate(
|
||||
local_key: str,
|
||||
*,
|
||||
valid_from: str,
|
||||
expires_on: str,
|
||||
) -> NormalizedObject:
|
||||
return NormalizedObject(
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
local_key=local_key,
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "other",
|
||||
"valid_from": valid_from,
|
||||
"expires_on": expires_on,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def discovered_certificate(
|
||||
local_key: str,
|
||||
*,
|
||||
valid_from: str,
|
||||
expires_on: str,
|
||||
) -> DiscoveredObject:
|
||||
return DiscoveredObject(
|
||||
object_type=CERTIFICATES,
|
||||
local_key=local_key,
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"provider": "other",
|
||||
"valid_from": valid_from,
|
||||
"expires_on": expires_on,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def normalized_proxy(
|
||||
local_key: str,
|
||||
) -> NormalizedObject:
|
||||
return NormalizedObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
logical_key="app.example.com",
|
||||
local_key=local_key,
|
||||
payload={
|
||||
"domain_names": ["app.example.com"],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 8080,
|
||||
"enabled": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ObservationAuthorityTests(unittest.TestCase):
|
||||
def test_failed_newer_certificate_cache_cannot_override_live_certificate(self):
|
||||
live_endpoint = Endpoint(
|
||||
"npm",
|
||||
"192.168.1.3:81",
|
||||
)
|
||||
failed_endpoint = Endpoint(
|
||||
"npm",
|
||||
"192.168.1.4:81",
|
||||
)
|
||||
|
||||
live = FakeAdapter(
|
||||
endpoint=live_endpoint,
|
||||
capabilities=capability_set(
|
||||
CERTIFICATES,
|
||||
),
|
||||
discovered={
|
||||
CERTIFICATES: [
|
||||
discovered_certificate(
|
||||
"20",
|
||||
valid_from=(
|
||||
"2026-09-15T00:00:00+00:00"
|
||||
),
|
||||
expires_on=(
|
||||
"2026-12-15T00:00:00+00:00"
|
||||
),
|
||||
)
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
failed = FakeAdapter(
|
||||
endpoint=failed_endpoint,
|
||||
capabilities=capability_set(
|
||||
CERTIFICATES,
|
||||
apply=True,
|
||||
),
|
||||
failures={
|
||||
CERTIFICATES: "offline"
|
||||
},
|
||||
)
|
||||
|
||||
cached_newer = normalized_certificate(
|
||||
"99",
|
||||
valid_from=(
|
||||
"2026-09-19T00:00:00+00:00"
|
||||
),
|
||||
expires_on=(
|
||||
"2026-12-19T00:00:00+00:00"
|
||||
),
|
||||
)
|
||||
|
||||
with SQLiteStateStore(":memory:") as store:
|
||||
store.replace_observations(
|
||||
failed_endpoint.identity,
|
||||
CERTIFICATES,
|
||||
[
|
||||
Observation(
|
||||
failed_endpoint.identity,
|
||||
cached_newer,
|
||||
T1,
|
||||
)
|
||||
],
|
||||
T1,
|
||||
)
|
||||
|
||||
result = discover_reconcile_plan(
|
||||
[
|
||||
live,
|
||||
failed,
|
||||
],
|
||||
store,
|
||||
target_ip="192.168.1.101",
|
||||
run_at=T2,
|
||||
)
|
||||
|
||||
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-15T00:00:00+00:00",
|
||||
)
|
||||
|
||||
# The failed target advertised APPLY, but it must not
|
||||
# receive a certificate mutation plan this run.
|
||||
self.assertEqual(
|
||||
result.plans,
|
||||
(),
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
any(
|
||||
observation.endpoint_id
|
||||
== failed_endpoint.identity
|
||||
and observation.object.local_key == "99"
|
||||
for observation in result.observations
|
||||
)
|
||||
)
|
||||
|
||||
def test_all_failed_sources_preserve_persisted_desired_without_planning(self):
|
||||
npm_endpoint = Endpoint(
|
||||
"npm",
|
||||
"192.168.1.3:81",
|
||||
)
|
||||
|
||||
npm = FakeAdapter(
|
||||
endpoint=npm_endpoint,
|
||||
capabilities=capability_set(
|
||||
PROXY_HOSTS,
|
||||
apply=True,
|
||||
),
|
||||
failures={
|
||||
PROXY_HOSTS: "offline"
|
||||
},
|
||||
)
|
||||
|
||||
cached_proxy = normalized_proxy("12")
|
||||
|
||||
previous_dns = NormalizedObject(
|
||||
object_type=DNS_RECORDS,
|
||||
logical_key="app.example.com",
|
||||
payload={
|
||||
"hostname": "app.example.com",
|
||||
"ip_address": "192.168.1.101",
|
||||
},
|
||||
)
|
||||
|
||||
with SQLiteStateStore(":memory:") as store:
|
||||
store.replace_observations(
|
||||
npm_endpoint.identity,
|
||||
PROXY_HOSTS,
|
||||
[
|
||||
Observation(
|
||||
npm_endpoint.identity,
|
||||
cached_proxy,
|
||||
T1,
|
||||
)
|
||||
],
|
||||
T1,
|
||||
)
|
||||
|
||||
store.replace_desired_state(
|
||||
DNS_RECORDS,
|
||||
[previous_dns],
|
||||
T1,
|
||||
)
|
||||
|
||||
result = discover_reconcile_plan(
|
||||
[npm],
|
||||
store,
|
||||
target_ip="192.168.1.101",
|
||||
run_at=T2,
|
||||
)
|
||||
|
||||
persisted_dns = store.load_desired_state(
|
||||
DNS_RECORDS
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result.desired,
|
||||
(),
|
||||
)
|
||||
self.assertEqual(
|
||||
result.plans,
|
||||
(),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
len(persisted_dns),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
persisted_dns[0].object,
|
||||
previous_dns,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user