Files
KindClustersDashboard/app/static/js/cluster_addons.js
Sergey Antropoff 4b703801e1 Kiali anonymous, журнал Helm, kubeconfig для контейнеров, UI аддонов
- Kiali: убран login, anonymous по умолчанию; удалены поля логина/пароля из UI и API
- Журнал Helm: install/upgrade/delete, message и колонка в journal.js
- Аддоны: values свёрнуты при подгрузке для установленных
- GET …/kubeconfig/docker: host.docker.internal:порт + tls-server-name; кнопка в UI
- apply_apiserver_endpoint_to_kubeconfig_file; KIND_K8S_APISERVER_GATEWAY_HOST в compose/env.example
- README и api_routes.md обновлены
2026-04-04 18:54:10 +03:00

1110 lines
43 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Страница Helm-аддонов: GET /api/v1/clusters, статус и POST/DELETE …/addons/*.
*
* Автор: Сергей Антропов
* Сайт: https://devops.org.ru
*/
(function () {
"use strict";
const body = document.body;
/* 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(/\/$/, "");
/** data-endpoint кнопки → поле ответа GET …/addons/status */
var ENDPOINT_TO_STATUS_KEY = {
"ingress-nginx": "ingress_nginx",
"kube-prometheus-stack": "kube_prometheus_stack",
"metrics-server": "metrics_server",
"istio-kiali": "istio_mesh_ready",
};
var ADDON_HUMAN_LABEL = {
"ingress-nginx": "ingress-nginx",
"kube-prometheus-stack": "kube-prometheus-stack",
"metrics-server": "metrics-server",
"istio-kiali": "Istio + Kiali",
};
/** Последний успешный снимок статуса (для подписи «Установить» / «Обновить»). */
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;
function isConfirmModalOpen() {
const el = document.getElementById("confirm-modal-overlay");
return !!(el && !el.classList.contains("hidden"));
}
function closeConfirmModal(confirmed) {
const ov = document.getElementById("confirm-modal-overlay");
const cancelBtn = document.getElementById("confirm-modal-cancel");
if (ov) ov.classList.add("hidden");
document.body.classList.remove("modal-open");
if (cancelBtn) cancelBtn.classList.remove("hidden");
if (confirmModalResolver) {
var fn = confirmModalResolver;
confirmModalResolver = null;
var out = confirmModalIsAlert ? true : !!confirmed;
confirmModalIsAlert = false;
fn(out);
} else {
confirmModalIsAlert = false;
}
}
/**
* Модальное подтверждение (разметка из partials/dashboard_modals.html).
* @param {{ title?: string, message: string, confirmLabel?: string, danger?: boolean, alert?: boolean }} opts
* @returns {Promise<boolean>}
*/
function openConfirmModal(opts) {
return new Promise(function (resolve) {
const ov = document.getElementById("confirm-modal-overlay");
const titleEl = document.getElementById("confirm-modal-title");
const msgEl = document.getElementById("confirm-modal-message");
const okBtn = document.getElementById("confirm-modal-ok");
const cancelBtn = document.getElementById("confirm-modal-cancel");
if (!ov || !titleEl || !msgEl || !okBtn) {
resolve(false);
return;
}
if (confirmModalResolver) {
const prevFn = confirmModalResolver;
confirmModalResolver = null;
confirmModalIsAlert = false;
prevFn(false);
ov.classList.add("hidden");
document.body.classList.remove("modal-open");
if (cancelBtn) cancelBtn.classList.remove("hidden");
}
confirmModalResolver = resolve;
confirmModalIsAlert = !!opts.alert;
titleEl.textContent = opts.title || "Подтвердите действие";
msgEl.textContent = opts.message || "";
okBtn.textContent = opts.confirmLabel || (confirmModalIsAlert ? "Понятно" : "Подтвердить");
okBtn.className = opts.danger ? "btn-danger" : "";
if (cancelBtn) {
cancelBtn.classList.toggle("hidden", confirmModalIsAlert);
}
ov.classList.remove("hidden");
document.body.classList.add("modal-open");
okBtn.focus();
});
}
function bindConfirmModalControls() {
const confirmOv = document.getElementById("confirm-modal-overlay");
const confirmCancel = document.getElementById("confirm-modal-cancel");
const confirmOk = document.getElementById("confirm-modal-ok");
if (confirmCancel) {
confirmCancel.addEventListener("click", function () {
closeConfirmModal(false);
});
}
if (confirmOk) {
confirmOk.addEventListener("click", function () {
closeConfirmModal(true);
});
}
if (confirmOv) {
confirmOv.addEventListener("click", function (ev) {
if (ev.target === confirmOv) closeConfirmModal(confirmModalIsAlert ? true : false);
});
}
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape" || !isConfirmModalOpen()) return;
closeConfirmModal(false);
});
}
/**
* @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;
}
/**
* Обёртка над 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");
const helmWrap = document.getElementById("addons-helm-progress-wrap");
const helmHint = document.getElementById("addons-helm-progress-hint");
const helmBar = document.getElementById("addons-helm-progress-bar");
const helmTrack = document.getElementById("addons-helm-progress-track");
const helmLabel = document.getElementById("addons-helm-progress-label");
const helmLogPanel = document.getElementById("addons-helm-log-panel");
const bootstrapMsg = document.getElementById("addons-bootstrap-msg");
function getClusterName() {
if (!sel || !sel.value) return "";
return String(sel.value).trim();
}
/** Карточки аддонов — только при выбранном кластере в селекте. */
function updateAddonsCardsVisibility() {
if (!addonsCardsWrap) return;
var n = getClusterName();
addonsCardsWrap.classList.toggle("hidden", !n);
addonsCardsWrap.setAttribute("aria-hidden", n ? "false" : "true");
}
/**
* Предупреждения загрузки списка кластеров / версий чартов (не путать с журналом Helm).
* @param {string} text пустая строка — скрыть блок
*/
function setBootstrapMsg(text) {
if (!bootstrapMsg) return;
var s = (text || "").trim();
if (!s) {
bootstrapMsg.classList.add("hidden");
bootstrapMsg.textContent = "";
return;
}
bootstrapMsg.classList.remove("hidden");
bootstrapMsg.textContent = s;
}
/** Известные 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) {
var p = Math.max(0, Math.min(100, Number(pct) || 0));
if (helmBar) helmBar.style.width = p + "%";
if (helmTrack) helmTrack.setAttribute("aria-valuenow", String(p));
if (helmLabel) helmLabel.textContent = stage || "—";
}
/** Полный текст журнала в панели с теми же классами, что «Журнал задания» при создании кластера. */
function setHelmLogText(text) {
if (!helmLogPanel) return;
helmLogPanel.textContent = text || "";
helmLogPanel.scrollTop = helmLogPanel.scrollHeight;
}
/**
* Блокировка форм на время синхронного запроса Helm (без полноэкранного спиннера).
* @param {boolean} disabled
*/
function setAddonFormsDisabled(disabled) {
document
.querySelectorAll(
"#addons-cards-wrap .cluster-addon-card button, #addons-cards-wrap .cluster-addon-card input, #addons-cards-wrap .cluster-addon-card select, #addons-cards-wrap .cluster-addon-card textarea",
)
.forEach(function (el) {
el.disabled = !!disabled;
});
if (sel) sel.disabled = !!disabled;
var refBtn = document.getElementById("addons-refresh-status");
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");
el.setAttribute("aria-busy", "false");
el.setAttribute("aria-hidden", "true");
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
*/
function renderAddonStatusMessage(text) {
if (!statusBar) return;
statusBar.className = "cluster-addons-status-bar cluster-addons-status-bar--message muted";
statusBar.replaceChildren();
const p = document.createElement("p");
p.className = "cluster-addons-status-msg";
p.textContent = text;
statusBar.appendChild(p);
}
/** Максимальная длина поля values_yaml на сервере (см. Pydantic max_length). */
var VALUES_YAML_MAX_LEN = 131072;
/**
* Полоса под селектором кластера: только сообщения (нет кластера / ошибка), без сетки статусов.
*/
function clearAddonToolbarStatusBar() {
if (!statusBar) return;
statusBar.className = "cluster-addons-status-bar";
statusBar.replaceChildren();
}
/**
* Бейджи «Установлен» / «Не установлен» в правом верхнем углу карточки аддона.
* @param {object | null | undefined} st ответ GET …/addons/status или null при сбросе
*/
function updateAddonCardBadges(st) {
document.querySelectorAll(".cluster-addon-card[data-addon]").forEach(function (card) {
var da = card.getAttribute("data-addon") || "";
var key = ENDPOINT_TO_STATUS_KEY[da];
var on = !!(st && key && st[key]);
var badge = card.querySelector(".cluster-addon-card__badge");
if (!badge) return;
badge.textContent = on ? "Установлен" : "Не установлен";
badge.className =
"cluster-addon-card__badge" + (on ? " cluster-addon-card__badge--on" : " cluster-addon-card__badge--off");
var hum = ADDON_HUMAN_LABEL[da] || da;
badge.setAttribute("aria-label", hum + ": " + (on ? "установлен" : "не установлен"));
});
}
/**
* Прикрепить к телу POST YAML из textarea (полные values чарта). Для istio-kiali — три поля.
* Пустой textarea → поле не отправляется (сервер соберёт defaults или пропустит -f).
* @param {string} ep data-endpoint
* @param {Record<string, unknown>} payload
* @returns {boolean}
*/
function attachAddonYamlToPayload(ep, payload) {
if (ep === "istio-kiali") {
var triple = [
{ id: "addons-values-yaml-istio-kiali", key: "values_yaml" },
{ id: "addons-values-yaml-istio-base", key: "istio_base_values_yaml" },
{ id: "addons-values-yaml-istio-istiod", key: "istio_istiod_values_yaml" },
];
for (var i = 0; i < triple.length; i++) {
var ta = document.getElementById(triple[i].id);
if (!ta) continue;
var s = String(ta.value || "").trim();
if (!s) continue;
if (s.length > VALUES_YAML_MAX_LEN) {
alert("Текст YAML слишком длинный (максимум " + VALUES_YAML_MAX_LEN + " символов).");
return false;
}
payload[triple[i].key] = s;
}
return true;
}
var id = "addons-values-yaml-" + ep;
var ta2 = document.getElementById(id);
if (!ta2) return true;
var s2 = String(ta2.value || "").trim();
if (!s2) return true;
if (s2.length > VALUES_YAML_MAX_LEN) {
alert("Текст YAML слишком длинный (максимум " + VALUES_YAML_MAX_LEN + " символов).");
return false;
}
payload.values_yaml = s2;
return true;
}
/** 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",
};
/**
* Скрыть блок Values и очистить textarea для одного аддона (смена версии / сброс).
* @param {string} addon ключ data-addon / endpoint
*/
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 = "";
}
/**
* Показать панель YAML values. Для установленного аддона при подгрузке values — блок <details> свёрнут.
* @param {string} addon
* @param {{ collapseDetails?: boolean } | undefined} opts
*/
function showAddonValuesPanel(addon, opts) {
opts = opts || {};
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");
var det = panel.querySelector("details.cluster-addon-card__values-details");
if (det) {
det.open = opts.collapseDetails ? false : true;
}
}
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, { collapseDetails: addonEndpointInstalled(addon) });
} catch (e) {
setBootstrapMsg("Не удалось загрузить values (helm show values): " + e.message);
} finally {
if (triggerBtn) {
triggerBtn.disabled = false;
triggerBtn.textContent = prevText || "Загрузить values";
}
}
}
function addonEndpointInstalled(endpoint) {
var key = ENDPOINT_TO_STATUS_KEY[endpoint];
return !!(lastAddonsStatus && key && lastAddonsStatus[key]);
}
/**
* Текст «Установить»/«Обновить», видимость «Удалить» только при установленном аддоне; равная ширина — в CSS.
*/
function updateAddonActionButtons() {
document.querySelectorAll(".cluster-addon-card__actions").forEach(function (row) {
var installBtn = row.querySelector(".btn-addon-install");
var removeBtn = row.querySelector(".btn-addon-remove");
if (!installBtn) return;
var ep = installBtn.getAttribute("data-endpoint") || "";
var on = addonEndpointInstalled(ep);
installBtn.textContent = on ? "Обновить" : "Установить";
var hum = ADDON_HUMAN_LABEL[ep] || ep;
installBtn.setAttribute("aria-label", (on ? "Обновить " : "Установить ") + hum);
if (removeBtn) {
if (on) {
removeBtn.classList.remove("hidden");
removeBtn.removeAttribute("hidden");
removeBtn.setAttribute("aria-label", "Удалить " + hum);
} else {
removeBtn.classList.add("hidden");
removeBtn.setAttribute("hidden", "hidden");
removeBtn.removeAttribute("aria-label");
}
}
});
}
/**
* Заполнить выпадающий список версиями чарта; первая опция — «последняя».
* @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;
}
}
}
}
/**
* В селекте отметить версию из кластера и подписать опцию; при отсутствии в списке репозитория — добавить опцию.
*/
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", { collapseDetails: true });
}
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", { collapseDetails: true });
}
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", { collapseDetails: true });
}
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", { collapseDetails: true });
}
}
}
/**
* Версии в селектах + 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("");
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);
resetAllAddonValuesPanels();
chartVersionsReady = true;
var n = getClusterName();
if (n) await maybeSyncInstalledAddonsFromCluster(n);
} catch (e) {
setBootstrapMsg("Список версий чартов недоступен: " + e.message);
}
}
async function loadClusters() {
if (!sel) return;
try {
const rows = await api("/clusters");
setBootstrapMsg("");
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);
setBootstrapMsg("Ошибка GET /clusters: " + e.message);
}
}
async function refreshStatus() {
const n = getClusterName();
if (!statusBar) {
updateAddonsCardsVisibility();
return;
}
if (!n) {
lastAddonsStatus = null;
clearAddonToolbarStatusBar();
updateAddonCardBadges(null);
updateAddonActionButtons();
updateAddonsCardsVisibility();
resetAllAddonValuesPanels();
return;
}
try {
const st = await api("/clusters/" + encodeURIComponent(n) + "/addons/status");
lastAddonsStatus = st;
clearAddonToolbarStatusBar();
updateAddonCardBadges(st);
updateAddonActionButtons();
} catch (e) {
lastAddonsStatus = null;
renderAddonStatusMessage("Статус недоступен: " + e.message);
updateAddonCardBadges(null);
updateAddonActionButtons();
}
updateAddonsCardsVisibility();
if (chartVersionsReady && getClusterName()) {
try {
await maybeSyncInstalledAddonsFromCluster(getClusterName());
} catch (e2) {
/* уже отражено в maybeSync */
}
}
}
async function runInstall(endpoint, payload) {
const n = getClusterName();
if (!n) {
alert("Выберите кластер.");
return;
}
const isUpdate = addonEndpointInstalled(endpoint);
const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
revealHelmProgressSection(endpoint);
setAddonFormsDisabled(true);
if (helmHint) {
helmHint.textContent = isUpdate
? "Обновление релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона."
: "Установка релиза «" + hum + "» через helm в контейнере; журнал под этой карточкой аддона.";
}
updateHelmProgressBar(8, isUpdate ? "Обновление Helm…" : "Установка Helm…");
setHelmLogText(
"Кластер: «" +
n +
"»\n" +
(isUpdate ? "POST (обновление)" : "POST (установка)") +
" /api/v1/clusters/…/addons/" +
endpoint +
"\n\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) : "{}",
});
var okLines = [
"Кластер: «" + n + "»",
(isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint,
"",
r.message || "OK",
];
if (r.log) {
okLines.push("");
okLines.push(String(r.log));
}
setHelmLogText(okLines.join("\n"));
updateHelmProgressBar(100, "Готово");
if (helmHint) {
helmHint.textContent =
"Операция завершена. Полный журнал добавлен в файл helm_addon_log.json в каталоге кластера (история записей).";
}
await refreshStatus();
} catch (e) {
var failLines = [
"Кластер: «" + n + "»",
(isUpdate ? "POST (обновление)" : "POST (установка)") + " /addons/" + endpoint,
"",
"Ошибка: " + e.message,
];
setHelmLogText(failLines.join("\n"));
updateHelmProgressBar(100, "Ошибка");
if (helmHint) {
helmHint.textContent =
"Запрос не удался. При ответе сервера об ошибке Helm в журнале кластера могла появиться запись со статусом failed.";
}
} finally {
setAddonFormsDisabled(false);
}
}
async function runDelete(endpoint) {
const n = getClusterName();
if (!n) {
alert("Выберите кластер.");
return;
}
const hum = ADDON_HUMAN_LABEL[endpoint] || endpoint;
const ok = await openConfirmModal({
title: "Удалить аддон?",
message:
"Кластер «" +
n +
"»: снять релиз «" +
hum +
с кластера (helm uninstall). Действие необратимо для установленных компонентов.",
confirmLabel: "Удалить",
danger: true,
});
if (!ok) return;
revealHelmProgressSection(endpoint);
setAddonFormsDisabled(true);
if (helmHint) {
helmHint.textContent =
"Удаление релиза «" + hum + "» (helm uninstall); журнал под этой карточкой аддона.";
}
updateHelmProgressBar(8, "Удаление Helm…");
setHelmLogText(
"Кластер: «" + n + "»\nDELETE /api/v1/clusters/…/addons/" + endpoint + "\n\nОжидаем завершение команды…",
);
try {
const r = await api("/clusters/" + encodeURIComponent(n) + "/addons/" + endpoint, {
method: "DELETE",
headers: { Accept: "application/json" },
});
var okDel = [
"Кластер: «" + n + "»",
"DELETE /addons/" + endpoint,
"",
r.message || "OK",
];
if (r.log) {
okDel.push("");
okDel.push(String(r.log));
}
setHelmLogText(okDel.join("\n"));
updateHelmProgressBar(100, "Готово");
if (helmHint) {
helmHint.textContent =
"Операция завершена. Запись добавлена в helm_addon_log.json в каталоге кластера (история).";
}
await refreshStatus();
} catch (e) {
var failDel = [
"Кластер: «" + n + "»",
"DELETE /addons/" + endpoint,
"",
"Ошибка: " + e.message,
];
setHelmLogText(failDel.join("\n"));
updateHelmProgressBar(100, "Ошибка");
if (helmHint) {
helmHint.textContent =
"Запрос не удался. При ошибке Helm на сервере в журнале кластера могла появиться запись со статусом failed.";
}
} finally {
setAddonFormsDisabled(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();
const payload = {};
if (v) payload.chart_version = v;
if (!attachAddonYamlToPayload(ep, payload)) return;
runInstall(ep, payload);
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();
if (!attachAddonYamlToPayload(ep, payload)) return;
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();
if (!attachAddonYamlToPayload(ep, payload)) return;
runInstall(ep, payload);
return;
}
if (ep === "istio-kiali") {
const isel = document.getElementById("addons-istio-version-select");
const ksel = document.getElementById("addons-kiali-version-select");
const payload = {};
if (isel && isel.value.trim()) payload.istio_chart_version = isel.value.trim();
if (ksel && ksel.value.trim()) payload.kiali_chart_version = ksel.value.trim();
if (!attachAddonYamlToPayload(ep, payload)) return;
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);
});
});
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(); });
if (sel) {
sel.addEventListener("change", function () { refreshStatus(); });
}
bindConfirmModalControls();
/** Смена версии — сбрасываем предпросмотр 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 () {
hideAddonValuesPanel(versionSelectToAddon[id]);
});
}
});
/* Логин/пароль Grafana: если правили после загрузки YAML — скрываем блок, чтобы не путать с устаревшим предпросмотром. */
var grafanaUserEl = document.getElementById("addons-grafana-user");
var grafanaPassEl = document.getElementById("addons-grafana-pass");
function onGrafanaCredChange() {
hideAddonValuesPanel("kube-prometheus-stack");
}
if (grafanaUserEl) grafanaUserEl.addEventListener("input", onGrafanaCredChange);
if (grafanaPassEl) grafanaPassEl.addEventListener("input", onGrafanaCredChange);
/**
* Оверлей убираем после списка кластеров и статуса — без ожидания 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);
}
})();
})();