Files

190 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""E2E-тест деплоя omnichannel-configserver-mcp с реальным config-agent.
Поднимает контейнер config-agent, который опрашивает config_server и реально
выполняет задачи через docker-compose, затем гоняет жизненный цикл сервиса
через MCP: set_env -> set_compose -> deploy -> restart -> down.
Требуется:
- поднятый стенд: docker compose -f examples/demo/docker-compose.yml up -d
- собранный бинарник: go build -o omnichannel-configserver-mcp .
- доступ к образу config-agent (registry.devlexicom.ru).
Запуск: python3 examples/demo/e2e.py
"""
import json
import os
import subprocess
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from demo import MCP # noqa: E402 (общий минимальный MCP-клиент)
AGENT_IMAGE = "registry.devlexicom.ru/omnichannel_platform/config_server/config-agent:v1.1.0"
AGENT_NAME = "omni-e2e-agent"
NETWORK = "omni-mcp-demo_default"
OPT_DIR = "/tmp/omni-e2e/opt"
APP = "demo_e2e"
APP_DIR = os.path.join(OPT_DIR, APP)
HOST_IP = "10.0.0.10"
INITIAL_COMPOSE = """services:
web:
image: alpine:3.20
command: ["sleep", "3600"]
"""
UPDATED_COMPOSE = """services:
web:
image: alpine:3.20
command: ["sleep", "7200"]
"""
def sh(*args, check=True):
return subprocess.run(args, capture_output=True, text=True, check=check)
def docker(*args, check=True):
return sh("docker", *args, check=check)
def prepare_app_dir():
os.makedirs(APP_DIR, exist_ok=True)
with open(os.path.join(APP_DIR, "docker-compose.yml"), "w") as fh:
fh.write(INITIAL_COMPOSE)
def start_agent():
docker("rm", "-f", AGENT_NAME, check=False)
docker(
"run", "-d", "--name", AGENT_NAME,
"--network", NETWORK,
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", "/usr/bin/docker:/usr/bin/docker:ro",
"-v", f"{OPT_DIR}:/opt",
"-e", "SERVER_URL=http://config-server:5000",
"-e", "AGENT_SECRET=demo-agent-secret",
"-e", f"AGENT_REGISTER_IP={HOST_IP}",
"-e", "TASK_CHECK_INTERVAL=3",
"-e", "SCAN_INTERVAL=15",
AGENT_IMAGE,
)
print(f"config-agent запущен: {AGENT_NAME}")
def stop_agent():
docker("rm", "-f", AGENT_NAME, check=False)
print("config-agent остановлен")
def find_app(mcp, timeout=60):
deadline = time.time() + timeout
while time.time() < deadline:
text, _ = mcp.call("list_applications", {})
try:
apps = json.loads(text)
except ValueError:
apps = []
for app in apps:
if app.get("name") == APP:
return app["id"]
time.sleep(2)
raise RuntimeError(f"агент не зарегистрировал приложение {APP} за {timeout}s")
def wait_task(mcp, task_id, timeout=120):
deadline = time.time() + timeout
while time.time() < deadline:
text, _ = mcp.call("get_task", {"task_id": task_id})
task = json.loads(text)
if task["status"] in ("completed", "failed"):
return task
time.sleep(2)
raise RuntimeError(f"задача {task_id} не завершилась за {timeout}s")
def call_json(mcp, tool, args):
text, is_error = mcp.call(tool, args)
if is_error:
raise RuntimeError(f"{tool}: {text}")
return json.loads(text)
def container_running(app):
out = docker("ps", "--filter", f"name={app}", "--format", "{{.Names}}").stdout
return app in out
def wait_file_contains(path, needle, timeout=40):
deadline = time.time() + timeout
while time.time() < deadline:
try:
if needle in open(path).read():
return True
except OSError:
pass
time.sleep(2)
return False
CHECKS = []
def check(name, ok):
CHECKS.append((name, ok))
print(f" [{'PASS' if ok else 'FAIL'}] {name}")
def main():
mcp = MCP()
try:
app_id = find_app(mcp)
print(f"Приложение найдено: {APP} (app_id={app_id})")
# 1. Изменение конфигурации через MCP и доставка агентом.
call_json(mcp, "set_env", {"app_id": app_id, "filename": ".env", "content": "E2E=1\n"})
call_json(mcp, "set_compose", {"app_id": app_id, "content": UPDATED_COMPOSE})
check("set_env/set_compose приняты сервером", True)
check("compose доставлен агентом на диск",
wait_file_contains(os.path.join(APP_DIR, "docker-compose.yml"), "7200"))
# 2. Deploy.
ack = call_json(mcp, "deploy", {"app_id": app_id, "wait": True})
task = ack.get("task", {})
check("deploy завершён (completed)", task.get("status") == "completed")
check("контейнер demo_e2e поднят", container_running(APP))
print(f" вывод deploy: {task.get('output','')[:160].strip()}")
# 3. Restart.
ack = call_json(mcp, "restart", {"app_id": app_id, "wait": True})
check("restart завершён (completed)", ack.get("task", {}).get("status") == "completed")
check("контейнер жив после restart", container_running(APP))
# 4. Down.
ack = call_json(mcp, "down", {"app_id": app_id, "confirm": True, "wait": True})
check("down завершён (completed)", ack.get("task", {}).get("status") == "completed")
time.sleep(2)
check("контейнер остановлен", not container_running(APP))
# 5. Защита без confirm.
text, is_error = mcp.call("down", {"app_id": app_id})
check("down без confirm отклонён", is_error)
finally:
mcp.close()
stop_agent()
failed = [name for name, ok in CHECKS if not ok]
print(f"\nE2E: {len(CHECKS) - len(failed)}/{len(CHECKS)} проверок пройдено")
if failed:
print("Провалено: " + "; ".join(failed))
sys.exit(1)
if __name__ == "__main__":
prepare_app_dir()
start_agent()
main()