#!/usr/bin/env python3 """Интерактивное создание ``.env`` по шаблону ``env.example`` в корне kind-k8s-develop. Запуск из корня репозитория: ``python3 scripts/setup_env_interactive.py`` или ``make setup``. Опционально: ``--template`` / ``--output`` для других путей. Автор: Сергей Антропов Сайт: https://devops.org.ru """ from __future__ import annotations import argparse import logging import re import secrets from pathlib import Path logger = logging.getLogger("setup_env_interactive") # Корень репозитория kind-k8s-develop (родитель каталога scripts/) REPO_ROOT = Path(__file__).resolve().parents[1] # Переменные, для которых предлагается сгенерировать секрет по вводу «g» _SECRET_KEYS = frozenset( { "SECRET_KEY", "CONNECTORS_ENCRYPTION_KEY", "K8S_KUBECONFIG_ENCRYPTION_KEY", "POSTGRES_PASSWORD", "MINIO_SECRET_KEY", "ROOT_PASSWORD", }, ) 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 _ask_line( key: str, default: str, *, collected: dict[str, str], ) -> str: """Спросить одну переменную; ``g`` — сгенерировать секрет (для известных ключей).""" extra = "" if key in _SECRET_KEYS: extra = " [g] — сгенерировать случайное значение" hint = default.replace("\n", " ")[:72] if len(default) > 72: hint += "…" print(f"\n{key}") print(f" По умолчанию из env.example: {hint!r}{extra}") raw = input(" Значение (Enter — по умолчанию): ").strip() 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: 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( *, template: Path, output: Path, force: bool = False, ) -> int: template = template.resolve() 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() if ans not in ("y", "yes", "д", "да"): print("Выход без изменений.") return 0 lines_in = template.read_text(encoding="utf-8").splitlines(keepends=True) collected: dict[str, str] = {} out_chunks: list[str] = [] 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 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": built = _default_database_url(collected) if built is not None: default = built print("\n--- DATABASE_URL: можно собрать из учётки PostgreSQL выше ---") value = _ask_line(key, default, collected=collected) collected[key] = value out_chunks.append(f"{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 по env.example") parser.add_argument( "--template", type=Path, default=REPO_ROOT / "env.example", help="Путь к шаблону (по умолчанию: env.example в корне репозитория)", ) 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(template=args.template, output=args.output, force=args.force)) if __name__ == "__main__": main()