Веб-интерфейс: страница /clusters, навигация и крошки для кластеров
- Выделена страница списка кластеров, панель упрощена; nav_active и крошки ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL. - Доработки дашборда, аддонов, журнала, стилей и API-документации. - Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
This commit is contained in:
+259
-58
@@ -37,6 +37,15 @@ HELM_REPOS: dict[str, str] = {
|
||||
"kiali": "https://kiali.org/helm-charts",
|
||||
}
|
||||
|
||||
# Идентификаторы аддонов, доступных для установки на странице «Аддоны» и через API ``/clusters/{name}/addons/…``.
|
||||
# Должен совпадать с набором в ``HelmComposeValuesRequest.addon`` и кнопками в ``cluster_addons.html``.
|
||||
HELM_INSTALLABLE_ADDON_IDS: tuple[str, ...] = (
|
||||
"ingress-nginx",
|
||||
"kube-prometheus-stack",
|
||||
"metrics-server",
|
||||
"istio-kiali",
|
||||
)
|
||||
|
||||
# Ссылки для ``helm search repo <ref> --versions`` (ключи совпадают с полями API версий чартов).
|
||||
HELM_ADDON_CHART_REFS: dict[str, str] = {
|
||||
"ingress_nginx": "ingress-nginx/ingress-nginx",
|
||||
@@ -59,6 +68,121 @@ class HelmAddonError(Exception):
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
||||
def dump_helm_values_yaml(data: dict[str, Any]) -> str:
|
||||
"""Сериализация mapping в YAML для UI и временных файлов Helm."""
|
||||
return yaml.safe_dump(
|
||||
data,
|
||||
allow_unicode=True,
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
)
|
||||
|
||||
|
||||
def helm_show_values_dict(chart_ref: str, chart_version: str | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Словарь дефолтных values чарта: ``helm show values <ref> [--version …]``.
|
||||
|
||||
При пустом stdout или ошибке разбора возвращает ``{}`` (ошибка helm — :class:`HelmAddonError`).
|
||||
"""
|
||||
require_helm_binary()
|
||||
cref = chart_ref.strip()
|
||||
if not cref:
|
||||
return {}
|
||||
cmd = ["helm", "show", "values", cref]
|
||||
ver = (chart_version or "").strip()
|
||||
if ver:
|
||||
cmd.extend(["--version", ver])
|
||||
rc, out, err = _run_cmd(cmd, timeout=180)
|
||||
if rc != 0:
|
||||
raise HelmAddonError(
|
||||
(err or out or f"helm show values {cref} код {rc}")[:4000],
|
||||
exit_code=rc,
|
||||
)
|
||||
raw = (out or "").strip()
|
||||
if not raw:
|
||||
return {}
|
||||
try:
|
||||
data = yaml.safe_load(raw)
|
||||
except yaml.YAMLError as e:
|
||||
logger.warning("YAML из helm show values %s: %s", cref, e)
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def _ingress_kind_service_overlay() -> dict[str, Any]:
|
||||
"""То же, что передаётся через ``--set`` при установке ingress для kind."""
|
||||
return {
|
||||
"controller": {
|
||||
"service": {
|
||||
"type": "NodePort",
|
||||
"nodePorts": {"http": 30080},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def compose_ingress_effective_values(chart_version: str | None = None) -> dict[str, Any]:
|
||||
"""Дефолты чарта ingress-nginx + сервис NodePort (как в install). Репозитории — см. :func:`helm_repo_ensure`."""
|
||||
base = helm_show_values_dict("ingress-nginx/ingress-nginx", chart_version)
|
||||
return deep_merge_values(base, _ingress_kind_service_overlay())
|
||||
|
||||
|
||||
def compose_kube_prometheus_effective_values(
|
||||
chart_version: str | None,
|
||||
grafana_admin_user: str,
|
||||
grafana_admin_password: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Дефолты kube-prometheus-stack + учётные данные Grafana."""
|
||||
base = helm_show_values_dict("prometheus-community/kube-prometheus-stack", chart_version)
|
||||
overlay: dict[str, Any] = {
|
||||
"grafana": {
|
||||
"adminUser": grafana_admin_user.strip(),
|
||||
"adminPassword": grafana_admin_password,
|
||||
}
|
||||
}
|
||||
return deep_merge_values(base, overlay)
|
||||
|
||||
|
||||
def compose_metrics_server_effective_values(chart_version: str | None = None) -> dict[str, Any]:
|
||||
"""Дефолты metrics-server + args для kind (как в install)."""
|
||||
base = helm_show_values_dict("metrics-server/metrics-server", chart_version)
|
||||
overlay: dict[str, Any] = {
|
||||
"args": [
|
||||
"--kubelet-insecure-tls",
|
||||
"--kubelet-preferred-address-types=InternalIP,Hostname,ExternalIP",
|
||||
]
|
||||
}
|
||||
return deep_merge_values(base, overlay)
|
||||
|
||||
|
||||
def compose_istio_mesh_preview_values(
|
||||
istio_chart_version: str | None = None,
|
||||
kiali_chart_version: str | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
||||
"""
|
||||
Три словаря values: ``kiali-server``, ``istio/base``, ``istio/istiod``
|
||||
(чистые ``helm show values`` для редактирования в UI).
|
||||
"""
|
||||
kiali = helm_show_values_dict("kiali/kiali-server", kiali_chart_version)
|
||||
ib = helm_show_values_dict("istio/base", istio_chart_version)
|
||||
ird = helm_show_values_dict("istio/istiod", istio_chart_version)
|
||||
return kiali, ib, ird
|
||||
|
||||
|
||||
def deep_merge_values(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Рекурсивное объединение mapping: ключи из ``override`` перекрывают ``base``;
|
||||
для вложенных dict сливаем дальше (как типичный merge для Helm values).
|
||||
"""
|
||||
out: dict[str, Any] = dict(base)
|
||||
for k, v in override.items():
|
||||
if k in out and isinstance(out[k], dict) and isinstance(v, dict):
|
||||
out[k] = deep_merge_values(out[k], v)
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _helm_timeout_sec() -> int:
|
||||
raw = (os.environ.get("KIND_K8S_HELM_TIMEOUT_SEC") or "900").strip()
|
||||
try:
|
||||
@@ -283,6 +407,7 @@ def _helm_upgrade_install(
|
||||
chart_version: str | None = None,
|
||||
extra_args: list[str] | None = None,
|
||||
values_file: Path | None = None,
|
||||
extra_values_files: list[Path] | None = None,
|
||||
) -> tuple[int, str, str]:
|
||||
kcfg = str(_kubeconfig_runtime_path(cluster_name))
|
||||
cmd = [
|
||||
@@ -305,6 +430,8 @@ def _helm_upgrade_install(
|
||||
cmd.extend(["--version", ver])
|
||||
if values_file is not None:
|
||||
cmd.extend(["-f", str(values_file)])
|
||||
for p in extra_values_files or []:
|
||||
cmd.extend(["-f", str(p)])
|
||||
if extra_args:
|
||||
cmd.extend(extra_args)
|
||||
return _run_cmd(cmd, timeout=_helm_timeout_sec() + 60)
|
||||
@@ -321,23 +448,41 @@ def _helm_uninstall(cluster_name: str, release: str, namespace: str) -> tuple[in
|
||||
# --- Установка / удаление по типам ---
|
||||
|
||||
|
||||
def install_ingress_nginx(cluster_name: str, chart_version: str | None) -> tuple[bool, str]:
|
||||
"""ingress-nginx с NodePort для kind."""
|
||||
def install_ingress_nginx(
|
||||
cluster_name: str,
|
||||
chart_version: str | None,
|
||||
user_values: dict[str, Any] | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
ingress-nginx: полный YAML values из UI (``helm show values`` + NodePort) или переданный словарь;
|
||||
после файла values остаются ``--set`` для NodePort (приоритет как у Helm).
|
||||
"""
|
||||
helm_repo_ensure()
|
||||
eff = user_values if user_values is not None else compose_ingress_effective_values(chart_version)
|
||||
extra: list[str] = [
|
||||
"--set",
|
||||
"controller.service.type=NodePort",
|
||||
"--set",
|
||||
"controller.service.nodePorts.http=30080",
|
||||
]
|
||||
rc, out, err = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"ingress-nginx",
|
||||
"ingress-nginx/ingress-nginx",
|
||||
"ingress-nginx",
|
||||
chart_version=chart_version,
|
||||
extra_args=extra,
|
||||
)
|
||||
user_tmp: Path | None = None
|
||||
try:
|
||||
fd, name = tempfile.mkstemp(suffix=".yaml", prefix="helm-ing-values-")
|
||||
os.close(fd)
|
||||
user_tmp = Path(name)
|
||||
user_tmp.write_text(dump_helm_values_yaml(eff), encoding="utf-8")
|
||||
rc, out, err = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"ingress-nginx",
|
||||
"ingress-nginx/ingress-nginx",
|
||||
"ingress-nginx",
|
||||
chart_version=chart_version,
|
||||
extra_args=extra,
|
||||
extra_values_files=[user_tmp],
|
||||
)
|
||||
finally:
|
||||
if user_tmp is not None:
|
||||
user_tmp.unlink(missing_ok=True)
|
||||
text = "\n".join(filter(None, [out, err])).strip()
|
||||
if rc != 0:
|
||||
raise HelmAddonError(text or f"helm exit {rc}", exit_code=rc)
|
||||
@@ -358,18 +503,23 @@ def install_kube_prometheus_stack(
|
||||
grafana_admin_user: str,
|
||||
grafana_admin_password: str,
|
||||
chart_version: str | None = None,
|
||||
user_values: dict[str, Any] | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Полный values из UI или автосборка (``helm show values`` + Grafana из формы)."""
|
||||
helm_repo_ensure()
|
||||
values = {
|
||||
"grafana": {
|
||||
"adminUser": grafana_admin_user,
|
||||
"adminPassword": grafana_admin_password,
|
||||
}
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, encoding="utf-8") as tmp:
|
||||
yaml.safe_dump(values, tmp, allow_unicode=True)
|
||||
tmp_path = Path(tmp.name)
|
||||
if user_values is not None:
|
||||
values = user_values
|
||||
else:
|
||||
values = compose_kube_prometheus_effective_values(
|
||||
chart_version,
|
||||
grafana_admin_user,
|
||||
grafana_admin_password,
|
||||
)
|
||||
fd, tmp_name = tempfile.mkstemp(suffix=".yaml", prefix="helm-prom-stack-")
|
||||
os.close(fd)
|
||||
tmp_path = Path(tmp_name)
|
||||
try:
|
||||
tmp_path.write_text(dump_helm_values_yaml(values), encoding="utf-8")
|
||||
rc, out, err = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"kube-prometheus-stack",
|
||||
@@ -394,18 +544,22 @@ def uninstall_kube_prometheus_stack(cluster_name: str) -> tuple[bool, str]:
|
||||
return True, text or "kube-prometheus-stack удалён (или отсутствовал)."
|
||||
|
||||
|
||||
def install_metrics_server(cluster_name: str, chart_version: str | None = None) -> tuple[bool, str]:
|
||||
def install_metrics_server(
|
||||
cluster_name: str,
|
||||
chart_version: str | None = None,
|
||||
user_values: dict[str, Any] | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Полный values из UI или автосборка (чарт + args для kind)."""
|
||||
helm_repo_ensure()
|
||||
values = {
|
||||
"args": [
|
||||
"--kubelet-insecure-tls",
|
||||
"--kubelet-preferred-address-types=InternalIP,Hostname,ExternalIP",
|
||||
]
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False, encoding="utf-8") as tmp:
|
||||
yaml.safe_dump(values, tmp, allow_unicode=True)
|
||||
tmp_path = Path(tmp.name)
|
||||
if user_values is not None:
|
||||
values = user_values
|
||||
else:
|
||||
values = compose_metrics_server_effective_values(chart_version)
|
||||
fd_m, tmp_ms = tempfile.mkstemp(suffix=".yaml", prefix="helm-metrics-")
|
||||
os.close(fd_m)
|
||||
tmp_path = Path(tmp_ms)
|
||||
try:
|
||||
tmp_path.write_text(dump_helm_values_yaml(values), encoding="utf-8")
|
||||
rc, out, err = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"metrics-server",
|
||||
@@ -437,8 +591,16 @@ def install_istio_and_kiali(
|
||||
kiali_password: str,
|
||||
istio_chart_version: str | None = None,
|
||||
kiali_chart_version: str | None = None,
|
||||
istio_base_values: dict[str, Any] | None = None,
|
||||
istiod_values: dict[str, Any] | None = None,
|
||||
kiali_user_values: dict[str, Any] | None = None,
|
||||
) -> tuple[bool, str]:
|
||||
"""Istio base + istiod + секрет входа Kiali + chart kiali-server (strategy=login)."""
|
||||
"""
|
||||
Istio base + istiod (опциональные values из UI) + секрет Kiali + kiali-server.
|
||||
|
||||
Для каждого чарта: если словарь ``None`` — без файла ``-f`` (дефолты чарта);
|
||||
если непустой dict — полный values как в редакторе.
|
||||
"""
|
||||
helm_repo_ensure()
|
||||
_ensure_namespace(cluster_name, "istio-system")
|
||||
|
||||
@@ -446,27 +608,53 @@ def install_istio_and_kiali(
|
||||
istio_ver = (istio_chart_version or "").strip() or None
|
||||
kiali_ver = (kiali_chart_version or "").strip() or None
|
||||
|
||||
rc, out, err = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"istio-base",
|
||||
"istio/base",
|
||||
"istio-system",
|
||||
chart_version=istio_ver,
|
||||
)
|
||||
log_parts.append(out or err)
|
||||
if rc != 0:
|
||||
raise HelmAddonError((err or out or f"istio-base exit {rc}")[:8000], exit_code=rc)
|
||||
base_tmp: Path | None = None
|
||||
try:
|
||||
base_file: Path | None = None
|
||||
if istio_base_values:
|
||||
fd, name = tempfile.mkstemp(suffix=".yaml", prefix="helm-istio-base-")
|
||||
os.close(fd)
|
||||
base_tmp = Path(name)
|
||||
base_tmp.write_text(dump_helm_values_yaml(istio_base_values), encoding="utf-8")
|
||||
base_file = base_tmp
|
||||
rc, out, err = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"istio-base",
|
||||
"istio/base",
|
||||
"istio-system",
|
||||
chart_version=istio_ver,
|
||||
values_file=base_file,
|
||||
)
|
||||
log_parts.append(out or err)
|
||||
if rc != 0:
|
||||
raise HelmAddonError((err or out or f"istio-base exit {rc}")[:8000], exit_code=rc)
|
||||
finally:
|
||||
if base_tmp is not None:
|
||||
base_tmp.unlink(missing_ok=True)
|
||||
|
||||
rc2, o2, e2 = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"istiod",
|
||||
"istio/istiod",
|
||||
"istio-system",
|
||||
chart_version=istio_ver,
|
||||
)
|
||||
log_parts.append(o2 or e2)
|
||||
if rc2 != 0:
|
||||
raise HelmAddonError((e2 or o2 or f"istiod exit {rc2}")[:8000], exit_code=rc2)
|
||||
istiod_tmp: Path | None = None
|
||||
try:
|
||||
istiod_file: Path | None = None
|
||||
if istiod_values:
|
||||
fd, name = tempfile.mkstemp(suffix=".yaml", prefix="helm-istiod-")
|
||||
os.close(fd)
|
||||
istiod_tmp = Path(name)
|
||||
istiod_tmp.write_text(dump_helm_values_yaml(istiod_values), encoding="utf-8")
|
||||
istiod_file = istiod_tmp
|
||||
rc2, o2, e2 = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"istiod",
|
||||
"istio/istiod",
|
||||
"istio-system",
|
||||
chart_version=istio_ver,
|
||||
values_file=istiod_file,
|
||||
)
|
||||
log_parts.append(o2 or e2)
|
||||
if rc2 != 0:
|
||||
raise HelmAddonError((e2 or o2 or f"istiod exit {rc2}")[:8000], exit_code=rc2)
|
||||
finally:
|
||||
if istiod_tmp is not None:
|
||||
istiod_tmp.unlink(missing_ok=True)
|
||||
|
||||
# Секрет для стратегии login (ключи username / passphrase — ожидание Kiali).
|
||||
secret = {
|
||||
@@ -484,14 +672,27 @@ def install_istio_and_kiali(
|
||||
raise HelmAddonError(f"Secret kiali: {msgs}", exit_code=rcs)
|
||||
log_parts.append(msgs)
|
||||
|
||||
rc3, o3, e3 = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"kiali-server",
|
||||
"kiali/kiali-server",
|
||||
"istio-system",
|
||||
chart_version=kiali_ver,
|
||||
extra_args=["--set", "auth.strategy=login"],
|
||||
)
|
||||
kiali_extra: list[Path] = []
|
||||
kiali_tmp: Path | None = None
|
||||
try:
|
||||
if kiali_user_values:
|
||||
fd, name = tempfile.mkstemp(suffix=".yaml", prefix="helm-kiali-user-")
|
||||
os.close(fd)
|
||||
kiali_tmp = Path(name)
|
||||
kiali_tmp.write_text(dump_helm_values_yaml(kiali_user_values), encoding="utf-8")
|
||||
kiali_extra.append(kiali_tmp)
|
||||
rc3, o3, e3 = _helm_upgrade_install(
|
||||
cluster_name,
|
||||
"kiali-server",
|
||||
"kiali/kiali-server",
|
||||
"istio-system",
|
||||
chart_version=kiali_ver,
|
||||
extra_args=["--set", "auth.strategy=login"],
|
||||
extra_values_files=kiali_extra or None,
|
||||
)
|
||||
finally:
|
||||
if kiali_tmp is not None:
|
||||
kiali_tmp.unlink(missing_ok=True)
|
||||
log_parts.append(o3 or e3)
|
||||
if rc3 != 0:
|
||||
raise HelmAddonError((e3 or o3 or f"kiali-server exit {rc3}")[:8000], exit_code=rc3)
|
||||
|
||||
Reference in New Issue
Block a user