Files
Sergey Antropoff 4b703801e1 Kiali anonymous, журнал Helm, kubeconfig для контейнеров, UI аддонов
- Kiali: убран login, anonymous по умолчанию; удалены поля логина/пароля из UI и API
- Журнал Helm: install/upgrade/delete, message и колонка в journal.js
- Аддоны: values свёрнуты при подгрузке для установленных
- GET …/kubeconfig/docker: host.docker.internal:порт + tls-server-name; кнопка в UI
- apply_apiserver_endpoint_to_kubeconfig_file; KIND_K8S_APISERVER_GATEWAY_HOST в compose/env.example
- README и api_routes.md обновлены
2026-04-04 18:54:10 +03:00

584 lines
26 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.

"""Модели запросов/ответов REST API веб-интерфейса.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from __future__ import annotations
from typing import Any, Literal
from pydantic import BaseModel, Field, model_validator
class ClusterCreateRequest(BaseModel):
"""Тело POST /api/v1/clusters — создание кластера."""
name: str = Field(..., min_length=1, max_length=63, description="DNS-имя кластера (a-z0-9-)")
kubernetes_version: str = Field(
...,
min_length=1,
description="Тег kindest/node: latest, 1.29.4, v1.29.4 и т.д.",
)
workers: int = Field(2, ge=0, le=20, description="Число worker-нод (020)")
class ClusterCreateAccepted(BaseModel):
"""Ответ 202 — задание поставлено в очередь."""
job_id: str
status: Literal["queued"] = "queued"
message: str = "Создание кластера выполняется в фоне; опросите GET /api/v1/jobs/{job_id}"
class JobView(BaseModel):
"""Статус фонового задания."""
job_id: str
kind: str
status: Literal["queued", "running", "success", "failed", "cancelled"]
cluster_name: str | None
created_at_utc: str
message: str | None = None
result: dict[str, Any] | None = None
progress_stage: str | None = Field(default=None, description="Текущий этап операции (пока задание активно)")
progress_percent: int | None = Field(default=None, description="Прогресс 0100 для индикатора в UI")
progress_log: list[str] = Field(
default_factory=list,
description="Хвост журнала (скачивание образа, длительные команды, этапы); при опросе GET /jobs/{id}",
)
class ClusterSummary(BaseModel):
"""Элемент списка кластеров."""
name: str
registered_in_kind: bool
kind_nodes_running: bool = Field(
default=False,
description="Запущены контейнеры узлов kind (имя-control-plane, имя-worker…; docker/podman ps)",
)
has_local_kubeconfig: bool
has_provision_log: bool = Field(
default=False,
description="Есть сохранённый полный журнал развёртывания (provision_log.json)",
)
meta: dict[str, Any] = Field(default_factory=dict)
class KindNodeResourceStat(BaseModel):
"""Одна нода kind (контейнер): снимок ``docker stats`` / ``podman stats``."""
container_name: str
cpu_percent: str | None = Field(default=None, description="Доля CPU, напр. 1.23%")
memory_usage: str | None = Field(default=None, description="Использование / лимит, напр. 120MiB / 7.7GiB")
memory_percent: str | None = Field(default=None, description="Процент памяти от лимита контейнера")
net_io: str | None = Field(default=None, description="Сетевой I/O (накопительно за жизнь контейнера)")
block_io: str | None = Field(default=None, description="Блочный I/O")
pids: int | None = Field(default=None, description="Число процессов в контейнере")
class KindClusterResources(BaseModel):
"""Ресурсы узлов одного кластера kind (только запущенные контейнеры)."""
cluster_name: str
nodes: list[KindNodeResourceStat] = Field(default_factory=list)
note: str | None = Field(default=None, description="Пояснение, если узлов нет или ошибка списка")
class AggregateResourcesSummary(BaseModel):
"""Сводка по всем запущенным узлам kind для донат-диаграмм на главной."""
nodes_count: int = Field(0, description="Число учтённых контейнеров-нод")
cpu_ring: float = Field(0, ge=0, le=100, description="Заполнение кольца «средний CPU», 0100")
cpu_label: str = Field("", description="Подпись в центре доната")
memory_percent_ring: float = Field(0, ge=0, le=100)
memory_percent_label: str = Field("")
memory_used_ratio_ring: float = Field(0, ge=0, le=100, description="Доля занятой RAM от лимита по строке MemUsage")
memory_used_ratio_label: str = Field("")
network_ring: float = Field(0, ge=0, le=100, description="Индикатор суммарного сетевого I/O (условная шкала)")
network_label: str = Field("")
disk_ring: float = Field(0, ge=0, le=100, description="Индикатор суммарного блочного I/O")
disk_label: str = Field("")
class StatsResponse(BaseModel):
"""Краткая статистика для дашборда."""
container_cli: str = Field(
description="CLI движка контейнеров (docker или podman): узлы kind и метрики собираются через него",
)
kind_clusters_count: int
local_cluster_dirs_count: int
total_workers_from_meta: int = Field(
0,
ge=0,
description="Сумма worker_nodes по meta.json каталогов; 0 если ни в одном meta нет поля или данных",
)
jobs_total: int
jobs_recent_failed: int
helm_addons_installable_count: int = Field(
0,
ge=0,
description="Число типовых Helm-аддонов в каталоге UI (страница «Аддоны»), доступных для установки",
)
cluster_resources: list[KindClusterResources] = Field(
default_factory=list,
description="CPU/RAM/I/O узлов kind по данным container CLI",
)
cluster_resources_error: str | None = Field(
default=None,
description="Глобальная ошибка сбора метрик (например CLI не найден)",
)
aggregate_cluster_resources: AggregateResourcesSummary = Field(
default_factory=AggregateResourcesSummary,
description="Агрегаты по узлам для донатов на главной странице",
)
class ClusterWorkloadsResponse(BaseModel):
"""Вывод kubectl по кластеру (узлы и поды)."""
cluster_name: str
nodes_rc: int | None = None
nodes_output: str | None = None
pods_rc: int | None = None
pods_output: str | None = None
error: str | None = None
class K8sListJsonBlock(BaseModel):
"""Фрагмент списка ресурсов Kubernetes (поле ``items`` из JSON List) для таблицы в UI."""
ok: bool = True
rc: int | None = Field(default=None, description="Код возврата при обращении к API кластера")
items: list[dict[str, Any]] = Field(default_factory=list)
message: str | None = Field(default=None, description="Текст ошибки или разбора JSON")
class PodRestartRequest(BaseModel):
"""Тело POST: перезапуск пода через ``kubectl delete pod`` (воссоздание контроллером)."""
namespace: str = Field(..., min_length=1, max_length=253, description="Namespace пода")
pod: str = Field(..., min_length=1, max_length=253, description="Имя пода (metadata.name)")
class PodRestartResponse(BaseModel):
"""Результат запроса на удаление/рестарт пода."""
cluster_name: str
namespace: str
pod: str
rc: int
message: str
class ClusterOverviewResponse(BaseModel):
"""Сводка для страницы кластера: мета, ресурсы узлов, таблицы ресурсов Kubernetes."""
cluster_name: str
registered_in_kind: bool = False
kind_nodes_running: bool = False
has_local_kubeconfig: bool = False
has_provision_log: bool = False
meta: dict[str, Any] = Field(default_factory=dict)
resources_error: str | None = Field(default=None, description="Ошибка сбора docker/podman stats")
cluster_resources: KindClusterResources = Field(
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 или недоступен API кластера")
nodes_rc: int | None = None
nodes_output: str | None = None
pods_rc: int | None = None
pods_output: str | None = None
deployments_rc: int | None = None
deployments_output: str | None = None
statefulsets_rc: int | None = None
statefulsets_output: str | None = None
daemonsets_rc: int | None = None
daemonsets_output: str | None = None
services_rc: int | None = None
services_output: str | None = None
ingresses_rc: int | None = None
ingresses_output: str | None = None
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)
k8s_services: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
k8s_ingresses: K8sListJsonBlock = Field(default_factory=K8sListJsonBlock)
k8s_namespaces: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="Namespaces",
)
k8s_replicasets: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="ReplicaSets, все namespace",
)
k8s_jobs: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="Jobs, все namespace",
)
k8s_cronjobs: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
description="CronJobs, все namespace",
)
k8s_pvcs: K8sListJsonBlock = Field(
default_factory=K8sListJsonBlock,
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="Актуальная сводка для строки таблицы / панели")
# --- Helm-аддоны (страница «Дополнения кластера») ---
class ClusterAddonsStatusResponse(BaseModel):
"""Какие релизы Helm обнаружены для кластера (по helm list -A)."""
cluster_name: str
ingress_nginx: bool = Field(description="Релиз ingress-nginx в ns ingress-nginx")
kube_prometheus_stack: bool = Field(description="Релиз kube-prometheus-stack в ns monitoring")
metrics_server: bool = Field(description="Релиз metrics-server в ns kube-system")
istio_base: bool
istiod: bool
kiali_server: bool
istio_mesh_ready: bool = Field(
description="Установлены istio-base, istiod и kiali-server в istio-system",
)
ingress_nginx_chart_version: str | None = Field(
default=None,
description="Версия чарта из helm list (если релиз установлен)",
)
kube_prometheus_stack_chart_version: str | None = Field(default=None)
metrics_server_chart_version: str | None = Field(default=None)
istiod_chart_version: str | None = Field(
default=None,
description="Версия чарта istiod в кластере (для селекта Istio на UI)",
)
istio_base_chart_version: str | None = Field(default=None)
kiali_server_chart_version: str | None = Field(default=None)
class InstalledAddonValuesBlob(BaseModel):
"""Снимок values одного релиза (helm get values --all)."""
chart_version: str | None = Field(default=None, description="Версия чарта из helm list")
values_yaml: str = Field(default="", description="YAML эффективных values")
class IstioKialiInstalledValuesBlob(BaseModel):
"""Три релиза mesh + версии для селектов UI."""
istio_chart_version: str | None = Field(
default=None,
description="Версия istiod (или base), для селекта «Версия чартов Istio»",
)
kiali_chart_version: str | None = None
istio_base_values_yaml: str = ""
istiod_values_yaml: str = ""
kiali_values_yaml: str = ""
class ClusterAddonsInstalledValuesResponse(BaseModel):
"""Values из кластера только для установленных аддонов (для редактирования перед upgrade)."""
cluster_name: str
ingress_nginx: InstalledAddonValuesBlob | None = None
kube_prometheus_stack: InstalledAddonValuesBlob | None = None
metrics_server: InstalledAddonValuesBlob | None = None
istio_kiali: IstioKialiInstalledValuesBlob | None = None
class HelmAddonApplyResponse(BaseModel):
"""Результат синхронной установки/удаления чарта."""
ok: bool = True
message: str
log: str = Field(default="", description="Сводный вывод helm/kubectl")
_VALUES_YAML_MAX = 131_072
class IngressNginxInstallRequest(BaseModel):
"""Тело POST …/addons/ingress-nginx — установка ingress-nginx."""
chart_version: str | None = Field(
default=None,
max_length=64,
description="Версия чарта; пусто — последняя из репозитория",
)
values_yaml: str | None = Field(
default=None,
max_length=_VALUES_YAML_MAX,
description=(
"Полный YAML values для чарта (корень — объект), как в редакторе на странице аддонов "
"(``helm show values`` + сервис для kind). Пусто — собрать автоматически на сервере. "
"После ``-f`` применяются ``--set`` NodePort (приоритет Helm)."
),
)
class KubePrometheusStackInstallRequest(BaseModel):
"""Тело POST …/addons/kube-prometheus-stack."""
grafana_admin_user: str = Field(default="admin", min_length=1, max_length=256)
grafana_admin_password: str = Field(..., min_length=8, max_length=256)
chart_version: str | None = Field(
default=None,
max_length=64,
description="Версия чарта kube-prometheus-stack; пусто — последняя",
)
values_yaml: str | None = Field(
default=None,
max_length=_VALUES_YAML_MAX,
description=(
"Полный YAML values чарта (``helm show values`` + логин/пароль Grafana из формы). "
"Пусто — собрать на сервере из версии чарта и полей формы."
),
)
class MetricsServerInstallRequest(BaseModel):
"""Тело POST …/addons/metrics-server (все поля необязательны)."""
chart_version: str | None = Field(
default=None,
max_length=64,
description="Версия чарта metrics-server; пусто — последняя",
)
values_yaml: str | None = Field(
default=None,
max_length=_VALUES_YAML_MAX,
description=(
"Полный YAML values чарта (``helm show values`` + args для kind). "
"Пусто — собрать на сервере."
),
)
class IstioKialiInstallRequest(BaseModel):
"""Тело POST …/addons/istio-kiali — Istio + Kiali; Kiali по умолчанию auth anonymous (без логина в UI/API)."""
istio_chart_version: str | None = Field(
default=None,
max_length=64,
description="Версия чартов istio/base и istio/istiod; пусто — последняя",
)
kiali_chart_version: str | None = Field(
default=None,
max_length=64,
description="Версия чарта kiali/kiali-server; пусто — последняя",
)
values_yaml: str | None = Field(
default=None,
max_length=_VALUES_YAML_MAX,
description=(
"Полный YAML values чарта **kiali-server** (``helm show values``). "
"Пусто — без файла ``-f`` (дефолты чарта). Если не задан ``auth.strategy`` в YAML, "
"сервер выставляет ``anonymous`` (см. документацию Kiali; стратегия ``login`` удалена)."
),
)
istio_base_values_yaml: str | None = Field(
default=None,
max_length=_VALUES_YAML_MAX,
description="Полный YAML values чарта **istio/base**; пусто — без ``-f``.",
)
istio_istiod_values_yaml: str | None = Field(
default=None,
max_length=_VALUES_YAML_MAX,
description="Полный YAML values чарта **istio/istiod**; пусто — без ``-f``.",
)
class HelmAddonComposeValuesRequest(BaseModel):
"""Тело POST /helm/addons/compose-values — собрать YAML для редактора (helm show values + форма)."""
addon: Literal["ingress-nginx", "kube-prometheus-stack", "metrics-server", "istio-kiali"]
chart_version: str | None = Field(
default=None,
max_length=64,
description="Версия чарта: ingress-nginx, kube-prometheus-stack или metrics-server",
)
grafana_admin_user: str | None = Field(default=None, max_length=256)
grafana_admin_password: str | None = Field(
default=None,
max_length=256,
description="Для preview kube-prometheus-stack; если пусто — подставляется временный пароль в YAML",
)
istio_chart_version: str | None = Field(default=None, max_length=64)
kiali_chart_version: str | None = Field(default=None, max_length=64)
class HelmAddonComposeValuesResponse(BaseModel):
"""Ответ compose-values: строки YAML для текстовых полей UI."""
values_yaml: str = Field(..., description="Основной чарт; для istio-kiali — kiali-server")
istio_base_values_yaml: str | None = Field(default=None, description="Только istio-kiali: istio/base")
istio_istiod_values_yaml: str | None = Field(default=None, description="Только istio-kiali: istio/istiod")
class HelmAddonComposeBatchRequest(BaseModel):
"""Тело POST /helm/addons/compose-values-batch — одним запросом обновить все редакторы на странице аддонов."""
ingress_chart_version: str | None = Field(default=None, max_length=64)
grafana_admin_user: str | None = Field(default=None, max_length=256)
grafana_admin_password: str | None = Field(default=None, max_length=256)
kube_prometheus_chart_version: str | None = Field(default=None, max_length=64)
metrics_server_chart_version: str | None = Field(default=None, max_length=64)
istio_chart_version: str | None = Field(default=None, max_length=64)
kiali_chart_version: str | None = Field(default=None, max_length=64)
class HelmAddonComposeBatchResponse(BaseModel):
"""Шесть строк YAML за один вызов (один ``helm repo update``)."""
ingress_nginx_values_yaml: str
kube_prometheus_values_yaml: str
metrics_server_values_yaml: str
kiali_values_yaml: str
istio_base_values_yaml: str
istio_istiod_values_yaml: str
class HelmAddonChartVersionsResponse(BaseModel):
"""Список версий чартов для выпадающих списков на странице аддонов (после helm repo update)."""
ingress_nginx: list[str] = Field(default_factory=list)
kube_prometheus_stack: list[str] = Field(default_factory=list)
metrics_server: list[str] = Field(default_factory=list)
istio: list[str] = Field(default_factory=list, description="Версии istio/base (для base+istiod)")
kiali_server: list[str] = Field(default_factory=list)
class JournalEntryModel(BaseModel):
"""Одна запись из ``clusters/<имя>/journal/jobs_history.json``."""
model_config = {"extra": "allow"}
version: int | None = Field(default=None, description="Версия схемы записи в файле")
job_id: str = ""
kind: str = ""
cluster_name: str = ""
status: str = ""
message: str | None = None
created_at_utc: str = ""
finished_at_utc: str | None = None
log_lines: list[str] = Field(default_factory=list)
result: dict[str, Any] | None = None
source_cluster: str | None = Field(
default=None,
description="Имя каталога кластера в ответе GET /journal/recent",
)
class ClusterJournalResponse(BaseModel):
"""Содержимое журнала одного кластера."""
cluster_name: str
file_version: int | None = None
entries: list[JournalEntryModel] = Field(default_factory=list)
class JournalRecentResponse(BaseModel):
"""Страница журнала заданий из ``journal/jobs_history.json`` (все кластеры или один)."""
limit: int = Field(description="Размер страницы (записей за запрос)")
offset: int = Field(description="Смещение от начала отсортированного списка (0 = первая страница)")
total: int = Field(ge=0, description="Всего записей во всех журналах")
page: int = Field(ge=1, description="Номер текущей страницы с 1")
total_pages: int = Field(ge=0, description="Число страниц при данном limit; 0 если записей нет")
entries: list[JournalEntryModel] = Field(default_factory=list)
class ClusterDirLogEntryModel(BaseModel):
"""Запись из ``provision_log.json`` или ``helm_addon_log.json`` (формат как у развёртывания)."""
model_config = {"extra": "allow"}
version: int | None = None
job_id: str = ""
kind: str = ""
cluster_name: str = ""
finished_at_utc: str = ""
status: str = ""
message: str | None = None
lines: list[str] = Field(default_factory=list)
result: dict[str, Any] | None = None
source_cluster: str | None = Field(
default=None,
description="Имя каталога кластера в агрегированных ответах /journal/provision и /journal/helm-addons",
)
class JournalPagedDirLogsResponse(BaseModel):
"""Пагинация по одному последнему логу на кластер (развёртывание или Helm-аддоны)."""
limit: int
offset: int
total: int
page: int
total_pages: int
entries: list[ClusterDirLogEntryModel] = Field(default_factory=list)