UI: карточка установленных аддонов на странице кластера; API аддонов и правки стилей
- Отдельная section.card после сводки ресурсов узлов, выравнивание бейджа Helm с заголовком - Расширение эндпоинтов аддонов, схемы, helm_addons, cluster_addons.js/html - Обновление api_routes.md
This commit is contained in:
@@ -22,7 +22,8 @@ from fastapi import APIRouter, Body, HTTPException
|
||||
from core.cluster_lifecycle import validate_cluster_name
|
||||
from core.helm_addons import (
|
||||
HelmAddonError,
|
||||
addon_status,
|
||||
collect_installed_addons_values,
|
||||
helm_addon_snapshot,
|
||||
require_helm_binary,
|
||||
install_ingress_nginx,
|
||||
install_istio_and_kiali,
|
||||
@@ -36,8 +37,11 @@ from core.helm_addons import (
|
||||
from core.kind_guard import kind_cluster_lock
|
||||
from core.provision_log import write_cluster_helm_addon_log
|
||||
from models.schemas import (
|
||||
ClusterAddonsInstalledValuesResponse,
|
||||
ClusterAddonsStatusResponse,
|
||||
HelmAddonApplyResponse,
|
||||
InstalledAddonValuesBlob,
|
||||
IstioKialiInstalledValuesBlob,
|
||||
IngressNginxInstallRequest,
|
||||
IstioKialiInstallRequest,
|
||||
KubePrometheusStackInstallRequest,
|
||||
@@ -151,10 +155,10 @@ async def get_cluster_addons_status(name: str) -> ClusterAddonsStatusResponse:
|
||||
except HelmAddonError as e:
|
||||
raise _map_helm_error(e) from e
|
||||
try:
|
||||
st = await asyncio.to_thread(addon_status, n)
|
||||
st = await asyncio.to_thread(helm_addon_snapshot, 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"))
|
||||
mesh = bool(st.get("istio_mesh_ready"))
|
||||
return ClusterAddonsStatusResponse(
|
||||
cluster_name=n,
|
||||
ingress_nginx=bool(st.get("ingress_nginx")),
|
||||
@@ -162,8 +166,65 @@ async def get_cluster_addons_status(name: str) -> ClusterAddonsStatusResponse:
|
||||
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")),
|
||||
kiali_server=bool(st.get("kiali_server")),
|
||||
istio_mesh_ready=mesh,
|
||||
ingress_nginx_chart_version=st.get("ingress_nginx_chart_version"),
|
||||
kube_prometheus_stack_chart_version=st.get("kube_prometheus_stack_chart_version"),
|
||||
metrics_server_chart_version=st.get("metrics_server_chart_version"),
|
||||
istiod_chart_version=st.get("istiod_chart_version"),
|
||||
istio_base_chart_version=st.get("istio_base_chart_version"),
|
||||
kiali_server_chart_version=st.get("kiali_server_chart_version"),
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/clusters/{name}/addons/installed-values",
|
||||
response_model=ClusterAddonsInstalledValuesResponse,
|
||||
summary="Values установленных аддонов из кластера (helm get values --all)",
|
||||
)
|
||||
async def get_cluster_addons_installed_values(name: str) -> ClusterAddonsInstalledValuesResponse:
|
||||
"""
|
||||
Для каждого установленного логического аддона возвращает версию чарта (как в ``helm list``)
|
||||
и полный YAML эффективных values (``helm get values --all -o yaml``) для правки перед upgrade.
|
||||
"""
|
||||
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:
|
||||
raw = await asyncio.to_thread(collect_installed_addons_values, n)
|
||||
except HelmAddonError as e:
|
||||
raise _map_helm_error(e) from e
|
||||
|
||||
def blob(key: str) -> InstalledAddonValuesBlob | None:
|
||||
b = raw.get(key)
|
||||
if not isinstance(b, dict):
|
||||
return None
|
||||
return InstalledAddonValuesBlob(
|
||||
chart_version=b.get("chart_version"),
|
||||
values_yaml=str(b.get("values_yaml") or ""),
|
||||
)
|
||||
|
||||
istio = raw.get("istio_kiali")
|
||||
istio_model: IstioKialiInstalledValuesBlob | None = None
|
||||
if isinstance(istio, dict):
|
||||
istio_model = IstioKialiInstalledValuesBlob(
|
||||
istio_chart_version=istio.get("istio_chart_version"),
|
||||
kiali_chart_version=istio.get("kiali_chart_version"),
|
||||
istio_base_values_yaml=str(istio.get("istio_base_values_yaml") or ""),
|
||||
istiod_values_yaml=str(istio.get("istiod_values_yaml") or ""),
|
||||
kiali_values_yaml=str(istio.get("kiali_values_yaml") or ""),
|
||||
)
|
||||
|
||||
return ClusterAddonsInstalledValuesResponse(
|
||||
cluster_name=n,
|
||||
ingress_nginx=blob("ingress_nginx"),
|
||||
kube_prometheus_stack=blob("kube_prometheus_stack"),
|
||||
metrics_server=blob("metrics_server"),
|
||||
istio_kiali=istio_model,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+181
-11
@@ -358,18 +358,188 @@ def helm_list_all(cluster_name: str) -> list[dict[str, Any]]:
|
||||
return []
|
||||
|
||||
|
||||
def addon_status(cluster_name: str) -> dict[str, bool]:
|
||||
"""Какие аддоны установлены (по имени релиза и namespace)."""
|
||||
lst = helm_list_all(cluster_name)
|
||||
names_ns = {(str(x.get("name", "")), str(x.get("namespace", ""))) for x in lst if isinstance(x, dict)}
|
||||
return {
|
||||
"ingress_nginx": ("ingress-nginx", "ingress-nginx") in names_ns,
|
||||
"kube_prometheus_stack": ("kube-prometheus-stack", "monitoring") in names_ns,
|
||||
"metrics_server": ("metrics-server", "kube-system") in names_ns,
|
||||
"istio_base": ("istio-base", "istio-system") in names_ns,
|
||||
"istiod": ("istiod", "istio-system") in names_ns,
|
||||
"kiali": ("kiali-server", "istio-system") in names_ns,
|
||||
def helm_release_entry(
|
||||
lst: list[dict[str, Any]],
|
||||
release: str,
|
||||
namespace: str,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Одна запись ``helm list -o json`` по имени релиза и namespace."""
|
||||
for x in lst:
|
||||
if not isinstance(x, dict):
|
||||
continue
|
||||
if str(x.get("name", "")) == release and str(x.get("namespace", "")) == namespace:
|
||||
return x
|
||||
return None
|
||||
|
||||
|
||||
def _chart_version_from_list_field(chart: str, release_name: str) -> str | None:
|
||||
"""
|
||||
Поле ``chart`` в выводе ``helm list`` имеет вид ``<имя_чарта>-<версия>`` (например ``ingress-nginx-4.11.2``).
|
||||
Для релиза Istio base в списке часто ``base-1.22.3``.
|
||||
"""
|
||||
chart = (chart or "").strip()
|
||||
if not chart:
|
||||
return None
|
||||
prefixes: dict[str, str] = {
|
||||
"ingress-nginx": "ingress-nginx-",
|
||||
"kube-prometheus-stack": "kube-prometheus-stack-",
|
||||
"metrics-server": "metrics-server-",
|
||||
"istio-base": "base-",
|
||||
"istiod": "istiod-",
|
||||
"kiali-server": "kiali-server-",
|
||||
}
|
||||
p = prefixes.get(release_name)
|
||||
if p and chart.startswith(p):
|
||||
v = chart[len(p) :].strip()
|
||||
return v or None
|
||||
return chart
|
||||
|
||||
|
||||
def helm_get_values_all_yaml(cluster_name: str, release: str, namespace: str) -> str:
|
||||
"""
|
||||
Эффективные values релиза: ``helm get values --all -o yaml`` (для правки при upgrade).
|
||||
"""
|
||||
kcfg = str(_kubeconfig_runtime_path(cluster_name))
|
||||
rc, out, err = _run_cmd(
|
||||
[
|
||||
"helm",
|
||||
"get",
|
||||
"values",
|
||||
release,
|
||||
"-n",
|
||||
namespace,
|
||||
"--all",
|
||||
"-o",
|
||||
"yaml",
|
||||
"--kubeconfig",
|
||||
kcfg,
|
||||
],
|
||||
timeout=120,
|
||||
)
|
||||
if rc != 0:
|
||||
raise HelmAddonError(
|
||||
(err or out or f"helm get values {release}/{namespace} код {rc}")[:8000],
|
||||
exit_code=rc,
|
||||
)
|
||||
return (out or "").strip()
|
||||
|
||||
|
||||
_VALUES_YAML_API_MAX = 131_072
|
||||
|
||||
|
||||
def trim_values_yaml_for_api(raw: str) -> str:
|
||||
"""Ограничение размера YAML в JSON-ответе API."""
|
||||
if len(raw) <= _VALUES_YAML_API_MAX:
|
||||
return raw
|
||||
return raw[: _VALUES_YAML_API_MAX - 120] + "\n\n# … YAML обрезан (лимит ответа API).\n"
|
||||
|
||||
|
||||
def helm_addon_snapshot(cluster_name: str) -> dict[str, Any]:
|
||||
"""
|
||||
Флаги установки, версии чартов из ``helm list`` и признак полного mesh Istio+Kiali.
|
||||
"""
|
||||
lst = helm_list_all(cluster_name)
|
||||
|
||||
def pair(rel: str, ns: str) -> tuple[bool, str | None]:
|
||||
e = helm_release_entry(lst, rel, ns)
|
||||
if not e:
|
||||
return False, None
|
||||
ch = str(e.get("chart") or "")
|
||||
ver = _chart_version_from_list_field(ch, rel)
|
||||
return True, ver
|
||||
|
||||
ing_on, ing_v = pair("ingress-nginx", "ingress-nginx")
|
||||
prom_on, prom_v = pair("kube-prometheus-stack", "monitoring")
|
||||
ms_on, ms_v = pair("metrics-server", "kube-system")
|
||||
base_on, base_v = pair("istio-base", "istio-system")
|
||||
istiod_on, istiod_v = pair("istiod", "istio-system")
|
||||
kiali_on, kiali_v = pair("kiali-server", "istio-system")
|
||||
mesh = bool(base_on and istiod_on and kiali_on)
|
||||
return {
|
||||
"ingress_nginx": ing_on,
|
||||
"ingress_nginx_chart_version": ing_v if ing_on else None,
|
||||
"kube_prometheus_stack": prom_on,
|
||||
"kube_prometheus_stack_chart_version": prom_v if prom_on else None,
|
||||
"metrics_server": ms_on,
|
||||
"metrics_server_chart_version": ms_v if ms_on else None,
|
||||
"istio_base": base_on,
|
||||
"istiod": istiod_on,
|
||||
"kiali_server": kiali_on,
|
||||
"istio_base_chart_version": base_v if base_on else None,
|
||||
"istiod_chart_version": istiod_v if istiod_on else None,
|
||||
"kiali_server_chart_version": kiali_v if kiali_on else None,
|
||||
"istio_mesh_ready": mesh,
|
||||
}
|
||||
|
||||
|
||||
def addon_status(cluster_name: str) -> dict[str, bool]:
|
||||
"""Какие аддоны установлены (по имени релиза и namespace). Совместимость со старым кодом."""
|
||||
s = helm_addon_snapshot(cluster_name)
|
||||
return {
|
||||
"ingress_nginx": bool(s["ingress_nginx"]),
|
||||
"kube_prometheus_stack": bool(s["kube_prometheus_stack"]),
|
||||
"metrics_server": bool(s["metrics_server"]),
|
||||
"istio_base": bool(s["istio_base"]),
|
||||
"istiod": bool(s["istiod"]),
|
||||
"kiali": bool(s["kiali_server"]),
|
||||
}
|
||||
|
||||
|
||||
def collect_installed_addons_values(cluster_name: str) -> dict[str, Any]:
|
||||
"""
|
||||
Для установленных релизов — версия чарта (как в snapshot) и YAML ``helm get values --all``.
|
||||
Ключи совпадают с логическими аддонами страницы UI.
|
||||
"""
|
||||
s = helm_addon_snapshot(cluster_name)
|
||||
out: dict[str, Any] = {}
|
||||
|
||||
if s["ingress_nginx"]:
|
||||
try:
|
||||
y = trim_values_yaml_for_api(
|
||||
helm_get_values_all_yaml(cluster_name, "ingress-nginx", "ingress-nginx"),
|
||||
)
|
||||
except HelmAddonError as e:
|
||||
y = f"# Не удалось прочитать values: {e}\n"
|
||||
out["ingress_nginx"] = {"chart_version": s["ingress_nginx_chart_version"], "values_yaml": y}
|
||||
|
||||
if s["kube_prometheus_stack"]:
|
||||
try:
|
||||
y = trim_values_yaml_for_api(
|
||||
helm_get_values_all_yaml(cluster_name, "kube-prometheus-stack", "monitoring"),
|
||||
)
|
||||
except HelmAddonError as e:
|
||||
y = f"# Не удалось прочитать values: {e}\n"
|
||||
out["kube_prometheus_stack"] = {"chart_version": s["kube_prometheus_stack_chart_version"], "values_yaml": y}
|
||||
|
||||
if s["metrics_server"]:
|
||||
try:
|
||||
y = trim_values_yaml_for_api(
|
||||
helm_get_values_all_yaml(cluster_name, "metrics-server", "kube-system"),
|
||||
)
|
||||
except HelmAddonError as e:
|
||||
y = f"# Не удалось прочитать values: {e}\n"
|
||||
out["metrics_server"] = {"chart_version": s["metrics_server_chart_version"], "values_yaml": y}
|
||||
|
||||
if s["istio_mesh_ready"]:
|
||||
try:
|
||||
kb = trim_values_yaml_for_api(
|
||||
helm_get_values_all_yaml(cluster_name, "istio-base", "istio-system"),
|
||||
)
|
||||
ki = trim_values_yaml_for_api(helm_get_values_all_yaml(cluster_name, "istiod", "istio-system"))
|
||||
kk = trim_values_yaml_for_api(
|
||||
helm_get_values_all_yaml(cluster_name, "kiali-server", "istio-system"),
|
||||
)
|
||||
except HelmAddonError as e:
|
||||
kb = ki = kk = f"# Не удалось прочитать values: {e}\n"
|
||||
out["istio_kiali"] = {
|
||||
"istio_chart_version": s["istiod_chart_version"] or s["istio_base_chart_version"],
|
||||
"kiali_chart_version": s["kiali_server_chart_version"],
|
||||
"istio_base_values_yaml": kb,
|
||||
"istiod_values_yaml": ki,
|
||||
"kiali_values_yaml": kk,
|
||||
}
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _kubectl_apply_manifest(cluster_name: str, manifest: dict[str, Any]) -> tuple[int, str]:
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|---------|----------|
|
||||
| `GET /` | HTML **Панель**: CTA создания кластера, карточка **Статистика** (среда kind/kubectl, счётчики), отдельная карточка **Ресурсы узлов (сводка)** (донаты по **`GET /api/v1/stats`**); полная таблица кластеров — на **`GET /clusters`**. |
|
||||
| `GET /clusters` | HTML **Кластеры**: шапка с кнопкой **Создать кластер** (`/cluster-create`), сводка **Ресурсы узлов**, таблица кластеров (**старт/стоп**, ссылка на `GET /cluster/<имя>`, модалки как на панели); скрипт **`dashboard.js`**. |
|
||||
| `GET /cluster/{name}` | HTML **страница кластера**: донаты «Ресурсы узлов (сводка)», карточки узлов, **таблицы Kubernetes** (данные API кластера в JSON), кнопка **Рестарт** у подов (**`POST …/pods/restart`**), те же кнопки действий, что в таблице на главной; данные — **`GET /api/v1/clusters/{name}/overview`** (автообновление с интервалом панели). В шапке активна пилюля **Кластеры** (`nav_active: clusters`). |
|
||||
| `GET /cluster/{name}` | HTML **страница кластера**: донаты «Ресурсы узлов (сводка)», карточки узлов, отдельная карточка под панелью действий — **мини-карточки установленных Helm-аддонов** (**`GET /api/v1/clusters/{name}/addons/status`**, ссылки на **`/cluster-addons`**), **таблицы Kubernetes** (данные API кластера в JSON), кнопка **Рестарт** у подов (**`POST …/pods/restart`**), те же кнопки действий, что в таблице на главной; данные — **`GET /api/v1/clusters/{name}/overview`** (автообновление с интервалом панели). В шапке активна пилюля **Кластеры** (`nav_active: clusters`). |
|
||||
| `GET /cluster/{name}/edit` | HTML **редактирование** сохранённого `kind-config.yaml` и полей `meta.json` (простой режим: тег/workers; расширенный: полный YAML kind Cluster). Сохранение — **`PUT /api/v1/clusters/{name}/config`**. В шапке активна **Кластеры**. |
|
||||
| `GET /cluster-addons` | HTML **Аддоны**: **`GET /helm/chart-versions`**, **`POST /helm/addons/compose-values`** (подстановка **реальных** `helm show values` + логины в редактор), три YAML для Istio, установка/удаление релизов. |
|
||||
| `GET /cluster-addons` | HTML **Аддоны**: **`GET /helm/chart-versions`** + **`GET …/addons/status`**; для **установленных** релизов — **`GET …/addons/installed-values`**: в селекте версия с пометкой «(текущая установленная версия)», в форме — values из кластера. По кнопке **«Загрузить values»** — шаблон из **`POST /helm/addons/compose-values`**. Установка/удаление релизов. |
|
||||
| `GET /journal` | HTML **Журналы**: переключатель (как «Простой/Расширенный» в редактировании кластера) — **по кластеру** (`journal/recent?cluster=`), **развёртывание** (`/journal/provision`), **Helm-аддоны** (`/journal/helm-addons`); пагинация по **30** записей. |
|
||||
| `GET /documentation` | HTML-оболочка; **`documentation.js`**: без `path` — **`GET /api/v1/docs/readme`**, с `?path=app/docs/…` — **`GET /api/v1/docs/file`**; разбор Markdown из **`/static/js/vendor/`** (marked, DOMPurify). Каждая секция по **H2** — **одна карточка** (заголовок h2 и содержимое до следующего h2 вместе). Заголовок вкладки браузера: **«Документация — …»** + текст **первого H1** документа + имя приложения (`KIND_K8S_APP_TITLE` на `body`). В шапке на этой странице активна только **Документация**; **Панель** как обычная пилюля (на дашборде активна **Панель**). Путь к README: `KIND_K8S_README_PATH` или `README.md` рядом с `app/`; в образе — `/opt/kind-k8s/README.md`. |
|
||||
| `GET /ui` | Редирект **307** на `/` (удобный ярлык). |
|
||||
@@ -57,7 +57,8 @@
|
||||
| GET | `/api/v1/clusters/{name}/overview` | Сводка для страницы UI: метрики узлов, агрегаты, блоки **`k8s_*`** (JSON из `kubectl get … -o json`) |
|
||||
| POST | `/api/v1/clusters/{name}/pods/restart` | Удалить под (`kubectl delete pod`) для мягкого рестарта |
|
||||
| DELETE | `/api/v1/clusters/{name}` | Удалить кластер и данные в `clusters/` |
|
||||
| GET | `/api/v1/clusters/{name}/addons/status` | Статус Helm-релизов (ingress-nginx, kube-prometheus-stack, metrics-server, Istio+Kiali); нужен **helm** в образе |
|
||||
| GET | `/api/v1/clusters/{name}/addons/status` | Статус Helm-релизов + **версии чартов** из `helm list` (`ingress_nginx_chart_version`, `kube_prometheus_stack_chart_version`, `metrics_server_chart_version`, `istiod_chart_version`, `istio_base_chart_version`, `kiali_server_chart_version`), если релиз установлен; нужен **helm** |
|
||||
| GET | `/api/v1/clusters/{name}/addons/installed-values` | Для **установленных** аддонов: эффективный YAML (`helm get values --all -o yaml`) и версии; поля `ingress_nginx`, `kube_prometheus_stack`, `metrics_server`, `istio_kiali` (или `null`); лимит размера YAML в ответе |
|
||||
| GET | `/api/v1/helm/chart-versions` | Версии чартов для UI (после `helm repo update`); кэш **`KIND_K8S_HELM_VERSIONS_CACHE_SEC`**; поля: `ingress_nginx`, `kube_prometheus_stack`, `metrics_server`, `istio`, `kiali_server` |
|
||||
| POST | `/api/v1/clusters/{name}/addons/ingress-nginx` | Установить **ingress-nginx**; тело: опционально `chart_version`, опционально **`values_yaml`** (полный YAML чарта, см. **compose-values** и раздел ниже) |
|
||||
| DELETE | `/api/v1/clusters/{name}/addons/ingress-nginx` | Удалить ingress-nginx |
|
||||
|
||||
@@ -306,6 +306,48 @@ class ClusterAddonsStatusResponse(BaseModel):
|
||||
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):
|
||||
|
||||
+430
-76
@@ -8,7 +8,9 @@
|
||||
"use strict";
|
||||
|
||||
const body = document.body;
|
||||
if ((body.getAttribute("data-dashboard-mode") || "") !== "addons") return;
|
||||
/* trim: лишний пробел/перенос в шаблоне ломал бы проверку — скрипт выходил бы до bootstrap и оверлей не снимался */
|
||||
var dashMode = (body.getAttribute("data-dashboard-mode") || body.dataset.dashboardMode || "").trim();
|
||||
if (dashMode !== "addons") return;
|
||||
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
|
||||
@@ -30,6 +32,12 @@
|
||||
/** Последний успешный снимок статуса (для подписи «Установить» / «Обновить»). */
|
||||
var lastAddonsStatus = null;
|
||||
|
||||
/** Список версий из GET /helm/chart-versions уже загружен — можно мержить версии из кластера. */
|
||||
var chartVersionsReady = false;
|
||||
|
||||
/** Суффикс подписи для версии, совпадающей с установленной в кластере. */
|
||||
var INSTALLED_VER_SUFFIX = " (текущая установленная версия)";
|
||||
|
||||
/** @type {((value: boolean) => void) | null} */
|
||||
var confirmModalResolver = null;
|
||||
var confirmModalIsAlert = false;
|
||||
@@ -149,6 +157,37 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обёртка над Promise: если запрос «висит» (сеть, прокси), await иначе не завершится и finally не выполнится.
|
||||
* @param {Promise<unknown>} promise
|
||||
* @param {number} ms
|
||||
* @param {string} timeoutMessage
|
||||
*/
|
||||
function promiseWithTimeout(promise, ms, timeoutMessage) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var finished = false;
|
||||
var to = setTimeout(function () {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
reject(new Error(timeoutMessage));
|
||||
}, ms);
|
||||
Promise.resolve(promise).then(
|
||||
function (v) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(to);
|
||||
resolve(v);
|
||||
},
|
||||
function (err) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
clearTimeout(to);
|
||||
reject(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const sel = document.getElementById("addons-cluster-select");
|
||||
const statusBar = document.getElementById("addons-status-bar");
|
||||
const addonsCardsWrap = document.getElementById("addons-cards-wrap");
|
||||
@@ -189,11 +228,54 @@
|
||||
bootstrapMsg.textContent = s;
|
||||
}
|
||||
|
||||
/** Показать карточку «Ход операции Helm» (как create-progress-wrap на странице создания кластера). */
|
||||
function revealHelmProgressSection() {
|
||||
/** Известные data-addon карточек (только они — безопасный перенос DOM без произвольного селектора). */
|
||||
var ADDON_CARD_MOUNT_KEYS = {
|
||||
"ingress-nginx": true,
|
||||
"kube-prometheus-stack": true,
|
||||
"metrics-server": true,
|
||||
"istio-kiali": true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Переносит блок «Ход операции Helm» в конец карточки выбранного аддона (под кнопки).
|
||||
* Если карточку не нашли — оставляем/возвращаем в #addons-helm-progress-dock.
|
||||
* @param {string} endpoint то же, что data-endpoint / data-addon
|
||||
*/
|
||||
function mountHelmProgressUnderAddon(endpoint) {
|
||||
if (!helmWrap) return;
|
||||
var dock = document.getElementById("addons-helm-progress-dock");
|
||||
var ep = typeof endpoint === "string" ? endpoint.trim() : "";
|
||||
var card = null;
|
||||
if (ep && ADDON_CARD_MOUNT_KEYS[ep]) {
|
||||
card = document.querySelector('.cluster-addon-card[data-addon="' + ep + '"]');
|
||||
}
|
||||
var parent = card || dock;
|
||||
if (!parent) return;
|
||||
parent.appendChild(helmWrap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Показать блок «Ход операции Helm»; при переданном endpoint — сразу под соответствующей карточкой.
|
||||
* @param {string} [endpoint]
|
||||
*/
|
||||
function revealHelmProgressSection(endpoint) {
|
||||
if (endpoint) {
|
||||
mountHelmProgressUnderAddon(endpoint);
|
||||
}
|
||||
if (!helmWrap) return;
|
||||
helmWrap.classList.remove("hidden");
|
||||
helmWrap.setAttribute("aria-hidden", "false");
|
||||
requestAnimationFrame(function () {
|
||||
try {
|
||||
helmWrap.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
} catch (e1) {
|
||||
try {
|
||||
helmWrap.scrollIntoView(true);
|
||||
} catch (e2) {
|
||||
/* старые браузеры */
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateHelmProgressBar(pct, stage) {
|
||||
@@ -227,8 +309,15 @@
|
||||
if (refBtn) refBtn.disabled = !!disabled;
|
||||
}
|
||||
|
||||
/** Таймер «последней надежды», если async bootstrap не дошёл до hide (ошибка до finally, зависший fetch). */
|
||||
var addonsOverlayFailsafeTimer = null;
|
||||
|
||||
/** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */
|
||||
function hideAddonsPageLoadingOverlay() {
|
||||
if (addonsOverlayFailsafeTimer !== null) {
|
||||
clearTimeout(addonsOverlayFailsafeTimer);
|
||||
addonsOverlayFailsafeTimer = null;
|
||||
}
|
||||
const el = document.getElementById("addons-page-loading-overlay");
|
||||
if (!el) return;
|
||||
el.classList.add("hidden");
|
||||
@@ -237,6 +326,24 @@
|
||||
document.body.classList.remove("cluster-addons-page-loading");
|
||||
}
|
||||
|
||||
/** Страховка на случай зависшего fetch без ответа/reject. */
|
||||
function armAddonsOverlayFailsafe(ms) {
|
||||
if (addonsOverlayFailsafeTimer !== null) {
|
||||
clearTimeout(addonsOverlayFailsafeTimer);
|
||||
addonsOverlayFailsafeTimer = null;
|
||||
}
|
||||
var delay = typeof ms === "number" && ms > 0 ? ms : 40000;
|
||||
addonsOverlayFailsafeTimer = setTimeout(function () {
|
||||
addonsOverlayFailsafeTimer = null;
|
||||
var el = document.getElementById("addons-page-loading-overlay");
|
||||
if (!el || el.classList.contains("hidden")) return;
|
||||
hideAddonsPageLoadingOverlay();
|
||||
setBootstrapMsg(
|
||||
"Превышено время ожидания ответа сервера. Проверьте сеть, прокси и доступность /api/v1, затем обновите страницу.",
|
||||
);
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Текстовое сообщение в полосе статуса (нет кластера / ошибка).
|
||||
* @param {string} text
|
||||
@@ -322,60 +429,118 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
var COMPOSE_DEBOUNCE_MS = 600;
|
||||
/** @type {ReturnType<typeof setTimeout> | null} */
|
||||
var composeDebounceTimer = null;
|
||||
/** id панели с YAML (скрыта до первой загрузки по кнопке). */
|
||||
var ADDON_VALUES_PANEL_ID = {
|
||||
"ingress-nginx": "addons-values-panel-ingress-nginx",
|
||||
"kube-prometheus-stack": "addons-values-panel-kube-prometheus-stack",
|
||||
"metrics-server": "addons-values-panel-metrics-server",
|
||||
"istio-kiali": "addons-values-panel-istio-kiali",
|
||||
};
|
||||
|
||||
/**
|
||||
* Заполнить все textarea: один POST **compose-values-batch** (один helm repo update на сервере).
|
||||
* Скрыть блок Values и очистить textarea для одного аддона (смена версии / сброс).
|
||||
* @param {string} addon ключ data-addon / endpoint
|
||||
*/
|
||||
async function refreshAllComposeValuesFromServer() {
|
||||
setBootstrapMsg("");
|
||||
var si = document.getElementById("addons-ing-version-select");
|
||||
var pv = document.getElementById("addons-prom-version-select");
|
||||
var ms = document.getElementById("addons-ms-version-select");
|
||||
var isel = document.getElementById("addons-istio-version-select");
|
||||
var ksel = document.getElementById("addons-kiali-version-select");
|
||||
var gu = document.getElementById("addons-grafana-user");
|
||||
var gp = document.getElementById("addons-grafana-pass");
|
||||
var batchBody = {
|
||||
ingress_chart_version: si && si.value.trim() ? si.value.trim() : null,
|
||||
kube_prometheus_chart_version: pv && pv.value.trim() ? pv.value.trim() : null,
|
||||
metrics_server_chart_version: ms && ms.value.trim() ? ms.value.trim() : null,
|
||||
istio_chart_version: isel && isel.value.trim() ? isel.value.trim() : null,
|
||||
kiali_chart_version: ksel && ksel.value.trim() ? ksel.value.trim() : null,
|
||||
grafana_admin_user: gu ? gu.value.trim() || null : null,
|
||||
grafana_admin_password: gp ? gp.value || null : null,
|
||||
};
|
||||
try {
|
||||
var r = await api("/helm/addons/compose-values-batch", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(batchBody),
|
||||
});
|
||||
var ti = document.getElementById("addons-values-yaml-ingress-nginx");
|
||||
var tp = document.getElementById("addons-values-yaml-kube-prometheus-stack");
|
||||
var tm = document.getElementById("addons-values-yaml-metrics-server");
|
||||
var tk = document.getElementById("addons-values-yaml-istio-kiali");
|
||||
var tb = document.getElementById("addons-values-yaml-istio-base");
|
||||
var tiod = document.getElementById("addons-values-yaml-istio-istiod");
|
||||
if (ti) ti.value = r.ingress_nginx_values_yaml || "";
|
||||
if (tp) tp.value = r.kube_prometheus_values_yaml || "";
|
||||
if (tm) tm.value = r.metrics_server_values_yaml || "";
|
||||
if (tk) tk.value = r.kiali_values_yaml || "";
|
||||
if (tb) tb.value = r.istio_base_values_yaml || "";
|
||||
if (tiod) tiod.value = r.istio_istiod_values_yaml || "";
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Не удалось подставить values из чартов (helm show values): " + e.message);
|
||||
function hideAddonValuesPanel(addon) {
|
||||
var pid = ADDON_VALUES_PANEL_ID[addon];
|
||||
if (pid) {
|
||||
var panel = document.getElementById(pid);
|
||||
if (panel) {
|
||||
panel.classList.add("hidden");
|
||||
panel.setAttribute("hidden", "hidden");
|
||||
panel.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
if (addon === "istio-kiali") {
|
||||
["addons-values-yaml-istio-kiali", "addons-values-yaml-istio-base", "addons-values-yaml-istio-istiod"].forEach(
|
||||
function (tid) {
|
||||
var ta = document.getElementById(tid);
|
||||
if (ta) ta.value = "";
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
var ta = document.getElementById("addons-values-yaml-" + addon);
|
||||
if (ta) ta.value = "";
|
||||
}
|
||||
|
||||
function scheduleComposeValuesRefresh() {
|
||||
if (composeDebounceTimer) clearTimeout(composeDebounceTimer);
|
||||
composeDebounceTimer = setTimeout(function () {
|
||||
composeDebounceTimer = null;
|
||||
refreshAllComposeValuesFromServer();
|
||||
}, COMPOSE_DEBOUNCE_MS);
|
||||
function showAddonValuesPanel(addon) {
|
||||
var pid = ADDON_VALUES_PANEL_ID[addon];
|
||||
if (!pid) return;
|
||||
var panel = document.getElementById(pid);
|
||||
if (!panel) return;
|
||||
panel.classList.remove("hidden");
|
||||
panel.removeAttribute("hidden");
|
||||
panel.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
||||
function resetAllAddonValuesPanels() {
|
||||
Object.keys(ADDON_VALUES_PANEL_ID).forEach(function (a) {
|
||||
hideAddonValuesPanel(a);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /helm/addons/compose-values для одной карточки: версии и поля Grafana читаются в момент нажатия кнопки.
|
||||
* @param {string} addon
|
||||
* @param {HTMLButtonElement | null} triggerBtn
|
||||
*/
|
||||
async function loadComposeValuesForAddon(addon, triggerBtn) {
|
||||
setBootstrapMsg("");
|
||||
var body = { addon: addon };
|
||||
if (addon === "ingress-nginx") {
|
||||
var si = document.getElementById("addons-ing-version-select");
|
||||
if (si && si.value.trim()) body.chart_version = si.value.trim();
|
||||
} else if (addon === "kube-prometheus-stack") {
|
||||
var pv = document.getElementById("addons-prom-version-select");
|
||||
var gu = document.getElementById("addons-grafana-user");
|
||||
var gp = document.getElementById("addons-grafana-pass");
|
||||
if (pv && pv.value.trim()) body.chart_version = pv.value.trim();
|
||||
if (gu) body.grafana_admin_user = gu.value.trim() || null;
|
||||
if (gp) body.grafana_admin_password = gp.value || null;
|
||||
} else if (addon === "metrics-server") {
|
||||
var ms = document.getElementById("addons-ms-version-select");
|
||||
if (ms && ms.value.trim()) body.chart_version = ms.value.trim();
|
||||
} else if (addon === "istio-kiali") {
|
||||
var isel = document.getElementById("addons-istio-version-select");
|
||||
var ksel = document.getElementById("addons-kiali-version-select");
|
||||
if (isel && isel.value.trim()) body.istio_chart_version = isel.value.trim();
|
||||
if (ksel && ksel.value.trim()) body.kiali_chart_version = ksel.value.trim();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
var prevText = triggerBtn ? triggerBtn.textContent : "";
|
||||
if (triggerBtn) {
|
||||
triggerBtn.disabled = true;
|
||||
triggerBtn.textContent = "Загрузка…";
|
||||
}
|
||||
try {
|
||||
var r = await api("/helm/addons/compose-values", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (addon === "istio-kiali") {
|
||||
var tk = document.getElementById("addons-values-yaml-istio-kiali");
|
||||
var tb = document.getElementById("addons-values-yaml-istio-base");
|
||||
var tiod = document.getElementById("addons-values-yaml-istio-istiod");
|
||||
if (tk) tk.value = r.values_yaml || "";
|
||||
if (tb) tb.value = r.istio_base_values_yaml || "";
|
||||
if (tiod) tiod.value = r.istio_istiod_values_yaml || "";
|
||||
} else {
|
||||
var t = document.getElementById("addons-values-yaml-" + addon);
|
||||
if (t) t.value = r.values_yaml || "";
|
||||
}
|
||||
showAddonValuesPanel(addon);
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Не удалось загрузить values (helm show values): " + e.message);
|
||||
} finally {
|
||||
if (triggerBtn) {
|
||||
triggerBtn.disabled = false;
|
||||
triggerBtn.textContent = prevText || "Загрузить values";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addonEndpointInstalled(endpoint) {
|
||||
@@ -440,7 +605,143 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* В селекте отметить версию из кластера и подписать опцию; при отсутствии в списке репозитория — добавить опцию.
|
||||
*/
|
||||
function mergeInstalledChartVersion(selectEl, version, isInstalled) {
|
||||
if (!selectEl || !isInstalled || !version) return;
|
||||
var v = String(version).trim();
|
||||
if (!v) return;
|
||||
var found = false;
|
||||
for (var i = 0; i < selectEl.options.length; i++) {
|
||||
var o = selectEl.options[i];
|
||||
if (o.value === v) {
|
||||
o.textContent = v + INSTALLED_VER_SUFFIX;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
var nu = document.createElement("option");
|
||||
nu.value = v;
|
||||
nu.textContent = v + INSTALLED_VER_SUFFIX;
|
||||
selectEl.appendChild(nu);
|
||||
}
|
||||
selectEl.value = v;
|
||||
}
|
||||
|
||||
/** Подставить в селекты версии из lastAddonsStatus (после fillVersionSelect). */
|
||||
function mergeChartVersionsFromStatus(st) {
|
||||
if (!st) return;
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-ing-version-select"),
|
||||
st.ingress_nginx_chart_version,
|
||||
st.ingress_nginx,
|
||||
);
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-prom-version-select"),
|
||||
st.kube_prometheus_stack_chart_version,
|
||||
st.kube_prometheus_stack,
|
||||
);
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-ms-version-select"),
|
||||
st.metrics_server_chart_version,
|
||||
st.metrics_server,
|
||||
);
|
||||
var istioVer = st.istiod_chart_version || st.istio_base_chart_version;
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-istio-version-select"),
|
||||
istioVer,
|
||||
st.istio_mesh_ready,
|
||||
);
|
||||
mergeInstalledChartVersion(
|
||||
document.getElementById("addons-kiali-version-select"),
|
||||
st.kiali_server_chart_version,
|
||||
st.istio_mesh_ready,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Заполнить textarea из GET …/addons/installed-values; снять панели с аддонов, которые не установлены.
|
||||
* @param {object} iv ответ API
|
||||
*/
|
||||
function applyInstalledValuesPayload(iv) {
|
||||
var st = lastAddonsStatus;
|
||||
if (!iv || !st) return;
|
||||
|
||||
if (!st.ingress_nginx) {
|
||||
hideAddonValuesPanel("ingress-nginx");
|
||||
} else if (iv.ingress_nginx && iv.ingress_nginx.values_yaml) {
|
||||
var ti = document.getElementById("addons-values-yaml-ingress-nginx");
|
||||
if (ti) ti.value = iv.ingress_nginx.values_yaml;
|
||||
showAddonValuesPanel("ingress-nginx");
|
||||
}
|
||||
|
||||
if (!st.kube_prometheus_stack) {
|
||||
hideAddonValuesPanel("kube-prometheus-stack");
|
||||
} else if (iv.kube_prometheus_stack && iv.kube_prometheus_stack.values_yaml) {
|
||||
var tp = document.getElementById("addons-values-yaml-kube-prometheus-stack");
|
||||
if (tp) tp.value = iv.kube_prometheus_stack.values_yaml;
|
||||
showAddonValuesPanel("kube-prometheus-stack");
|
||||
}
|
||||
|
||||
if (!st.metrics_server) {
|
||||
hideAddonValuesPanel("metrics-server");
|
||||
} else if (iv.metrics_server && iv.metrics_server.values_yaml) {
|
||||
var tm = document.getElementById("addons-values-yaml-metrics-server");
|
||||
if (tm) tm.value = iv.metrics_server.values_yaml;
|
||||
showAddonValuesPanel("metrics-server");
|
||||
}
|
||||
|
||||
if (!st.istio_mesh_ready) {
|
||||
hideAddonValuesPanel("istio-kiali");
|
||||
} else if (iv.istio_kiali) {
|
||||
var ik = iv.istio_kiali;
|
||||
if (ik.kiali_values_yaml) {
|
||||
var tk = document.getElementById("addons-values-yaml-istio-kiali");
|
||||
if (tk) tk.value = ik.kiali_values_yaml;
|
||||
}
|
||||
if (ik.istio_base_values_yaml) {
|
||||
var tb = document.getElementById("addons-values-yaml-istio-base");
|
||||
if (tb) tb.value = ik.istio_base_values_yaml;
|
||||
}
|
||||
if (ik.istiod_values_yaml) {
|
||||
var tiod = document.getElementById("addons-values-yaml-istio-istiod");
|
||||
if (tiod) tiod.value = ik.istiod_values_yaml;
|
||||
}
|
||||
if (ik.kiali_values_yaml || ik.istio_base_values_yaml || ik.istiod_values_yaml) {
|
||||
showAddonValuesPanel("istio-kiali");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Версии в селектах + YAML из кластера для установленных аддонов (после статуса и списка версий репозитория).
|
||||
* @param {string} clusterName
|
||||
*/
|
||||
async function maybeSyncInstalledAddonsFromCluster(clusterName) {
|
||||
if (!clusterName || !lastAddonsStatus) return;
|
||||
mergeChartVersionsFromStatus(lastAddonsStatus);
|
||||
var st = lastAddonsStatus;
|
||||
var any =
|
||||
st.ingress_nginx ||
|
||||
st.kube_prometheus_stack ||
|
||||
st.metrics_server ||
|
||||
st.istio_mesh_ready;
|
||||
if (!any) {
|
||||
resetAllAddonValuesPanels();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var iv = await api("/clusters/" + encodeURIComponent(clusterName) + "/addons/installed-values");
|
||||
applyInstalledValuesPayload(iv);
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Не удалось загрузить values из кластера: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChartVersions() {
|
||||
setBootstrapMsg("Загрузка списка версий Helm-чартов (helm repo update, может занять до минуты)…");
|
||||
try {
|
||||
const d = await api("/helm/chart-versions");
|
||||
setBootstrapMsg("");
|
||||
@@ -449,7 +750,10 @@
|
||||
fillVersionSelect(document.getElementById("addons-ms-version-select"), d.metrics_server);
|
||||
fillVersionSelect(document.getElementById("addons-istio-version-select"), d.istio);
|
||||
fillVersionSelect(document.getElementById("addons-kiali-version-select"), d.kiali_server);
|
||||
await refreshAllComposeValuesFromServer();
|
||||
resetAllAddonValuesPanels();
|
||||
chartVersionsReady = true;
|
||||
var n = getClusterName();
|
||||
if (n) await maybeSyncInstalledAddonsFromCluster(n);
|
||||
} catch (e) {
|
||||
setBootstrapMsg("Список версий чартов недоступен: " + e.message);
|
||||
}
|
||||
@@ -501,6 +805,7 @@
|
||||
updateAddonCardBadges(null);
|
||||
updateAddonActionButtons();
|
||||
updateAddonsCardsVisibility();
|
||||
resetAllAddonValuesPanels();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -516,6 +821,13 @@
|
||||
updateAddonActionButtons();
|
||||
}
|
||||
updateAddonsCardsVisibility();
|
||||
if (chartVersionsReady && getClusterName()) {
|
||||
try {
|
||||
await maybeSyncInstalledAddonsFromCluster(getClusterName());
|
||||
} catch (e2) {
|
||||
/* уже отражено в maybeSync */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstall(endpoint, payload) {
|
||||
@@ -526,12 +838,12 @@
|
||||
}
|
||||
const isUpdate = addonEndpointInstalled(endpoint);
|
||||
const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
|
||||
revealHelmProgressSection();
|
||||
revealHelmProgressSection(endpoint);
|
||||
setAddonFormsDisabled(true);
|
||||
if (helmHint) {
|
||||
helmHint.textContent = isUpdate
|
||||
? "Обновление релиза «" + hum + "» через helm в контейнере; ниже — полный вывод по завершении."
|
||||
: "Установка релиза «" + hum + "» через helm в контейнере; ниже — полный вывод по завершении.";
|
||||
? "Обновление релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона."
|
||||
: "Установка релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона.";
|
||||
}
|
||||
updateHelmProgressBar(8, isUpdate ? "Обновление Helm…" : "Установка Helm…");
|
||||
setHelmLogText(
|
||||
@@ -603,11 +915,11 @@
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
revealHelmProgressSection();
|
||||
revealHelmProgressSection(endpoint);
|
||||
setAddonFormsDisabled(true);
|
||||
if (helmHint) {
|
||||
helmHint.textContent =
|
||||
"Удаление релиза «" + hum + "» (helm uninstall); ниже — полный вывод по завершении.";
|
||||
"Удаление релиза «" + hum + "» (helm uninstall); журнал под этой карточкой аддона.";
|
||||
}
|
||||
updateHelmProgressBar(8, "Удаление Helm…");
|
||||
setHelmLogText(
|
||||
@@ -716,6 +1028,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".btn-addon-load-values").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var ad = btn.getAttribute("data-addon") || "";
|
||||
if (ad) loadComposeValuesForAddon(ad, btn);
|
||||
});
|
||||
});
|
||||
|
||||
const refreshBtn = document.getElementById("addons-refresh-status");
|
||||
if (refreshBtn) refreshBtn.addEventListener("click", function () { refreshStatus(); });
|
||||
|
||||
@@ -725,29 +1044,64 @@
|
||||
|
||||
bindConfirmModalControls();
|
||||
|
||||
[
|
||||
"addons-ing-version-select",
|
||||
"addons-prom-version-select",
|
||||
"addons-ms-version-select",
|
||||
"addons-istio-version-select",
|
||||
"addons-kiali-version-select",
|
||||
].forEach(function (id) {
|
||||
/** Смена версии — сбрасываем предпросмотр YAML (устаревший контент). */
|
||||
var versionSelectToAddon = {
|
||||
"addons-ing-version-select": "ingress-nginx",
|
||||
"addons-prom-version-select": "kube-prometheus-stack",
|
||||
"addons-ms-version-select": "metrics-server",
|
||||
"addons-istio-version-select": "istio-kiali",
|
||||
"addons-kiali-version-select": "istio-kiali",
|
||||
};
|
||||
Object.keys(versionSelectToAddon).forEach(function (id) {
|
||||
var el = document.getElementById(id);
|
||||
if (el) el.addEventListener("change", function () { scheduleComposeValuesRefresh(); });
|
||||
if (el) {
|
||||
el.addEventListener("change", function () {
|
||||
hideAddonValuesPanel(versionSelectToAddon[id]);
|
||||
});
|
||||
}
|
||||
});
|
||||
/* Логин/пароль Grafana: если правили после загрузки YAML — скрываем блок, чтобы не путать с устаревшим предпросмотром. */
|
||||
var grafanaUserEl = document.getElementById("addons-grafana-user");
|
||||
var grafanaPassEl = document.getElementById("addons-grafana-pass");
|
||||
if (grafanaUserEl) grafanaUserEl.addEventListener("input", function () { scheduleComposeValuesRefresh(); });
|
||||
if (grafanaPassEl) grafanaPassEl.addEventListener("input", function () { scheduleComposeValuesRefresh(); });
|
||||
function onGrafanaCredChange() {
|
||||
hideAddonValuesPanel("kube-prometheus-stack");
|
||||
}
|
||||
if (grafanaUserEl) grafanaUserEl.addEventListener("input", onGrafanaCredChange);
|
||||
if (grafanaPassEl) grafanaPassEl.addEventListener("input", onGrafanaCredChange);
|
||||
|
||||
loadClusters()
|
||||
.then(function () {
|
||||
return refreshStatus();
|
||||
})
|
||||
.then(function () {
|
||||
return loadChartVersions();
|
||||
})
|
||||
.finally(function () {
|
||||
/**
|
||||
* Оверлей убираем после списка кластеров и статуса — без ожидания GET /helm/chart-versions.
|
||||
* Кластеры/статус оборачиваем в таймаут: иначе при «висящем» fetch await не завершается и finally не вызывается.
|
||||
*/
|
||||
armAddonsOverlayFailsafe(45000);
|
||||
|
||||
(async function bootstrapAddonsPage() {
|
||||
try {
|
||||
await promiseWithTimeout(
|
||||
loadClusters(),
|
||||
30000,
|
||||
"Таймаут: список кластеров не получен за 30 с. Проверьте GET /api/v1/clusters.",
|
||||
);
|
||||
await promiseWithTimeout(
|
||||
refreshStatus(),
|
||||
30000,
|
||||
"Таймаут: статус аддонов не получен за 30 с.",
|
||||
);
|
||||
} catch (e) {
|
||||
var msg0 = e && e.message ? e.message : String(e);
|
||||
setBootstrapMsg(msg0);
|
||||
} finally {
|
||||
hideAddonsPageLoadingOverlay();
|
||||
});
|
||||
}
|
||||
try {
|
||||
await promiseWithTimeout(
|
||||
loadChartVersions(),
|
||||
180000,
|
||||
"Таймаут: версии Helm-чартов не получены за 3 мин. (helm repo update).",
|
||||
);
|
||||
} catch (e) {
|
||||
var msg1 = e && e.message ? e.message : String(e);
|
||||
setBootstrapMsg("Не удалось загрузить версии чартов: " + msg1);
|
||||
}
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -1557,6 +1557,125 @@
|
||||
bindActionTooltipHosts(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрыть секцию «Установленные аддоны» (#cluster-detail-addons-section) и очистить сетку.
|
||||
*/
|
||||
function clearClusterDetailAddonsStrip() {
|
||||
var strip = document.getElementById("cluster-detail-addons-strip");
|
||||
var section = document.getElementById("cluster-detail-addons-section");
|
||||
if (strip) strip.replaceChildren();
|
||||
if (section) {
|
||||
section.classList.add("hidden");
|
||||
section.setAttribute("hidden", "");
|
||||
section.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Мини-карточки установленных Helm-аддонов в отдельной section.card под hero.
|
||||
* @param {string} clusterName
|
||||
* @param {object} st ответ GET /clusters/…/addons/status
|
||||
*/
|
||||
function renderClusterDetailAddonsStrip(clusterName, st) {
|
||||
var strip = document.getElementById("cluster-detail-addons-strip");
|
||||
var section = document.getElementById("cluster-detail-addons-section");
|
||||
if (!strip || !section || !st) {
|
||||
clearClusterDetailAddonsStrip();
|
||||
return;
|
||||
}
|
||||
var items = [];
|
||||
if (st.ingress_nginx) {
|
||||
items.push({
|
||||
title: "ingress-nginx",
|
||||
ver: st.ingress_nginx_chart_version,
|
||||
ns: "ingress-nginx",
|
||||
});
|
||||
}
|
||||
if (st.kube_prometheus_stack) {
|
||||
items.push({
|
||||
title: "kube-prometheus-stack",
|
||||
ver: st.kube_prometheus_stack_chart_version,
|
||||
ns: "monitoring",
|
||||
});
|
||||
}
|
||||
if (st.metrics_server) {
|
||||
items.push({
|
||||
title: "metrics-server",
|
||||
ver: st.metrics_server_chart_version,
|
||||
ns: "kube-system",
|
||||
});
|
||||
}
|
||||
if (st.istio_mesh_ready) {
|
||||
var iv = st.istiod_chart_version || st.istio_base_chart_version;
|
||||
var kv = st.kiali_server_chart_version;
|
||||
var parts = [];
|
||||
if (iv) parts.push("Istio " + iv);
|
||||
if (kv) parts.push("Kiali " + kv);
|
||||
items.push({
|
||||
title: "Istio + Kiali",
|
||||
verLine: parts.length ? parts.join(" · ") : null,
|
||||
ns: "istio-system",
|
||||
});
|
||||
}
|
||||
strip.replaceChildren();
|
||||
if (!items.length) {
|
||||
section.classList.add("hidden");
|
||||
section.setAttribute("hidden", "");
|
||||
section.setAttribute("aria-hidden", "true");
|
||||
return;
|
||||
}
|
||||
section.classList.remove("hidden");
|
||||
section.removeAttribute("hidden");
|
||||
section.setAttribute("aria-hidden", "false");
|
||||
items.forEach(function (it) {
|
||||
var a = document.createElement("a");
|
||||
a.className = "cluster-detail-addon-mini";
|
||||
a.href = "/cluster-addons";
|
||||
a.setAttribute("role", "listitem");
|
||||
a.setAttribute(
|
||||
"title",
|
||||
"Открыть страницу аддонов Helm; выберите кластер «" + clusterName + "» для настройки.",
|
||||
);
|
||||
var t = document.createElement("span");
|
||||
t.className = "cluster-detail-addon-mini__title";
|
||||
t.textContent = it.title;
|
||||
a.appendChild(t);
|
||||
var v = document.createElement("span");
|
||||
v.className = "cluster-detail-addon-mini__ver";
|
||||
if (it.verLine) {
|
||||
v.textContent = it.verLine;
|
||||
} else if (it.ver) {
|
||||
v.textContent = "Чарт " + it.ver;
|
||||
} else {
|
||||
v.textContent = "Версия чарта не определена";
|
||||
}
|
||||
a.appendChild(v);
|
||||
var n = document.createElement("span");
|
||||
n.className = "cluster-detail-addon-mini__ns";
|
||||
n.textContent = "ns: " + it.ns;
|
||||
a.appendChild(n);
|
||||
strip.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузить статус аддонов и обновить карточку «Установленные аддоны» (при наличии kubeconfig на диске).
|
||||
* @param {string} clusterName
|
||||
* @param {boolean} hasLocalKubeconfig
|
||||
*/
|
||||
async function refreshClusterDetailAddonsStrip(clusterName, hasLocalKubeconfig) {
|
||||
if (!clusterName || !hasLocalKubeconfig) {
|
||||
clearClusterDetailAddonsStrip();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var st = await api("/clusters/" + encodeURIComponent(clusterName) + "/addons/status");
|
||||
renderClusterDetailAddonsStrip(clusterName, st);
|
||||
} catch (e) {
|
||||
clearClusterDetailAddonsStrip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка ``GET /clusters/{name}/overview`` и заполнение блоков страницы кластера.
|
||||
* @param {string} name
|
||||
@@ -1578,6 +1697,7 @@
|
||||
has_local_kubeconfig: !!d.has_local_kubeconfig,
|
||||
has_provision_log: !!d.has_provision_log,
|
||||
});
|
||||
await refreshClusterDetailAddonsStrip(d.cluster_name, !!d.has_local_kubeconfig);
|
||||
const cliRaw =
|
||||
(cachedHealthPayload && cachedHealthPayload.container_cli) || "docker";
|
||||
renderAggregateDonutsToRoot(
|
||||
@@ -1611,6 +1731,7 @@
|
||||
fillClusterK8sTablesFromOverview(d);
|
||||
} catch (e) {
|
||||
const errText = e && e.message ? e.message : String(e);
|
||||
clearClusterDetailAddonsStrip();
|
||||
if (msg) {
|
||||
msg.textContent =
|
||||
"Кластер или API недоступны: " +
|
||||
|
||||
+196
-9
@@ -2097,6 +2097,150 @@ html[data-theme="light"] .cluster-detail-actions-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Карточка «Установленные аддоны» на странице кластера (отдельный section.card) */
|
||||
.cluster-detail-addons-card.hidden {
|
||||
display: none;
|
||||
}
|
||||
.cluster-detail-addons-card__header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.cluster-detail-addons-card__head-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
/* Бейдж и заголовок в одной линии по вертикали: сброс margin у .cluster-detail-section-title (он задаётся ниже по файлу). */
|
||||
.cluster-detail-addons-card__head-row h2.cluster-detail-section-title {
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.cluster-detail-addons-card__helm-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
padding: 0.22rem 0.55rem;
|
||||
min-height: 1.35em;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, #0f766e 0%, #0d9488 40%, #14b8a6 100%);
|
||||
box-shadow: 0 1px 3px rgba(15, 118, 110, 0.35);
|
||||
}
|
||||
.cluster-detail-addons-card__title {
|
||||
margin: 0;
|
||||
font-size: 1.12rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.cluster-detail-addons-card__lead {
|
||||
margin: 0;
|
||||
max-width: 44rem;
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.cluster-detail-addons-card__lead code {
|
||||
font-size: 0.82em;
|
||||
}
|
||||
.cluster-detail-addons-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
|
||||
gap: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cluster-detail-addon-mini {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.35rem;
|
||||
padding: 0.75rem 0.85rem 0.7rem;
|
||||
min-height: 5.5rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-left: 4px solid rgba(59, 130, 246, 0.75);
|
||||
background: var(--surface);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.15s ease,
|
||||
background 0.18s ease;
|
||||
}
|
||||
html[data-theme="light"] .cluster-detail-addon-mini {
|
||||
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.cluster-detail-addon-mini:hover {
|
||||
border-color: rgba(59, 130, 246, 0.4);
|
||||
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.12);
|
||||
transform: translateY(-2px);
|
||||
background: rgba(59, 130, 246, 0.04);
|
||||
}
|
||||
.cluster-detail-addon-mini:focus-visible {
|
||||
outline: 2px solid rgba(59, 130, 246, 0.65);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.cluster-detail-addon-mini__title {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.3;
|
||||
color: var(--text);
|
||||
}
|
||||
.cluster-detail-addon-mini__ver {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.4;
|
||||
color: var(--muted);
|
||||
word-break: break-word;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.cluster-detail-addon-mini__ns {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
padding: 0.15rem 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
color: var(--muted);
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-radius: 4px;
|
||||
}
|
||||
html[data-theme="light"] .cluster-detail-addon-mini__ns {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
.cluster-detail-addons-card__footer {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.85rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.cluster-detail-addons-card__cta {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: rgba(59, 130, 246, 0.95);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.cluster-detail-addons-card__cta:hover {
|
||||
color: rgba(96, 165, 250, 1);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cluster-detail-addons-card__cta:focus-visible {
|
||||
outline: 2px solid rgba(59, 130, 246, 0.55);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cluster-k8s-scope-hint {
|
||||
margin: 0 0 0.85rem;
|
||||
font-size: 0.86rem;
|
||||
@@ -2998,26 +3142,27 @@ html[data-theme="light"] .cluster-addon-card__values-textarea {
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.cluster-addon-card__actions--equal {
|
||||
/* Кнопки «Установить / Обновить / Удалить» — по содержимому, не на всю ширину карточки */
|
||||
.cluster-addon-card__actions--inline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.85rem;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.cluster-addon-card__actions--equal .btn-addon-install,
|
||||
.cluster-addon-card__actions--equal .btn-addon-remove {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
.cluster-addon-card__actions--inline .btn-addon-install,
|
||||
.cluster-addon-card__actions--inline .btn-addon-remove {
|
||||
flex: 0 0 auto;
|
||||
min-width: 7.5rem;
|
||||
min-height: 2.65rem;
|
||||
margin-top: 0;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.cluster-addon-card--wide {
|
||||
@@ -3026,10 +3171,52 @@ html[data-theme="light"] .cluster-addon-card__values-textarea {
|
||||
.cluster-addon-istio-grid {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.cluster-addon-istio-load-field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.cluster-addon-card__field-label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
.cluster-addon-version-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem 0.65rem;
|
||||
}
|
||||
.cluster-addon-version-row .cluster-addons-version-select {
|
||||
flex: 1 1 12rem;
|
||||
width: auto;
|
||||
min-width: 10rem;
|
||||
max-width: 22rem;
|
||||
}
|
||||
.cluster-addon-card__values-panel {
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
.btn-addon-load-values {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Якорь для блока прогресса до первой операции; пустой, когда секция перенесена в карточку */
|
||||
.cluster-addons-helm-dock:empty {
|
||||
display: none;
|
||||
}
|
||||
.cluster-addons-helm-dock:not(:empty) {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
/* Карточка хода Helm на странице аддонов (разметка как у create-progress-wrap + job-log-panel) */
|
||||
.cluster-addons-helm-progress {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.cluster-addon-card > .cluster-addons-helm-progress {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cluster-addons-helm-progress .job-log-panel {
|
||||
max-height: min(55vh, 420px);
|
||||
}
|
||||
|
||||
+174
-126
@@ -18,7 +18,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{# Полноэкранный спиннер до загрузки списка кластеров, статуса и версий чартов; скрывается в cluster_addons.js #}
|
||||
{# Полноэкранный спиннер до списка кластеров и статуса; версии чартов подгружаются после (helm repo update может быть долгим). #}
|
||||
<div
|
||||
id="addons-page-loading-overlay"
|
||||
class="page-loading-overlay"
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="page-loading-backdrop" aria-hidden="true"></div>
|
||||
<div class="page-loading-center">
|
||||
<span class="spinner page-loading-spinner" aria-hidden="true"></span>
|
||||
<span class="page-loading-label">Загрузка списка кластеров и версий чартов…</span>
|
||||
<span class="page-loading-label">Загрузка списка кластеров…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,26 +68,31 @@
|
||||
</header>
|
||||
<p class="muted cluster-addon-card__desc">Контроллер Ingress.</p>
|
||||
<div class="create-form-field">
|
||||
<label for="addons-ing-version-select">Версия чарта</label>
|
||||
<select id="addons-ing-version-select" class="cluster-addons-version-select" aria-label="Версия чарта ingress-nginx">
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
<span class="cluster-addon-card__field-label" id="addons-ing-version-label">Версия чарта</span>
|
||||
<div class="cluster-addon-version-row" role="group" aria-labelledby="addons-ing-version-label">
|
||||
<select id="addons-ing-version-select" class="cluster-addons-version-select" aria-label="Версия чарта ingress-nginx">
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
<button type="button" class="btn-secondary btn-addon-load-values" data-addon="ingress-nginx">Загрузить values</button>
|
||||
</div>
|
||||
</div>
|
||||
<details class="cluster-addon-card__values-details">
|
||||
<summary class="cluster-addon-card__values-summary">Values чарта (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
Содержимое из <code>helm show values</code> плюс сервис для kind; подставляется при смене версии чарта. После <code>-f</code> Helm применяет <code>--set</code> для NodePort.
|
||||
</p>
|
||||
<label for="addons-values-yaml-ingress-nginx" class="sr-only">Полные YAML values для ingress-nginx</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-ingress-nginx"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта ingress-nginx"
|
||||
></textarea>
|
||||
</details>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--equal">
|
||||
<div id="addons-values-panel-ingress-nginx" class="cluster-addon-card__values-panel hidden" hidden aria-hidden="true">
|
||||
<details class="cluster-addon-card__values-details" open>
|
||||
<summary class="cluster-addon-card__values-summary">Values чарта (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
Содержимое из <code>helm show values</code> плюс сервис для kind. После <code>-f</code> Helm применяет <code>--set</code> для NodePort. Нажмите «Загрузить values» для выбранной версии.
|
||||
</p>
|
||||
<label for="addons-values-yaml-ingress-nginx" class="sr-only">Полные YAML values для ingress-nginx</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-ingress-nginx"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта ingress-nginx"
|
||||
></textarea>
|
||||
</details>
|
||||
</div>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--inline">
|
||||
<button type="button" class="btn-addon-install" data-endpoint="ingress-nginx">Установить</button>
|
||||
<button type="button" class="btn-secondary btn-addon-remove hidden" data-endpoint="ingress-nginx" hidden>Удалить</button>
|
||||
</div>
|
||||
@@ -108,26 +113,35 @@
|
||||
<input id="addons-grafana-pass" type="password" minlength="8" maxlength="256" autocomplete="new-password" placeholder="не менее 8 символов" />
|
||||
</div>
|
||||
<div class="create-form-field">
|
||||
<label for="addons-prom-version-select">Версия чарта</label>
|
||||
<select id="addons-prom-version-select" class="cluster-addons-version-select">
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
<span class="cluster-addon-card__field-label" id="addons-prom-version-label">Версия чарта</span>
|
||||
<div class="cluster-addon-version-row" role="group" aria-labelledby="addons-prom-version-label">
|
||||
<select
|
||||
id="addons-prom-version-select"
|
||||
class="cluster-addons-version-select"
|
||||
aria-labelledby="addons-prom-version-label"
|
||||
>
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
<button type="button" class="btn-secondary btn-addon-load-values" data-addon="kube-prometheus-stack">Загрузить values</button>
|
||||
</div>
|
||||
</div>
|
||||
<details class="cluster-addon-card__values-details">
|
||||
<summary class="cluster-addon-card__values-summary">Values чарта (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
Вывод <code>helm show values</code> с подставленными логином и паролем Grafana из полей выше; обновляется при смене версии или учётных данных. Пустое поле при установке — сервер соберёт values сам.
|
||||
</p>
|
||||
<label for="addons-values-yaml-kube-prometheus-stack" class="sr-only">Полные YAML values для kube-prometheus-stack</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-kube-prometheus-stack"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта kube-prometheus-stack"
|
||||
></textarea>
|
||||
</details>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--equal">
|
||||
<div id="addons-values-panel-kube-prometheus-stack" class="cluster-addon-card__values-panel hidden" hidden aria-hidden="true">
|
||||
<details class="cluster-addon-card__values-details" open>
|
||||
<summary class="cluster-addon-card__values-summary">Values чарта (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
Вывод <code>helm show values</code> с подстановкой логина и пароля Grafana из полей выше на момент нажатия «Загрузить values». Пустой textarea при установке — сервер соберёт values сам.
|
||||
</p>
|
||||
<label for="addons-values-yaml-kube-prometheus-stack" class="sr-only">Полные YAML values для kube-prometheus-stack</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-kube-prometheus-stack"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта kube-prometheus-stack"
|
||||
></textarea>
|
||||
</details>
|
||||
</div>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--inline">
|
||||
<button type="button" class="btn-addon-install" data-endpoint="kube-prometheus-stack">Установить</button>
|
||||
<button type="button" class="btn-secondary btn-addon-remove hidden" data-endpoint="kube-prometheus-stack" hidden>Удалить</button>
|
||||
</div>
|
||||
@@ -140,26 +154,35 @@
|
||||
</header>
|
||||
<p class="muted cluster-addon-card__desc">Метрики узлов и подов для <code>kubectl top</code>; для kind включён <code>--kubelet-insecure-tls</code>.</p>
|
||||
<div class="create-form-field">
|
||||
<label for="addons-ms-version-select">Версия чарта</label>
|
||||
<select id="addons-ms-version-select" class="cluster-addons-version-select">
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
<span class="cluster-addon-card__field-label" id="addons-ms-version-label">Версия чарта</span>
|
||||
<div class="cluster-addon-version-row" role="group" aria-labelledby="addons-ms-version-label">
|
||||
<select
|
||||
id="addons-ms-version-select"
|
||||
class="cluster-addons-version-select"
|
||||
aria-labelledby="addons-ms-version-label"
|
||||
>
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
<button type="button" class="btn-secondary btn-addon-load-values" data-addon="metrics-server">Загрузить values</button>
|
||||
</div>
|
||||
</div>
|
||||
<details class="cluster-addon-card__values-details">
|
||||
<summary class="cluster-addon-card__values-summary">Values чарта (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
<code>helm show values</code> плюс аргументы для kind (<code>--kubelet-insecure-tls</code> и т.д.). Пустое поле — автосборка на сервере.
|
||||
</p>
|
||||
<label for="addons-values-yaml-metrics-server" class="sr-only">Полные YAML values для metrics-server</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-metrics-server"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта metrics-server"
|
||||
></textarea>
|
||||
</details>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--equal">
|
||||
<div id="addons-values-panel-metrics-server" class="cluster-addon-card__values-panel hidden" hidden aria-hidden="true">
|
||||
<details class="cluster-addon-card__values-details" open>
|
||||
<summary class="cluster-addon-card__values-summary">Values чарта (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
<code>helm show values</code> плюс аргументы для kind (<code>--kubelet-insecure-tls</code> и т.д.). Пустое поле при установке — автосборка на сервере.
|
||||
</p>
|
||||
<label for="addons-values-yaml-metrics-server" class="sr-only">Полные YAML values для metrics-server</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-metrics-server"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="12"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта metrics-server"
|
||||
></textarea>
|
||||
</details>
|
||||
</div>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--inline">
|
||||
<button type="button" class="btn-addon-install" data-endpoint="metrics-server">Установить</button>
|
||||
<button type="button" class="btn-secondary btn-addon-remove hidden" data-endpoint="metrics-server" hidden>Удалить</button>
|
||||
</div>
|
||||
@@ -195,41 +218,49 @@
|
||||
<option value="">Последняя (из репозитория)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="create-form-field cluster-addon-istio-load-field">
|
||||
<span class="cluster-addon-card__field-label" id="addons-istio-values-label">Предпросмотр YAML</span>
|
||||
<div class="cluster-addon-version-row" role="group" aria-labelledby="addons-istio-values-label">
|
||||
<button type="button" class="btn-secondary btn-addon-load-values" data-addon="istio-kiali">Загрузить values</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<details class="cluster-addon-card__values-details">
|
||||
<summary class="cluster-addon-card__values-summary">Values чартов Istio и Kiali (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
Три блока из <code>helm show values</code> для выбранных версий. Логин и пароль Kiali вносятся в секрет Kubernetes отдельно; для kiali-server дополнительно задаётся <code>auth.strategy=login</code> через <code>--set</code>.
|
||||
</p>
|
||||
<p class="cluster-addon-card__values-subhead">kiali-server</p>
|
||||
<label for="addons-values-yaml-istio-kiali" class="sr-only">YAML values kiali-server</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-istio-kiali"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта kiali-server"
|
||||
></textarea>
|
||||
<p class="cluster-addon-card__values-subhead">istio/base</p>
|
||||
<label for="addons-values-yaml-istio-base" class="sr-only">YAML values istio-base</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-istio-base"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта istio/base"
|
||||
></textarea>
|
||||
<p class="cluster-addon-card__values-subhead">istio/istiod</p>
|
||||
<label for="addons-values-yaml-istio-istiod" class="sr-only">YAML values istiod</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-istio-istiod"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта istio/istiod"
|
||||
></textarea>
|
||||
</details>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--equal">
|
||||
<div id="addons-values-panel-istio-kiali" class="cluster-addon-card__values-panel hidden" hidden aria-hidden="true">
|
||||
<details class="cluster-addon-card__values-details" open>
|
||||
<summary class="cluster-addon-card__values-summary">Values чартов Istio и Kiali (YAML)</summary>
|
||||
<p class="muted cluster-addon-card__values-hint">
|
||||
Три блока из <code>helm show values</code> для выбранных версий Istio и Kiali. Логин и пароль Kiali задаются в секрете при установке; здесь — только шаблон values чартов.
|
||||
</p>
|
||||
<p class="cluster-addon-card__values-subhead">kiali-server</p>
|
||||
<label for="addons-values-yaml-istio-kiali" class="sr-only">YAML values kiali-server</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-istio-kiali"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта kiali-server"
|
||||
></textarea>
|
||||
<p class="cluster-addon-card__values-subhead">istio/base</p>
|
||||
<label for="addons-values-yaml-istio-base" class="sr-only">YAML values istio-base</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-istio-base"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта istio/base"
|
||||
></textarea>
|
||||
<p class="cluster-addon-card__values-subhead">istio/istiod</p>
|
||||
<label for="addons-values-yaml-istio-istiod" class="sr-only">YAML values istiod</label>
|
||||
<textarea
|
||||
id="addons-values-yaml-istio-istiod"
|
||||
class="cluster-addon-card__values-textarea mono"
|
||||
rows="10"
|
||||
spellcheck="false"
|
||||
aria-label="Полные YAML values чарта istio/istiod"
|
||||
></textarea>
|
||||
</details>
|
||||
</div>
|
||||
<div class="cluster-addon-card__actions row cluster-addon-card__actions--inline">
|
||||
<button type="button" class="btn-addon-install" data-endpoint="istio-kiali">Установить</button>
|
||||
<button type="button" class="btn-secondary btn-addon-remove hidden" data-endpoint="istio-kiali" hidden>Удалить</button>
|
||||
</div>
|
||||
@@ -237,40 +268,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Журнал операции Helm: те же блоки, что на странице создания кластера (прогресс + job-log-panel). Без полноэкранного спиннера. #}
|
||||
<section
|
||||
id="addons-helm-progress-wrap"
|
||||
class="card create-progress-wrap cluster-addons-helm-progress hidden"
|
||||
aria-hidden="true"
|
||||
aria-labelledby="addons-helm-progress-title"
|
||||
>
|
||||
<h2 class="create-cluster-card__title" id="addons-helm-progress-title">Ход операции Helm</h2>
|
||||
<p class="create-progress-hint muted" id="addons-helm-progress-hint">
|
||||
Выполняется команда в контейнере веб-приложения; полный вывод появится по завершении.
|
||||
</p>
|
||||
<div
|
||||
class="progress-track"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="0"
|
||||
aria-labelledby="addons-helm-progress-label"
|
||||
id="addons-helm-progress-track"
|
||||
{# Ход Helm: секция в dock; при установке/удалении переносится внутрь карточки аддона (cluster_addons.js). #}
|
||||
<div id="addons-helm-progress-dock" class="cluster-addons-helm-dock" aria-hidden="true">
|
||||
<section
|
||||
id="addons-helm-progress-wrap"
|
||||
class="card create-progress-wrap cluster-addons-helm-progress hidden"
|
||||
aria-hidden="true"
|
||||
aria-labelledby="addons-helm-progress-title"
|
||||
>
|
||||
<div class="progress-bar" id="addons-helm-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="progress-label" id="addons-helm-progress-label">—</p>
|
||||
<div class="job-log-panel-wrap">
|
||||
<label for="addons-helm-log-panel" class="job-log-label muted">Журнал установки / удаления</label>
|
||||
<pre
|
||||
id="addons-helm-log-panel"
|
||||
class="mono job-log-panel"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions"
|
||||
></pre>
|
||||
</div>
|
||||
</section>
|
||||
<h2 class="create-cluster-card__title" id="addons-helm-progress-title">Ход операции Helm</h2>
|
||||
<p class="create-progress-hint muted" id="addons-helm-progress-hint">
|
||||
Выполняется команда в контейнере веб-приложения; полный вывод появится по завершении.
|
||||
</p>
|
||||
<div
|
||||
class="progress-track"
|
||||
role="progressbar"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="0"
|
||||
aria-labelledby="addons-helm-progress-label"
|
||||
id="addons-helm-progress-track"
|
||||
>
|
||||
<div class="progress-bar" id="addons-helm-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="progress-label" id="addons-helm-progress-label">—</p>
|
||||
<div class="job-log-panel-wrap">
|
||||
<label for="addons-helm-log-panel" class="job-log-label muted">Журнал установки / удаления</label>
|
||||
<pre
|
||||
id="addons-helm-log-panel"
|
||||
class="mono job-log-panel"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions"
|
||||
></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{# Если внешний cluster_addons.js не выполнился (404, ошибка синтаксиса), оверлей снимается через 50 с. #}
|
||||
<script>
|
||||
(function () {
|
||||
var ms = 50000;
|
||||
setTimeout(function () {
|
||||
var el = document.getElementById("addons-page-loading-overlay");
|
||||
if (!el || el.classList.contains("hidden")) return;
|
||||
el.classList.add("hidden");
|
||||
el.setAttribute("aria-busy", "false");
|
||||
el.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("cluster-addons-page-loading");
|
||||
}, ms);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% include "partials/dashboard_modals.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,6 +65,31 @@
|
||||
<div id="cluster-page-aggregate-donuts" class="aggregate-donuts" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
{# Карточка «Установленные аддоны» — после сводки по узлам (показ/содержимое — dashboard.js, GET …/addons/status). #}
|
||||
<section
|
||||
id="cluster-detail-addons-section"
|
||||
class="card cluster-detail-addons-card hidden"
|
||||
hidden
|
||||
aria-hidden="true"
|
||||
aria-labelledby="cluster-detail-addons-heading"
|
||||
>
|
||||
<header class="cluster-detail-addons-card__header">
|
||||
<div class="cluster-detail-addons-card__head-row">
|
||||
<span class="cluster-detail-addons-card__helm-badge" aria-hidden="true">Helm</span>
|
||||
<h2 id="cluster-detail-addons-heading" class="cluster-detail-section-title cluster-detail-addons-card__title">
|
||||
Установленные аддоны
|
||||
</h2>
|
||||
</div>
|
||||
<p class="cluster-detail-addons-card__lead muted">
|
||||
Релизы в кластере по данным <code>helm list</code>. Нажмите на карточку, чтобы перейти к установке, обновлению и values.
|
||||
</p>
|
||||
</header>
|
||||
<div id="cluster-detail-addons-strip" class="cluster-detail-addons-grid" role="list"></div>
|
||||
<footer class="cluster-detail-addons-card__footer">
|
||||
<a href="/cluster-addons" class="cluster-detail-addons-card__cta">Все аддоны и настройки →</a>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<section class="card cluster-detail-nodes-card" aria-labelledby="cluster-nodes-heading">
|
||||
<h2 id="cluster-nodes-heading" class="cluster-detail-section-title">Ресурсы узлов</h2>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user