Files
KindClustersDashboard/app/kubeconfig_patch.py
T
Sergey Antropoff af0d1705cc Веб-UI кластера: страница деталей, kubectl по карточкам, мета 3 колонки
- Страница /cluster/<имя>: сводка, ресурсы узлов полосами, отдельные карточки на каждый kubectl get … -o json, рестарт пода.
- API: overview с блоками k8s_*, POST pods/restart, расширенный набор ресурсов (-A).
- Панель: спиннер загрузки, правки дашборда и стилей; документация api_routes, compose и прочие сопутствующие изменения.
2026-04-04 09:13:08 +03:00

342 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Правка kubeconfig kind для доступа к API с хоста (после create из контейнера).
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
"""
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)."""
return (os.environ.get("CONTAINER_CLI") or "docker").strip() or "docker"
def _control_plane_container_name(cluster_name: str) -> str:
return f"{cluster_name}-control-plane"
def _parse_docker_port_line(line: str) -> tuple[str, str] | None:
"""Строка вида '0.0.0.0:32768' или '127.0.0.1:32768' -> (host, port)."""
line = line.strip()
if ":" not in line:
return None
# IPv6 [::]:port
if line.startswith("["):
rb = line.rfind("]")
if rb == -1:
return None
host = line[1:rb]
rest = line[rb + 1 :].lstrip(":")
if not rest.isdigit():
return None
return host, rest
host, _, port = line.rpartition(":")
if not port.isdigit():
return None
host = host.strip()
return host, port
def _host_bind_for_kubeconfig(host: str) -> str:
if host in ("0.0.0.0", "::", ""):
return "127.0.0.1"
if host == "[::]":
return "127.0.0.1"
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) привязки проброса 6443/tcp (host из вывода docker port, порт — число)."""
cli = _container_cli()
ctr = _control_plane_container_name(cluster_name)
p = subprocess.run(
[cli, "port", ctr, "6443/tcp"],
capture_output=True,
text=True,
)
if p.returncode != 0:
logger.info(
"Не удалось %s port %s 6443/tcp: %s",
cli,
ctr,
(p.stderr or p.stdout or "").strip(),
)
return None
for raw in (p.stdout or "").splitlines():
parsed = _parse_docker_port_line(raw)
if not parsed:
continue
h, port = parsed
return _host_bind_for_kubeconfig(h), port
return None
def _kubeconfig_cluster_name(kube_path: Path, logical_name: str) -> str:
"""Имя блока cluster в kubeconfig (обычно kind-<имя>)."""
p = subprocess.run(
[
"kubectl",
"--kubeconfig",
str(kube_path),
"config",
"view",
"-o",
'jsonpath={range .clusters[*]}{.name}{"\n"}{end}',
],
capture_output=True,
text=True,
)
names = [x.strip() for x in (p.stdout or "").splitlines() if x.strip()]
want = f"kind-{logical_name}"
if want in names:
return want
for n in names:
if n.startswith("kind-"):
return n
return want
def patch_kubeconfig_server_for_host(
*,
cluster_name: str,
kube_path: Path,
) -> bool:
"""
Подставить в kubeconfig server=https://<хост>:<порт> для доступа с хоста.
Порт берётся из ``docker port`` / аналога к сокету; для Podman — тот же клиент
при ``DOCKER_HOST`` на podman.sock.
"""
hp = get_apiserver_host_port(cluster_name)
if not hp:
print(
"Предупреждение: не удалось получить порт apiserver с хоста; "
"kubeconfig оставлен как выдал kind (с хоста может не открываться).",
)
return False
_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(
[
"kubectl",
"--kubeconfig",
str(kube_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()
print(f"Предупреждение: kubectl config set-cluster не удался: {err}")
return False
print(f"Kubeconfig для хоста: apiserver → {server}")
return True
def should_patch_after_create() -> bool:
"""Патчить после create, если задано явно или kind шёл из контейнера."""
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