- Цель 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 документации (ранее в рабочем дереве)
184 lines
6.2 KiB
Python
Executable File
184 lines
6.2 KiB
Python
Executable File
#!/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()
|