Files
KindClustersDashboard/app/api/v1/endpoints/cluster_addons.py
T
Sergey Antropoff 17f6233fd7 Журнал: пагинация 30 записей на страницу, API offset/total_pages
- job_journal: collect_recent_journal_entries_page_sync(limit, offset)
- GET /api/v1/journal/recent: limit по умолчанию 30, offset, total, page, total_pages
- journal.html/js: навигация Первая/Назад/номера/Вперёд/Последняя, стили
- app/docs/api_routes.md: описание query и пример ответа
- Прочие изменения UI/API (аддоны, helm, job_journal в кластерах) в том же коммите
2026-04-04 11:16:43 +03:00

268 lines
10 KiB
Python
Raw 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.
"""Helm-аддоны для выбранного кластера: ingress-nginx, kube-prometheus-stack, metrics-server, Istio+Kiali.
Операции синхронные (долгие таймауты — см. ``KIND_K8S_HELM_TIMEOUT_SEC``).
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from __future__ import annotations
import asyncio
import logging
from fastapi import APIRouter, Body, HTTPException
from core.cluster_lifecycle import validate_cluster_name
from core.helm_addons import (
HelmAddonError,
addon_status,
require_helm_binary,
install_ingress_nginx,
install_istio_and_kiali,
install_kube_prometheus_stack,
install_metrics_server,
uninstall_ingress_nginx,
uninstall_istio_and_kiali,
uninstall_kube_prometheus_stack,
uninstall_metrics_server,
)
from core.kind_guard import kind_cluster_lock
from models.schemas import (
ClusterAddonsStatusResponse,
HelmAddonApplyResponse,
IngressNginxInstallRequest,
IstioKialiInstallRequest,
KubePrometheusStackInstallRequest,
MetricsServerInstallRequest,
)
logger = logging.getLogger("kind_k8s.api.cluster_addons")
router = APIRouter(tags=["cluster-addons"])
_LOG_MAX = 24000
def _trim_log(s: str) -> str:
if len(s) <= _LOG_MAX:
return s
return s[: _LOG_MAX - 20] + "\n… (обрезано)"
def _map_helm_error(e: HelmAddonError) -> HTTPException:
msg = str(e)
if e.exit_code == 127:
return HTTPException(status_code=503, detail=msg)
return HTTPException(status_code=500, detail=msg)
@router.get(
"/clusters/{name}/addons/status",
response_model=ClusterAddonsStatusResponse,
summary="Статус Helm-аддонов в кластере",
)
async def get_cluster_addons_status(name: str) -> ClusterAddonsStatusResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
try:
require_helm_binary()
except HelmAddonError as e:
raise _map_helm_error(e) from e
try:
st = await asyncio.to_thread(addon_status, n)
except HelmAddonError as e:
raise _map_helm_error(e) from e
mesh = bool(st.get("istio_base") and st.get("istiod") and st.get("kiali"))
return ClusterAddonsStatusResponse(
cluster_name=n,
ingress_nginx=bool(st.get("ingress_nginx")),
kube_prometheus_stack=bool(st.get("kube_prometheus_stack")),
metrics_server=bool(st.get("metrics_server")),
istio_base=bool(st.get("istio_base")),
istiod=bool(st.get("istiod")),
kiali_server=bool(st.get("kiali")),
istio_mesh_ready=mesh,
)
@router.post(
"/clusters/{name}/addons/ingress-nginx",
response_model=HelmAddonApplyResponse,
summary="Установить ingress-nginx (Helm)",
)
async def post_install_ingress_nginx(
name: str,
body: IngressNginxInstallRequest | None = Body(default=None),
) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
ver: str | None = None
if body is not None and body.chart_version and body.chart_version.strip():
ver = body.chart_version.strip()
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(install_ingress_nginx, n, ver)
except HelmAddonError as e:
raise _map_helm_error(e) from e
logger.info("Установлен ingress-nginx для кластера «%s»", n)
return HelmAddonApplyResponse(ok=True, message="ingress-nginx установлен.", log=_trim_log(log))
@router.delete(
"/clusters/{name}/addons/ingress-nginx",
response_model=HelmAddonApplyResponse,
summary="Удалить ingress-nginx",
)
async def delete_ingress_nginx(name: str) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(uninstall_ingress_nginx, n)
except HelmAddonError as e:
raise _map_helm_error(e) from e
return HelmAddonApplyResponse(ok=True, message="ingress-nginx удалён.", log=_trim_log(log))
@router.post(
"/clusters/{name}/addons/kube-prometheus-stack",
response_model=HelmAddonApplyResponse,
summary="Установить kube-prometheus-stack (Prometheus Operator + Grafana)",
)
async def post_install_kube_prometheus_stack(
name: str,
body: KubePrometheusStackInstallRequest,
) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
prom_ver: str | None = None
if body.chart_version and body.chart_version.strip():
prom_ver = body.chart_version.strip()
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(
install_kube_prometheus_stack,
n,
grafana_admin_user=body.grafana_admin_user.strip(),
grafana_admin_password=body.grafana_admin_password,
chart_version=prom_ver,
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
logger.info("Установлен kube-prometheus-stack для кластера «%s»", n)
return HelmAddonApplyResponse(ok=True, message="kube-prometheus-stack установлен.", log=_trim_log(log))
@router.delete(
"/clusters/{name}/addons/kube-prometheus-stack",
response_model=HelmAddonApplyResponse,
summary="Удалить kube-prometheus-stack",
)
async def delete_kube_prometheus_stack(name: str) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(uninstall_kube_prometheus_stack, n)
except HelmAddonError as e:
raise _map_helm_error(e) from e
return HelmAddonApplyResponse(ok=True, message="kube-prometheus-stack удалён.", log=_trim_log(log))
@router.post(
"/clusters/{name}/addons/metrics-server",
response_model=HelmAddonApplyResponse,
summary="Установить metrics-server",
)
async def post_install_metrics_server(
name: str,
body: MetricsServerInstallRequest | None = Body(default=None),
) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
ms_ver: str | None = None
if body is not None and body.chart_version and body.chart_version.strip():
ms_ver = body.chart_version.strip()
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(install_metrics_server, n, ms_ver)
except HelmAddonError as e:
raise _map_helm_error(e) from e
logger.info("Установлен metrics-server для кластера «%s»", n)
return HelmAddonApplyResponse(ok=True, message="metrics-server установлен.", log=_trim_log(log))
@router.delete(
"/clusters/{name}/addons/metrics-server",
response_model=HelmAddonApplyResponse,
summary="Удалить metrics-server",
)
async def delete_metrics_server(name: str) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(uninstall_metrics_server, n)
except HelmAddonError as e:
raise _map_helm_error(e) from e
return HelmAddonApplyResponse(ok=True, message="metrics-server удалён.", log=_trim_log(log))
@router.post(
"/clusters/{name}/addons/istio-kiali",
response_model=HelmAddonApplyResponse,
summary="Установить Istio (base+istiod) и Kiali с логином",
)
async def post_install_istio_kiali(
name: str,
body: IstioKialiInstallRequest,
) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
istio_ver: str | None = None
if body.istio_chart_version and body.istio_chart_version.strip():
istio_ver = body.istio_chart_version.strip()
kiali_ver: str | None = None
if body.kiali_chart_version and body.kiali_chart_version.strip():
kiali_ver = body.kiali_chart_version.strip()
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(
install_istio_and_kiali,
n,
kiali_username=body.kiali_username.strip(),
kiali_password=body.kiali_password,
istio_chart_version=istio_ver,
kiali_chart_version=kiali_ver,
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
logger.info("Установлены Istio+Kiali для кластера «%s»", n)
return HelmAddonApplyResponse(ok=True, message="Istio и Kiali установлены.", log=_trim_log(log))
@router.delete(
"/clusters/{name}/addons/istio-kiali",
response_model=HelmAddonApplyResponse,
summary="Удалить Kiali, istiod и istio-base",
)
async def delete_istio_kiali(name: str) -> HelmAddonApplyResponse:
n = name.strip()
if not validate_cluster_name(n):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
async with kind_cluster_lock:
try:
_, log = await asyncio.to_thread(uninstall_istio_and_kiali, n)
except HelmAddonError as e:
raise _map_helm_error(e) from e
return HelmAddonApplyResponse(ok=True, message="Istio/Kiali удалены.", log=_trim_log(log))