Веб-UI кластера: страница деталей, kubectl по карточкам, мета 3 колонки
- Страница /cluster/<имя>: сводка, ресурсы узлов полосами, отдельные карточки на каждый kubectl get … -o json, рестарт пода. - API: overview с блоками k8s_*, POST pods/restart, расширенный набор ресурсов (-A). - Панель: спиннер загрузки, правки дашборда и стилей; документация api_routes, compose и прочие сопутствующие изменения.
This commit is contained in:
+190
-6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user