diff --git a/haproxy_mcp/db.py b/haproxy_mcp/db.py index 99053f9..09a9069 100644 --- a/haproxy_mcp/db.py +++ b/haproxy_mcp/db.py @@ -355,6 +355,21 @@ def db_remove_domain(domain: str) -> None: conn.commit() +def db_remove_wildcard(domain: str) -> bool: + """Remove only the wildcard entry for a domain from the database. + + Args: + domain: Base domain name (without leading dot, e.g., "nocodb.inouter.com"). + + Returns: + True if a wildcard entry was deleted, False if not found. + """ + conn = get_connection() + cur = conn.execute("DELETE FROM domains WHERE domain = ? AND is_wildcard = 1", (f".{domain}",)) + conn.commit() + return cur.rowcount > 0 + + def db_find_available_pool() -> Optional[str]: """Find the first available pool not assigned to any domain. diff --git a/haproxy_mcp/tools/domains.py b/haproxy_mcp/tools/domains.py index ab43666..a96b8ff 100644 --- a/haproxy_mcp/tools/domains.py +++ b/haproxy_mcp/tools/domains.py @@ -34,7 +34,7 @@ from ..file_ops import ( remove_domain_from_map, find_available_pool, ) -from ..db import db_load_certs +from ..db import db_load_certs, db_remove_wildcard, sync_map_files from ..utils import parse_servers_state, disable_server_slot @@ -408,3 +408,48 @@ def register_domain_tools(mcp): return f"Error: Failed to update map file: {e}" except HaproxyError as e: return f"Error: {e}" + + @mcp.tool() + def haproxy_cleanup_wildcards() -> str: + """Remove unnecessary wildcard entries for subdomain domains. + + Scans all wildcard entries and removes those where the domain is a + subdomain (not a base domain). Base domains keep their wildcards. + + Example: *.nocodb.inouter.com is removed (subdomain of inouter.com), + but *.anvil.it.com is kept (anvil.it.com is a base domain). + """ + entries = get_map_contents() + + # Build registered domains set + registered_domains: set[str] = set() + for entry_domain, _ in entries: + if not entry_domain.startswith("."): + registered_domains.add(entry_domain) + + # Find wildcard entries that should be removed + removed = [] + for entry_domain, backend in entries: + if not entry_domain.startswith("."): + continue + domain = entry_domain[1:] # strip leading dot + is_sub, parent = _check_subdomain(domain, registered_domains) + if is_sub: + # Remove from DB + if db_remove_wildcard(domain): + # Remove from HAProxy runtime map + try: + haproxy_cmd(f"del map {WILDCARDS_MAP_FILE_CONTAINER} {entry_domain}") + except HaproxyError as e: + logger.warning("Failed to remove wildcard map entry %s: %s", entry_domain, e) + removed.append(f"{entry_domain} -> {backend} (subdomain of {parent})") + + if removed: + # Sync map files from DB + sync_map_files() + result = f"Removed {len(removed)} unnecessary wildcard(s):\n" + result += "\n".join(f" • {r}" for r in removed) + else: + result = "No unnecessary wildcard entries found." + + return result