/** * Страница Helm-аддонов: GET /api/v1/clusters, статус и POST/DELETE …/addons/*. * * Автор: Сергей Антропов * Сайт: https://devops.org.ru */ (function () { "use strict"; const body = document.body; if ((body.getAttribute("data-dashboard-mode") || "") !== "addons") return; const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, ""); /** * @param {string} path * @param {RequestInit} [opts] */ async function api(path, opts) { const url = path.startsWith("http") ? path : API + path; const r = await fetch(url, opts); const text = await r.text(); var data; try { data = text ? JSON.parse(text) : null; } catch (e) { data = { raw: text }; } if (!r.ok) { const msg = typeof data.detail === "string" ? data.detail : Array.isArray(data.detail) ? data.detail.map(function (x) { return x.msg || JSON.stringify(x); }).join("; ") : text || r.statusText; const err = new Error(msg); err.status = r.status; throw err; } return data; } const sel = document.getElementById("addons-cluster-select"); const statusBar = document.getElementById("addons-status-bar"); const logPre = document.getElementById("addons-log-pre"); const overlay = document.getElementById("addons-busy-overlay"); const busyLabel = document.getElementById("addons-busy-label"); function getClusterName() { if (!sel || !sel.value) return ""; return String(sel.value).trim(); } function setBusy(show, label) { if (!overlay) return; overlay.classList.toggle("hidden", !show); overlay.setAttribute("aria-busy", show ? "true" : "false"); overlay.setAttribute("aria-hidden", show ? "false" : "true"); document.body.classList.toggle("cluster-addons-busy", !!show); if (busyLabel && label) busyLabel.textContent = label; } /** Скрыть полноэкранный спиннер первой загрузки страницы (как на панели и странице кластера). */ function hideAddonsPageLoadingOverlay() { const el = document.getElementById("addons-page-loading-overlay"); if (!el) return; el.classList.add("hidden"); el.setAttribute("aria-busy", "false"); el.setAttribute("aria-hidden", "true"); document.body.classList.remove("cluster-addons-page-loading"); } function appendLog(line) { if (!logPre) return; logPre.textContent = (logPre.textContent ? logPre.textContent + "\n" : "") + line; logPre.scrollTop = logPre.scrollHeight; } function formatStatus(st) { if (!st) return ""; const parts = [ "ingress-nginx: " + (st.ingress_nginx ? "да" : "нет"), "prometheus-stack: " + (st.kube_prometheus_stack ? "да" : "нет"), "metrics-server: " + (st.metrics_server ? "да" : "нет"), "Istio+Kiali: " + (st.istio_mesh_ready ? "да" : "нет"), ]; return parts.join(" · "); } /** * Заполнить выпадающий список версиями чарта; первая опция — «последняя». * @param {HTMLSelectElement | null} selectEl * @param {string[] | undefined} versions */ function fillVersionSelect(selectEl, versions) { if (!selectEl) return; const cur = selectEl.value; selectEl.innerHTML = ""; const o0 = document.createElement("option"); o0.value = ""; o0.textContent = "Последняя (из репозитория)"; selectEl.appendChild(o0); (versions || []).forEach(function (v) { if (!v) return; const o = document.createElement("option"); o.value = v; o.textContent = v; selectEl.appendChild(o); }); if (cur) { for (let i = 0; i < selectEl.options.length; i++) { if (selectEl.options[i].value === cur) { selectEl.value = cur; break; } } } } async function loadChartVersions() { try { const d = await api("/helm/chart-versions"); fillVersionSelect(document.getElementById("addons-ing-version-select"), d.ingress_nginx); fillVersionSelect(document.getElementById("addons-prom-version-select"), d.kube_prometheus_stack); 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); } catch (e) { appendLog("Список версий чартов недоступен: " + e.message); } } async function loadClusters() { if (!sel) return; try { const rows = await api("/clusters"); sel.innerHTML = ""; const withKube = rows.filter(function (c) { return c.has_local_kubeconfig; }); if (!withKube.length) { const o = document.createElement("option"); o.value = ""; o.textContent = "Нет кластеров с kubeconfig на диске"; sel.appendChild(o); return; } const o0 = document.createElement("option"); o0.value = ""; o0.textContent = "— выберите кластер —"; sel.appendChild(o0); withKube.forEach(function (c) { const o = document.createElement("option"); o.value = c.name; o.textContent = c.name + (c.kind_nodes_running ? "" : " (узлы остановлены)"); sel.appendChild(o); }); } catch (e) { sel.innerHTML = ""; const o = document.createElement("option"); o.value = ""; o.textContent = "Ошибка загрузки списка"; sel.appendChild(o); appendLog("Ошибка GET /clusters: " + e.message); } } async function refreshStatus() { const n = getClusterName(); if (!statusBar) return; if (!n) { statusBar.textContent = "Выберите кластер для просмотра статуса аддонов."; return; } try { const st = await api("/clusters/" + encodeURIComponent(n) + "/addons/status"); statusBar.textContent = formatStatus(st); } catch (e) { statusBar.textContent = "Статус недоступен: " + e.message; } } async function runInstall(endpoint, payload) { const n = getClusterName(); if (!n) { alert("Выберите кластер."); return; } setBusy(true, "Установка (Helm), подождите…"); if (logPre) logPre.textContent = ""; appendLog("POST …/addons/" + endpoint + " для «" + n + "»"); try { const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: payload !== undefined ? JSON.stringify(payload) : "{}", }); appendLog(r.message || "OK"); if (r.log) appendLog(r.log); await refreshStatus(); } catch (e) { appendLog("Ошибка: " + e.message); } finally { setBusy(false); } } async function runDelete(endpoint) { const n = getClusterName(); if (!n) { alert("Выберите кластер."); return; } if (!window.confirm("Удалить аддон «" + endpoint + "» в кластере «" + n + "»?")) return; setBusy(true, "Удаление релиза, подождите…"); if (logPre) logPre.textContent = ""; appendLog("DELETE …/addons/" + endpoint + " для «" + n + "»"); try { const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, { method: "DELETE", headers: { Accept: "application/json" }, }); appendLog(r.message || "OK"); if (r.log) appendLog(r.log); await refreshStatus(); } catch (e) { appendLog("Ошибка: " + e.message); } finally { setBusy(false); } } document.querySelectorAll(".btn-addon-install").forEach(function (btn) { btn.addEventListener("click", function () { const ep = btn.getAttribute("data-endpoint") || ""; if (ep === "ingress-nginx") { const sel = document.getElementById("addons-ing-version-select"); const v = sel && sel.value.trim(); runInstall(ep, v ? { chart_version: v } : {}); return; } if (ep === "kube-prometheus-stack") { const u = document.getElementById("addons-grafana-user"); const p = document.getElementById("addons-grafana-pass"); const pv = document.getElementById("addons-prom-version-select"); const user = u ? u.value.trim() : "admin"; const pw = p ? p.value : ""; if (pw.length < 8) { alert("Задайте пароль Grafana не короче 8 символов."); return; } const payload = { grafana_admin_user: user, grafana_admin_password: pw }; if (pv && pv.value.trim()) payload.chart_version = pv.value.trim(); runInstall(ep, payload); return; } if (ep === "metrics-server") { const ms = document.getElementById("addons-ms-version-select"); const payload = {}; if (ms && ms.value.trim()) payload.chart_version = ms.value.trim(); runInstall(ep, Object.keys(payload).length ? payload : {}); return; } if (ep === "istio-kiali") { const u = document.getElementById("addons-kiali-user"); const p = document.getElementById("addons-kiali-pass"); const isel = document.getElementById("addons-istio-version-select"); const ksel = document.getElementById("addons-kiali-version-select"); const user = u ? u.value.trim() : "kiali-admin"; const pw = p ? p.value : ""; if (pw.length < 8) { alert("Задайте пароль Kiali не короче 8 символов."); return; } const payload = { kiali_username: user, kiali_password: pw }; if (isel && isel.value.trim()) payload.istio_chart_version = isel.value.trim(); if (ksel && ksel.value.trim()) payload.kiali_chart_version = ksel.value.trim(); runInstall(ep, payload); } }); }); document.querySelectorAll(".btn-addon-remove").forEach(function (btn) { btn.addEventListener("click", function () { const ep = btn.getAttribute("data-endpoint") || ""; if (ep) runDelete(ep); }); }); const refreshBtn = document.getElementById("addons-refresh-status"); if (refreshBtn) refreshBtn.addEventListener("click", function () { refreshStatus(); }); if (sel) { sel.addEventListener("change", function () { refreshStatus(); }); } loadClusters() .then(function () { return refreshStatus(); }) .then(function () { return loadChartVersions(); }) .finally(function () { hideAddonsPageLoadingOverlay(); }); })();