Веб-UI кластера: страница деталей, kubectl по карточкам, мета 3 колонки
- Страница /cluster/<имя>: сводка, ресурсы узлов полосами, отдельные карточки на каждый kubectl get … -o json, рестарт пода. - API: overview с блоками k8s_*, POST pods/restart, расширенный набор ресурсов (-A). - Панель: спиннер загрузки, правки дашборда и стилей; документация api_routes, compose и прочие сопутствующие изменения.
This commit is contained in:
@@ -17,13 +17,19 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from kind_k8s_paths import clusters_dir, data_root
|
||||
from kindest_node_tags import normalize_tag_v_prefix
|
||||
from kubeconfig_patch import patch_kubeconfig_server_for_host, should_patch_after_create
|
||||
from kubeconfig_patch import (
|
||||
kubeconfig_host_file,
|
||||
kubeconfig_path_for_container_kubectl,
|
||||
patch_kubeconfig_server_for_host,
|
||||
should_patch_after_create,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("kind_k8s.cluster_lifecycle")
|
||||
|
||||
@@ -398,19 +404,31 @@ def _wait_nodes_timeout_sec() -> int:
|
||||
return 300
|
||||
|
||||
|
||||
def wait_nodes_ready(*, kubeconfig_path: Path, timeout_sec: int | None = None) -> tuple[bool, str]:
|
||||
def wait_nodes_ready(
|
||||
*,
|
||||
cluster_name: str,
|
||||
kubeconfig_path: Path,
|
||||
timeout_sec: int | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
Дождаться condition=Ready для всех нод через kubectl wait.
|
||||
|
||||
В контейнере веб-приложения путь к kubeconfig проходит через
|
||||
``kubeconfig_path_for_container_kubectl`` (шлюз хоста + проброшенный порт или имя узла kind).
|
||||
|
||||
Возвращает (успех, сообщение для лога/UI).
|
||||
"""
|
||||
if timeout_sec is None:
|
||||
timeout_sec = _wait_nodes_timeout_sec()
|
||||
cfg = kubeconfig_path_for_container_kubectl(
|
||||
cluster_name=cluster_name,
|
||||
kube_source_path=kubeconfig_path,
|
||||
)
|
||||
t = f"{timeout_sec}s"
|
||||
cmd = [
|
||||
"kubectl",
|
||||
"--kubeconfig",
|
||||
str(kubeconfig_path),
|
||||
str(cfg),
|
||||
"wait",
|
||||
"--for=condition=Ready",
|
||||
"nodes",
|
||||
@@ -561,10 +579,14 @@ def create_cluster_non_interactive(
|
||||
_rollback_after_cancel(cluster_name=name, out_dir=out_dir)
|
||||
raise KindClusterError("Операция отменена")
|
||||
|
||||
host_kube_path = kubeconfig_host_file(kube_path.parent)
|
||||
patched = False
|
||||
if should_patch_after_create():
|
||||
_progress("Настройка доступа с вашего компьютера", 72)
|
||||
patched = patch_kubeconfig_server_for_host(cluster_name=name, kube_path=kube_path)
|
||||
shutil.copyfile(kube_path, host_kube_path, follow_symlinks=True)
|
||||
patched = patch_kubeconfig_server_for_host(cluster_name=name, kube_path=host_kube_path)
|
||||
if not patched:
|
||||
host_kube_path.unlink(missing_ok=True)
|
||||
|
||||
if _cancelled():
|
||||
_rollback_after_cancel(cluster_name=name, out_dir=out_dir)
|
||||
@@ -574,7 +596,7 @@ def create_cluster_non_interactive(
|
||||
nodes_msg: str | None = None
|
||||
if _wait_nodes_enabled():
|
||||
_progress("Ожидание готовности узлов", 82)
|
||||
ok, msg = wait_nodes_ready(kubeconfig_path=kube_path)
|
||||
ok, msg = wait_nodes_ready(cluster_name=name, kubeconfig_path=kube_path)
|
||||
nodes_ready = ok
|
||||
nodes_msg = msg
|
||||
if ok:
|
||||
@@ -606,6 +628,8 @@ def create_cluster_non_interactive(
|
||||
"nodes_ready_message": nodes_msg,
|
||||
"provisioned_from_existing_config": use_existing_config,
|
||||
}
|
||||
if patched:
|
||||
meta["kubeconfig_host_path"] = str(host_kube_path.relative_to(root))
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
_progress("Завершение", 95)
|
||||
@@ -872,13 +896,15 @@ def container_engine_ping(*, timeout_sec: float = 12.0) -> tuple[bool, str, str]
|
||||
return False, err[:800], cli
|
||||
|
||||
|
||||
def kubectl_nodes_wide(*, kubeconfig: str | Path) -> tuple[int, str]:
|
||||
def kubectl_nodes_wide(*, cluster_name: str, kubeconfig: str | Path) -> tuple[int, str]:
|
||||
"""``kubectl get nodes -o wide``; возвращает (код, объединённый вывод)."""
|
||||
src = Path(kubeconfig)
|
||||
cfg = kubeconfig_path_for_container_kubectl(cluster_name=cluster_name, kube_source_path=src)
|
||||
p = subprocess.run(
|
||||
[
|
||||
"kubectl",
|
||||
"--kubeconfig",
|
||||
str(kubeconfig),
|
||||
str(cfg),
|
||||
"get",
|
||||
"nodes",
|
||||
"-o",
|
||||
@@ -894,13 +920,15 @@ def kubectl_nodes_wide(*, kubeconfig: str | Path) -> tuple[int, str]:
|
||||
return p.returncode, msg
|
||||
|
||||
|
||||
def kubectl_pods_all_namespaces(*, kubeconfig: str | Path) -> tuple[int, str]:
|
||||
def kubectl_pods_all_namespaces(*, cluster_name: str, kubeconfig: str | Path) -> tuple[int, str]:
|
||||
"""``kubectl get pods -A``; сводка подов по кластеру."""
|
||||
src = Path(kubeconfig)
|
||||
cfg = kubeconfig_path_for_container_kubectl(cluster_name=cluster_name, kube_source_path=src)
|
||||
p = subprocess.run(
|
||||
[
|
||||
"kubectl",
|
||||
"--kubeconfig",
|
||||
str(kubeconfig),
|
||||
str(cfg),
|
||||
"get",
|
||||
"pods",
|
||||
"-A",
|
||||
@@ -915,6 +943,128 @@ def kubectl_pods_all_namespaces(*, kubeconfig: str | Path) -> tuple[int, str]:
|
||||
return p.returncode, msg
|
||||
|
||||
|
||||
def kubectl_get_all_namespaces_wide(
|
||||
*,
|
||||
cluster_name: str,
|
||||
kubeconfig: str | Path,
|
||||
resource: str,
|
||||
timeout: str = "30s",
|
||||
) -> tuple[int, str]:
|
||||
"""
|
||||
``kubectl get <resource> -A -o wide`` — Deployments, StatefulSets, DaemonSets, Service, Ingress и т.д.
|
||||
|
||||
``resource`` — аргумент kubectl (``deployments``, ``pods``, ``svc`` …).
|
||||
"""
|
||||
src = Path(kubeconfig)
|
||||
cfg = kubeconfig_path_for_container_kubectl(cluster_name=cluster_name, kube_source_path=src)
|
||||
p = subprocess.run(
|
||||
[
|
||||
"kubectl",
|
||||
"--kubeconfig",
|
||||
str(cfg),
|
||||
"get",
|
||||
resource,
|
||||
"-A",
|
||||
"-o",
|
||||
"wide",
|
||||
f"--request-timeout={timeout}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
out = (p.stdout or "").strip()
|
||||
err = (p.stderr or "").strip()
|
||||
msg = out if out else err
|
||||
return p.returncode, msg
|
||||
|
||||
|
||||
def _parse_kubectl_list_json(stdout: str) -> tuple[list[dict[str, Any]] | None, str]:
|
||||
"""Разбор вывода ``kubectl get … -o json`` (Kind List)."""
|
||||
if not (stdout or "").strip():
|
||||
return [], ""
|
||||
try:
|
||||
data = json.loads(stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
return None, f"JSON: {e}"
|
||||
if not isinstance(data, dict):
|
||||
return None, "корень ответа не объект"
|
||||
raw_items = data.get("items")
|
||||
if raw_items is None:
|
||||
return [], ""
|
||||
if not isinstance(raw_items, list):
|
||||
return None, "поле items не список"
|
||||
out: list[dict[str, Any]] = [x for x in raw_items if isinstance(x, dict)]
|
||||
return out, ""
|
||||
|
||||
|
||||
def kubectl_get_json(
|
||||
*,
|
||||
cluster_name: str,
|
||||
kubeconfig: str | Path,
|
||||
resource_args: list[str],
|
||||
timeout: str = "30s",
|
||||
) -> tuple[int, list[dict[str, Any]] | None, str]:
|
||||
"""
|
||||
``kubectl get <resource_args…> -o json``.
|
||||
|
||||
``resource_args`` — например ``["nodes"]`` или ``["pods", "-A"]``, ``["deployments", "-A"]``.
|
||||
Возвращает (код, список items или None, сообщение об ошибке).
|
||||
"""
|
||||
src = Path(kubeconfig)
|
||||
cfg = kubeconfig_path_for_container_kubectl(cluster_name=cluster_name, kube_source_path=src)
|
||||
cmd = [
|
||||
"kubectl",
|
||||
"--kubeconfig",
|
||||
str(cfg),
|
||||
"get",
|
||||
*resource_args,
|
||||
"-o",
|
||||
"json",
|
||||
f"--request-timeout={timeout}",
|
||||
]
|
||||
p = subprocess.run(cmd, capture_output=True, text=True)
|
||||
out = (p.stdout or "").strip()
|
||||
err = (p.stderr or "").strip()
|
||||
if p.returncode != 0:
|
||||
return p.returncode, None, err or out or f"код {p.returncode}"
|
||||
items, perr = _parse_kubectl_list_json(out)
|
||||
if items is None:
|
||||
return p.returncode, None, perr
|
||||
return 0, items, ""
|
||||
|
||||
|
||||
def kubectl_delete_pod(
|
||||
*,
|
||||
cluster_name: str,
|
||||
kubeconfig: str | Path,
|
||||
namespace: str,
|
||||
pod_name: str,
|
||||
grace_period: int = 30,
|
||||
) -> tuple[int, str]:
|
||||
"""
|
||||
Удаление пода (контроллер при необходимости создаст новый) — «мягкий рестарт».
|
||||
|
||||
``grace_period`` — секунды до SIGKILL (0 — сразу; для системных подов осторожно).
|
||||
"""
|
||||
src = Path(kubeconfig)
|
||||
cfg = kubeconfig_path_for_container_kubectl(cluster_name=cluster_name, kube_source_path=src)
|
||||
cmd = [
|
||||
"kubectl",
|
||||
"--kubeconfig",
|
||||
str(cfg),
|
||||
"delete",
|
||||
"pod",
|
||||
pod_name,
|
||||
"-n",
|
||||
namespace,
|
||||
f"--grace-period={grace_period}",
|
||||
"--request-timeout=60s",
|
||||
]
|
||||
p = subprocess.run(cmd, capture_output=True, text=True)
|
||||
msg = ((p.stdout or "").strip() or (p.stderr or "").strip() or f"код {p.returncode}")[:8000]
|
||||
return p.returncode, msg
|
||||
|
||||
|
||||
def cluster_summary_for_api(name: str) -> dict[str, object]:
|
||||
"""Сводка по кластеру для JSON API (без блокирующих долгих вызовов)."""
|
||||
from core.provision_log import provision_log_file_path
|
||||
|
||||
Reference in New Issue
Block a user