Веб-UI FastAPI, REST API v1, интерактивный setup без env.example
- Дашборд (Jinja2 + static), управление кластерами kind, задания и kubeconfig. - API: health, stats, clusters CRUD, versions, jobs; документация app/docs/api_routes.md. - Docker Compose: том app, uvicorn reload, KIND_K8S_PATCH_KUBECONFIG по умолчанию 1. - setup_env_interactive.py: список переменных в скрипте, удалён env.example. - Makefile: явный префикс docker/podman; прочие правки CLI и ядра кластеров.
This commit is contained in:
25
scripts/run_uvicorn.sh
Executable file
25
scripts/run_uvicorn.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# Запуск uvicorn для kind-k8s-web.
|
||||
# При KIND_K8S_UVICORN_RELOAD=1 (по умолчанию) включается --reload: правки в смонтированном ./app
|
||||
# подхватываются без пересборки образа.
|
||||
#
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
set -e
|
||||
cd /opt/kind-k8s/app
|
||||
|
||||
REL="${KIND_K8S_UVICORN_RELOAD:-1}"
|
||||
if [ "$REL" = "0" ] || [ "$REL" = "false" ] || [ "$REL" = "no" ]; then
|
||||
exec python3 -m uvicorn main:app --host 0.0.0.0 --port 6000
|
||||
fi
|
||||
|
||||
exec python3 -m uvicorn main:app \
|
||||
--host 0.0.0.0 \
|
||||
--port 6000 \
|
||||
--reload \
|
||||
--reload-dir /opt/kind-k8s/app \
|
||||
--reload-include "*.py" \
|
||||
--reload-include "*.html" \
|
||||
--reload-include "*.css" \
|
||||
--reload-include "*.js"
|
||||
@@ -1,10 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Интерактивное создание ``.env`` по шаблону ``env.example`` в корне kind-k8s-develop.
|
||||
"""Интерактивное создание ``.env`` в корне kind-k8s-develop.
|
||||
|
||||
Запуск из корня репозитория: ``python3 scripts/setup_env_interactive.py``
|
||||
или ``make setup``.
|
||||
Список переменных и подсказок задаётся в этом файле (не используется env.example).
|
||||
Дефолты совпадают с ``docker-compose.yml`` / приложением; Enter — записать предложенное значение.
|
||||
|
||||
Опционально: ``--template`` / ``--output`` для других путей.
|
||||
Запуск: ``python3 scripts/setup_env_interactive.py`` или ``make setup``.
|
||||
|
||||
Опции: ``--output`` / ``-f`` (перезапись без вопроса).
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
@@ -14,8 +16,8 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("setup_env_interactive")
|
||||
@@ -23,7 +25,29 @@ logger = logging.getLogger("setup_env_interactive")
|
||||
# Корень репозитория kind-k8s-develop (родитель каталога scripts/)
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
# Переменные, для которых предлагается сгенерировать секрет по вводу «g»
|
||||
# Значения по умолчанию при нажатии 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": "6000",
|
||||
"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-k8s-develop",
|
||||
"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",
|
||||
@@ -36,36 +60,114 @@ _SECRET_KEYS = frozenset(
|
||||
)
|
||||
|
||||
|
||||
@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",
|
||||
"Порт на хосте для веб-интерфейса; внутри контейнера всегда 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 _is_comment_or_blank(line: str) -> bool:
|
||||
s = line.strip()
|
||||
return not s or s.startswith("#")
|
||||
|
||||
|
||||
def _parse_assignment(line: str) -> tuple[str, str] | None:
|
||||
"""Строка ``KEY=значение`` без ведущего ``#``; иначе ``None``."""
|
||||
raw = line.rstrip("\n\r")
|
||||
if raw.lstrip().startswith("#"):
|
||||
return None
|
||||
if "=" not in raw:
|
||||
return None
|
||||
key, _, value = raw.partition("=")
|
||||
k = key.strip()
|
||||
if not k or not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", k):
|
||||
return None
|
||||
return k, value
|
||||
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:
|
||||
"""Спросить одну переменную; ``g`` — сгенерировать секрет (для известных ключей)."""
|
||||
) -> str | None:
|
||||
"""
|
||||
Спросить одну переменную.
|
||||
|
||||
Возвращает ``None``, если строку в ``.env`` не записывать (пропуск).
|
||||
"""
|
||||
extra = ""
|
||||
if key in _SECRET_KEYS:
|
||||
extra = " [g] — сгенерировать случайное значение"
|
||||
@@ -73,9 +175,17 @@ def _ask_line(
|
||||
hint = default.replace("\n", " ")[:72]
|
||||
if len(default) > 72:
|
||||
hint += "…"
|
||||
if not default:
|
||||
hint = "пустая строка"
|
||||
|
||||
print(f"\n{key}")
|
||||
print(f" По умолчанию из env.example: {hint!r}{extra}")
|
||||
raw = input(" Значение (Enter — по умолчанию): ").strip()
|
||||
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
|
||||
@@ -89,6 +199,7 @@ def _ask_line(
|
||||
|
||||
|
||||
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")
|
||||
@@ -97,19 +208,9 @@ def _default_database_url(collected: dict[str, str]) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def run(
|
||||
*,
|
||||
template: Path,
|
||||
output: Path,
|
||||
force: bool = False,
|
||||
) -> int:
|
||||
template = template.resolve()
|
||||
def run(*, output: Path, force: bool = False) -> int:
|
||||
output = output.resolve()
|
||||
|
||||
if not template.is_file():
|
||||
logger.error("Не найден шаблон: %s", template)
|
||||
return 1
|
||||
|
||||
if output.exists() and not force:
|
||||
print(f"Файл уже существует: {output}")
|
||||
ans = input("Перезаписать? [y/N]: ").strip().lower()
|
||||
@@ -117,32 +218,31 @@ def run(
|
||||
print("Выход без изменений.")
|
||||
return 0
|
||||
|
||||
lines_in = template.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
print(
|
||||
"\nИнтерактивное заполнение .env (список переменных в scripts/setup_env_interactive.py).\n"
|
||||
"Enter без ввода — записать значение по умолчанию из подсказки.\n",
|
||||
)
|
||||
|
||||
collected: dict[str, str] = {}
|
||||
out_chunks: list[str] = []
|
||||
out_chunks: list[str] = [_ENV_FILE_HEADER]
|
||||
|
||||
for line in lines_in:
|
||||
if _is_comment_or_blank(line.rstrip("\n\r")):
|
||||
out_chunks.append(line if line.endswith("\n") else line + "\n")
|
||||
continue
|
||||
for spec in _SETUP_PROMPTS:
|
||||
if spec.section_comment:
|
||||
out_chunks.append(f"{spec.section_comment}\n")
|
||||
|
||||
parsed = _parse_assignment(line)
|
||||
if parsed is None:
|
||||
out_chunks.append(line if line.endswith("\n") else line + "\n")
|
||||
continue
|
||||
|
||||
key, template_default = parsed
|
||||
|
||||
default = template_default
|
||||
if key == "DATABASE_URL":
|
||||
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: можно собрать из учётки PostgreSQL выше ---")
|
||||
print("\n--- DATABASE_URL: собрано из POSTGRES_* ---")
|
||||
|
||||
value = _ask_line(key, default, collected=collected)
|
||||
collected[key] = value
|
||||
out_chunks.append(f"{key}={value}\n")
|
||||
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")
|
||||
@@ -152,12 +252,8 @@ def run(
|
||||
|
||||
def main() -> None:
|
||||
_configure_logging()
|
||||
parser = argparse.ArgumentParser(description="Интерактивное заполнение .env по env.example")
|
||||
parser.add_argument(
|
||||
"--template",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "env.example",
|
||||
help="Путь к шаблону (по умолчанию: env.example в корне репозитория)",
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Интерактивное заполнение .env для kind-k8s-develop (переменные задаются в этом скрипте)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
@@ -167,7 +263,7 @@ def main() -> None:
|
||||
)
|
||||
parser.add_argument("-f", "--force", action="store_true", help="Не спрашивать подтверждение перезаписи")
|
||||
args = parser.parse_args()
|
||||
raise SystemExit(run(template=args.template, output=args.output, force=args.force))
|
||||
raise SystemExit(run(output=args.output, force=args.force))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user