Files
KindClustersDashboard/app/create_cluster.py
Sergey Antropoff eb063aec20 Веб-интерфейс: страница /clusters, навигация и крошки для кластеров
- Выделена страница списка кластеров, панель упрощена; nav_active и крошки
  ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL.
- Доработки дашборда, аддонов, журнала, стилей и API-документации.
- Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
2026-04-04 13:42:21 +03:00

287 lines
12 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.

#!/usr/bin/env python3
"""Интерактивное или пакетное создание локального кластера Kubernetes через kind.
Сохраняет kind-config.yaml, kubeconfig и meta.json в подпапку clusters/<имя>/.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
Требования: kind, клиент контейнеров (``docker`` к сокету Docker/Podman) и kubectl в PATH.
Рекомендуется: веб-интерфейс (``make docker up``) — всё внутри Docker, на хосте только Docker и make.
Пакетный режим: ``--non-interactive --name X --kubernetes-version 1.29.4 [--workers N]``.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import shutil
import subprocess
import sys
from core.cluster_lifecycle import (
CreateClusterResult,
KindClusterError,
create_cluster_non_interactive,
list_registered_kind_clusters,
normalize_k8s_version,
validate_cluster_name,
)
from kindest_node_tags import fetch_kindest_node_tags, normalize_tag_v_prefix
from kind_k8s_paths import clusters_dir, container_cli_name
# Имя кластера: поддомен DNS (RFC 1123) — дублируем только для CLI-подсказок;
# основная проверка в core.cluster_lifecycle.
def _which(cmd: str) -> str | None:
return shutil.which(cmd)
def _container_cli_bin() -> str:
return container_cli_name()
def _in_container() -> bool:
return os.environ.get("KIND_K8S_IN_CONTAINER", "").strip() == "1"
def _ask(prompt: str, default: str | None = None) -> str:
if default is not None:
line = input(f"{prompt} [{default}]: ").strip()
return line if line else default
line = input(f"{prompt}: ").strip()
return line
def _ask_int(prompt: str, default: int, *, min_v: int, max_v: int) -> int:
while True:
raw = _ask(prompt, str(default))
try:
n = int(raw, 10)
except ValueError:
print("Введите целое число.")
continue
if n < min_v or n > max_v:
print(f"Допустимый диапазон: {min_v}{max_v}.")
continue
return n
def _configure_logging() -> None:
"""Базовая настройка логов для вспомогательных модулей (kindest_node_tags и т.д.)."""
if logging.root.handlers:
return
level = logging.DEBUG if os.environ.get("KIND_K8S_DEBUG", "").strip() in ("1", "true", "yes") else logging.INFO
logging.basicConfig(level=level, format="%(levelname)s %(name)s: %(message)s")
def _display_limit_for_version_list() -> int:
raw = (os.environ.get("KIND_K8S_VERSION_LIST_DISPLAY") or "50").strip()
try:
return max(5, min(int(raw), 500))
except ValueError:
return 50
def _interactive_k8s_version_tag() -> str:
"""
Спросить версию Kubernetes: загрузить теги kindest/node с Docker Hub (1.19+), показать список,
выбор по номеру или ввод тега вручную.
"""
skip = os.environ.get("KIND_K8S_SKIP_VERSION_LIST", "").strip().lower() in ("1", "true", "yes", "да")
tags: list[str] = []
if not skip:
print(
"Загрузка списка стабильных тегов kindest/node с Docker Hub (Kubernetes 1.19+, нужна сеть)…",
flush=True,
)
tags = fetch_kindest_node_tags()
if not tags:
print(
"Предупреждение: список тегов недоступен (сеть, лимит Docker Hub или пустой ответ). "
"Введите версию вручную.",
flush=True,
)
else:
print("Загрузка списка пропущена (переменная KIND_K8S_SKIP_VERSION_LIST).", flush=True)
if not tags:
raw = _ask("Версия Kubernetes / тег образа kindest/node (например 1.29.4)", "1.29.4")
return normalize_k8s_version(raw)
display_n = _display_limit_for_version_list()
print(
f"\nДоступные теги (всего {len(tags)}): сначала latest, затем стабильные семверы от новых к старым:",
flush=True,
)
for i, t in enumerate(tags[:display_n], start=1):
print(f" {i:3}) {t}", flush=True)
if len(tags) > display_n:
print(
f" … показаны первые {display_n} из {len(tags)}; можно ввести номер от 1 до {len(tags)} "
"или тег вручную (например 1.25.11).",
flush=True,
)
default_choice = "1"
while True:
raw = _ask(f"Номер строки (1{len(tags)}) или версия вручную", default_choice)
choice = raw if raw else default_choice
if choice.isdigit():
idx = int(choice, 10)
if 1 <= idx <= len(tags):
picked = tags[idx - 1]
print(f"Выбран образ kindest/node:{picked}", flush=True)
return picked
print(f"Введите число от 1 до {len(tags)} или тег версии (например 1.28.0).", flush=True)
continue
ver = normalize_k8s_version(choice)
canon = normalize_tag_v_prefix(ver)
if canon not in tags:
print(
f"Примечание: «{ver}» нет среди загруженных тегов; при отсутствии образа в реестре kind сообщит об ошибке.",
flush=True,
)
return ver
def _run_interactive() -> None:
print("=== Создание кластера kind ===\n")
if not _which("kind"):
print("Не найден бинарник kind.", file=sys.stderr)
print(" Установка kind на хост: https://kind.sigs.k8s.io/docs/user/quick-start/#installation", file=sys.stderr)
print(" Через Docker: из корня репозитория выполните «make docker up» и откройте веб-интерфейс.", file=sys.stderr)
sys.exit(127)
cli = _container_cli_bin()
if not _which(cli):
print(f"Не найден «{cli}» (CLI к API контейнеров).", file=sys.stderr)
sys.exit(127)
existing = list_registered_kind_clusters()
default_name = "dev"
if default_name in existing:
default_name = "dev2"
while True:
name = _ask("Имя кластера (DNS-имя, a-z0-9-)", default_name)
if not validate_cluster_name(name):
print("Некорректное имя: только строчные буквы, цифры, дефис; не длиннее 63 символов.")
continue
if name in existing:
print(f"Кластер «{name}» уже существует в kind. Удалите его в веб-интерфейсе или другое имя.")
continue
break
ver_tag = _interactive_k8s_version_tag()
workers = _ask_int(
"Количество worker-нод (0 = только control-plane, он же может принимать поды)",
2,
min_v=0,
max_v=20,
)
try:
result = create_cluster_non_interactive(name=name, kubernetes_version_tag=ver_tag, workers=workers)
except KindClusterError as e:
print(str(e), file=sys.stderr)
raise SystemExit(getattr(e, "exit_code", 1)) from e
print("\nГотово.")
print(f" kubeconfig (в среде запуска): {result.kubeconfig_path}")
if _in_container():
print(f" Том на хосте: clusters/{result.cluster_name}/ в корне репозитория (рядом с Makefile)")
if result.kubeconfig_patched_for_host:
print(
f" Kubectl **с хоста**: clusters/{result.cluster_name}/kubeconfig.host "
f"(или скачивание из веб-UI) — server=<KIND_K8S_KUBECONFIG_CLIENT_HOST или localhost>:<порт>.",
)
print(
" Файл kubeconfig (без .host) — как выдал kind; статистика в UI и kubectl внутри приложения "
"используют apiserver через шлюз хоста (host.docker.internal:<порт> из docker port; см. compose extra_hosts).",
)
else:
print(
f" Проверка в этом контейнере: kubectl --kubeconfig=/work/clusters/{result.cluster_name}/kubeconfig get nodes",
)
else:
print(" Проверка без kubectl на хосте: веб-интерфейс (кластер → узлы/поды) или из корня репозитория:")
print(f" make docker kubectl CLUSTER={result.cluster_name} # или: make podman kubectl …")
print(" (сервис kind-k8s-web должен быть запущен: make docker up).")
kc_for_shell = (
result.kubeconfig_path.parent / "kubeconfig.host"
if result.kubeconfig_patched_for_host
else result.kubeconfig_path
)
print(f" Локально при установленном kubectl: kubectl --kubeconfig={kc_for_shell} get nodes")
if result.kubeconfig_patched_for_host:
print(" (файл kubeconfig.host — apiserver: localhost или KIND_K8S_KUBECONFIG_CLIENT_HOST и порт Docker.)")
if result.nodes_ready is False and result.nodes_ready_message:
print(f" Предупреждение (ожидание нод): {result.nodes_ready_message}", file=sys.stderr)
def _parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Создание кластера kind (интерактивно или --non-interactive).")
p.add_argument(
"--non-interactive",
action="store_true",
help="Без диалогов; обязательны --name и --kubernetes-version",
)
p.add_argument("--name", help="Имя кластера (DNS, a-z0-9-)")
p.add_argument(
"--kubernetes-version",
dest="kubernetes_version",
help="Версия / тег kindest/node, например 1.29.4 или v1.29.4",
)
p.add_argument("--workers", type=int, default=2, help="Число worker-нод (020), по умолчанию 2")
return p.parse_args()
def main() -> None:
_configure_logging()
args = _parse_args()
if args.non_interactive:
if not args.name or not args.kubernetes_version:
print("В режиме --non-interactive нужны --name и --kubernetes-version.", file=sys.stderr)
raise SystemExit(2)
try:
result = create_cluster_non_interactive(
name=args.name.strip(),
kubernetes_version_tag=args.kubernetes_version.strip(),
workers=args.workers,
)
except KindClusterError as e:
print(str(e), file=sys.stderr)
raise SystemExit(getattr(e, "exit_code", 1)) from e
print(_json_result_summary(result))
return
_run_interactive()
def _json_result_summary(result: CreateClusterResult) -> str:
"""JSON для stdout в пакетном режиме ``--non-interactive``."""
payload = {
"cluster_name": result.cluster_name,
"kubernetes_version_tag": result.ver_tag,
"node_image": result.node_image,
"workers": result.workers,
"kubeconfig_path": str(result.kubeconfig_path),
"kubeconfig_patched_for_host": result.kubeconfig_patched_for_host,
"nodes_ready": result.nodes_ready,
"nodes_ready_message": result.nodes_ready_message,
}
return json.dumps(payload, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()