Проект разделен на два репозитория
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from sqlalchemy import select, text, update
|
||||
|
||||
from app.config import validate_config
|
||||
from app.db import ConfigVersion, SafetyAudit, engine_and_sessions
|
||||
from app.settings import _secret
|
||||
|
||||
|
||||
async def execute(args: argparse.Namespace) -> None:
|
||||
url = _secret("MESSAGE_SAFETY_CONFIG_ADMIN_DATABASE_URL")
|
||||
assert url
|
||||
artifacts = Path(os.getenv("MESSAGE_SAFETY_ARTIFACTS_DIR", "/app/app/artifacts"))
|
||||
document = (
|
||||
yaml.safe_load(await asyncio.to_thread(Path(args.file).read_text, encoding="utf-8"))
|
||||
if args.file
|
||||
else None
|
||||
)
|
||||
if document:
|
||||
_, _, digest = validate_config(document, artifacts)
|
||||
engine, sessions = engine_and_sessions(url)
|
||||
try:
|
||||
if args.command == "validate":
|
||||
print(json.dumps({"valid": True, "config_sha256": digest.hex()}))
|
||||
return
|
||||
async with sessions.begin() as session:
|
||||
await session.execute(
|
||||
text("SELECT pg_advisory_xact_lock(hashtext('message_safety.config_activation'))")
|
||||
)
|
||||
if args.command == "create":
|
||||
exists = await session.scalar(
|
||||
select(ConfigVersion.id).where(ConfigVersion.version == args.version)
|
||||
)
|
||||
if exists:
|
||||
raise ValueError("config version already exists")
|
||||
session.add(
|
||||
ConfigVersion(
|
||||
version=args.version,
|
||||
schema_version=document["schema_version"],
|
||||
state="draft",
|
||||
config=document,
|
||||
config_sha256=digest,
|
||||
created_by=args.actor,
|
||||
created_at=datetime.now(UTC),
|
||||
)
|
||||
)
|
||||
elif args.command == "activate":
|
||||
row = await session.scalar(
|
||||
select(ConfigVersion)
|
||||
.where(ConfigVersion.version == args.version)
|
||||
.with_for_update()
|
||||
)
|
||||
if not row or row.state != "draft":
|
||||
raise ValueError("only a draft config can be activated")
|
||||
validate_config(row.config, artifacts)
|
||||
now = datetime.now(UTC)
|
||||
await session.execute(
|
||||
update(ConfigVersion)
|
||||
.where(ConfigVersion.state == "active")
|
||||
.values(state="retired", retired_at=now)
|
||||
)
|
||||
row.state = "active"
|
||||
row.approved_by = args.approved_by
|
||||
row.approved_at = now
|
||||
row.activated_at = now
|
||||
session.add(
|
||||
SafetyAudit(
|
||||
id=uuid.uuid4(),
|
||||
event="config_activated",
|
||||
processing_mode="standard",
|
||||
config_version=row.version,
|
||||
created_at=now,
|
||||
purge_after=now + timedelta(days=180),
|
||||
)
|
||||
)
|
||||
print(json.dumps({"ok": True, "version": args.version}))
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
result = argparse.ArgumentParser()
|
||||
commands = result.add_subparsers(dest="command", required=True)
|
||||
validate = commands.add_parser("validate")
|
||||
validate.add_argument("file")
|
||||
create = commands.add_parser("create")
|
||||
create.add_argument("file")
|
||||
create.add_argument("--version", type=int, required=True)
|
||||
create.add_argument("--actor", required=True)
|
||||
activate = commands.add_parser("activate")
|
||||
activate.add_argument("--version", type=int, required=True)
|
||||
activate.add_argument("--approved-by", required=True)
|
||||
activate.set_defaults(file=None)
|
||||
return result
|
||||
|
||||
|
||||
def main() -> None:
|
||||
asyncio.run(execute(parser().parse_args()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user