Веб-UI: логи kind create, старт/стоп кластеров, документация README
- Потоковые логи в job_store и UI; kind create через Popen с построчным выводом
- POST /clusters/{name}/start|stop; create по сохранённому kind-config.yaml
- Страница /documentation: GET /api/v1/docs/readme, marked+DOMPurify из static/vendor
- Иконки действий, плавающие подсказки, модалка подтверждения вместо confirm
- Makefile: make docker|podman rebuild; compose: монтирование README.md
- Dockerfile: COPY README.md; readme_doc: несколько путей к README
Автор: Сергей Антропов — https://devops.org.ru
This commit is contained in:
+52
-1
@@ -12,9 +12,11 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -29,6 +31,46 @@ JobStatus = Literal["queued", "running", "success", "failed", "cancelled"]
|
||||
_thread_lock = threading.Lock()
|
||||
_cancel_events: dict[str, threading.Event] = {}
|
||||
_progress: dict[str, tuple[str, int]] = {}
|
||||
# Хвост логов для активных заданий (kind create и т.д.); после завершения копируется в JobRecord.log_lines
|
||||
_job_log_deques: dict[str, deque[str]] = {}
|
||||
|
||||
|
||||
def _max_job_log_lines() -> int:
|
||||
raw = (os.environ.get("KIND_K8S_JOB_LOG_MAX_LINES") or "500").strip()
|
||||
try:
|
||||
return max(50, min(int(raw), 5000))
|
||||
except ValueError:
|
||||
return 500
|
||||
|
||||
|
||||
def append_log_sync(job_id: str, line: str) -> None:
|
||||
"""Добавить строку в журнал задания (вызывается из worker-thread во время долгих команд)."""
|
||||
text = (line or "").rstrip()
|
||||
if not text:
|
||||
return
|
||||
cap = _max_job_log_lines()
|
||||
with _thread_lock:
|
||||
if job_id not in _job_log_deques:
|
||||
_job_log_deques[job_id] = deque(maxlen=cap)
|
||||
_job_log_deques[job_id].append(text)
|
||||
|
||||
|
||||
def get_logs_snapshot_sync(job_id: str) -> list[str]:
|
||||
"""Снимок текущего журнала (для API во время running/queued)."""
|
||||
with _thread_lock:
|
||||
d = _job_log_deques.get(job_id)
|
||||
return list(d) if d else []
|
||||
|
||||
|
||||
def take_logs_finalize_sync(job_id: str) -> list[str]:
|
||||
"""
|
||||
Забрать журнал в список и удалить deque (после успеха/ошибки/отмены).
|
||||
|
||||
Вызывать перед или внутри обновления JobRecord.
|
||||
"""
|
||||
with _thread_lock:
|
||||
d = _job_log_deques.pop(job_id, None)
|
||||
return list(d) if d else []
|
||||
|
||||
|
||||
def begin_job_tracking(job_id: str) -> None:
|
||||
@@ -43,6 +85,7 @@ def end_job_tracking(job_id: str) -> None:
|
||||
with _thread_lock:
|
||||
_cancel_events.pop(job_id, None)
|
||||
_progress.pop(job_id, None)
|
||||
_job_log_deques.pop(job_id, None)
|
||||
|
||||
|
||||
def set_progress_sync(job_id: str, stage: str, percent: int) -> None:
|
||||
@@ -88,6 +131,8 @@ class JobRecord:
|
||||
created_at_utc: str
|
||||
message: str | None = None
|
||||
result: dict[str, Any] | None = None
|
||||
# Журнал после завершения (stdout/stderr kind create и этапы); пока задание активно — см. deque
|
||||
log_lines: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class JobStore:
|
||||
@@ -127,25 +172,31 @@ class JobStore:
|
||||
set_progress_sync(job_id, "Запуск создания кластера…", 5)
|
||||
|
||||
async def set_success(self, job_id: str, *, result: dict[str, Any] | None = None, message: str | None = None) -> None:
|
||||
logs = take_logs_finalize_sync(job_id)
|
||||
async with self._lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id].status = "success"
|
||||
self._jobs[job_id].result = result
|
||||
self._jobs[job_id].message = message
|
||||
self._jobs[job_id].log_lines = logs
|
||||
set_progress_sync(job_id, "Готово", 100)
|
||||
|
||||
async def set_failed(self, job_id: str, message: str) -> None:
|
||||
logs = take_logs_finalize_sync(job_id)
|
||||
async with self._lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id].status = "failed"
|
||||
self._jobs[job_id].message = message
|
||||
self._jobs[job_id].log_lines = logs
|
||||
logger.warning("Задание %s завершилось ошибкой: %s", job_id, message)
|
||||
|
||||
async def set_cancelled(self, job_id: str, message: str = "Создание отменено пользователем") -> None:
|
||||
logs = take_logs_finalize_sync(job_id)
|
||||
async with self._lock:
|
||||
if job_id in self._jobs:
|
||||
self._jobs[job_id].status = "cancelled"
|
||||
self._jobs[job_id].message = message
|
||||
self._jobs[job_id].log_lines = logs
|
||||
logger.info("Задание %s отменено: %s", job_id, message)
|
||||
|
||||
async def get(self, job_id: str) -> JobRecord | None:
|
||||
|
||||
Reference in New Issue
Block a user