serverus: DockerHost - установка Docker и ingress Caddy, deploy приложения с проверкой compose и caddy.conf до изменений и пересозданием только изменившихся сервисов, status, control, logs, remove. nftables перезапускается только при изменении конфига, Docker следом восстанавливает свои правила; release восстанавливает конфиг Debian вместо остановки nftables. Telegram: прием документов и отправка файлов, раздача файлов своего Bot API сервера через nginx (telegram-files). App: общая модель Managed для Point и Platform, DockerApp и версии файлов AppVersion, разбор архивов (uploads), сценарии платформы (platforms), задачи и меню docker-серверов в боте. Проверено на стенде Debian 13 с local_certs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
69 lines
2.4 KiB
Python
69 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
||
"""Состояние docker-приложений: app_status.py <apps_dir> [<app>].
|
||
|
||
Сервисы берутся из compose-файла и сверяются с docker compose ps -a: описан, но контейнера нет - state missing.
|
||
Контейнеры, которых нет в compose-файле (остались от прошлой версии), тоже попадают в список.
|
||
Печатает JSON: {"apps": {app: {"services": {service: {"state", "health", "status"}}, "error": ""}}}.
|
||
"""
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
|
||
COMPOSE_FILE = 'docker-compose.yaml'
|
||
|
||
|
||
def compose(app_dir, *args):
|
||
result = subprocess.run(
|
||
['docker', 'compose', *args], cwd=app_dir, capture_output=True, text=True, timeout=120,
|
||
)
|
||
if result.returncode != 0:
|
||
raise RuntimeError((result.stderr or result.stdout).strip()[-500:])
|
||
return result.stdout
|
||
|
||
|
||
def containers(output):
|
||
"""ps --format json: старый compose печатает массив, новый - объект на строку."""
|
||
output = output.strip()
|
||
if not output:
|
||
return []
|
||
if output.startswith('['):
|
||
return json.loads(output)
|
||
return [json.loads(line) for line in output.splitlines() if line.strip()]
|
||
|
||
|
||
def app_status(app_dir):
|
||
services = {
|
||
name: {'state': 'missing', 'health': '', 'status': ''}
|
||
for name in compose(app_dir, 'config', '--services').split()
|
||
}
|
||
for container in containers(compose(app_dir, 'ps', '-a', '--format', 'json')):
|
||
state = {
|
||
'state': container.get('State', ''),
|
||
'health': container.get('Health', ''),
|
||
'status': container.get('Status', ''),
|
||
}
|
||
name = container.get('Service', '')
|
||
# несколько реплик: показываем проблемную
|
||
if services.get(name, {}).get('state') in (None, 'missing', 'running'):
|
||
services[name] = state
|
||
return services
|
||
|
||
|
||
def main(apps_dir, only=''):
|
||
apps = {}
|
||
names = [only] if only else sorted(os.listdir(apps_dir)) if os.path.isdir(apps_dir) else []
|
||
for name in names:
|
||
app_dir = os.path.join(apps_dir, name)
|
||
if name.startswith('.') or not os.path.isfile(os.path.join(app_dir, COMPOSE_FILE)):
|
||
continue
|
||
try:
|
||
apps[name] = {'services': app_status(app_dir), 'error': ''}
|
||
except (RuntimeError, ValueError, subprocess.TimeoutExpired) as error:
|
||
apps[name] = {'services': {}, 'error': str(error)}
|
||
print(json.dumps({'apps': apps}))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main(*sys.argv[1:])
|