mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
The entity name keys a role's instance directory, its compose project and its container name, and was derived only by stripping the longest matching category prefix. Two roles under different categories whose names share a suffix therefore collapse onto one entity and overwrite each other's rendered stack, which no provides: can repair. A literal entity_name in vars/main.yml now wins over the derivation. web-app-seaweedfs-console takes its shorter name back and declares the entity seaweedfs-console, which frees the seaweedfs key in its own services map for the engine consumer and removes the provides:, covers: and alias entries that stood in for it. A new lint pairs the declaration with the primary key it has to match, because a drift between them would drop the role from the provider set in silence. Compared across all 288 roles, exactly one entity changes, and it restores what main already keeps on disk, so no instance directory moves. The function and the filter lose their get_ prefix. Two lints read that prefix as the discriminator between the variable and the filter that fills it; both discriminate on the pipe now. Ansible's templar resolves a filter and a same-named variable independently, verified against ansible-core 2.20.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
225 lines
7.6 KiB
Python
225 lines
7.6 KiB
Python
"""Attachment computation for the compose networks renderers.
|
|
|
|
Which provider overlays a role attaches to, and which proxy aliases the
|
|
default network harvests, derived from the service_registry. Kept apart from
|
|
the emitters in :mod:`utils.networks.render` so a change to the consumer rules
|
|
cannot be confused with a change to the YAML they produce.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from utils.roles.entity.name import entity_name
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
|
|
def _is_consumer(
|
|
entry: dict[str, Any],
|
|
application_id: str,
|
|
lookup_config: Callable[[str, str, Any], Any],
|
|
lookup_database: Callable[[str, str], Any],
|
|
) -> bool:
|
|
overlay = entry.get("overlay") or {}
|
|
consumer = overlay.get("consumer") or {}
|
|
declared = consumer.get("kind") or "services_flags"
|
|
kinds = declared if isinstance(declared, list) else [declared]
|
|
return any(
|
|
_matches_kind(
|
|
str(kind), entry, consumer, application_id, lookup_config, lookup_database
|
|
)
|
|
for kind in kinds
|
|
)
|
|
|
|
|
|
def _matches_kind(
|
|
kind: str,
|
|
entry: dict[str, Any],
|
|
consumer: dict[str, Any],
|
|
application_id: str,
|
|
lookup_config: Callable[[str, str, Any], Any],
|
|
lookup_database: Callable[[str, str], Any],
|
|
) -> bool:
|
|
if kind == "database":
|
|
if not _coerce_bool(lookup_database(application_id, "enabled")):
|
|
return False
|
|
if not _coerce_bool(lookup_database(application_id, "shared")):
|
|
return False
|
|
return lookup_database(application_id, "id") == entry.get("role")
|
|
if kind == "services_flags":
|
|
key = consumer.get("key") or entry.get("provides") or entry.get("entity_name")
|
|
flags = consumer.get("flags") or ["enabled", "shared"]
|
|
for flag in flags:
|
|
if not _coerce_bool(
|
|
lookup_config(application_id, f"services.{key}.{flag}", False)
|
|
):
|
|
return False
|
|
return True
|
|
if kind == "mcp_client":
|
|
if not _coerce_bool(lookup_config(application_id, "mcp.enabled", False)):
|
|
return False
|
|
if not _coerce_bool(lookup_config(application_id, "mcp.shared", False)):
|
|
return False
|
|
direction = str(
|
|
lookup_config(application_id, "mcp.direction", "") or ""
|
|
).strip()
|
|
if direction not in ("client", "both"):
|
|
return False
|
|
consumer_key = entity_name(application_id)
|
|
if (
|
|
lookup_config(application_id, f"services.{consumer_key}.mcp_consumer", None)
|
|
is not True
|
|
):
|
|
return False
|
|
refusal = lookup_config(
|
|
entry.get("role") or "", f"services.{consumer_key}.mcp_consumer", True
|
|
)
|
|
return refusal is not False
|
|
if kind == "onion_sso":
|
|
provider = consumer.get("provider")
|
|
if not provider:
|
|
return False
|
|
key = consumer.get("key") or "sso"
|
|
if not _coerce_bool(
|
|
lookup_config(application_id, f"services.{key}.enabled", False)
|
|
):
|
|
return False
|
|
provider_key = consumer.get("provider_key") or "tor"
|
|
return _coerce_bool(
|
|
lookup_config(provider, f"services.{provider_key}.enabled", False)
|
|
)
|
|
if kind == "web_facing":
|
|
return application_id.startswith(("web-app-", "web-svc-"))
|
|
return False
|
|
|
|
|
|
def _coerce_bool(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in ("true", "1", "yes")
|
|
return bool(value)
|
|
|
|
|
|
def _compute_attachments(
|
|
registry: dict[str, dict[str, Any]],
|
|
application_id: str,
|
|
deployment_mode: str,
|
|
lookup_config: Callable[[str, str, Any], Any],
|
|
lookup_database: Callable[[str, str], Any],
|
|
) -> tuple[list[dict[str, Any]], list[str]]:
|
|
attachments: list[dict[str, Any]] = []
|
|
default_aliases: list[str] = []
|
|
|
|
for entry in registry.values():
|
|
if "canonical" in entry:
|
|
continue
|
|
overlay = entry.get("overlay")
|
|
if not overlay:
|
|
continue
|
|
if deployment_mode not in overlay.get("modes", []):
|
|
continue
|
|
|
|
is_provider = application_id == entry.get("role")
|
|
topology = overlay.get("topology")
|
|
|
|
if is_provider:
|
|
if topology == "default_net":
|
|
default_aliases.extend(overlay.get("aliases") or [])
|
|
for peer in registry.values():
|
|
peer_overlay = peer.get("overlay")
|
|
if not peer_overlay:
|
|
continue
|
|
if not peer_overlay.get("proxy_resolvable"):
|
|
continue
|
|
if deployment_mode not in peer_overlay.get("modes", []):
|
|
continue
|
|
if "canonical" in peer:
|
|
continue
|
|
if peer.get("role") == entry.get("role"):
|
|
continue
|
|
default_aliases.extend(
|
|
peer_overlay.get("proxy_aliases")
|
|
or peer_overlay.get("aliases")
|
|
or []
|
|
)
|
|
elif topology:
|
|
aliases = list(
|
|
overlay.get("aliases", [entry.get("entity_name")])
|
|
or [entry.get("entity_name")]
|
|
)
|
|
if overlay.get("collect_proxy_resolvable"):
|
|
for peer in registry.values():
|
|
peer_overlay = peer.get("overlay")
|
|
if not peer_overlay:
|
|
continue
|
|
if not peer_overlay.get("proxy_resolvable"):
|
|
continue
|
|
if deployment_mode not in peer_overlay.get("modes", []):
|
|
continue
|
|
if "canonical" in peer:
|
|
continue
|
|
if peer.get("role") == entry.get("role"):
|
|
continue
|
|
aliases.extend(
|
|
peer_overlay.get("proxy_aliases")
|
|
or peer_overlay.get("aliases")
|
|
or []
|
|
)
|
|
attachments.append(
|
|
{
|
|
"role": entry["role"],
|
|
"topology": topology,
|
|
"aliases": aliases,
|
|
"is_provider": True,
|
|
}
|
|
)
|
|
continue
|
|
|
|
if not topology:
|
|
continue
|
|
if _is_consumer(entry, application_id, lookup_config, lookup_database):
|
|
attachments.append(
|
|
{
|
|
"role": entry["role"],
|
|
"topology": topology,
|
|
"aliases": [],
|
|
"is_provider": False,
|
|
}
|
|
)
|
|
|
|
return attachments, default_aliases
|
|
|
|
|
|
def _suppress_default(
|
|
application_id: str, lookup_database: Callable[[str, str], Any]
|
|
) -> bool:
|
|
if not application_id.startswith(("svc-db-", "svc-ai-")):
|
|
return False
|
|
return not bool(lookup_database(application_id, "local"))
|
|
|
|
|
|
def _own_shared_net_provider(
|
|
attachments: list[dict[str, Any]],
|
|
own_entity: str,
|
|
entity_name: Callable[[str], str],
|
|
) -> bool:
|
|
return any(
|
|
att["is_provider"]
|
|
and att["topology"] == "shared_net"
|
|
and entity_name(att["role"]) == own_entity
|
|
for att in attachments
|
|
)
|
|
|
|
|
|
def _shared_network_key(
|
|
attachments: list[dict[str, Any]],
|
|
own_entity: str,
|
|
entity_name: Callable[[str], str],
|
|
) -> str:
|
|
if _own_shared_net_provider(attachments, own_entity, entity_name):
|
|
return own_entity
|
|
return "default"
|