"""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))