Build capability-filtered endpoint plans

This commit is contained in:
Lyncoln
2026-09-19 18:14:50 -04:00
parent 9ad40c2742
commit 70774c5f3c
2 changed files with 356 additions and 0 deletions
+126
View File
@@ -3,9 +3,15 @@ from collections.abc import Iterable, Mapping, Sequence
from typing import Protocol
from .adapters import (
Capability,
CapabilityOperation,
DNS_RECORDS,
Endpoint,
EndpointPlan,
NormalizedObject,
Observation,
PlanAction,
PlanOperation,
)
@@ -16,6 +22,15 @@ class ReconciliationConflict(RuntimeError):
"""Raised when observations cannot be reconciled safely."""
class PlanningConflict(RuntimeError):
"""Raised when an endpoint cannot be planned safely."""
class PlanningAdapter(Protocol):
endpoint: Endpoint
capabilities: frozenset[Capability]
class ReconciliationPolicy(Protocol):
object_type: str
@@ -134,3 +149,114 @@ def reconcile_observations(
desired.append(policy.reconcile(logical_key, rows))
return desired
def build_endpoint_plans(
adapters: Iterable[PlanningAdapter],
desired_objects: Iterable[NormalizedObject],
observations: Iterable[Observation],
) -> tuple[EndpointPlan, ...]:
desired_by_identity: dict[
tuple[str, str],
NormalizedObject,
] = {}
for desired in desired_objects:
if desired.local_key is not None:
raise RuntimeError(
"Desired objects in plans cannot contain "
"an endpoint-local key"
)
identity = (
desired.object_type,
desired.logical_key,
)
if identity in desired_by_identity:
raise RuntimeError(
"Duplicate desired object identity: "
f"{desired.object_type} {desired.logical_key}"
)
desired_by_identity[identity] = desired
current_by_identity: dict[
tuple[str, str, str],
list[Observation],
] = defaultdict(list)
for observation in observations:
current_by_identity[
(
observation.endpoint_id,
observation.object.object_type,
observation.object.logical_key,
)
].append(observation)
adapters_by_endpoint: dict[str, PlanningAdapter] = {}
for adapter in adapters:
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
plans: list[EndpointPlan] = []
for endpoint_id, adapter in sorted(
adapters_by_endpoint.items()
):
apply_types = {
capability.object_type
for capability in adapter.capabilities
if capability.operation == CapabilityOperation.APPLY
}
if not apply_types:
continue
actions: list[PlanAction] = []
for identity, desired in sorted(
desired_by_identity.items()
):
object_type, logical_key = identity
if object_type not in apply_types:
continue
current_rows = current_by_identity.get(
(endpoint_id, object_type, logical_key),
[],
)
if len(current_rows) > 1:
raise PlanningConflict(
f"Multiple endpoint objects for {endpoint_id} "
f"{object_type} {logical_key!r}"
)
current = (
current_rows[0].object
if current_rows
else None
)
if current is None:
operation = PlanOperation.CREATE
elif current.payload == desired.payload:
operation = PlanOperation.NOOP
else:
operation = PlanOperation.UPDATE
actions.append(
PlanAction(
endpoint_id=endpoint_id,
operation=operation,
desired=desired,
current=current,
)
)
plans.append(
EndpointPlan(
endpoint_id=endpoint_id,
actions=tuple(actions),
)
)
return tuple(plans)