- Порт хоста по умолчанию 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.
271 lines
11 KiB
Python
271 lines
11 KiB
Python
#!/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()
|