175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
#!/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()
|