Require live observations for convergence
This commit is contained in:
+153
-22
@@ -52,6 +52,12 @@ class ReconciliationRun:
|
||||
failures: tuple[DiscoveryFailure, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _PlanningAdapterView:
|
||||
endpoint: Endpoint
|
||||
capabilities: frozenset[Capability]
|
||||
|
||||
|
||||
def _discovery_types(
|
||||
adapter: ReconciliationAdapter,
|
||||
) -> tuple[str, ...]:
|
||||
@@ -75,6 +81,90 @@ def _configured_observations(
|
||||
]
|
||||
|
||||
|
||||
def _live_discovery_pairs(
|
||||
adapters: Iterable[ReconciliationAdapter],
|
||||
store: SQLiteStateStore,
|
||||
attempted_at: datetime,
|
||||
) -> frozenset[tuple[str, str]]:
|
||||
live: set[tuple[str, str]] = set()
|
||||
|
||||
for adapter in adapters:
|
||||
endpoint_id = adapter.endpoint.identity
|
||||
|
||||
for object_type in _discovery_types(adapter):
|
||||
status = store.observation_status(
|
||||
endpoint_id,
|
||||
object_type,
|
||||
)
|
||||
|
||||
if (
|
||||
status is not None
|
||||
and status.is_live
|
||||
and status.last_attempt_at == attempted_at
|
||||
):
|
||||
live.add(
|
||||
(
|
||||
endpoint_id,
|
||||
object_type,
|
||||
)
|
||||
)
|
||||
|
||||
return frozenset(live)
|
||||
|
||||
|
||||
def _authoritative_observations(
|
||||
observations: Iterable[Observation],
|
||||
live_pairs: frozenset[tuple[str, str]],
|
||||
object_types: frozenset[str],
|
||||
) -> list[Observation]:
|
||||
return [
|
||||
observation
|
||||
for observation in observations
|
||||
if (
|
||||
observation.object.object_type
|
||||
in object_types
|
||||
and (
|
||||
observation.endpoint_id,
|
||||
observation.object.object_type,
|
||||
)
|
||||
in live_pairs
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _planning_adapter_views(
|
||||
adapters: Iterable[ReconciliationAdapter],
|
||||
live_pairs: frozenset[tuple[str, str]],
|
||||
) -> list[_PlanningAdapterView]:
|
||||
views: list[_PlanningAdapterView] = []
|
||||
|
||||
for adapter in adapters:
|
||||
endpoint_id = adapter.endpoint.identity
|
||||
|
||||
capabilities = frozenset(
|
||||
capability
|
||||
for capability in adapter.capabilities
|
||||
if (
|
||||
capability.operation
|
||||
!= CapabilityOperation.APPLY
|
||||
or (
|
||||
endpoint_id,
|
||||
capability.object_type,
|
||||
)
|
||||
in live_pairs
|
||||
)
|
||||
)
|
||||
|
||||
views.append(
|
||||
_PlanningAdapterView(
|
||||
endpoint=adapter.endpoint,
|
||||
capabilities=capabilities,
|
||||
)
|
||||
)
|
||||
|
||||
return views
|
||||
|
||||
|
||||
def _filter_projected_dns(
|
||||
observations: Iterable[Observation],
|
||||
*,
|
||||
@@ -189,15 +279,35 @@ def discover_reconcile_plan(
|
||||
store,
|
||||
set(adapters_by_endpoint),
|
||||
)
|
||||
npm_observations = [
|
||||
observation
|
||||
for observation in observations
|
||||
if observation.object.object_type
|
||||
in {
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
}
|
||||
]
|
||||
|
||||
# Cached observations remain visible, but only a successful
|
||||
# discovery from this exact run may establish desired state or
|
||||
# make an endpoint eligible for mutation.
|
||||
live_pairs = _live_discovery_pairs(
|
||||
adapter_list,
|
||||
store,
|
||||
timestamp,
|
||||
)
|
||||
|
||||
certificate_authoritative = any(
|
||||
object_type == CERTIFICATES
|
||||
for _, object_type in live_pairs
|
||||
)
|
||||
proxy_authoritative = any(
|
||||
object_type == PROXY_HOSTS
|
||||
for _, object_type in live_pairs
|
||||
)
|
||||
|
||||
npm_observations = _authoritative_observations(
|
||||
observations,
|
||||
live_pairs,
|
||||
frozenset(
|
||||
{
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
npm_desired = (
|
||||
reconcile_observations(npm_observations)
|
||||
@@ -205,17 +315,22 @@ def discover_reconcile_plan(
|
||||
else []
|
||||
)
|
||||
|
||||
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,
|
||||
proxy_observations = _authoritative_observations(
|
||||
observations,
|
||||
live_pairs,
|
||||
frozenset({PROXY_HOSTS}),
|
||||
)
|
||||
|
||||
projected_dns = (
|
||||
project_proxy_hosts_to_dns(
|
||||
proxy_observations,
|
||||
target_ip,
|
||||
include_disabled=include_disabled,
|
||||
)
|
||||
if proxy_authoritative
|
||||
else []
|
||||
)
|
||||
|
||||
projected_dns = _filter_projected_dns(
|
||||
projected_dns,
|
||||
domain_matching=domain_matching,
|
||||
@@ -229,18 +344,31 @@ def discover_reconcile_plan(
|
||||
)
|
||||
|
||||
# Keep the reconciliation domains separate until each has
|
||||
# produced portable desired state. In particular, router DNS
|
||||
# observations must never participate in the proxy->DNS union.
|
||||
# produced portable desired state. Router DNS observations are
|
||||
# current target state, never a desired-state authority.
|
||||
desired = [
|
||||
*npm_desired,
|
||||
*dns_desired,
|
||||
]
|
||||
|
||||
authority_by_type = {
|
||||
CERTIFICATES: certificate_authoritative,
|
||||
PROXY_HOSTS: proxy_authoritative,
|
||||
# DNS desired state is projected from proxy-host authority.
|
||||
DNS_RECORDS: proxy_authoritative,
|
||||
}
|
||||
|
||||
for object_type in (
|
||||
CERTIFICATES,
|
||||
PROXY_HOSTS,
|
||||
DNS_RECORDS,
|
||||
):
|
||||
if not authority_by_type[object_type]:
|
||||
# No successful source discovery this run: retain the
|
||||
# previously selected desired snapshot for diagnostics,
|
||||
# but do not use it for this run's plans.
|
||||
continue
|
||||
|
||||
store.replace_desired_state(
|
||||
object_type,
|
||||
[
|
||||
@@ -252,7 +380,10 @@ def discover_reconcile_plan(
|
||||
)
|
||||
|
||||
plans = build_endpoint_plans(
|
||||
adapter_list,
|
||||
_planning_adapter_views(
|
||||
adapter_list,
|
||||
live_pairs,
|
||||
),
|
||||
desired,
|
||||
observations,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user