Панель: редактирование конфига кластера, reapply при старте, UI и таблица

- PUT/GET конфигурации кластера, страница редактирования и модалка после сохранения
- После смены kind-config: флаг в meta и start_cluster_reapply (kind delete + create)
- Старт/стоп: полноэкранный спиннер до завершения job; модалки и документация API
- Таблица кластеров: колонка Имя 40% при table-layout fixed; чекбоксы без width 100%
- Карточки ресурсов узлов на странице кластера: до 3 в ряд; прочие правки стилей и dashboard.js
This commit is contained in:
Sergey Antropoff
2026-04-04 10:49:40 +03:00
parent af0d1705cc
commit 52538d9816
14 changed files with 2571 additions and 137 deletions
+72 -13
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
from typing import Any, Literal
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
class ClusterCreateRequest(BaseModel):
@@ -143,12 +143,12 @@ class ClusterWorkloadsResponse(BaseModel):
class K8sListJsonBlock(BaseModel):
"""Фрагмент ответа ``kubectl get … -o json`` (items) для таблицы в UI."""
"""Фрагмент списка ресурсов Kubernetes (поле ``items`` из JSON List) для таблицы в UI."""
ok: bool = True
rc: int | None = Field(default=None, description="Код выхода kubectl")
rc: int | None = Field(default=None, description="Код возврата при обращении к API кластера")
items: list[dict[str, Any]] = Field(default_factory=list)
message: str | None = Field(default=None, description="Ошибка kubectl или разбора JSON")
message: str | None = Field(default=None, description="Текст ошибки или разбора JSON")
class PodRestartRequest(BaseModel):
@@ -169,7 +169,7 @@ class PodRestartResponse(BaseModel):
class ClusterOverviewResponse(BaseModel):
"""Сводка для страницы кластера: мета, ресурсы узлов, kubectl (поды, workloads)."""
"""Сводка для страницы кластера: мета, ресурсы узлов, таблицы ресурсов Kubernetes."""
cluster_name: str
registered_in_kind: bool = False
@@ -182,7 +182,7 @@ class ClusterOverviewResponse(BaseModel):
default_factory=lambda: KindClusterResources(cluster_name="", nodes=[], note=None),
)
aggregate_resources: AggregateResourcesSummary = Field(default_factory=AggregateResourcesSummary)
kubeconfig_error: str | None = Field(default=None, description="Нет kubeconfig или ошибка kubectl")
kubeconfig_error: str | None = Field(default=None, description="Нет kubeconfig или недоступен API кластера")
nodes_rc: int | None = None
nodes_output: str | None = None
pods_rc: int | None = None
@@ -197,8 +197,8 @@ class ClusterOverviewResponse(BaseModel):
services_output: str | None = None
ingresses_rc: int | None = None
ingresses_output: str | None = None
k8s_nodes: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock, description="kubectl get nodes -o json")
k8s_pods: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock, description="kubectl get pods -A -o json")
k8s_nodes: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock, description="Узлы кластера")
k8s_pods: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock, description="Поды, все namespace")
k8s_deployments: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
k8s_statefulsets: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
k8s_daemonsets: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
@@ -206,21 +206,80 @@ class ClusterOverviewResponse(BaseModel):
k8s_ingresses: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
k8s_namespaces: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="kubectl get namespaces -o json",
description="Namespaces",
)
k8s_replicasets: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="kubectl get replicasets -A -o json",
description="ReplicaSets, все namespace",
)
k8s_jobs: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="kubectl get jobs -A -o json",
description="Jobs, все namespace",
)
k8s_cronjobs: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="kubectl get cronjobs -A -o json",
description="CronJobs, все namespace",
)
k8s_pvcs: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="kubectl get pvc -A -o json",
description="PVC, все namespace",
)
class ClusterConfigGetResponse(BaseModel):
"""Ответ GET /clusters/{name}/config — текущие meta и kind-config.yaml."""
cluster_name: str
meta: dict[str, Any] = Field(default_factory=dict)
kind_config_yaml: str | None = Field(default=None, description="Содержимое kind-config.yaml или null, если файла нет")
has_kind_config: bool = False
registered_in_kind: bool = Field(
default=False,
description="Есть ли кластер в списке kind (важно для подсказки про применение конфига)",
)
kind_note: str = Field(
default="",
description="Пояснение: kind не меняет топологию уже созданного кластера без пересоздания",
)
class ClusterConfigUpdateRequest(BaseModel):
"""Тело PUT /clusters/{name}/config.
Если передан непустой ``kind_config_yaml``, он перезаписывает файл целиком; поля ``kubernetes_version``
и ``workers`` в этом запросе для генерации YAML не используются (можно не отправлять).
"""
kubernetes_version: str | None = Field(
default=None,
description="Тег kindest/node для простого режима (пересборка YAML: control-plane + N worker)",
)
workers: int | None = Field(default=None, ge=0, le=20)
kind_config_yaml: str | None = Field(default=None, description="Полный YAML манифеста kind Cluster")
description: str | None = Field(
default=None,
max_length=2000,
description="Текст в meta.json (пустая строка сбрасывает поле)",
)
@model_validator(mode="after")
def at_least_one_field(self) -> ClusterConfigUpdateRequest:
has_ver = self.kubernetes_version is not None
has_w = self.workers is not None
yaml_s = (self.kind_config_yaml or "").strip()
has_yaml = self.kind_config_yaml is not None and yaml_s != ""
has_desc = self.description is not None
if not (has_ver or has_w or has_yaml or has_desc):
raise ValueError("Укажите хотя бы одно поле для обновления.")
return self
class ClusterConfigUpdateResponse(BaseModel):
"""Результат сохранения конфигурации кластера на диск."""
cluster_name: str
updated_kind_config_yaml: bool = Field(description="Был ли перезаписан kind-config.yaml")
message: str
registered_in_kind: bool
kind_note: str
summary: ClusterSummary = Field(description="Актуальная сводка для строки таблицы / панели")