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 обновлены
This commit is contained in:
Sergey Antropoff
2026-04-04 18:54:10 +03:00
parent 349b78d24d
commit 4b703801e1
13 changed files with 415 additions and 123 deletions
+74 -14
View File
@@ -97,11 +97,63 @@ def _parse_optional_values_yaml(raw: str | None) -> dict[str, Any] | None:
return data
# Идентификатор аддона в API/UI → ключ :func:`helm_addon_snapshot` и суффикс ``job_kind`` (snake_case).
_HELM_ADDON_SNAPSHOT_KEY: dict[str, str] = {
"ingress-nginx": "ingress_nginx",
"kube-prometheus-stack": "kube_prometheus_stack",
"metrics-server": "metrics_server",
"istio-kiali": "istio_mesh_ready",
}
_HELM_ADDON_JOB_SLUG: dict[str, str] = {
"ingress-nginx": "ingress_nginx",
"kube-prometheus-stack": "kube_prometheus_stack",
"metrics-server": "metrics_server",
"istio-kiali": "istio_kiali",
}
def _helm_journal_message_ru(addon_id: str, action: str) -> str:
"""Краткая подпись операции для колонки «Сообщение» в журнале Helm и для UI."""
name = {
"ingress-nginx": "ingress-nginx",
"kube-prometheus-stack": "kube-prometheus-stack",
"metrics-server": "metrics-server",
"istio-kiali": "Istio + Kiali",
}.get(addon_id, addon_id)
verb = {
"install": "Установка",
"upgrade": "Обновление",
"delete": "Удаление",
}.get(action, action)
return f"{verb}: {name}"
async def _helm_install_action_and_job_kind(cluster_name: str, addon_id: str) -> tuple[str, str]:
"""
Перед ``helm upgrade --install``: если релиз уже есть — операция **upgrade**, иначе **install**.
Возвращает ``(action, job_kind)``, например ``("upgrade", "helm_addon_upgrade_ingress_nginx")``.
"""
snap_key = _HELM_ADDON_SNAPSHOT_KEY.get(addon_id)
slug = _HELM_ADDON_JOB_SLUG.get(addon_id, addon_id.replace("-", "_"))
if not snap_key:
return "install", f"helm_addon_install_{slug}"
try:
st = await asyncio.to_thread(helm_addon_snapshot, cluster_name)
installed = bool(st.get(snap_key))
except HelmAddonError:
installed = False
action = "upgrade" if installed else "install"
return action, f"helm_addon_{action}_{slug}"
async def _run_helm_with_journal(
cluster_name: str,
job_kind: str,
result_meta: dict[str, Any],
sync_call: Callable[[], tuple[bool, str]],
*,
journal_message: str | None = None,
) -> str:
"""
Выполнить операцию под блокировкой kind, добавить запись в историю ``helm_addon_log.json``.
@@ -118,7 +170,7 @@ async def _run_helm_with_journal(
job_id=str(uuid.uuid4()),
job_kind=job_kind,
status="success",
message=None,
message=journal_message,
lines=log.splitlines() if log else [],
result=result_meta,
)
@@ -244,13 +296,15 @@ async def post_install_ingress_nginx(
if body is not None and body.chart_version and body.chart_version.strip():
ver = body.chart_version.strip()
user_v = _parse_optional_values_yaml(body.values_yaml if body else None)
meta = {"addon": "ingress-nginx", "action": "install", "chart_version": ver}
action, job_kind = await _helm_install_action_and_job_kind(n, "ingress-nginx")
meta = {"addon": "ingress-nginx", "action": action, "chart_version": ver}
try:
log = await _run_helm_with_journal(
n,
"helm_addon_install_ingress_nginx",
job_kind,
meta,
lambda: install_ingress_nginx(n, ver, user_v),
journal_message=_helm_journal_message_ru("ingress-nginx", action),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -274,6 +328,7 @@ async def delete_ingress_nginx(name: str) -> HelmAddonApplyResponse:
"helm_addon_delete_ingress_nginx",
meta,
lambda: uninstall_ingress_nginx(n),
journal_message=_helm_journal_message_ru("ingress-nginx", "delete"),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -297,16 +352,17 @@ async def post_install_kube_prometheus_stack(
prom_ver = body.chart_version.strip()
u = body.grafana_admin_user.strip()
user_v = _parse_optional_values_yaml(body.values_yaml)
action, job_kind = await _helm_install_action_and_job_kind(n, "kube-prometheus-stack")
meta = {
"addon": "kube-prometheus-stack",
"action": "install",
"action": action,
"chart_version": prom_ver,
"grafana_admin_user": u,
}
try:
log = await _run_helm_with_journal(
n,
"helm_addon_install_kube_prometheus_stack",
job_kind,
meta,
lambda: install_kube_prometheus_stack(
n,
@@ -315,6 +371,7 @@ async def post_install_kube_prometheus_stack(
chart_version=prom_ver,
user_values=user_v,
),
journal_message=_helm_journal_message_ru("kube-prometheus-stack", action),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -338,6 +395,7 @@ async def delete_kube_prometheus_stack(name: str) -> HelmAddonApplyResponse:
"helm_addon_delete_kube_prometheus_stack",
meta,
lambda: uninstall_kube_prometheus_stack(n),
journal_message=_helm_journal_message_ru("kube-prometheus-stack", "delete"),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -360,13 +418,15 @@ async def post_install_metrics_server(
if body is not None and body.chart_version and body.chart_version.strip():
ms_ver = body.chart_version.strip()
user_v = _parse_optional_values_yaml(body.values_yaml if body else None)
meta = {"addon": "metrics-server", "action": "install", "chart_version": ms_ver}
action, job_kind = await _helm_install_action_and_job_kind(n, "metrics-server")
meta = {"addon": "metrics-server", "action": action, "chart_version": ms_ver}
try:
log = await _run_helm_with_journal(
n,
"helm_addon_install_metrics_server",
job_kind,
meta,
lambda: install_metrics_server(n, ms_ver, user_v),
journal_message=_helm_journal_message_ru("metrics-server", action),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -390,6 +450,7 @@ async def delete_metrics_server(name: str) -> HelmAddonApplyResponse:
"helm_addon_delete_metrics_server",
meta,
lambda: uninstall_metrics_server(n),
journal_message=_helm_journal_message_ru("metrics-server", "delete"),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -399,7 +460,7 @@ async def delete_metrics_server(name: str) -> HelmAddonApplyResponse:
@router.post(
"/clusters/{name}/addons/istio-kiali",
response_model=HelmAddonApplyResponse,
summary="Установить Istio (base+istiod) и Kiali с логином",
summary="Установить Istio (base+istiod) и Kiali (по умолчанию auth anonymous)",
)
async def post_install_istio_kiali(
name: str,
@@ -414,32 +475,30 @@ async def post_install_istio_kiali(
kiali_ver: str | None = None
if body.kiali_chart_version and body.kiali_chart_version.strip():
kiali_ver = body.kiali_chart_version.strip()
ku = body.kiali_username.strip()
kiali_uv = _parse_optional_values_yaml(body.values_yaml)
istio_base_v = _parse_optional_values_yaml(body.istio_base_values_yaml)
istiod_v = _parse_optional_values_yaml(body.istio_istiod_values_yaml)
action, job_kind = await _helm_install_action_and_job_kind(n, "istio-kiali")
meta = {
"addon": "istio-kiali",
"action": "install",
"action": action,
"istio_chart_version": istio_ver,
"kiali_chart_version": kiali_ver,
"kiali_username": ku,
}
try:
log = await _run_helm_with_journal(
n,
"helm_addon_install_istio_kiali",
job_kind,
meta,
lambda: install_istio_and_kiali(
n,
kiali_username=ku,
kiali_password=body.kiali_password,
istio_chart_version=istio_ver,
kiali_chart_version=kiali_ver,
istio_base_values=istio_base_v,
istiod_values=istiod_v,
kiali_user_values=kiali_uv,
),
journal_message=_helm_journal_message_ru("istio-kiali", action),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
@@ -463,6 +522,7 @@ async def delete_istio_kiali(name: str) -> HelmAddonApplyResponse:
"helm_addon_delete_istio_kiali",
meta,
lambda: uninstall_istio_and_kiali(n),
journal_message=_helm_journal_message_ru("istio-kiali", "delete"),
)
except HelmAddonError as e:
raise _map_helm_error(e) from e
+55 -1
View File
@@ -66,7 +66,11 @@ from core.job_store import (
from core.provision_log import provision_log_file_path, write_cluster_provision_log
from core.kind_guard import kind_cluster_lock
from kind_k8s_paths import clusters_dir
from kubeconfig_patch import kubeconfig_host_file, patch_kubeconfig_server_for_host
from kubeconfig_patch import (
kubeconfig_host_file,
patch_kubeconfig_server_for_docker_network,
patch_kubeconfig_server_for_host,
)
from models.schemas import (
AggregateResourcesSummary,
ClusterConfigGetResponse,
@@ -351,6 +355,56 @@ async def put_cluster_config(name: str, body: ClusterConfigUpdateRequest) -> Clu
)
@router.get(
"/clusters/{name}/kubeconfig/docker",
summary="Скачать kubeconfig для kubectl из другого контейнера (host.docker.internal:порт + SNI)",
responses={404: {"description": "Файл не найден"}, 500: {"description": "Ошибка подготовки файла"}},
)
async def download_kubeconfig_docker_network(name: str) -> FileResponse:
"""
Kubeconfig как для **kubectl внутри веб-контейнера**: ``https://<шлюз>:<порт>`` (по умолчанию
**host.docker.internal** и порт из ``docker port … 6443``) плюс **tls-server-name** (обычно **localhost**).
Доступен из **любого** контейнера, где резолвится шлюз (на Linux: ``extra_hosts: host.docker.internal:host-gateway``).
Запасной вариант без порта на хосте — ``https://<имя>-control-plane:6443`` (нужна общая сеть с kind).
Копия ``clusters/<имя>/kubeconfig`` с подстановкой ``server`` / ``tls-server-name`` (см. ``kubeconfig_patch.py``).
"""
if not validate_cluster_name(name):
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
n = name.strip()
primary = clusters_dir() / n / "kubeconfig"
if not primary.is_file():
raise HTTPException(status_code=404, detail="kubeconfig не найден")
fd, tmp_name = tempfile.mkstemp(suffix=".yaml", prefix="kind-k8s-kube-docker-")
os.close(fd)
tmp_path = Path(tmp_name)
try:
shutil.copyfile(primary, tmp_path, follow_symlinks=True)
except OSError as e:
tmp_path.unlink(missing_ok=True)
logger.warning("Копирование kubeconfig (docker network): %s", e)
raise HTTPException(status_code=500, detail="Не удалось подготовить kubeconfig") from e
patched = await asyncio.to_thread(
partial(patch_kubeconfig_server_for_docker_network, cluster_name=n, kube_path=tmp_path),
)
if not patched:
tmp_path.unlink(missing_ok=True)
raise HTTPException(
status_code=500,
detail="Не удалось подготовить kubeconfig для контейнера (kubectl config set-cluster / tls-server-name)",
)
logger.info("Отдача kubeconfig (docker network) для кластера %s", n)
return FileResponse(
path=str(tmp_path),
filename=f"kubeconfig-docker-{n}.yaml",
media_type="application/x-yaml",
background=BackgroundTask(_unlink_temp_kubeconfig, tmp_path),
)
@router.get(
"/clusters/{name}/kubeconfig",
summary="Скачать kubeconfig",