Files
KindClustersDashboard/app/kubeconfig_patch.py
Sergey Antropoff 4b703801e1 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 обновлены
2026-04-04 18:54:10 +03:00

413 lines
16 KiB
Python
Raw Permalink 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`).
- Скачивание через API **``GET …/kubeconfig/docker``** — тот же endpoint, что для kubectl в веб-контейнере:
``https://<KIND_K8S_APISERVER_GATEWAY_HOST>:<порт>`` (по умолчанию **host.docker.internal** и порт из ``docker port``)
плюс **tls-server-name** (см. :func:`patch_kubeconfig_server_for_docker_network`), чтобы **любой** контейнер с
``extra_hosts: host.docker.internal:host-gateway`` достучался до API через хост, без общей сети с kind.
Если патч отключён или не удалился, ``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
from kind_k8s_paths import container_cli_name
logger = logging.getLogger("kind_k8s.kubeconfig_patch")
# Имя файла kubeconfig с server для доступа с хоста (не использовать для kubectl в контейнере веб-приложения).
KUBECONFIG_HOST_BASENAME = "kubeconfig.host"
def _container_cli() -> str:
"""CLI для ``port`` к смонтированному сокету (в контейнере веб-UI — ``docker``)."""
return container_cli_name()
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 apply_apiserver_endpoint_to_kubeconfig_file(
*,
cluster_name: str,
kube_path: Path,
) -> tuple[bool, str, str | None, str]:
"""
Выставить в kubeconfig ``server`` и при необходимости ``tls-server-name`` по
:func:`_web_container_apiserver_endpoint` (шлюз хоста + порт или запасной ``*-control-plane:6443``).
Возвращает ``(успех, server, tls_server_name|None, cluster_id_в_kubeconfig)``.
"""
cluster_id = _kubeconfig_cluster_name(kube_path, cluster_name)
server, tls_server_name = _web_container_apiserver_endpoint(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()
logger.warning(
"apply apiserver kubeconfig: set-cluster не удался для %s: %s",
cluster_name,
err[:500],
)
return False, server, tls_server_name, cluster_id
if tls_server_name:
p_tls = subprocess.run(
[
"kubectl",
"--kubeconfig",
str(kube_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(
"apply apiserver kubeconfig: tls-server-name для %s: %s",
cluster_name,
err_tls[:400],
)
else:
subprocess.run(
[
"kubectl",
"--kubeconfig",
str(kube_path),
"config",
"unset",
f"clusters.{cluster_id}.tls-server-name",
],
capture_output=True,
text=True,
)
return True, server, tls_server_name, cluster_id
def patch_kubeconfig_server_for_docker_network(
*,
cluster_name: str,
kube_path: Path,
) -> bool:
"""
Kubeconfig для **kubectl из произвольного контейнера** (не обязательно в сети kind).
При успешном ``docker port … 6443/tcp``: ``server=https://<шлюз>:<порт>`` (по умолчанию
**host.docker.internal**, см. ``KIND_K8S_APISERVER_GATEWAY_HOST``) и **tls-server-name**
(по умолчанию **localhost**, ``KIND_K8S_KUBECONFIG_TLS_SERVER_NAME``) — как у
:func:`kubeconfig_path_for_container_kubectl`. Трафик идёт на хост и в проброшенный порт
control plane, DNS **имени узла kind не нужен**.
Если порт с хоста не получить — запасной ``https://<имя>-control-plane:6443`` без SNI
(нужна общая Docker-сеть с узлом kind).
"""
ok, server, tls_sni, cluster_id = apply_apiserver_endpoint_to_kubeconfig_file(
cluster_name=cluster_name,
kube_path=kube_path,
)
if ok:
logger.info(
"kubeconfig для стороннего контейнера: %s tls-server-name=%s (cluster=%s)",
server,
tls_sni or "",
cluster_id,
)
return ok
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
ok, server, tls_server_name, cluster_id = apply_apiserver_endpoint_to_kubeconfig_file(
cluster_name=cluster_name,
kube_path=tmp_path,
)
if not ok:
tmp_path.unlink(missing_ok=True)
return kube_source_path
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