- Выделена страница списка кластеров, панель упрощена; nav_active и крошки ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL. - Доработки дашборда, аддонов, журнала, стилей и API-документации. - Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
539 lines
24 KiB
Python
539 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""Интерактивное создание ``.env`` в корне репозитория Kind Clusters Dashboard.
|
||
|
||
Список переменных и подсказок задаётся в этом файле (не используется env.example).
|
||
В начале мастера выбирается только **docker** или **podman** — путь ``CONTAINER_SOCKET`` подставляется автоматически.
|
||
Дефолты совпадают с ``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 os
|
||
import secrets
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
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: сокет + CONTAINER_CLI + user/HOME (доступ к сокету rootless Podman) ---",
|
||
"CONTAINER_SOCKET",
|
||
"",
|
||
),
|
||
_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,
|
||
"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",
|
||
"Только файл глобального снимка заданий (очередь и running; после перезапуска контейнера). "
|
||
"Пусто — <корень_данных>/clusters/kind_k8s_jobs.json (корень = KIND_K8S_WORKDIR; в compose внутри контейнера /work, "
|
||
"на хосте тот же том — обычно ./clusters/kind_k8s_jobs.json). "
|
||
"Архив завершённых операций и страница «Журнал» — clusters/<имя>/journal/jobs_history.json; "
|
||
"полный лог create/start/reapply — clusters/<имя>/provision_log.json; история Helm-аддонов — "
|
||
"clusters/<имя>/helm_addon_log.json — эти пути KIND_K8S_JOBS_JSON не переопределяет.",
|
||
),
|
||
_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.",
|
||
),
|
||
)
|
||
|
||
# Путь к сокету Docker в примерах и значение по умолчанию для CONTAINER_SOCKET (не подставляется из UID).
|
||
_DOCKER_SOCKET_EXAMPLE = "/var/run/docker.sock"
|
||
|
||
|
||
def _process_uid() -> int | None:
|
||
"""UID процесса (то же, что ``id -u`` для пользователя, запустившего скрипт)."""
|
||
try:
|
||
return int(os.getuid())
|
||
except AttributeError:
|
||
return None
|
||
|
||
|
||
def _podman_socket_path_for_user() -> str:
|
||
"""
|
||
Путь к podman.sock для подсказок: ``$XDG_RUNTIME_DIR/podman/podman.sock`` или
|
||
``/run/user/<UID>/podman/podman.sock`` (UID — результат :func:`_process_uid`, иначе 1000).
|
||
"""
|
||
xdg = (os.environ.get("XDG_RUNTIME_DIR") or "").strip()
|
||
if xdg:
|
||
return f"{xdg.rstrip('/')}/podman/podman.sock"
|
||
uid = _process_uid()
|
||
if uid is not None:
|
||
return f"/run/user/{uid}/podman/podman.sock"
|
||
return "/run/user/1000/podman/podman.sock"
|
||
|
||
|
||
def _unix_url_for_path(absolute_path: str) -> str:
|
||
"""Ссылка вида ``unix:///…`` для абсолютного пути к сокету."""
|
||
p = absolute_path if absolute_path.startswith("/") else f"/{absolute_path}"
|
||
return f"unix://{p}"
|
||
|
||
|
||
def _docker_host_unix_path_from_env() -> str | None:
|
||
"""Путь из ``DOCKER_HOST=unix://…``, если задан."""
|
||
dh = (os.environ.get("DOCKER_HOST") or "").strip()
|
||
if not dh.startswith("unix://"):
|
||
return None
|
||
path = dh[7:]
|
||
if path and not path.startswith("/"):
|
||
path = "/" + path.lstrip("/")
|
||
return path or None
|
||
|
||
|
||
def _first_existing_socket(candidates: list[str]) -> str | None:
|
||
"""Первый существующий unix-socket из списка путей."""
|
||
for c in candidates:
|
||
if not c:
|
||
continue
|
||
p = Path(c).expanduser()
|
||
try:
|
||
if p.is_socket():
|
||
return str(p.resolve())
|
||
except OSError:
|
||
logger.debug("Пропуск кандидата сокета (ошибка доступа): %s", p, exc_info=True)
|
||
return None
|
||
|
||
|
||
def _detect_docker_socket_path() -> str:
|
||
"""
|
||
Авто-поиск сокета Docker на машине, где запущен мастер: ``DOCKER_HOST``, ``/var/run/docker.sock``,
|
||
типичные пути Docker Desktop / Colima.
|
||
"""
|
||
cands: list[str] = []
|
||
envp = _docker_host_unix_path_from_env()
|
||
if envp:
|
||
cands.append(envp)
|
||
cands.append(_DOCKER_SOCKET_EXAMPLE)
|
||
home = Path.home()
|
||
cands.append(str(home / ".docker/run/docker.sock"))
|
||
cands.append(str(home / ".colima/default/docker.sock"))
|
||
found = _first_existing_socket(cands)
|
||
if found:
|
||
logger.info("Найден сокет Docker: %s", found)
|
||
return found
|
||
logger.info("Сокет Docker не найден на диске — подставляю стандартный путь %s", _DOCKER_SOCKET_EXAMPLE)
|
||
return _DOCKER_SOCKET_EXAMPLE
|
||
|
||
|
||
def _podman_uidgid_for_env() -> str:
|
||
"""
|
||
Строка ``uid:gid`` для ``KIND_K8S_CONTAINER_UIDGID`` при Podman.
|
||
|
||
Берётся владелец файла сокета (как у ``make podman …``), чтобы на macOS и в VM
|
||
не расходилось с ``id -u`` / ``id -g`` интерактивного мастера.
|
||
"""
|
||
script = Path(__file__).resolve().parent / "detect_podman_socket.py"
|
||
try:
|
||
proc = subprocess.run(
|
||
[sys.executable, str(script), "--print-owner"],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=25,
|
||
)
|
||
if proc.returncode == 0:
|
||
s = (proc.stdout or "").strip()
|
||
parts = s.split(":", 1)
|
||
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
|
||
return s
|
||
except (OSError, subprocess.TimeoutExpired):
|
||
logger.debug("detect_podman_socket.py --print-owner недоступен", exc_info=True)
|
||
try:
|
||
return f"{os.getuid()}:{os.getgid()}"
|
||
except AttributeError:
|
||
return "1000:1000"
|
||
|
||
|
||
def _detect_podman_socket_path() -> str:
|
||
"""
|
||
Путь к ``podman.sock`` — тот же алгоритм, что ``scripts/detect_podman_socket.py``
|
||
(``podman info``, ``podman machine inspect`` на macOS, типичные пути).
|
||
"""
|
||
script = Path(__file__).resolve().parent / "detect_podman_socket.py"
|
||
try:
|
||
proc = subprocess.run(
|
||
[sys.executable, str(script)],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=35,
|
||
)
|
||
if proc.returncode == 0:
|
||
line = (proc.stdout or "").strip().splitlines()
|
||
candidate = (line[0] if line else "").strip()
|
||
if candidate:
|
||
logger.info("Путь к сокету Podman (detect_podman_socket.py): %s", candidate)
|
||
return candidate
|
||
except (OSError, subprocess.TimeoutExpired) as e:
|
||
logger.debug("detect_podman_socket.py не удалось: %s", e)
|
||
fallback = _podman_socket_path_for_user()
|
||
logger.info("Сокет Podman не определён скриптом — подставляю типичный путь %s", fallback)
|
||
return fallback
|
||
|
||
|
||
def ask_container_engine() -> tuple[str, str]:
|
||
"""Спросить только docker или podman; вернуть ``(CONTAINER_CLI, CONTAINER_SOCKET)`` с автоподбором пути."""
|
||
print(
|
||
"\n--- Движок контейнеров для compose ---\n"
|
||
" Укажите, чем пользуетесь на этом компьютере. Путь к сокету (CONTAINER_SOCKET) подставится сам;\n"
|
||
" CONTAINER_CLI в .env будет docker или podman (в образе веб-UI к сокету ходит клиент docker).\n",
|
||
)
|
||
pod_hint = _podman_socket_path_for_user()
|
||
print(" 1 или d / docker — Docker")
|
||
print(f" 2 или p / podman — Podman (типичный путь при отсутствии сокета: {pod_hint})")
|
||
print(" Enter — вариант 1 (Docker)")
|
||
raw = input(" Ваш выбор [1]: ").strip().lower()
|
||
|
||
if raw in ("", "1", "d", "docker"):
|
||
cli, sock = "docker", _detect_docker_socket_path()
|
||
elif raw in ("2", "p", "podman"):
|
||
cli, sock = "podman", _detect_podman_socket_path()
|
||
else:
|
||
print(" Не распознано — принимаю Docker.")
|
||
cli, sock = "docker", _detect_docker_socket_path()
|
||
|
||
warn = ""
|
||
try:
|
||
if not Path(sock).is_socket():
|
||
warn = " Внимание: по этому пути сейчас нет socket-файла — проверьте, что движок запущен, или поправьте CONTAINER_SOCKET в .env."
|
||
except OSError:
|
||
warn = " Внимание: нет доступа к пути сокета — при необходимости поправьте CONTAINER_SOCKET в .env."
|
||
if cli == "podman":
|
||
ug = _podman_uidgid_for_env()
|
||
print(
|
||
f" Для rootless Podman в .env также будут записаны KIND_K8S_CONTAINER_UIDGID={ug} "
|
||
"и KIND_K8S_CONTAINER_HOME=/tmp (иначе в контейнере будет permission denied на сокет).\n",
|
||
)
|
||
else:
|
||
print(
|
||
" Для Docker в .env будут KIND_K8S_CONTAINER_UIDGID=0:0 и KIND_K8S_CONTAINER_HOME=/root.\n",
|
||
)
|
||
print(f" В .env: CONTAINER_CLI={cli}, CONTAINER_SOCKET={sock}.{warn}\n")
|
||
return cli, sock
|
||
|
||
|
||
def _compose_user_env_lines(engine_cli: str) -> list[str]:
|
||
"""
|
||
Строки для .env: путь монтирования сокета и URI для ``DOCKER_HOST``, затем ``user``/``HOME``.
|
||
|
||
Для Podman сокет в контейнере — ``/run/podman/podman.sock``, для Docker — ``/var/run/docker.sock``
|
||
(должно совпадать с ``KIND_K8S_REMOTE_SOCKET_URI``).
|
||
"""
|
||
el = engine_cli.strip().lower()
|
||
block: list[str] = []
|
||
if el == "podman":
|
||
block.extend(
|
||
[
|
||
"# Том сокета внутри контейнера и URI API (не docker.sock — явно путь Podman)\n",
|
||
"CONTAINER_SOCKET_MOUNT_TARGET=/run/podman/podman.sock\n",
|
||
"KIND_K8S_REMOTE_SOCKET_URI=unix:///run/podman/podman.sock\n",
|
||
],
|
||
)
|
||
else:
|
||
block.extend(
|
||
[
|
||
"CONTAINER_SOCKET_MOUNT_TARGET=/var/run/docker.sock\n",
|
||
"KIND_K8S_REMOTE_SOCKET_URI=unix:///var/run/docker.sock\n",
|
||
],
|
||
)
|
||
block.append(
|
||
"# KIND_K8S_CONTAINER_UIDGID — user: в compose; KIND_K8S_CONTAINER_HOME — переменная HOME в контейнере\n",
|
||
)
|
||
if el == "podman":
|
||
ug = _podman_uidgid_for_env()
|
||
block.append(f"KIND_K8S_CONTAINER_UIDGID={ug}\n")
|
||
block.append("KIND_K8S_CONTAINER_HOME=/tmp\n")
|
||
else:
|
||
block.append("KIND_K8S_CONTAINER_UIDGID=0:0\n")
|
||
block.append("KIND_K8S_CONTAINER_HOME=/root\n")
|
||
return block
|
||
|
||
|
||
def _build_env_file_header() -> str:
|
||
"""Заголовок записываемого .env."""
|
||
pod = _podman_socket_path_for_user()
|
||
url = _unix_url_for_path(pod)
|
||
uid = _process_uid()
|
||
uid_line = (
|
||
f"# UID пользователя, запустившего мастер (os.getuid / id -u): {uid}\n"
|
||
if uid is not None
|
||
else "# UID недоступен на этой ОС.\n"
|
||
)
|
||
return (
|
||
"# Файл .env для Kind Clusters Dashboard\n"
|
||
"# Создан интерактивно: scripts/setup_env_interactive.py\n"
|
||
"#\n"
|
||
"# В docker-compose.yml заданы литералами (не из этого файла): KIND_K8S_IN_CONTAINER, KIND_K8S_WORKDIR=/work.\n"
|
||
"# DOCKER_HOST в контейнере берётся из KIND_K8S_REMOTE_SOCKET_URI (см. ниже).\n"
|
||
"#\n"
|
||
"# CONTAINER_SOCKET и CONTAINER_CLI — в мастере (только docker/podman); путь к сокету подбирается автоматически.\n"
|
||
"# KIND_K8S_CONTAINER_UIDGID / KIND_K8S_CONTAINER_HOME — для доступа к сокету (Podman rootless: ваш uid:gid).\n"
|
||
f"# Справочно, типичные пути: Docker — {_DOCKER_SOCKET_EXAMPLE}; Podman — {pod} (URL: {url}).\n"
|
||
f"{uid_line}"
|
||
"#\n\n"
|
||
)
|
||
|
||
|
||
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",
|
||
)
|
||
|
||
engine_cli, engine_sock = ask_container_engine()
|
||
|
||
collected: dict[str, str] = {}
|
||
out_chunks: list[str] = [_build_env_file_header()]
|
||
|
||
for spec in _SETUP_PROMPTS:
|
||
if spec.section_comment:
|
||
out_chunks.append(f"{spec.section_comment}\n")
|
||
|
||
if spec.key == "CONTAINER_SOCKET":
|
||
collected["CONTAINER_SOCKET"] = engine_sock
|
||
collected["CONTAINER_CLI"] = engine_cli
|
||
out_chunks.append(f"CONTAINER_SOCKET={engine_sock}\n")
|
||
out_chunks.append(f"CONTAINER_CLI={engine_cli}\n")
|
||
out_chunks.extend(_compose_user_env_lines(engine_cli))
|
||
continue
|
||
|
||
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()
|