first commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user