- Цель 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 документации (ранее в рабочем дереве)
140 lines
5.1 KiB
Python
140 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
||
"""Интерактивное или пакетное удаление кластера kind и локальной папки с конфигами.
|
||
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
|
||
Пакетный режим: ``--non-interactive --name ИМЯ [--yes]`` (``--yes`` пропускает подтверждение).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import logging
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
|
||
from core.cluster_lifecycle import KindClusterError, delete_kind_cluster_and_data
|
||
from kind_k8s_paths import clusters_dir
|
||
|
||
|
||
def _configure_logging() -> None:
|
||
if logging.root.handlers:
|
||
return
|
||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||
|
||
|
||
def _list_kind_clusters() -> 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 x.lower() not in ("no kind clusters found.", "no kind clusters found")]
|
||
|
||
|
||
def _ask(prompt: str) -> str:
|
||
return input(f"{prompt}: ").strip()
|
||
|
||
|
||
def _interactive() -> None:
|
||
print("=== Удаление кластера kind ===\n")
|
||
CLUSTERS_DIR = clusters_dir()
|
||
if not shutil.which("kind"):
|
||
print("Не найден kind.", file=sys.stderr)
|
||
print(" Через Docker: из корня репозитория «make docker up» и удаление в веб-интерфейсе.", file=sys.stderr)
|
||
sys.exit(127)
|
||
|
||
clusters = _list_kind_clusters()
|
||
if not clusters:
|
||
print("Нет зарегистрированных кластеров kind.")
|
||
only_dirs = (
|
||
sorted(p.name for p in CLUSTERS_DIR.iterdir() if p.is_dir() and not p.name.startswith("."))
|
||
if CLUSTERS_DIR.is_dir()
|
||
else []
|
||
)
|
||
if only_dirs:
|
||
print("Есть локальные папки в clusters/: ", ", ".join(only_dirs))
|
||
name = _ask("Удалить только данные в clusters/<имя> (без kind delete)? Введите имя или пусто для выхода")
|
||
if not name:
|
||
return
|
||
d = CLUSTERS_DIR / name
|
||
if d.is_dir():
|
||
shutil.rmtree(d)
|
||
print(f"Удалена папка {d}")
|
||
else:
|
||
print("Папка не найдена.")
|
||
return
|
||
|
||
print("Существующие кластеры kind:")
|
||
for i, c in enumerate(clusters, 1):
|
||
print(f" {i}) {c}")
|
||
raw = _ask("\nВведите имя кластера или номер из списка")
|
||
if not raw:
|
||
print("Отмена.")
|
||
return
|
||
if raw.isdigit():
|
||
idx = int(raw, 10)
|
||
if idx < 1 or idx > len(clusters):
|
||
print("Неверный номер.")
|
||
sys.exit(1)
|
||
name = clusters[idx - 1]
|
||
else:
|
||
name = raw
|
||
if name not in clusters:
|
||
print(f"Кластер «{name}» не найден в kind. Отмена.")
|
||
sys.exit(1)
|
||
|
||
confirm = _ask(f"Удалить кластер «{name}» и папку clusters/{name}? (yes/no)")
|
||
if confirm.lower() not in ("yes", "y", "да", "д"):
|
||
print("Отмена.")
|
||
return
|
||
|
||
try:
|
||
kind_ok, summary = delete_kind_cluster_and_data(name=name, log_to_stdout=True)
|
||
except KindClusterError as e:
|
||
print(str(e), file=sys.stderr)
|
||
raise SystemExit(getattr(e, "exit_code", 1)) from e
|
||
|
||
if not kind_ok:
|
||
print("kind delete завершился с ошибкой; локальная папка удалена при наличии.", file=sys.stderr)
|
||
print(summary)
|
||
print("Готово.")
|
||
|
||
|
||
def _parse_args() -> argparse.Namespace:
|
||
p = argparse.ArgumentParser(description="Удаление кластера kind")
|
||
p.add_argument("--non-interactive", action="store_true", help="Без диалогов")
|
||
p.add_argument("--name", help="Имя кластера")
|
||
p.add_argument("--yes", "-y", action="store_true", help="Не спрашивать подтверждение (только с --non-interactive)")
|
||
return p.parse_args()
|
||
|
||
|
||
def main() -> None:
|
||
_configure_logging()
|
||
args = _parse_args()
|
||
|
||
if args.non_interactive:
|
||
if not args.name:
|
||
print("Нужен --name в режиме --non-interactive.", file=sys.stderr)
|
||
raise SystemExit(2)
|
||
if not args.yes:
|
||
print("Добавьте --yes для подтверждения удаления в пакетном режиме.", file=sys.stderr)
|
||
raise SystemExit(2)
|
||
if not shutil.which("kind"):
|
||
print("Не найден kind.", file=sys.stderr)
|
||
sys.exit(127)
|
||
try:
|
||
kind_ok, summary = delete_kind_cluster_and_data(name=args.name.strip())
|
||
except KindClusterError as e:
|
||
print(str(e), file=sys.stderr)
|
||
raise SystemExit(getattr(e, "exit_code", 1)) from e
|
||
print(summary)
|
||
raise SystemExit(0 if kind_ok else 1)
|
||
|
||
_interactive()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|