import asyncio from datetime import datetime, timezone from typing import Dict from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from services.runner import run_cmd def container_uptime(started_at: str) -> str: """ started_at: 2026-02-06T21:14:33.123456789Z """ try: start = datetime.fromisoformat( started_at.replace("Z", "+00:00") ).astimezone(timezone.utc) delta = datetime.now(timezone.utc) - start days = delta.days hours = delta.seconds // 3600 minutes = (delta.seconds % 3600) // 60 if days > 0: return f"{days}d {hours}h" if hours > 0: return f"{hours}h {minutes}m" return f"{minutes}m" except Exception: return "unknown" async def build_docker_map(cfg) -> Dict[str, str]: docker_cfg = cfg.get("docker", {}) result: Dict[str, str] = {} # 1. autodiscovery if docker_cfg.get("autodiscovery"): rc, raw = await docker_cmd(["ps", "--format", "{{.Names}}"], timeout=20) if rc == 0: names = raw.splitlines() patterns = docker_cfg.get("match", []) for name in names: if any(p in name for p in patterns): result[name] = name # 2. aliases override aliases = docker_cfg.get("aliases", {}) for alias, real in aliases.items(): result[alias] = real return result async def discover_containers(cfg) -> Dict[str, str]: """ returns: alias -> real container name """ docker_cfg = cfg.get("docker", {}) result: Dict[str, str] = {} # --- autodiscovery --- if docker_cfg.get("autodiscovery"): rc, raw = await docker_cmd(["ps", "--format", "{{.Names}}"], timeout=20) if rc == 0: found = raw.splitlines() label = docker_cfg.get("label") patterns = docker_cfg.get("match", []) for name in found: # label-based discovery if label: key, val = label.split("=", 1) rc2, lbl = await docker_cmd( ["inspect", "-f", f"{{{{ index .Config.Labels \"{key}\" }}}}", name], timeout=10 ) if rc2 == 0 and lbl.strip() == val: result[name] = name continue # name-pattern discovery if any(p in name for p in patterns): result[name] = name # --- manual aliases ALWAYS override --- aliases = docker_cfg.get("aliases", {}) for alias, real in aliases.items(): result[alias] = real return result async def docker_cmd(args: list[str], timeout: int = 20): rc, out = await run_cmd(["docker"] + args, timeout=timeout) if rc == 0: return rc, out return await run_cmd(["sudo", "docker"] + args, timeout=timeout) async def docker_watchdog(container_map, notify, bot, chat_id): last = {} while True: if not last: for alias, real in container_map.items(): rc, raw = await docker_cmd( ["inspect", "-f", "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}", real], timeout=10 ) if rc != 0: last[alias] = ("error", "n/a") continue parts = raw.strip().split("|", 1) status = parts[0] if parts else "unknown" health = parts[1] if len(parts) > 1 else "n/a" last[alias] = (status, health) await asyncio.sleep(120) continue for alias, real in container_map.items(): rc, raw = await docker_cmd( ["inspect", "-f", "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}n/a{{end}}", real], timeout=10 ) if rc != 0: status, health = "error", "n/a" else: parts = raw.strip().split("|", 1) status = parts[0] if parts else "unknown" health = parts[1] if len(parts) > 1 else "n/a" if last.get(alias) != (status, health): if status != "running": kb = InlineKeyboardMarkup( inline_keyboard=[[ InlineKeyboardButton( text="🔄 Restart", callback_data=f"wdrestart:{alias}" ) ]] ) await bot.send_message( chat_id, f"🐳 {alias}: {status}", reply_markup=kb, ) elif health not in ("healthy", "n/a"): await notify( bot, chat_id, f"⚠️ {alias} health: {health}", level="warn", key=f"docker_health:{alias}", ) else: await notify( bot, chat_id, f"🐳 {alias}: {status}", level="info", key=f"docker_status:{alias}:{status}", ) last[alias] = (status, health) await asyncio.sleep(120)