Веб-UI кластера: страница деталей, kubectl по карточкам, мета 3 колонки

- Страница /cluster/<имя>: сводка, ресурсы узлов полосами, отдельные карточки на каждый kubectl get … -o json, рестарт пода.
- API: overview с блоками k8s_*, POST pods/restart, расширенный набор ресурсов (-A).
- Панель: спиннер загрузки, правки дашборда и стилей; документация api_routes, compose и прочие сопутствующие изменения.
This commit is contained in:
Sergey Antropoff
2026-04-04 09:13:08 +03:00
parent 4546f50aef
commit af0d1705cc
17 changed files with 2605 additions and 208 deletions
+190 -6
View File
@@ -1,8 +1,22 @@
"""Правка kubeconfig kind для доступа к API с хоста (после create из контейнера).
Kind внутри Docker видит другой адрес apiserver; на хосте нужен 127.0.0.1:<порт>
из проброса `docker port <cluster>-control-plane 6443/tcp` (Podman — тот же клиент
`docker` к docker-совместимому сокету).
Kind внутри Docker видит другой адрес apiserver; для **kubectl на машине пользователя**
в ``kubeconfig.host`` подставляется ``https://<клиентский_хост>:<порт>`` (порт из
``docker port <cluster>-control-plane 6443/tcp``). Хост по умолчанию — **localhost**
(часто надёжнее ``127.0.0.1`` с TLS); переопределение: ``KIND_K8S_KUBECONFIG_CLIENT_HOST``
(например IP хоста в LAN).
**Два файла в ``clusters/<имя>/``:**
- ``kubeconfig`` — как выдал ``kind get kubeconfig``; для ``kubectl`` **в процессе веб-приложения**
:func:`kubeconfig_path_for_container_kubectl` задаёт ``server=https://host.docker.internal:<порт>``
и ``tls-server-name=localhost`` (иначе x509 не совпадает с SAN kind); см. ``KIND_K8S_KUBECONFIG_TLS_SERVER_NAME``.
Запасной URL: ``https://<имя>-control-plane:6443`` (без доп. SNI).
- ``kubeconfig.host`` — копия с ``server=`` для kubectl на хосте (см.
:func:`kubeconfig_download_client_hostname`).
Если патч отключён или не удалился, ``kubeconfig.host`` может отсутствовать — тогда
отдаётся основной ``kubeconfig``.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
@@ -12,11 +26,17 @@ from __future__ import annotations
import logging
import os
import shutil
import subprocess
import tempfile
import threading
from pathlib import Path
logger = logging.getLogger("kind_k8s.kubeconfig_patch")
# Имя файла kubeconfig с server для доступа с хоста (не использовать для kubectl в контейнере веб-приложения).
KUBECONFIG_HOST_BASENAME = "kubeconfig.host"
def _container_cli() -> str:
"""CLI для `port` (обычно docker к сокету Docker или Podman)."""
@@ -57,8 +77,19 @@ def _host_bind_for_kubeconfig(host: str) -> str:
return host
def kubeconfig_download_client_hostname() -> str:
"""
Имя хоста для поля ``server`` в kubeconfig, скачиваемом пользователем.
По умолчанию ``localhost`` (совпадает с SAN в сертификате kind чаще, чем проблемный 127.0.0.1).
Задаётся ``KIND_K8S_KUBECONFIG_CLIENT_HOST`` (например ``192.168.1.10`` для удалённого клиента).
"""
raw = (os.environ.get("KIND_K8S_KUBECONFIG_CLIENT_HOST") or "localhost").strip()
return raw or "localhost"
def get_apiserver_host_port(cluster_name: str) -> tuple[str, str] | None:
"""Узнать (host, port) с хоста для доступа к apiserver."""
"""Узнать (host, port) привязки проброса 6443/tcp (host из вывода docker port, порт — число)."""
cli = _container_cli()
ctr = _control_plane_container_name(cluster_name)
p = subprocess.run(
@@ -126,8 +157,9 @@ def patch_kubeconfig_server_for_host(
"kubeconfig оставлен как выдал kind (с хоста может не открываться).",
)
return False
host, port = hp
server = f"https://{host}:{port}"
_bind_host, port = hp
client_host = kubeconfig_download_client_hostname()
server = f"https://{client_host}:{port}"
cluster_id = _kubeconfig_cluster_name(kube_path, cluster_name)
p = subprocess.run(
[
@@ -155,3 +187,155 @@ def should_patch_after_create() -> bool:
if os.environ.get("KIND_K8S_PATCH_KUBECONFIG", "").strip().lower() in ("1", "true", "yes", "да"):
return True
return os.environ.get("KIND_K8S_IN_CONTAINER", "").strip() == "1"
def in_kind_k8s_container() -> bool:
"""Процесс веб-приложения в Docker/Podman (``KIND_K8S_IN_CONTAINER=1``)."""
return os.environ.get("KIND_K8S_IN_CONTAINER", "").strip() == "1"
def kubeconfig_host_file(cluster_data_dir: Path) -> Path:
"""Путь к kubeconfig для скачивания на хост: ``clusters/<имя>/kubeconfig.host``."""
return cluster_data_dir / KUBECONFIG_HOST_BASENAME
def apiserver_url_docker_bridge(cluster_name: str) -> str:
"""
URL apiserver по имени узла kind в общей Docker-сети (если она есть).
Стандартное имя контрольной плоскости: ``<cluster>-control-plane``.
"""
return f"https://{_control_plane_container_name(cluster_name)}:6443"
def _web_container_apiserver_endpoint(cluster_name: str) -> tuple[str, str | None]:
"""
Пара ``(server URL, tls-server-name)`` для kubeconfig процесса в контейнере веб-UI.
Через шлюз (``host.docker.internal:<порт>``) в сертификате kind нет этого имени;
задаём ``tls-server-name`` (по умолчанию ``localhost``), которое есть в SAN.
Без проброса порта — прямой ``https://<имя>-control-plane:6443``, ``tls-server-name`` не нужен.
"""
hp = get_apiserver_host_port(cluster_name)
if hp:
_bind_host, port = hp
gw = (os.environ.get("KIND_K8S_APISERVER_GATEWAY_HOST") or "host.docker.internal").strip()
if not gw:
gw = "host.docker.internal"
server = f"https://{gw}:{port}"
tls_name = (os.environ.get("KIND_K8S_KUBECONFIG_TLS_SERVER_NAME") or "localhost").strip() or "localhost"
return server, tls_name
return apiserver_url_docker_bridge(cluster_name), None
def apiserver_url_for_web_container_process(cluster_name: str) -> str:
"""Только URL apiserver для ``kubectl`` в контейнере веб-приложения (см. :func:`_web_container_apiserver_endpoint`)."""
url, _tls = _web_container_apiserver_endpoint(cluster_name)
return url
# Кэш временных kubeconfig для kubectl из контейнера: ключ → (mtime_ns исходника, путь).
# Версия в ключе — сброс кэша при смене логики (например tls-server-name).
_RUNTIME_KUBECONFIG_CACHE_VER = 2
_kubectl_runtime_cache: dict[str, tuple[int, Path]] = {}
_kubectl_runtime_lock = threading.Lock()
def kubeconfig_path_for_container_kubectl(*, cluster_name: str, kube_source_path: Path) -> Path:
"""
Путь к kubeconfig для вызова ``kubectl`` из процесса в контейнере.
Если ``KIND_K8S_IN_CONTAINER=1``, копия во временный файл: ``server`` через шлюз хоста
и при необходимости ``tls-server-name`` (SAN kind: ``localhost``, …).
Иначе возвращает ``kube_source_path`` без изменений.
Поддерживает старые установки с одним «хостовым» kubeconfig на диске.
"""
if not in_kind_k8s_container():
return kube_source_path
try:
src = kube_source_path.resolve()
mtime_ns = int(src.stat().st_mtime_ns)
except OSError as e:
logger.debug("kubeconfig runtime: нет исходника %s: %s", kube_source_path, e)
return kube_source_path
key = f"{cluster_name}\x00{src}\x00{_RUNTIME_KUBECONFIG_CACHE_VER}"
with _kubectl_runtime_lock:
hit = _kubectl_runtime_cache.get(key)
if hit is not None and hit[0] == mtime_ns:
return hit[1]
try:
fd, temp_name = tempfile.mkstemp(suffix=".kubeconfig", prefix="kind-k8s-kubectl-")
os.close(fd)
tmp_path = Path(temp_name)
shutil.copyfile(src, tmp_path, follow_symlinks=True)
except OSError as e:
logger.warning("kubeconfig runtime: не удалось скопировать %s: %s", src, e)
return kube_source_path
cluster_id = _kubeconfig_cluster_name(tmp_path, cluster_name)
server, tls_server_name = _web_container_apiserver_endpoint(cluster_name)
p = subprocess.run(
[
"kubectl",
"--kubeconfig",
str(tmp_path),
"config",
"set-cluster",
cluster_id,
f"--server={server}",
],
capture_output=True,
text=True,
)
if p.returncode != 0:
err = (p.stderr or p.stdout or "").strip()
logger.warning(
"kubeconfig runtime: kubectl set-cluster не удался для %s: %s",
cluster_name,
err[:500],
)
tmp_path.unlink(missing_ok=True)
return kube_source_path
if tls_server_name:
p_tls = subprocess.run(
[
"kubectl",
"--kubeconfig",
str(tmp_path),
"config",
"set-cluster",
cluster_id,
f"--tls-server-name={tls_server_name}",
],
capture_output=True,
text=True,
)
if p_tls.returncode != 0:
err_tls = (p_tls.stderr or p_tls.stdout or "").strip()
logger.warning(
"kubeconfig runtime: tls-server-name для %s не задан: %s",
cluster_name,
err_tls[:400],
)
prev = _kubectl_runtime_cache.pop(key, None)
if prev is not None:
try:
prev[1].unlink(missing_ok=True)
except OSError:
pass
_kubectl_runtime_cache[key] = (mtime_ns, tmp_path)
logger.debug(
"kubeconfig runtime: кластер «%s» server=%s tls-sni=%s (cluster=%s)",
cluster_name,
server,
tls_server_name or "",
cluster_id,
)
return tmp_path