Files
KindClustersDashboard/scripts/setup_env_interactive.py
Sergey Antropoff 02f4c655b9 UI: автообновление, прогресс, отмена; порт 8080; меню-пилюли и отдельные окна
- Порт хоста по умолчанию 8080 (Chrome ERR_UNSAFE_PORT на 6000); compose, setup, config, README.
- Дашборд: одна hero-карточка, прогресс создания, POST /jobs/{id}/cancel, JobView progress_*.
- job_store: отмена и прогресс (thread-safe); cluster_lifecycle этапы и откат.
- Навигация: стили nav-pill; Swagger/ReDoc/Health через window.open.
- main.py: TemplateResponse(request, …) для Starlette.
- Документация: README, app/docs (api_routes, README); Makefile ps; .gitignore clusters.
2026-04-04 05:58:11 +03:00

271 lines
11 KiB
Python
Raw 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
"""Интерактивное создание ``.env`` в корне kind-k8s-develop.
Список переменных и подсказок задаётся в этом файле (не используется env.example).
Дефолты совпадают с ``docker-compose.yml`` / приложением; Enter — записать предложенное значение.
Запуск: ``python3 scripts/setup_env_interactive.py`` или ``make setup``.
Опции: ``--output`` / ``-f`` (перезапись без вопроса).
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from __future__ import annotations
import argparse
import logging
import secrets
from dataclasses import dataclass
from pathlib import Path
logger = logging.getLogger("setup_env_interactive")
# Корень репозитория kind-k8s-develop (родитель каталога scripts/)
REPO_ROOT = Path(__file__).resolve().parents[1]
# Значения по умолчанию при нажатии Enter (как в docker-compose / Dockerfile / Settings).
# KUBECTL_VERSION: в Dockerfile пустой build-arg = stable.txt; здесь — закреплённый тег для воспроизводимости.
_SETUP_DEFAULTS: dict[str, str] = {
"KIND_VERSION": "0.24.0",
"KUBECTL_VERSION": "v1.32.0",
"CONTAINER_SOCKET": "/var/run/docker.sock",
"KIND_K8S_WEB_PORT": "8080",
"KIND_K8S_PATCH_KUBECONFIG": "1",
"CONTAINER_CLI": "docker",
"KIND_K8S_SKIP_VERSION_LIST": "",
"KIND_K8S_VERSION_LIST_DISPLAY": "50",
"KIND_K8S_HUB_TAGS_MAX_PAGES": "60",
"KIND_K8S_DEBUG": "",
"KIND_K8S_WAIT_NODES": "1",
"KIND_K8S_WAIT_NODES_TIMEOUT_SEC": "300",
"KIND_K8S_APP_TITLE": "Kind Clusters Dashboard",
"KIND_K8S_UVICORN_RELOAD": "1",
"KIND_K8S_WEB_HOST": "0.0.0.0",
"KIND_K8S_WORKDIR": "",
"COMPOSE_BUILD_FLAGS": "",
}
# Переменные, для которых по вводу «g» генерируется секрет (на будущее / другие проекты).
_SECRET_KEYS = frozenset(
{
"SECRET_KEY",
"CONNECTORS_ENCRYPTION_KEY",
"K8S_KUBECONFIG_ENCRYPTION_KEY",
"POSTGRES_PASSWORD",
"MINIO_SECRET_KEY",
"ROOT_PASSWORD",
},
)
@dataclass(frozen=True)
class _EnvPrompt:
"""Одна переменная в мастере setup: опциональный заголовок секции в .env и текст помощи."""
section_comment: str | None
key: str
help_ru: str
# Порядок опроса и человекочитаемые пояснения (секции — комментарии в записанном .env).
_SETUP_PROMPTS: tuple[_EnvPrompt, ...] = (
_EnvPrompt(
"# --- docker-compose.yml: build-args (образ kind-k8s-tools:local) ---",
"KIND_VERSION",
"Версия бинарника kind при сборке образа (build-arg KIND_VERSION).",
),
_EnvPrompt(
None,
"KUBECTL_VERSION",
"Версия kubectl в образе (build-arg). Пустая строка в .env при ручном вводе возможна; "
"дефолт скрипта — закреплённый тег (в Dockerfile без аргумента берётся stable.txt).",
),
_EnvPrompt(
"# --- docker-compose.yml: том сокета (Docker / Podman rootless) ---",
"CONTAINER_SOCKET",
"Путь к сокету на хосте для volume (Docker: /var/run/docker.sock; Podman rootless — см. README).",
),
_EnvPrompt(
"# --- docker-compose.yml: публикация веб-UI (хост:контейнер → …:6000 внутри) ---",
"KIND_K8S_WEB_PORT",
"Порт на хосте для браузера (дефолт 8080 — 6000 на хосте блокирует Chrome как ERR_UNSAFE_PORT); внутри контейнера uvicorn слушает 6000.",
),
_EnvPrompt(
"# --- docker-compose.yml: environment сервиса kind-k8s-web ---",
"KIND_K8S_PATCH_KUBECONFIG",
"1/true/yes — после create всегда патчить server в kubeconfig на 127.0.0.1:<порт> для доступа с хоста; "
"0 или пусто — не форсировать (в контейнере compose всё равно может сработать KIND_K8S_IN_CONTAINER). "
"По умолчанию в скрипте: 1 (включено).",
),
_EnvPrompt(None, "CONTAINER_CLI", "Имя CLI для вызовов к движку контейнеров: docker или podman."),
_EnvPrompt(
None,
"KIND_K8S_SKIP_VERSION_LIST",
"1 — не запрашивать теги kindest/node с Docker Hub (изолированная сеть); иначе пусто.",
),
_EnvPrompt(None, "KIND_K8S_VERSION_LIST_DISPLAY", "Сколько тегов отдавать в API/списке версий в UI."),
_EnvPrompt(None, "KIND_K8S_HUB_TAGS_MAX_PAGES", "Лимит страниц при обходе Docker Hub API."),
_EnvPrompt(None, "KIND_K8S_DEBUG", "1/true/yes — уровень DEBUG в логах приложения; иначе пусто (INFO)."),
_EnvPrompt(
None,
"KIND_K8S_WAIT_NODES",
"1 — после kind create ждать Ready нод через kubectl wait; 0 — не ждать.",
),
_EnvPrompt(None, "KIND_K8S_WAIT_NODES_TIMEOUT_SEC", "Таймаут kubectl wait (секунды)."),
_EnvPrompt(None, "KIND_K8S_APP_TITLE", "Заголовок OpenAPI и веб-интерфейса."),
_EnvPrompt(
None,
"KIND_K8S_UVICORN_RELOAD",
"1 — uvicorn --reload (правки в ./app без пересборки образа); 0 — один процесс без reload.",
),
_EnvPrompt(
"# --- pydantic Settings: KIND_K8S_WEB_HOST (в compose не передаётся; см. run_uvicorn.sh) ---",
"KIND_K8S_WEB_HOST",
"Хост привязки uvicorn при локальном запуске вне compose (в контейнере задаётся скриптом).",
),
_EnvPrompt(
"# --- Только локальный запуск app/*.py без docker-compose ---",
"KIND_K8S_WORKDIR",
"Корень данных на машине разработчика (в compose в контейнере задан /work литералом).",
),
_EnvPrompt(
"# --- Только Makefile (не переменные окружения compose) ---",
"COMPOSE_BUILD_FLAGS",
"Доп. флаги для «make docker compose-build», например --platform linux/arm64.",
),
)
_ENV_FILE_HEADER = """# Файл .env для kind-k8s-develop
# Создан интерактивно: scripts/setup_env_interactive.py
#
# В docker-compose.yml заданы литералами (не из этого файла):
# DOCKER_HOST, KIND_K8S_IN_CONTAINER, KIND_K8S_WORKDIR=/work в контейнере.
#
"""
def _configure_logging() -> None:
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
def _default_for_key(key: str) -> str:
"""Значение по умолчанию для ключа (только словарь скрипта)."""
return _SETUP_DEFAULTS.get(key, "")
def _ask_line(
key: str,
default: str,
*,
help_ru: str,
collected: dict[str, str],
) -> str | None:
"""
Спросить одну переменную.
Возвращает ``None``, если строку в ``.env`` не записывать (пропуск).
"""
extra = ""
if key in _SECRET_KEYS:
extra = " [g] — сгенерировать случайное значение"
hint = default.replace("\n", " ")[:72]
if len(default) > 72:
hint += ""
if not default:
hint = "пустая строка"
print(f"\n{key}")
print(f" {help_ru}")
print(f" По умолчанию: {hint!r}{extra}")
print(" Enter — подставить это значение в .env; - или «пропустить» — не добавлять строку")
raw = input(" Значение: ").strip()
if raw in ("-", "пропустить", "skip"):
return None
if not raw:
return default
if raw.lower() == "g" and key in _SECRET_KEYS:
generated = secrets.token_urlsafe(32)
print(f" Сгенерировано ({len(generated)} символов).")
return generated
return raw
def _default_database_url(collected: dict[str, str]) -> str | None:
"""Сборка DATABASE_URL из POSTGRES_* (если такие ключи появятся в мастере)."""
u = collected.get("POSTGRES_USER")
p = collected.get("POSTGRES_PASSWORD")
d = collected.get("POSTGRES_DB")
if u is not None and p is not None and d is not None:
return f"postgresql://{u}:{p}@postgres:5432/{d}"
return None
def run(*, output: Path, force: bool = False) -> int:
output = output.resolve()
if output.exists() and not force:
print(f"Файл уже существует: {output}")
ans = input("Перезаписать? [y/N]: ").strip().lower()
if ans not in ("y", "yes", "д", "да"):
print("Выход без изменений.")
return 0
print(
"\nИнтерактивное заполнение .env (список переменных в scripts/setup_env_interactive.py).\n"
"Enter без ввода — записать значение по умолчанию из подсказки.\n",
)
collected: dict[str, str] = {}
out_chunks: list[str] = [_ENV_FILE_HEADER]
for spec in _SETUP_PROMPTS:
if spec.section_comment:
out_chunks.append(f"{spec.section_comment}\n")
default = _default_for_key(spec.key)
if spec.key == "DATABASE_URL":
built = _default_database_url(collected)
if built is not None:
default = built
print("\n--- DATABASE_URL: собрано из POSTGRES_* ---")
value = _ask_line(spec.key, default, help_ru=spec.help_ru, collected=collected)
if value is None:
logger.debug("Пропуск переменной %s", spec.key)
continue
collected[spec.key] = value
out_chunks.append(f"{spec.key}={value}\n")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text("".join(out_chunks), encoding="utf-8")
print(f"\nГотово: записан файл {output}")
return 0
def main() -> None:
_configure_logging()
parser = argparse.ArgumentParser(
description="Интерактивное заполнение .env для kind-k8s-develop (переменные задаются в этом скрипте)",
)
parser.add_argument(
"--output",
type=Path,
default=REPO_ROOT / ".env",
help="Куда записать .env (по умолчанию: .env в корне репозитория)",
)
parser.add_argument("-f", "--force", action="store_true", help="Не спрашивать подтверждение перезаписи")
args = parser.parse_args()
raise SystemExit(run(output=args.output, force=args.force))
if __name__ == "__main__":
main()