first commit
This commit is contained in:
174
app/cluster_status.py
Executable file
174
app/cluster_status.py
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/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 (как на хосте после make create).")
|
||||
|
||||
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 -C kind-k8s-develop status (kind внутри образа).", 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.", 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()
|
||||
Reference in New Issue
Block a user