Панель: редактирование конфига кластера, reapply при старте, UI и таблица
- PUT/GET конфигурации кластера, страница редактирования и модалка после сохранения - После смены kind-config: флаг в meta и start_cluster_reapply (kind delete + create) - Старт/стоп: полноэкранный спиннер до завершения job; модалки и документация API - Таблица кластеров: колонка Имя 40% при table-layout fixed; чекбоксы без width 100% - Карточки ресурсов узлов на странице кластера: до 3 в ряд; прочие правки стилей и dashboard.js
This commit is contained in:
@@ -0,0 +1,811 @@
|
||||
/**
|
||||
* Страница редактирования кластера: GET/PUT /api/v1/clusters/{name}/config.
|
||||
* Расширенный режим — форма по документации kind (networking, nodes, extraPortMappings, kubeadmConfigPatches).
|
||||
*
|
||||
* Автор: Сергей Антропов
|
||||
* Сайт: https://devops.org.ru
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const body = document.body;
|
||||
const API = (body.dataset.apiBase || "/api/v1").replace(/\/$/, "");
|
||||
const clusterName = (body.getAttribute("data-cluster-name") || "").trim();
|
||||
if (!clusterName) return;
|
||||
|
||||
/** @type {string} */
|
||||
let initialTag = "";
|
||||
/** @type {number | null} */
|
||||
let initialWorkers = null;
|
||||
/** @type {string} */
|
||||
let initialDescription = "";
|
||||
/** @type {string | null} */
|
||||
let loadedYamlSnapshot = null;
|
||||
/** @type {string} */
|
||||
let initialAdvancedSnapshot = "";
|
||||
|
||||
/**
|
||||
* @typedef {object} AdvancedFormState
|
||||
* @property {string} apiVersion
|
||||
* @property {string} image
|
||||
* @property {number} controlPlanes
|
||||
* @property {number} workers
|
||||
* @property {string} ipFamily
|
||||
* @property {string} apiServerAddress
|
||||
* @property {string} apiServerPort
|
||||
* @property {string} podSubnet
|
||||
* @property {string} serviceSubnet
|
||||
* @property {string} kubeProxyMode
|
||||
* @property {boolean} disableDefaultCNI
|
||||
* @property {boolean} portEnabled
|
||||
* @property {string} hostPort
|
||||
* @property {number} containerPort
|
||||
* @property {string} portListenAddress
|
||||
* @property {string} portProtocol
|
||||
* @property {string} kubeadmPatch
|
||||
*/
|
||||
|
||||
function formatApiError(data, fallback) {
|
||||
if (!data) return fallback;
|
||||
if (typeof data.detail === "string") return data.detail;
|
||||
if (Array.isArray(data.detail)) {
|
||||
return data.detail
|
||||
.map(function (x) {
|
||||
return (x.msg || x) + (x.loc ? " (" + x.loc.join(".") + ")" : "");
|
||||
})
|
||||
.join("; ");
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 = formatApiError(data, text || r.statusText);
|
||||
const err = new Error(msg);
|
||||
err.status = r.status;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Собрать содержимое модалки: как на диске сохранилось и как это доходит до kind.
|
||||
* @param {boolean} updatedYaml
|
||||
* @param {boolean} registeredInKind
|
||||
* @returns {DocumentFragment}
|
||||
*/
|
||||
function buildSaveInfoModalContent(updatedYaml, registeredInKind) {
|
||||
const frag = document.createDocumentFragment();
|
||||
if (!updatedYaml) {
|
||||
const p = document.createElement("p");
|
||||
p.textContent =
|
||||
"Сохранены поля без изменения kind-config.yaml (например, только описание в meta.json). " +
|
||||
"Топология кластера в kind и уже запущенные узлы от этого не меняются.";
|
||||
frag.appendChild(p);
|
||||
return frag;
|
||||
}
|
||||
const lead = document.createElement("p");
|
||||
lead.className = "cluster-edit-save-modal-lead";
|
||||
lead.textContent = "Файл kind-config.yaml и связанные поля meta.json записаны на диск.";
|
||||
frag.appendChild(lead);
|
||||
|
||||
if (registeredInKind) {
|
||||
const pIntro = document.createElement("p");
|
||||
pIntro.textContent = "Кластер уже зарегистрирован в kind. Новый конфиг применяется так:";
|
||||
frag.appendChild(pIntro);
|
||||
const ul = document.createElement("ul");
|
||||
const lines = [
|
||||
"До нажатия «Старт» продолжают работать текущие узлы (старая топология и образы).",
|
||||
"При «Старт» на панели или на странице кластера выполняется пересоздание: удаление записи в kind без удаления каталога clusters/" +
|
||||
clusterName +
|
||||
"/, затем создание кластера по обновлённому YAML.",
|
||||
"«Стоп» перед «Старт» не обязателен — можно нажать «Старт» сразу после сохранения.",
|
||||
];
|
||||
lines.forEach(function (line) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = line;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
frag.appendChild(ul);
|
||||
const note = document.createElement("p");
|
||||
note.style.marginTop = "0.65rem";
|
||||
note.style.fontSize = "0.88rem";
|
||||
note.textContent =
|
||||
"Состояние Kubernetes внутри кластера при пересоздании начинается заново; kubeconfig в каталоге кластера обновится после успешного «Старт».";
|
||||
frag.appendChild(note);
|
||||
} else {
|
||||
const p2 = document.createElement("p");
|
||||
p2.textContent =
|
||||
"Кластер сейчас не в списке kind. При «Старт» он будет поднят по сохранённому kind-config.yaml (полный цикл создания, как при восстановлении из файла).";
|
||||
frag.appendChild(p2);
|
||||
}
|
||||
return frag;
|
||||
}
|
||||
|
||||
function openClusterEditSaveModal(updatedYaml, registeredInKind) {
|
||||
const overlay = document.getElementById("cluster-edit-save-modal-overlay");
|
||||
const titleEl = document.getElementById("cluster-edit-save-modal-title");
|
||||
const bodyEl = document.getElementById("cluster-edit-save-modal-body");
|
||||
if (!overlay || !titleEl || !bodyEl) return;
|
||||
|
||||
titleEl.textContent = updatedYaml ? "Конфигурация сохранена" : "Сохранено";
|
||||
bodyEl.replaceChildren();
|
||||
bodyEl.appendChild(buildSaveInfoModalContent(updatedYaml, registeredInKind));
|
||||
|
||||
overlay.classList.remove("hidden");
|
||||
overlay.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("modal-open");
|
||||
|
||||
const okBtn = document.getElementById("cluster-edit-save-modal-ok");
|
||||
if (okBtn) okBtn.focus();
|
||||
}
|
||||
|
||||
function closeClusterEditSaveModal() {
|
||||
const overlay = document.getElementById("cluster-edit-save-modal-overlay");
|
||||
if (!overlay) return;
|
||||
overlay.classList.add("hidden");
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("modal-open");
|
||||
}
|
||||
|
||||
/** Закрытие модалки после сохранения и переход на страницу кластера (только кнопка «Понятно»). */
|
||||
function onSaveInfoModalOk() {
|
||||
closeClusterEditSaveModal();
|
||||
window.location.href = "/cluster/" + encodeURIComponent(clusterName);
|
||||
}
|
||||
|
||||
function wireSaveInfoModal() {
|
||||
const overlay = document.getElementById("cluster-edit-save-modal-overlay");
|
||||
const ok = document.getElementById("cluster-edit-save-modal-ok");
|
||||
if (!overlay || !ok) return;
|
||||
ok.addEventListener("click", onSaveInfoModalOk);
|
||||
overlay.addEventListener("click", function (ev) {
|
||||
if (ev.target === overlay) closeClusterEditSaveModal();
|
||||
});
|
||||
document.addEventListener("keydown", function (ev) {
|
||||
if (ev.key === "Escape" && !overlay.classList.contains("hidden")) {
|
||||
closeClusterEditSaveModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wireVersionSelect(selId, inputId) {
|
||||
const sel = document.getElementById(selId);
|
||||
const verInput = document.getElementById(inputId);
|
||||
if (!sel || !verInput) return;
|
||||
sel.onchange = function () {
|
||||
if (!sel.value) return;
|
||||
if (inputId === "fld-adv-image") {
|
||||
verInput.value =
|
||||
sel.value === "latest" ? "kindest/node:latest" : "kindest/node:" + sel.value.replace(/^v/, "");
|
||||
} else {
|
||||
verInput.value = sel.value === "latest" ? "latest" : sel.value.replace(/^v/, "");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadVersions() {
|
||||
const ids = [
|
||||
["version-select-edit", "kubernetes_version_edit"],
|
||||
["version-select-adv", "fld-adv-image"],
|
||||
];
|
||||
ids.forEach(function (pair) {
|
||||
const sel = document.getElementById(pair[0]);
|
||||
if (sel) sel.innerHTML = "<option value=\"\">— загрузка —</option>";
|
||||
});
|
||||
try {
|
||||
const data = await api("/versions");
|
||||
ids.forEach(function (pair) {
|
||||
const sel = document.getElementById(pair[0]);
|
||||
if (!sel) return;
|
||||
sel.innerHTML = "";
|
||||
if (!data.tags || !data.tags.length) {
|
||||
sel.innerHTML = "<option value=\"\">(список пуст — введите вручную)</option>";
|
||||
return;
|
||||
}
|
||||
const opt0 = document.createElement("option");
|
||||
opt0.value = "";
|
||||
opt0.textContent = "— выберите тег —";
|
||||
sel.appendChild(opt0);
|
||||
data.tags.forEach(function (t) {
|
||||
const o = document.createElement("option");
|
||||
o.value = t;
|
||||
o.textContent = t === "latest" ? "latest (kindest/node)" : t;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
});
|
||||
wireVersionSelect("version-select-edit", "kubernetes_version_edit");
|
||||
wireVersionSelect("version-select-adv", "fld-adv-image");
|
||||
} catch (e) {
|
||||
ids.forEach(function (pair) {
|
||||
const sel = document.getElementById(pair[0]);
|
||||
if (sel) sel.innerHTML = "<option value=\"\">(ошибка загрузки тегов)</option>";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Разбор секции networking: (документация kind — cluster-wide networking).
|
||||
* @param {string} text
|
||||
* @returns {Pick<AdvancedFormState, 'ipFamily'|'apiServerAddress'|'apiServerPort'|'podSubnet'|'serviceSubnet'|'kubeProxyMode'|'disableDefaultCNI'>}
|
||||
*/
|
||||
function parseNetworkingSection(text) {
|
||||
const out = {
|
||||
ipFamily: "",
|
||||
apiServerAddress: "",
|
||||
apiServerPort: "",
|
||||
podSubnet: "",
|
||||
serviceSubnet: "",
|
||||
kubeProxyMode: "",
|
||||
disableDefaultCNI: false,
|
||||
};
|
||||
const lines = text.split("\n");
|
||||
let i = 0;
|
||||
while (i < lines.length) {
|
||||
if (/^\s*networking:\s*$/.test(lines[i])) {
|
||||
i++;
|
||||
while (i < lines.length) {
|
||||
const L = lines[i];
|
||||
const t = L.trim();
|
||||
if (!t) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (/^[a-zA-Z]/.test(L) && !/^\s/.test(L)) break;
|
||||
const kv = L.match(/^\s{2}([a-zA-Z0-9]+):\s*(.*)$/);
|
||||
if (kv) {
|
||||
const k = kv[1];
|
||||
let v = kv[2].trim();
|
||||
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
if (k === "ipFamily") out.ipFamily = v;
|
||||
else if (k === "apiServerAddress") out.apiServerAddress = v;
|
||||
else if (k === "apiServerPort") out.apiServerPort = v;
|
||||
else if (k === "podSubnet") out.podSubnet = v;
|
||||
else if (k === "serviceSubnet") out.serviceSubnet = v;
|
||||
else if (k === "kubeProxyMode") out.kubeProxyMode = v.replace(/^["']|["']$/g, "");
|
||||
else if (k === "disableDefaultCNI") out.disableDefaultCNI = v === "true";
|
||||
}
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdvancedFormState} f
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildNetworkingYaml(f) {
|
||||
const rows = [];
|
||||
if (f.ipFamily && f.ipFamily !== "ipv4") rows.push(" ipFamily: " + f.ipFamily);
|
||||
const addr = (f.apiServerAddress || "").trim();
|
||||
if (addr) rows.push(' apiServerAddress: "' + addr.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"');
|
||||
const ap = String(f.apiServerPort || "").trim();
|
||||
if (ap) {
|
||||
const pn = parseInt(ap, 10);
|
||||
if (pn >= 1 && pn <= 65535) rows.push(" apiServerPort: " + pn);
|
||||
}
|
||||
const ps = (f.podSubnet || "").trim();
|
||||
if (ps) rows.push(' podSubnet: "' + ps.replace(/"/g, '\\"') + '"');
|
||||
const ss = (f.serviceSubnet || "").trim();
|
||||
if (ss) rows.push(' serviceSubnet: "' + ss.replace(/"/g, '\\"') + '"');
|
||||
const kpm = (f.kubeProxyMode || "").trim();
|
||||
if (kpm) rows.push(' kubeProxyMode: "' + kpm.replace(/"/g, '\\"') + '"');
|
||||
if (f.disableDefaultCNI) rows.push(" disableDefaultCNI: true");
|
||||
if (!rows.length) return "";
|
||||
return "networking:\n" + rows.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @returns {AdvancedFormState}
|
||||
*/
|
||||
function parseKindYamlToAdvancedForm(text) {
|
||||
/** @type {AdvancedFormState} */
|
||||
const out = {
|
||||
apiVersion: "kind.x-k8s.io/v1alpha4",
|
||||
image: "",
|
||||
controlPlanes: 1,
|
||||
workers: 0,
|
||||
ipFamily: "",
|
||||
apiServerAddress: "",
|
||||
apiServerPort: "",
|
||||
podSubnet: "",
|
||||
serviceSubnet: "",
|
||||
kubeProxyMode: "",
|
||||
disableDefaultCNI: false,
|
||||
portEnabled: false,
|
||||
hostPort: "",
|
||||
containerPort: 80,
|
||||
portListenAddress: "",
|
||||
portProtocol: "TCP",
|
||||
kubeadmPatch: "",
|
||||
};
|
||||
if (!text || !text.trim()) return out;
|
||||
const av = text.match(/^\s*apiVersion:\s*(\S+)/m);
|
||||
if (av) out.apiVersion = av[1].trim().replace(/['"]/g, "");
|
||||
const net = parseNetworkingSection(text);
|
||||
Object.assign(out, net);
|
||||
|
||||
const lines = text.split("\n");
|
||||
let inNodes = false;
|
||||
let cp = 0;
|
||||
let wk = 0;
|
||||
/** @type {string | null} */
|
||||
let pendingRole = null;
|
||||
const images = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const t = line.trim();
|
||||
if (t === "nodes:" || t.startsWith("nodes:")) {
|
||||
inNodes = true;
|
||||
pendingRole = null;
|
||||
continue;
|
||||
}
|
||||
if (!inNodes) continue;
|
||||
if (t && !line.startsWith(" ") && !line.startsWith("\t")) break;
|
||||
const roleM = line.match(/^\s*-\s*role:\s*(\S+)/);
|
||||
if (roleM) {
|
||||
pendingRole = roleM[1];
|
||||
if (pendingRole === "control-plane") cp++;
|
||||
else if (pendingRole === "worker") wk++;
|
||||
continue;
|
||||
}
|
||||
const imgM = line.match(/^\s*image:\s*(.+)$/);
|
||||
if (imgM && pendingRole) {
|
||||
images.push(imgM[1].trim().replace(/['"]/g, ""));
|
||||
}
|
||||
}
|
||||
if (cp > 0) out.controlPlanes = cp;
|
||||
out.workers = wk;
|
||||
const uniq = [];
|
||||
images.forEach(function (im) {
|
||||
if (uniq.indexOf(im) < 0) uniq.push(im);
|
||||
});
|
||||
out.image = uniq[0] || "";
|
||||
|
||||
const portSplit = text.split("extraPortMappings:");
|
||||
if (portSplit.length > 1) {
|
||||
const portBlock = portSplit[1];
|
||||
const hh = portBlock.match(/hostPort:\s*(\d+)/);
|
||||
const cc = portBlock.match(/containerPort:\s*(\d+)/);
|
||||
const la =
|
||||
portBlock.match(/listenAddress:\s*"([^"]+)"/) ||
|
||||
portBlock.match(/listenAddress:\s*'([^']+)'/);
|
||||
const pr = portBlock.match(/protocol:\s*(\w+)/);
|
||||
if (hh) {
|
||||
out.portEnabled = true;
|
||||
out.hostPort = hh[1];
|
||||
if (cc) out.containerPort = parseInt(cc[1], 10) || 80;
|
||||
if (la) out.portListenAddress = la[1];
|
||||
if (pr && /UDP|SCTP|TCP/i.test(pr[1])) out.portProtocol = pr[1].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
const kubStart = text.indexOf("kubeadmConfigPatches:");
|
||||
if (kubStart >= 0) {
|
||||
const sub = text.slice(kubStart);
|
||||
const pipeM = sub.match(/kubeadmConfigPatches:\s*\n\s*-\s*\|\s*\n([\s\S]+)$/);
|
||||
if (pipeM) {
|
||||
const raw = pipeM[1];
|
||||
const linesPatch = raw.split("\n");
|
||||
const outLines = [];
|
||||
for (let j = 0; j < linesPatch.length; j++) {
|
||||
const ln = linesPatch[j];
|
||||
if (/^\s*-\s*role:\s*/.test(ln)) break;
|
||||
outLines.push(ln.replace(/^ /, ""));
|
||||
}
|
||||
out.kubeadmPatch = outLines.join("\n").replace(/\n+$/, "");
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function ensureApiVersionOption(apiVersion) {
|
||||
const sel = document.getElementById("fld-adv-api-version");
|
||||
if (!sel || !apiVersion) return;
|
||||
let found = false;
|
||||
sel.querySelectorAll("option").forEach(function (o) {
|
||||
if (o.value === apiVersion) found = true;
|
||||
});
|
||||
if (!found) {
|
||||
const o = document.createElement("option");
|
||||
o.value = apiVersion;
|
||||
o.textContent = apiVersion;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdvancedFormState} f
|
||||
* @returns {string}
|
||||
*/
|
||||
function serializeAdvancedKindCluster(f) {
|
||||
const api = f.apiVersion || "kind.x-k8s.io/v1alpha4";
|
||||
const img = (f.image || "").trim();
|
||||
if (!img) throw new Error("Укажите образ узла (kindest/node:…).");
|
||||
const cp = Math.min(5, Math.max(1, parseInt(String(f.controlPlanes), 10) || 1));
|
||||
const wk = Math.min(20, Math.max(0, parseInt(String(f.workers), 10) || 0));
|
||||
|
||||
const lines = ["kind: Cluster", "apiVersion: " + api];
|
||||
const netBlock = buildNetworkingYaml(f);
|
||||
if (netBlock) lines.push(netBlock.trimEnd());
|
||||
|
||||
lines.push("nodes:");
|
||||
const firstExtras = [];
|
||||
if (f.portEnabled && f.hostPort) {
|
||||
const hp = parseInt(String(f.hostPort), 10);
|
||||
const ctp = parseInt(String(f.containerPort), 10) || 80;
|
||||
if (hp >= 1 && hp <= 65535) {
|
||||
firstExtras.push(" extraPortMappings:");
|
||||
firstExtras.push(" - containerPort: " + ctp);
|
||||
firstExtras.push(" hostPort: " + hp);
|
||||
const listen = (f.portListenAddress || "").trim();
|
||||
if (listen) {
|
||||
firstExtras.push(' listenAddress: "' + listen.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"');
|
||||
}
|
||||
const proto = (f.portProtocol || "TCP").toUpperCase();
|
||||
if (proto === "TCP" || proto === "UDP" || proto === "SCTP") {
|
||||
firstExtras.push(" protocol: " + proto);
|
||||
}
|
||||
}
|
||||
}
|
||||
const patch = (f.kubeadmPatch || "").trim();
|
||||
if (patch) {
|
||||
firstExtras.push(" kubeadmConfigPatches:");
|
||||
firstExtras.push(" - |");
|
||||
patch.split("\n").forEach(function (ln) {
|
||||
firstExtras.push(" " + ln);
|
||||
});
|
||||
}
|
||||
let i;
|
||||
for (i = 0; i < cp; i++) {
|
||||
lines.push(" - role: control-plane");
|
||||
lines.push(" image: " + img);
|
||||
if (i === 0 && firstExtras.length) {
|
||||
firstExtras.forEach(function (x) {
|
||||
lines.push(x);
|
||||
});
|
||||
}
|
||||
}
|
||||
for (i = 0; i < wk; i++) {
|
||||
lines.push(" - role: worker");
|
||||
lines.push(" image: " + img);
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {AdvancedFormState}
|
||||
*/
|
||||
function readAdvancedFormFromDom() {
|
||||
const pick = function (id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value : "";
|
||||
};
|
||||
const pickNum = function (id, def) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !el.value.trim()) return def;
|
||||
const n = parseInt(el.value, 10);
|
||||
return Number.isNaN(n) ? def : n;
|
||||
};
|
||||
const pickChk = function (id) {
|
||||
const el = document.getElementById(id);
|
||||
return !!(el && el.checked);
|
||||
};
|
||||
return {
|
||||
apiVersion: pick("fld-adv-api-version") || "kind.x-k8s.io/v1alpha4",
|
||||
image: pick("fld-adv-image").trim(),
|
||||
controlPlanes: pickNum("fld-adv-cp", 1),
|
||||
workers: pickNum("fld-adv-workers", 0),
|
||||
ipFamily: pick("fld-adv-ip-family"),
|
||||
apiServerAddress: pick("fld-adv-api-addr"),
|
||||
apiServerPort: pick("fld-adv-api-port"),
|
||||
podSubnet: pick("fld-adv-pod-subnet"),
|
||||
serviceSubnet: pick("fld-adv-svc-subnet"),
|
||||
kubeProxyMode: pick("fld-adv-kube-proxy"),
|
||||
disableDefaultCNI: pickChk("fld-adv-disable-cni"),
|
||||
portEnabled: pickChk("fld-adv-port-enable"),
|
||||
hostPort: pick("fld-adv-host-port"),
|
||||
containerPort: pickNum("fld-adv-container-port", 80),
|
||||
portListenAddress: pick("fld-adv-listen-addr"),
|
||||
portProtocol: pick("fld-adv-port-proto") || "TCP",
|
||||
kubeadmPatch: (function () {
|
||||
const el = document.getElementById("fld-adv-kubeadm");
|
||||
return el ? el.value : "";
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
function advancedFormSnapshotString() {
|
||||
try {
|
||||
return JSON.stringify(readAdvancedFormFromDom());
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AdvancedFormState} f
|
||||
*/
|
||||
function applyAdvancedFormToDom(f) {
|
||||
const set = function (id, v) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = v != null ? String(v) : "";
|
||||
};
|
||||
ensureApiVersionOption(f.apiVersion || "kind.x-k8s.io/v1alpha4");
|
||||
set("fld-adv-api-version", f.apiVersion || "kind.x-k8s.io/v1alpha4");
|
||||
set("fld-adv-ip-family", f.ipFamily || "");
|
||||
set("fld-adv-api-addr", f.apiServerAddress || "");
|
||||
set("fld-adv-api-port", f.apiServerPort || "");
|
||||
set("fld-adv-pod-subnet", f.podSubnet || "");
|
||||
set("fld-adv-svc-subnet", f.serviceSubnet || "");
|
||||
set("fld-adv-kube-proxy", f.kubeProxyMode || "");
|
||||
const cni = document.getElementById("fld-adv-disable-cni");
|
||||
if (cni) cni.checked = !!f.disableDefaultCNI;
|
||||
set("fld-adv-image", f.image || "");
|
||||
set("fld-adv-cp", String(Math.min(5, Math.max(1, f.controlPlanes || 1))));
|
||||
set("fld-adv-workers", String(Math.min(20, Math.max(0, f.workers || 0))));
|
||||
const pe = document.getElementById("fld-adv-port-enable");
|
||||
if (pe) pe.checked = !!f.portEnabled;
|
||||
set("fld-adv-host-port", f.hostPort || "");
|
||||
set("fld-adv-container-port", String(f.containerPort || 80));
|
||||
set("fld-adv-listen-addr", f.portListenAddress || "");
|
||||
set("fld-adv-port-proto", f.portProtocol || "TCP");
|
||||
const kub = document.getElementById("fld-adv-kubeadm");
|
||||
if (kub) kub.value = f.kubeadmPatch || "";
|
||||
togglePortRow();
|
||||
}
|
||||
|
||||
function togglePortRow() {
|
||||
const pe = document.getElementById("fld-adv-port-enable");
|
||||
const row = document.getElementById("adv-port-fields-row");
|
||||
const on = !!(pe && pe.checked);
|
||||
if (row) {
|
||||
row.classList.toggle("hidden", !on);
|
||||
row.setAttribute("aria-hidden", on ? "false" : "true");
|
||||
}
|
||||
}
|
||||
|
||||
function updateAdvancedPreview() {
|
||||
const pre = document.getElementById("adv-yaml-preview");
|
||||
if (!pre) return;
|
||||
try {
|
||||
pre.textContent = serializeAdvancedKindCluster(readAdvancedFormFromDom());
|
||||
} catch (e) {
|
||||
pre.textContent = "# " + (e.message || "Заполните образ узла");
|
||||
}
|
||||
}
|
||||
|
||||
function setModeUi(isAdvanced) {
|
||||
const simple = document.getElementById("edit-simple-block");
|
||||
const adv = document.getElementById("edit-advanced-block");
|
||||
const hint = document.getElementById("cluster-edit-mode-hint");
|
||||
const radios = document.querySelectorAll('input[name="save_mode"]');
|
||||
radios.forEach(function (r) {
|
||||
r.checked = (isAdvanced && r.value === "yaml") || (!isAdvanced && r.value === "simple");
|
||||
});
|
||||
if (simple) {
|
||||
simple.classList.toggle("hidden", isAdvanced);
|
||||
simple.setAttribute("aria-hidden", isAdvanced ? "true" : "false");
|
||||
}
|
||||
if (adv) {
|
||||
adv.classList.toggle("hidden", !isAdvanced);
|
||||
adv.setAttribute("aria-hidden", !isAdvanced ? "true" : "false");
|
||||
}
|
||||
if (hint) {
|
||||
hint.textContent = isAdvanced
|
||||
? "Секции соответствуют kind Cluster: networking, nodes, extraPortMappings и kubeadmConfigPatches (к первой control-plane)."
|
||||
: "Только тег kindest/node и число worker-нод; конфиг пересобирается как при создании кластера.";
|
||||
}
|
||||
if (isAdvanced) updateAdvancedPreview();
|
||||
}
|
||||
|
||||
function getSaveMode() {
|
||||
const c = document.querySelector('input[name="save_mode"]:checked');
|
||||
return c && c.value === "yaml" ? "yaml" : "simple";
|
||||
}
|
||||
|
||||
function normTag(s) {
|
||||
return String(s || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^v/, "");
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
const noteEl = document.getElementById("edit-kind-note");
|
||||
const msg = document.getElementById("edit-msg");
|
||||
const verInput = document.getElementById("kubernetes_version_edit");
|
||||
const workersEl = document.getElementById("fld-workers-edit");
|
||||
const descEl = document.getElementById("fld-description");
|
||||
try {
|
||||
const d = await api("/clusters/" + encodeURIComponent(clusterName) + "/config");
|
||||
if (noteEl) {
|
||||
noteEl.textContent = d.kind_note || "";
|
||||
}
|
||||
const meta = d.meta || {};
|
||||
initialTag = meta.kubernetes_version_tag != null ? String(meta.kubernetes_version_tag) : "";
|
||||
if (verInput) {
|
||||
const tag = initialTag;
|
||||
verInput.value = tag ? tag.replace(/^v/i, "") : "";
|
||||
}
|
||||
if (meta.worker_nodes != null) {
|
||||
const wn = Number(meta.worker_nodes);
|
||||
initialWorkers = Number.isNaN(wn) ? 0 : wn;
|
||||
} else {
|
||||
initialWorkers = null;
|
||||
}
|
||||
if (workersEl) {
|
||||
workersEl.value = String(initialWorkers !== null ? initialWorkers : 0);
|
||||
}
|
||||
initialDescription = meta.description != null ? String(meta.description) : "";
|
||||
if (descEl) {
|
||||
descEl.value = initialDescription;
|
||||
}
|
||||
const yamlText = d.kind_config_yaml != null ? String(d.kind_config_yaml) : "";
|
||||
loadedYamlSnapshot = yamlText;
|
||||
const advParsed = parseKindYamlToAdvancedForm(yamlText);
|
||||
if (!advParsed.image && meta.node_image) {
|
||||
advParsed.image = String(meta.node_image);
|
||||
}
|
||||
if (advParsed.controlPlanes < 1) advParsed.controlPlanes = 1;
|
||||
applyAdvancedFormToDom(advParsed);
|
||||
initialAdvancedSnapshot = advancedFormSnapshotString();
|
||||
updateAdvancedPreview();
|
||||
if (msg) msg.textContent = "";
|
||||
} catch (e) {
|
||||
if (noteEl) noteEl.textContent = "";
|
||||
if (msg) {
|
||||
msg.textContent = e.status === 404 ? "Каталог кластера не найден." : "Ошибка загрузки: " + e.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bindAdvancedInputs() {
|
||||
const ids = [
|
||||
"fld-adv-api-version",
|
||||
"fld-adv-ip-family",
|
||||
"fld-adv-api-addr",
|
||||
"fld-adv-api-port",
|
||||
"fld-adv-pod-subnet",
|
||||
"fld-adv-svc-subnet",
|
||||
"fld-adv-kube-proxy",
|
||||
"fld-adv-disable-cni",
|
||||
"fld-adv-image",
|
||||
"fld-adv-cp",
|
||||
"fld-adv-workers",
|
||||
"fld-adv-port-enable",
|
||||
"fld-adv-host-port",
|
||||
"fld-adv-container-port",
|
||||
"fld-adv-listen-addr",
|
||||
"fld-adv-port-proto",
|
||||
"fld-adv-kubeadm",
|
||||
];
|
||||
ids.forEach(function (id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.addEventListener("input", updateAdvancedPreview);
|
||||
el.addEventListener("change", updateAdvancedPreview);
|
||||
});
|
||||
const pe = document.getElementById("fld-adv-port-enable");
|
||||
if (pe) {
|
||||
pe.addEventListener("change", function () {
|
||||
togglePortRow();
|
||||
updateAdvancedPreview();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindForm() {
|
||||
const form = document.getElementById("form-cluster-edit");
|
||||
if (!form) return;
|
||||
document.querySelectorAll('input[name="save_mode"]').forEach(function (r) {
|
||||
r.addEventListener("change", function () {
|
||||
setModeUi(getSaveMode() === "yaml");
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async function (ev) {
|
||||
ev.preventDefault();
|
||||
const msg = document.getElementById("edit-msg");
|
||||
const mode = getSaveMode();
|
||||
const descEl = document.getElementById("fld-description");
|
||||
const descVal = descEl ? descEl.value : "";
|
||||
/** @type {Record<string, unknown>} */
|
||||
const payload = { description: descVal };
|
||||
|
||||
if (mode === "yaml") {
|
||||
let yamlText;
|
||||
try {
|
||||
yamlText = serializeAdvancedKindCluster(readAdvancedFormFromDom());
|
||||
} catch (e) {
|
||||
if (msg) msg.textContent = e.message || "Проверьте поля расширенного режима.";
|
||||
return;
|
||||
}
|
||||
const yamlChanged = yamlText !== (loadedYamlSnapshot || "");
|
||||
const advChanged = advancedFormSnapshotString() !== initialAdvancedSnapshot;
|
||||
const descChanged = descVal !== initialDescription;
|
||||
if (!yamlChanged && !advChanged && !descChanged) {
|
||||
if (msg) msg.textContent = "Изменений нет.";
|
||||
return;
|
||||
}
|
||||
if (yamlChanged || advChanged) {
|
||||
payload.kind_config_yaml = yamlText;
|
||||
}
|
||||
} else {
|
||||
const verInput = document.getElementById("kubernetes_version_edit");
|
||||
const workersEl = document.getElementById("fld-workers-edit");
|
||||
const ver = verInput ? verInput.value.trim() : "";
|
||||
const wParsed = workersEl ? parseInt(workersEl.value, 10) : NaN;
|
||||
const workersOk = !Number.isNaN(wParsed);
|
||||
const effInitW = initialWorkers !== null && initialWorkers !== undefined ? initialWorkers : 0;
|
||||
const workersChanged = workersOk && wParsed !== effInitW;
|
||||
const verChanged = normTag(ver) !== normTag(initialTag);
|
||||
const descChanged = descVal !== initialDescription;
|
||||
if (!verChanged && !workersChanged && !descChanged) {
|
||||
if (msg) msg.textContent = "Изменений нет.";
|
||||
return;
|
||||
}
|
||||
if (verChanged) {
|
||||
if (!ver) {
|
||||
if (msg) msg.textContent = "Укажите тег образа или откатите поле к прежнему значению.";
|
||||
return;
|
||||
}
|
||||
payload.kubernetes_version = ver;
|
||||
}
|
||||
if (workersChanged) {
|
||||
if (!workersOk || wParsed < 0 || wParsed > 20) {
|
||||
if (msg) msg.textContent = "Число worker-нод должно быть от 0 до 20.";
|
||||
return;
|
||||
}
|
||||
payload.workers = wParsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (msg) msg.textContent = "Сохранение…";
|
||||
try {
|
||||
const r = await api("/clusters/" + encodeURIComponent(clusterName) + "/config", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (msg) {
|
||||
msg.textContent = r.message || "Сохранено.";
|
||||
}
|
||||
openClusterEditSaveModal(!!r.updated_kind_config_yaml, !!r.registered_in_kind);
|
||||
await loadConfig();
|
||||
} catch (e) {
|
||||
if (msg) msg.textContent = "Ошибка: " + e.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
wireSaveInfoModal();
|
||||
|
||||
loadVersions();
|
||||
loadConfig().then(function () {
|
||||
bindAdvancedInputs();
|
||||
togglePortRow();
|
||||
});
|
||||
bindForm();
|
||||
setModeUi(false);
|
||||
})();
|
||||
Reference in New Issue
Block a user