Files
KindClustersDashboard/app/cluster_status.py
Sergey Antropoff d1a01cca9e Документация и kubectl из контейнера; Kind Clusters Dashboard
- Цель make docker|podman kubectl CLUSTER=… (KUBECTL_ARGS) — exec kubectl в kind-k8s-web
- README: без kubectl на хосте; раздел про проверку API из контейнера
- create_cluster/cluster_status: подсказки для UI, make kubectl и exec в контейнере
- app/docs: api_routes.md и README.md про kubectl и API workloads
- Прочее: переименование проекта, документация, UI документации (ранее в рабочем дереве)
2026-04-04 06:27:18 +03:00

184 lines
6.2 KiB
Python
Executable File
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
"""Проверка статуса кластеров kind: регистрация, kubeconfig, узлы (kubectl).
Запуск без аргументов — все кластеры из `kind get clusters`.
Один аргумент — только указанное имя.
Автор: Сергей Антропов
Сайт: https://devops.org.ru
Требования: kind и kubectl в PATH (в образе kind-k8s-tools уже есть).
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from kind_k8s_paths import clusters_dir
def _kind_cluster_names() -> list[str]:
p = subprocess.run(["kind", "get", "clusters"], capture_output=True, text=True)
if p.returncode != 0:
return []
lines = [x.strip() for x in (p.stdout or "").splitlines() if x.strip()]
return [x for x in lines if "no kind" not in x.lower()]
def _kubeconfig_for_cluster(name: str) -> tuple[str | None, str]:
"""Временный файл kubeconfig или None; второе значение — пояснение."""
p = subprocess.run(
["kind", "get", "kubeconfig", "--name", name],
capture_output=True,
text=True,
)
if p.returncode != 0:
err = (p.stderr or p.stdout or "").strip()
return None, err or "kind get kubeconfig не удался"
data = (p.stdout or "").strip()
if not data:
return None, "пустой kubeconfig"
with tempfile.NamedTemporaryFile(
mode="w",
suffix=".kubeconfig",
prefix=f"kind-{name}-",
delete=False,
encoding="utf-8",
) as f:
f.write(data)
return f.name, ""
def _kubectl_nodes(kubeconfig: str) -> tuple[int, str]:
p = subprocess.run(
[
"kubectl",
"--kubeconfig",
kubeconfig,
"get",
"nodes",
"-o",
"wide",
"--request-timeout=10s",
],
capture_output=True,
text=True,
)
out = (p.stdout or "").strip()
err = (p.stderr or "").strip()
msg = out if out else err
return p.returncode, msg
def _local_meta(name: str) -> dict[str, str] | None:
meta = CLUSTERS_DIR / name / "meta.json"
if not meta.is_file():
return None
try:
import json
raw = json.loads(meta.read_text(encoding="utf-8"))
if isinstance(raw, dict):
return {str(k): str(v) for k, v in raw.items() if isinstance(k, str)}
except Exception:
pass
return None
def _print_cluster(name: str, *, kube_path_saved: Path | None) -> None:
print(f"── Кластер: {name} ──")
if kube_path_saved and kube_path_saved.is_file():
print(f" Сохранённый kubeconfig: {kube_path_saved}")
meta = _local_meta(name)
if meta:
ver = meta.get("kubernetes_version_tag") or meta.get("node_image", "")
wn = meta.get("worker_nodes", "")
print(f" meta.json: версия={ver}, workers={wn}")
# С хоста удобнее тот же файл, что после create (в т.ч. с патчем 127.0.0.1:порт).
use_path: str | None = None
if kube_path_saved and kube_path_saved.is_file():
use_path = str(kube_path_saved)
print(" Проверка API: kubectl с сохранённым kubeconfig (тот же файл, что пишет create / UI).")
tmp_kc: str | None = None
if not use_path:
tmp_kc, kerr = _kubeconfig_for_cluster(name)
if not tmp_kc:
print(f" Статус API: недоступен ({kerr})")
return
use_path = tmp_kc
try:
rc, msg = _kubectl_nodes(use_path)
if rc == 0:
print(" Узлы (kubectl get nodes -o wide):")
for line in msg.splitlines():
print(f" {line}")
else:
print(f" kubectl: код {rc}")
for line in msg.splitlines()[:20]:
print(f" {line}")
finally:
if tmp_kc:
Path(tmp_kc).unlink(missing_ok=True)
def main() -> None:
CLUSTERS_DIR = clusters_dir()
parser = argparse.ArgumentParser(description="Статус кластеров kind")
parser.add_argument(
"cluster",
nargs="?",
help="Имя кластера (если не указано — все из kind get clusters)",
)
args = parser.parse_args()
if not shutil.which("kind"):
print("Не найден kind.", file=sys.stderr)
print(" Статус узлов — в веб-интерфейсе (make docker up) или внутри контейнера kind-k8s-web.", file=sys.stderr)
print(
" Либо установите kind на хост: https://kind.sigs.k8s.io/docs/user/quick-start/#installation",
file=sys.stderr,
)
sys.exit(127)
if not shutil.which("kubectl"):
print("Не найден kubectl в PATH.", file=sys.stderr)
print(
" Запустите скрипт внутри контейнера приложения (там kubectl в образе), например:",
file=sys.stderr,
)
print(
" docker compose exec kind-k8s-web python3 /opt/kind-k8s/app/cluster_status.py <имя>",
file=sys.stderr,
)
print(" Либо смотрите узлы в веб-интерфейсе; на хост kubectl не обязателен.", file=sys.stderr)
sys.exit(127)
names = _kind_cluster_names()
if args.cluster:
if args.cluster not in names:
print(f"Кластер «{args.cluster}» не найден в kind get clusters.", file=sys.stderr)
print("Известные:", ", ".join(names) if names else "(пусто)", file=sys.stderr)
sys.exit(1)
names = [args.cluster]
if not names:
print("Нет кластеров kind (kind get clusters).")
return
for name in names:
saved = CLUSTERS_DIR / name / "kubeconfig"
kube_saved = saved if saved.is_file() else None
_print_cluster(name, kube_path_saved=kube_saved)
print()
if __name__ == "__main__":
main()