Files
orchestrator/tests/test_secrets_gen.py
claude-bot f1635ddb39
All checks were successful
CI / test (push) Successful in 57s
CI / test (pull_request) Successful in 55s
feat(replication): расхардкод хоста + секреты нового хоста + smoke-runbook
Фундамент тиража 10-common (эпик ORCH-10): платформа разворачивается на
новой инфре без правки кода — только env/конфиг. Каждый дефолт = боевому
значению (пустой .env => поведение 1:1, kill-switch-природа, NFR-2);
STAGE_TRANSITIONS/QG_CHECKS/check_*/machine-verdict/схема БД не тронуты.

- config: agent_home_dir / agent_git_name / git_email_domain / staging_port
  (ADR-001 D2/D4); код-блокеры A1-A4 закрыты: plane_sync ссылки из
  gitea_public_url+gitea_owner, launcher - единый agent_git_env() (x2 места),
  self_deploy/post_deploy - HOME+домен из Settings (имена системных акторов -
  платформенные литералы)
- image_freshness: staging_port из конфига + fail-closed guard
  staging_port == прод-порт -> отказ ДО ssh/build (инвариант ORCH-058 AC-9
  стал исполняемым); REPO= передаётся хуку явно обоими инвокерами (D7)
- SELF_HOSTING_REPO - нормативная платформенная константа (D3, пин-тест)
- compose: полная ${VAR:-default}-интерполяция (реестр B, карта D6); группа
  ORCH-040 uid/gid/HOME/маунты двигается согласованно (build.args APP_*);
  group_add "МИНА 1" сохранён x3; оба app-сервиса с явным command:
- Dockerfile: ARG APP_UID/APP_GID/APP_USER/APP_HOME (CMD exec-form 8500
  сознательно не тронут - D5); deploy-hook: REPO="${REPO:-...}" (D1 реестра)
- секреты: stdlib scripts/gen_secrets.py (token_hex(32); печать по умолчанию;
  --write никогда не перезаписывает существующий .env молча, exit=2;
  перезапись только --force); .env.example дополнен до полноты ключей старта
- доки: новый docs/operations/REPLICATION.md (карта env, чек-лист секретов,
  smoke-процедура с PASS/FAIL, границы 10-common/Lite/Bundled), INFRA.md,
  README, CLAUDE.md, CHANGELOG
- анти-регресс: tests/test_no_host_hardcodes.py (tokenize-сканер запрещённых
  литералов, config-модули - структурное исключение, allowlist пуст,
  негативная самопроверка) + test_host_config_keys / test_infra_parametrization
  / test_secrets_gen / test_replication_smoke; согласованные структурные
  правки test_orch040_compose (судит резолв дефолтов) и
  test_deploy_hook_rollback_sim (REPO через env-override = контракт D7)

Полный регресс: 1764 passed.

Refs: ORCH-101

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:50:43 +03:00

107 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ORCH-101 (TC-09, AC-5 / NFR-3): scripts/gen_secrets.py — выпуск нового
комплекта секретов.
Контракт D8: криптослучайные webhook-секреты (>= 32 байт энтропии, hex);
повторный запуск даёт другие значения; существующий .env никогда не
перезаписывается молча (отказ exit=2, перезапись только --force); состав
ключей вывода согласован с .env.example.
"""
import importlib.util
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "scripts/gen_secrets.py"
ENV_EXAMPLE = REPO_ROOT / ".env.example"
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
def _load_module():
spec = importlib.util.spec_from_file_location("gen_secrets", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _values(fragment: str) -> dict[str, str]:
out = {}
for line in fragment.splitlines():
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
out[k] = v
return out
def test_secret_is_cryptorandom_64_hex():
mod = _load_module()
assert mod.TOKEN_BYTES >= 32 # AC-5: >= 32 байта энтропии
value = mod.generate_secret()
assert _HEX64.match(value), value
def test_two_runs_give_different_values():
mod = _load_module()
a, b = _values(mod.build_fragment()), _values(mod.build_fragment())
for key in mod.GENERATED_KEYS:
assert _HEX64.match(a[key]) and _HEX64.match(b[key])
assert a[key] != b[key], f"{key}: повторный запуск дал то же значение"
def test_external_tokens_are_placeholders():
mod = _load_module()
vals = _values(mod.build_fragment())
for key in mod.EXTERNAL_KEYS:
assert vals[key] == "", f"{key} должен быть пустым плейсхолдером"
def test_output_keys_consistent_with_env_example():
"""AC-5: каждое имя ключа из вывода генератора существует в .env.example."""
mod = _load_module()
example = ENV_EXAMPLE.read_text(encoding="utf-8")
example_keys = {
line.split("=", 1)[0].strip()
for line in example.splitlines()
if "=" in line and not line.strip().startswith("#")
}
for key in (*mod.GENERATED_KEYS, *mod.EXTERNAL_KEYS):
assert key in example_keys, f"{key} отсутствует в .env.example"
def test_default_mode_prints_and_touches_no_files(tmp_path, capsys, monkeypatch):
mod = _load_module()
monkeypatch.chdir(tmp_path)
rc = mod.main([])
assert rc == 0
out = capsys.readouterr().out
assert "ORCH_PLANE_WEBHOOK_SECRET=" in out
assert list(tmp_path.iterdir()) == [] # filesystem untouched
def test_write_refuses_existing_file_without_force(tmp_path):
mod = _load_module()
target = tmp_path / ".env"
target.write_text("KEEP=1\n", encoding="utf-8")
rc = mod.main(["--write", str(target)])
assert rc == 2 # отказ, не перезапись
assert target.read_text(encoding="utf-8") == "KEEP=1\n" # содержимое цело
def test_write_creates_new_file_and_force_overwrites(tmp_path):
mod = _load_module()
target = tmp_path / ".env"
assert mod.main(["--write", str(target)]) == 0
first = _values(target.read_text(encoding="utf-8"))
assert _HEX64.match(first["ORCH_GITEA_WEBHOOK_SECRET"])
# --force перезаписывает, и секреты другие (не детерминированы).
assert mod.main(["--write", str(target), "--force"]) == 0
second = _values(target.read_text(encoding="utf-8"))
assert first["ORCH_GITEA_WEBHOOK_SECRET"] != second["ORCH_GITEA_WEBHOOK_SECRET"]
def test_no_real_secret_committed_anywhere_near():
"""Генератор не несёт зашитых значений: единственный источник — CSPRNG."""
text = SCRIPT.read_text(encoding="utf-8")
assert not re.search(r"[0-9a-f]{32,}", text), "зашитое hex-значение в генераторе"