Журнал: пагинация 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 в кластерах) в том же коммите
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user