Files
KindClustersDashboard/scripts/setup_env_interactive.py
Sergey Antropoff 4546f50aef UI: навигация, документация, favicon; журнал развёртывания; валидация формы
- Меню API: ссылка «Теги образов», обёртка прокрутки пилюль, z-index и padding против обрезки hover
- Документация: ширина колонки как у дашборда (72rem)
- Favicon SVG + GET /favicon.ico, link в base.html
- provision_log.json, GET .../provision-log, кнопка в таблице кластеров
- Валидация create: имя, workers, тег kindest; модалка alert
- Прочие правки из сессии (clusters, job_store, стили, шаблоны)
2026-04-04 08:15:15 +03:00

291 lines
12 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 Clusters Dashboard.
Список переменных и подсказок задаётся в этом файле (не используется 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 Clusters Dashboard (родитель каталога 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": "120",
"KIND_K8S_DEBUG": "",
"KIND_K8S_WAIT_NODES": "1",
"KIND_K8S_WAIT_NODES_TIMEOUT_SEC": "300",
"KIND_K8S_APP_TITLE": "Kind Clusters Dashboard",
"KIND_K8S_README_PATH": "",
"KIND_K8S_UVICORN_RELOAD": "1",
"KIND_K8S_WEB_HOST": "0.0.0.0",
"KIND_K8S_WORKDIR": "",
"KIND_K8S_JOBS_JSON": "",
"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",
"Сколько строк показывать в интерактивном CLI при выборе версии (веб-UI отдаёт полный список).",
),
_EnvPrompt(
None,
"KIND_K8S_HUB_TAGS_MAX_PAGES",
"Лимит страниц Docker Hub API при сборе тегов (по умолчанию в коде 120, макс. 500); больше страниц — больше старых семверов, дольше запрос.",
),
_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_JOBS_JSON",
"Путь к JSON с историей заданий (создание/старт/стоп кластера); пусто — файл kind_k8s_jobs.json в каталоге clusters/ на томе данных.",
),
_EnvPrompt(
None,
"KIND_K8S_README_PATH",
"Путь к README.md для страницы /documentation; пусто — README рядом с app/ (в образе /opt/kind-k8s/README.md).",
),
_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 build», например --platform linux/arm64.",
),
)
_ENV_FILE_HEADER = """# Файл .env для Kind Clusters Dashboard
# Создан интерактивно: 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 Clusters Dashboard (переменные задаются в этом скрипте)",
)
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()