Wire NPM desired state into orchestration
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from dns_sync.adapters import (
|
||||
CERTIFICATES,
|
||||
DNS_RECORDS,
|
||||
PROXY_HOSTS,
|
||||
Capability,
|
||||
CapabilityOperation,
|
||||
DiscoveredObject,
|
||||
Endpoint,
|
||||
ObjectReference,
|
||||
)
|
||||
from dns_sync.orchestrator import discover_reconcile_plan
|
||||
from dns_sync.persistence import SQLiteStateStore
|
||||
|
||||
|
||||
NOW = datetime(
|
||||
2026,
|
||||
9,
|
||||
20,
|
||||
9,
|
||||
30,
|
||||
tzinfo=timezone.utc,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeNpmAdapter:
|
||||
endpoint: Endpoint
|
||||
|
||||
@property
|
||||
def capabilities(self):
|
||||
return frozenset(
|
||||
{
|
||||
Capability(
|
||||
CERTIFICATES,
|
||||
CapabilityOperation.DISCOVER,
|
||||
),
|
||||
Capability(
|
||||
CERTIFICATES,
|
||||
CapabilityOperation.VERIFY,
|
||||
),
|
||||
Capability(
|
||||
PROXY_HOSTS,
|
||||
CapabilityOperation.DISCOVER,
|
||||
),
|
||||
Capability(
|
||||
PROXY_HOSTS,
|
||||
CapabilityOperation.VERIFY,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def connect(self):
|
||||
return None
|
||||
|
||||
def discover(self, object_type):
|
||||
if object_type == CERTIFICATES:
|
||||
return [
|
||||
DiscoveredObject(
|
||||
object_type=CERTIFICATES,
|
||||
local_key="67",
|
||||
payload={
|
||||
"domain_names": [
|
||||
"app.example.com",
|
||||
],
|
||||
"provider": "letsencrypt",
|
||||
"nice_name": "app",
|
||||
"valid_from": (
|
||||
"2026-09-01T00:00:00Z"
|
||||
),
|
||||
"expires_on": (
|
||||
"2026-12-01T00:00:00Z"
|
||||
),
|
||||
"cluster_managed": True,
|
||||
"acme_renew": True,
|
||||
"dns_challenge": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
if object_type == PROXY_HOSTS:
|
||||
return [
|
||||
DiscoveredObject(
|
||||
object_type=PROXY_HOSTS,
|
||||
local_key="12",
|
||||
payload={
|
||||
"domain_names": [
|
||||
"app.example.com",
|
||||
],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "backend.internal",
|
||||
"forward_port": 8080,
|
||||
"ssl_forced": True,
|
||||
"caching_enabled": False,
|
||||
"block_exploits": True,
|
||||
"allow_websocket_upgrade": True,
|
||||
"http2_support": True,
|
||||
"hsts_enabled": True,
|
||||
"hsts_subdomains": False,
|
||||
"enabled": True,
|
||||
},
|
||||
references=(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
local_key="67",
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
raise RuntimeError(
|
||||
f"unsupported discovery type: {object_type}"
|
||||
)
|
||||
|
||||
def apply(self, action):
|
||||
raise AssertionError(
|
||||
"NPM APPLY must remain unavailable"
|
||||
)
|
||||
|
||||
def verify(self, object):
|
||||
return True
|
||||
|
||||
|
||||
class NpmDesiredStateOrchestrationTests(
|
||||
unittest.TestCase
|
||||
):
|
||||
def test_orchestrator_builds_and_persists_all_three_desired_types(
|
||||
self,
|
||||
):
|
||||
adapter = FakeNpmAdapter(
|
||||
Endpoint(
|
||||
"npm",
|
||||
"192.168.1.3:81",
|
||||
)
|
||||
)
|
||||
|
||||
with SQLiteStateStore(":memory:") as store:
|
||||
run = discover_reconcile_plan(
|
||||
[adapter],
|
||||
store,
|
||||
target_ip="192.168.1.101",
|
||||
run_at=NOW,
|
||||
)
|
||||
|
||||
desired_by_type = {
|
||||
object.object_type: object
|
||||
for object in run.desired
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set(desired_by_type),
|
||||
{
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
DNS_RECORDS,
|
||||
},
|
||||
)
|
||||
|
||||
certificate = desired_by_type[
|
||||
CERTIFICATES
|
||||
]
|
||||
proxy = desired_by_type[PROXY_HOSTS]
|
||||
dns = desired_by_type[DNS_RECORDS]
|
||||
|
||||
self.assertIsNone(
|
||||
certificate.local_key
|
||||
)
|
||||
self.assertIsNone(
|
||||
proxy.local_key
|
||||
)
|
||||
self.assertIsNone(
|
||||
dns.local_key
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
proxy.references,
|
||||
(
|
||||
ObjectReference(
|
||||
relation="certificate",
|
||||
object_type=CERTIFICATES,
|
||||
logical_key="app.example.com",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
dns.payload["hostname"],
|
||||
"app.example.com",
|
||||
)
|
||||
self.assertEqual(
|
||||
dns.payload["ip"],
|
||||
"192.168.1.101",
|
||||
)
|
||||
|
||||
for object_type in (
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
DNS_RECORDS,
|
||||
):
|
||||
persisted = (
|
||||
store.load_desired_state(
|
||||
object_type
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
len(persisted),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
persisted[0].object,
|
||||
desired_by_type[
|
||||
object_type
|
||||
],
|
||||
)
|
||||
|
||||
# The NPM adapter intentionally does not advertise
|
||||
# APPLY, so this read-only orchestration must not
|
||||
# produce an NPM endpoint plan.
|
||||
self.assertEqual(
|
||||
run.plans,
|
||||
(),
|
||||
)
|
||||
|
||||
def test_empty_npm_snapshot_clears_stale_npm_desired_state(
|
||||
self,
|
||||
):
|
||||
adapter = FakeNpmAdapter(
|
||||
Endpoint(
|
||||
"npm",
|
||||
"192.168.1.3:81",
|
||||
)
|
||||
)
|
||||
|
||||
with SQLiteStateStore(":memory:") as store:
|
||||
first = discover_reconcile_plan(
|
||||
[adapter],
|
||||
store,
|
||||
target_ip="192.168.1.101",
|
||||
run_at=NOW,
|
||||
)
|
||||
|
||||
self.assertTrue(first.desired)
|
||||
self.assertEqual(
|
||||
len(
|
||||
store.load_desired_state(
|
||||
CERTIFICATES
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(
|
||||
len(
|
||||
store.load_desired_state(
|
||||
PROXY_HOSTS
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
class EmptyNpmAdapter(
|
||||
FakeNpmAdapter
|
||||
):
|
||||
def discover(
|
||||
self,
|
||||
object_type,
|
||||
):
|
||||
if object_type in {
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
}:
|
||||
return []
|
||||
|
||||
return super().discover(
|
||||
object_type
|
||||
)
|
||||
|
||||
empty = EmptyNpmAdapter(
|
||||
adapter.endpoint
|
||||
)
|
||||
|
||||
second = discover_reconcile_plan(
|
||||
[empty],
|
||||
store,
|
||||
target_ip="192.168.1.101",
|
||||
run_at=NOW.replace(minute=31),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
second.desired,
|
||||
(),
|
||||
)
|
||||
self.assertEqual(
|
||||
store.load_desired_state(
|
||||
CERTIFICATES
|
||||
),
|
||||
[],
|
||||
)
|
||||
self.assertEqual(
|
||||
store.load_desired_state(
|
||||
PROXY_HOSTS
|
||||
),
|
||||
[],
|
||||
)
|
||||
self.assertEqual(
|
||||
store.load_desired_state(
|
||||
DNS_RECORDS
|
||||
),
|
||||
[],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user