Build capability-filtered endpoint plans
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user