#!/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 писать clusters/<имя>/kubeconfig.host (server с KIND_K8S_KUBECONFIG_CLIENT_HOST, иначе localhost); " "основной kubeconfig — как у kind. 0 или пусто — не форсировать. По умолчанию в скрипте: 1 (включено).", ), _EnvPrompt( None, "KIND_K8S_KUBECONFIG_CLIENT_HOST", "Имя хоста в server= для скачиваемого kubeconfig (kubectl на машине пользователя). Пусто — localhost (лучше 127.0.0.1 с TLS kind). Для доступа с другой машины — IP/имя хоста Docker.", ), _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()