# -*- coding: utf-8 -*- """ docker.py — Discovery Docker контейнеров, compose, volumes, сети """ import json import os import re from core.runner import run, run_json, exists from core.color import info, warn, success, log_cmd def find_container(name_or_id): """ Ищет контейнер по имени (частичное совпадение) или ID. Возвращает tuple (container_id, container_name, inspect_dict). """ if not exists("docker"): raise RuntimeError("Docker не найден на сервере") # Сначала exact id или name try: out = run_json(f"docker inspect --type=container {name_or_id}") if out and len(out) > 0: c = out[0] return c["Id"], c["Name"].lstrip("/"), c except RuntimeError: pass # Частичный поиск по имени ps_out = run("docker ps -a --format json", check=False) lines = [ln for ln in ps_out.stdout.strip().splitlines() if ln.strip()] matches = [] for ln in lines: try: item = json.loads(ln) names = item.get("Names", "") image = item.get("Image", "") cid = item.get("ID", "") if name_or_id.lower() in names.lower() or name_or_id.lower() in image.lower(): matches.append((cid, names)) except json.JSONDecodeError: continue if not matches: raise RuntimeError(f"Контейнер '{name_or_id}' не найден среди docker ps -a") if len(matches) == 1: cid, cname = matches[0] out = run_json(f"docker inspect {cid}") c = out[0] return c["Id"], c["Name"].lstrip("/"), c else: from core.color import prompt print("Найдено несколько контейнеров:") for i, (cid, cname) in enumerate(matches, 1): print(f" {i}. {cname} ({cid[:12]})") sel = prompt("Выберите номер") idx = int(sel) - 1 cid, cname = matches[idx] out = run_json(f"docker inspect {cid}") c = out[0] return c["Id"], c["Name"].lstrip("/"), c def get_compose_file_from_container(inspect): """ Пытается найти compose-файл по Labels или по рабочей директории контейнера. """ labels = inspect.get("Config", {}).get("Labels", {}) or {} # Compose labels: com.docker.compose.project.working_dir working_dir = labels.get("com.docker.compose.project.working_dir") compose_file = labels.get("com.docker.compose.project.config_files") if compose_file and os.path.isfile(compose_file): return compose_file # Если labels содержит несколько файлов через запятую — берём первый существующий if compose_file and ", " in compose_file: for fp in compose_file.split(", "): fp = fp.strip() if os.path.isfile(fp): return fp elif compose_file and "," in compose_file: for fp in compose_file.split(","): fp = fp.strip() if os.path.isfile(fp): return fp # Fallback: ищем docker-compose.yml / compose.yml рядом с working_dir if working_dir and os.path.isdir(working_dir): for fname in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"): fp = os.path.join(working_dir, fname) if os.path.isfile(fp): return fp # Fallback по Env (PWD если контейнер запущен через compose) env = inspect.get("Config", {}).get("Env", []) pwd = None for e in env: if e.startswith("PWD="): pwd = e.split("=", 1)[1] break if pwd and os.path.isdir(pwd): for fname in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"): fp = os.path.join(pwd, fname) if os.path.isfile(fp): return fp return None def get_env_file(compose_path, inspect): """ Ищет .env рядом с compose-файлом или в working_dir. """ candidates = [] if compose_path: candidates.append(os.path.join(os.path.dirname(compose_path), ".env")) labels = inspect.get("Config", {}).get("Labels", {}) or {} wd = labels.get("com.docker.compose.project.working_dir") if wd: candidates.append(os.path.join(wd, ".env")) env_list = inspect.get("Config", {}).get("Env", []) pwd = None for e in env_list: if e.startswith("PWD="): pwd = e.split("=", 1)[1] break if pwd: candidates.append(os.path.join(pwd, ".env")) for c in candidates: if os.path.isfile(c): return c return None def get_mounts_and_volumes(inspect): """ Возвращает список dict: {type: bind|volume|tmpfs, source, destination, mode} """ mounts = inspect.get("Mounts", []) or [] result = [] for m in mounts: result.append({ "type": m.get("Type", "unknown"), "source": m.get("Source", ""), "destination": m.get("Destination", ""), "mode": m.get("Mode", ""), }) return result def get_ports(inspect): """ Возвращает словарь host_port -> container_port/proto. """ hostconfig = inspect.get("HostConfig", {}) port_bindings = hostconfig.get("PortBindings", {}) or {} result = {} for container_port_proto, bindings in port_bindings.items(): if bindings: for b in bindings: hp = b.get("HostPort") ha = b.get("HostIp", "0.0.0.0") result[f"{ha}:{hp}"] = container_port_proto return result def get_networks(inspect): nets = inspect.get("NetworkSettings", {}).get("Networks", {}) or {} return list(nets.keys()) def get_host_config_flags(inspect): hc = inspect.get("HostConfig", {}) return { "network_mode": hc.get("NetworkMode"), "privileged": hc.get("Privileged"), "cap_add": hc.get("CapAdd", []), "devices": hc.get("Devices", []), "restart_policy": hc.get("RestartPolicy", {}), "pid_mode": hc.get("PidMode"), "ipc_mode": hc.get("IpcMode"), } def get_image(inspect): return inspect.get("Config", {}).get("Image", inspect.get("Image", "")) def discover_docker(name_or_id): """ Главная функция. Возвращает dict с полной информацией docker-части. """ info(f"Ищем Docker контейнер: {name_or_id} ...") cid, cname, inspect = find_container(name_or_id) success(f"Найден контейнер: {cname} ({cid[:12]})") compose_file = get_compose_file_from_container(inspect) env_file = get_env_file(compose_file, inspect) if compose_file else None mounts = get_mounts_and_volumes(inspect) ports = get_ports(inspect) networks = get_networks(inspect) hostcfg = get_host_config_flags(inspect) image = get_image(inspect) data = { "container_id": cid, "container_name": cname, "image": image, "status": inspect.get("State", {}).get("Status", "unknown"), "compose_file": compose_file, "env_file": env_file, "mounts": mounts, "ports": ports, "networks": networks, "host_config": hostcfg, "labels": inspect.get("Config", {}).get("Labels", {}), } if compose_file: success(f"Найден compose-файл: {compose_file}") else: warn("Compose-файл не найден автоматически (возможно, запуск через docker run)") if env_file: success(f"Найден .env: {env_file}") return data def get_container_pid(cid): """Возвращает PID контейнера на хосте (для nsenter)""" try: out = run(f"docker inspect -f '{{{{.State.Pid}}}}' {cid}", check=False) return int(out.stdout.strip()) except Exception: return None