"""Lookup ``engine``: resolve a central/embedded engine connection for a consumer. {{ lookup('engine', 'redis', application_id) }} # full dict {{ lookup('engine', 'redis', application_id, 'url') }} # one field {{ lookup('engine', 'redis', application_id, 'session_path') }} # phpredis session.save_path Mirrors the ``database`` lookup but for the non-RDBMS engines registered in :data:`utils.roles.applications.services.engines.ENGINES` (redis, memcached, rabbitmq, elasticsearch, typesense, qdrant, unbound). When the consumer declares ``services..shared: true`` the connection points at the central ``svc-db-*`` / ``svc-dns-*`` stack with the consumer's own provisioned credentials; otherwise it points at the embedded sidecar inside the consumer's own stack. See ``docs/architecture/central-engines.md``. """ from __future__ import annotations from typing import Any from urllib.parse import quote from ansible.errors import AnsibleError from ansible.plugins.loader import lookup_loader from ansible.plugins.lookup import LookupBase from utils.manager.credential_key import OVERRIDE_SECTION from utils.roles.applications.config import get from utils.roles.applications.services.engines import ( ENGINES, engine_service, is_engine_enabled, is_engine_shared, ) from utils.roles.entity.name import get_entity_name _FIELDS = ( "engine", "enabled", "shared", "local", "host", "port", "username", "password", "instance", "prefix", "url", "container", "session_path", ) def redis_session_path( host: str, port: int, prefix: str, username: str, password: str ) -> str: """Return the phpredis ``session.save_path`` of one consumer. Args: host: redis host the consumer reaches. port: redis port. prefix: consumer prefix; session keys become ``:session:``. username: ACL user, sent only together with a password. password: redis password; empty for an embedded sidecar. """ path = f"tcp://{host}:{port}?prefix={quote(prefix, safe='')}:session:" if password: path += ( f"&auth[user]={quote(username, safe='')}" f"&auth[pass]={quote(password, safe='')}" ) return path def qdrant_consumer_key(applications: dict[str, Any], svc_id: str) -> str: """Return the engine-wide key a data-owning qdrant consumer presents. Args: applications: the resolved applications mapping. svc_id: the qdrant service role id. qdrant has no per-consumer principals, only two engine-wide keys, so a consumer that owns data here holds the engine's own key. The read-only key is deliberately never returned: it belongs to the MCP adapter, whose whole read-only guarantee rests on being unable to present anything else. """ return str( get( applications, svc_id, f"{OVERRIDE_SECTION}.api_key", strict=False, default="", skip_missing_app=True, ) or "" ).strip() class LookupModule(LookupBase): def run( self, terms: list[Any], variables: dict[str, Any] | None = None, **kwargs: Any, ) -> list[Any]: terms = terms or [] if len(terms) not in (2, 3): raise AnsibleError( "engine: requires engine_type, consumer_id [, want_path]" ) engine = str(terms[0]).strip() consumer_id = str(terms[1]).strip() want = str(terms[2]).strip() if len(terms) == 3 else "all" if engine not in ENGINES: raise AnsibleError( f"engine: unknown engine '{engine}'; known: {', '.join(ENGINES)}" ) if not consumer_id: raise AnsibleError("engine: consumer_id must not be empty") vars_ = variables or self._templar.available_variables applications = lookup_loader.get( "applications", loader=self._loader, templar=getattr(self, "_templar", None) ).run([], variables=vars_)[0] descriptor = ENGINES[engine] svc_id = descriptor["svc"] port = int(descriptor["port"]) entity = get_entity_name(consumer_id) enabled = is_engine_enabled(applications, consumer_id, engine) shared = is_engine_shared(applications, consumer_id, engine) svc_cfg = engine_service(applications, consumer_id, engine) central_name = str( get( applications, svc_id, f"services.{engine}.name", strict=False, default=engine, skip_missing_app=True, ) or engine ).strip() username = entity prefix = entity password = str( svc_cfg.get("password") or get( applications, consumer_id, f"{OVERRIDE_SECTION}.{engine}_password", strict=False, default="", ) or "" ).strip() host = central_name if shared else engine instance = central_name if shared else entity # Exception: shared redis authenticates as the central `default` user, not # a per-consumer ACL user — ACL users live only in memory and are lost when # a deploy recreates the central redis after the consumer was provisioned. if engine == "redis" and shared: username = "default" password = str( get( applications, svc_id, f"{OVERRIDE_SECTION}.REDIS_PASSWORD", strict=False, default="", skip_missing_app=True, ) or "" ).strip() if engine == "qdrant" and shared: password = qdrant_consumer_key(applications, svc_id) if engine == "rabbitmq": url = ( f"amqp://{username}:{password}@{host}:{port}/{entity}" if shared else f"amqp://{host}:{port}/" ) elif engine in ("redis", "memcached"): scheme = "redis" if engine == "redis" else "memcached" auth = f"{username}:{password}@" if (shared and password) else "" url = f"{scheme}://{auth}{host}:{port}" elif engine in ("elasticsearch", "typesense", "qdrant"): url = f"http://{host}:{port}" else: url = host consumer_name = str( get( applications, consumer_id, f"services.{engine}.name", strict=False, default="", skip_missing_app=True, ) or "" ).strip() container = central_name if shared else (consumer_name or f"{entity}-{engine}") resolved = { "engine": engine, "enabled": enabled, "shared": shared, "local": bool(enabled and not shared), "host": host, "port": port, "username": username if shared else "", "password": password if shared else "", "instance": instance, "prefix": prefix, "url": url, "container": container, "session_path": ( redis_session_path( host, port, prefix, username if shared else "", password if shared else "", ) if engine == "redis" else "" ), } if want == "all": return [resolved] if want not in _FIELDS: raise AnsibleError( f"engine: unknown want '{want}'; known: {', '.join(_FIELDS)}" ) return [resolved.get(want, "")]