508 lines
19 KiB
Python
508 lines
19 KiB
Python
"""Environment 子域 Router。
|
||
|
||
外部系统限界上下文的环境管理 API,覆盖环境 CRUD / 默认 / 启停 / 克隆 /
|
||
连通性测试 / 差异 / 导出 / 健康状态 / 统计 / 跨系统查询 /
|
||
批量克隆 / 批量启停。所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,
|
||
经 ``environment_service`` 端口调用用例。
|
||
|
||
路径顺序约束:静态路径(``/diff`` / ``/active`` / ``/by-key`` / ``/stats`` /
|
||
``/cross-system`` / ``/batch-test-connectivity`` / ``/batch-clone`` /
|
||
``/batch-enabled``)必须在 ``/{env_id}`` 前声明,避免被路径参数捕获。
|
||
|
||
认证策略:查询类端点使用 ``get_required_user``,写操作端点使用 ``get_admin_user``。
|
||
``export_environment`` 因 ``include_secrets=true`` 会泄露明文密钥,统一要求 ``get_admin_user``。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import UTC, datetime
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, Query
|
||
from fastapi.responses import StreamingResponse
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from yuxi.external_systems.infrastructure.container import create_use_cases_from_db
|
||
from yuxi.external_systems.use_cases.dto.environment import (
|
||
BatchCloneEnvironmentsInput,
|
||
BatchEnableEnvironmentsInput,
|
||
BulkTestEnvironmentsInput,
|
||
CloneEnvironmentInput,
|
||
CreateEnvironmentInput,
|
||
CrossSystemEnvironmentsInput,
|
||
DeleteEnvironmentInput,
|
||
DiffEnvironmentsInput,
|
||
DisableEnvironmentInput,
|
||
EnableEnvironmentInput,
|
||
ExportEnvironmentInput,
|
||
GetActiveEnvironmentInput,
|
||
GetEnvironmentByKeyInput,
|
||
GetEnvironmentHealthInput,
|
||
GetEnvironmentInput,
|
||
GetEnvironmentStatsInput,
|
||
ListEnvironmentsInput,
|
||
SetDefaultEnvironmentInput,
|
||
TestEnvironmentConnectionInput,
|
||
UpdateEnvironmentInput,
|
||
)
|
||
from yuxi.storage.postgres.models_business import User
|
||
|
||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||
|
||
environment_router = APIRouter(
|
||
prefix="/environments",
|
||
tags=["external-systems-environment"],
|
||
)
|
||
|
||
|
||
# ---------------- Request Schemas ----------------
|
||
|
||
|
||
class CreateEnvironmentRequest(BaseModel):
|
||
"""创建环境请求体。字段对齐 ``CreateEnvironmentInput``。
|
||
|
||
字段长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义,在边界层拦截非法输入:
|
||
``env_key`` 对齐 String(32) 并沿用 slug 标识符格式,``name`` 对齐 String(128)。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
system_id: int
|
||
env_key: str = Field(..., min_length=1, max_length=32, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$")
|
||
name: str = Field(..., min_length=1, max_length=128)
|
||
connection_config: dict[str, Any] = Field(default_factory=dict)
|
||
auth_config: dict[str, Any] | None = None
|
||
is_default: bool = False
|
||
enabled: bool = True
|
||
|
||
|
||
class UpdateEnvironmentRequest(BaseModel):
|
||
"""更新环境请求体。``id`` 与 ``system_id`` 由路径/Query 提供,不在 body 中。
|
||
|
||
字段长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义,仅透传客户端显式设置的字段。
|
||
不含 ``is_default``:默认环境切换必须通过 ``PATCH /{env_id}/default`` 端点,
|
||
该端点会清除同系统其他环境的默认标记,避免违反部分唯一约束。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
name: str | None = Field(default=None, min_length=1, max_length=128)
|
||
connection_config: dict[str, Any] | None = None
|
||
auth_config: dict[str, Any] | None = None
|
||
enabled: bool | None = None
|
||
|
||
|
||
class SetEnvironmentEnabledRequest(BaseModel):
|
||
"""启停环境请求体。按 ``enabled`` 值分派 enable/disable。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
enabled: bool
|
||
|
||
|
||
class CloneEnvironmentRequest(BaseModel):
|
||
"""克隆环境请求体。``id`` 与 ``system_id`` 由路径/Query 提供。
|
||
|
||
字段长度约束对齐 ``ExternalSystemEnvironment`` ORM 列定义,``new_env_key`` 与
|
||
``CreateEnvironmentRequest.env_key`` 保持一致格式。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
new_env_key: str = Field(..., min_length=1, max_length=32, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$")
|
||
new_name: str | None = Field(default=None, min_length=1, max_length=128)
|
||
|
||
|
||
class BatchTestConnectivityRequest(BaseModel):
|
||
"""批量测试连通性请求体。字段对齐 ``BulkTestEnvironmentsInput``。"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
system_id: int
|
||
env_keys: list[str] | None = None
|
||
|
||
|
||
class BatchCloneEnvironmentsRequest(BaseModel):
|
||
"""批量克隆环境请求体。字段对齐 ``BatchCloneEnvironmentsInput``(不含 ``created_by``)。
|
||
|
||
``created_by`` 由 ``current_user.uid`` 填充,不在请求体中暴露。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
system_id: int
|
||
source_env_ids: list[int]
|
||
new_env_key_suffix: str = Field(..., min_length=1, max_length=32)
|
||
new_name_suffix: str | None = Field(default=None, min_length=1, max_length=128)
|
||
|
||
|
||
class BatchEnableEnvironmentsRequest(BaseModel):
|
||
"""批量启停环境请求体。字段对齐 ``BatchEnableEnvironmentsInput``(不含 ``updated_by``)。
|
||
|
||
``updated_by`` 由 ``current_user.uid`` 填充,不在请求体中暴露。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
system_id: int
|
||
env_ids: list[int]
|
||
enabled: bool
|
||
|
||
|
||
# ---------------- Endpoints ----------------
|
||
|
||
|
||
@environment_router.get("/diff", response_model=dict)
|
||
async def diff_environments(
|
||
system_id: int = Query(...),
|
||
source_env_key: str = Query(..., max_length=32),
|
||
target_env_key: str = Query(..., max_length=32),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""对比两个环境的配置差异。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = DiffEnvironmentsInput(
|
||
system_id=system_id,
|
||
source_env_key=source_env_key,
|
||
target_env_key=target_env_key,
|
||
)
|
||
output = await use_cases.environment_service.diff_environments(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.get("", response_model=dict)
|
||
async def list_environments(
|
||
system_id: int = Query(...),
|
||
env_key: str | None = Query(None, max_length=32),
|
||
enabled: bool | None = Query(None),
|
||
limit: int = Query(100, ge=1, le=500),
|
||
offset: int = Query(0, ge=0),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""分页列出指定外部系统下的环境。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ListEnvironmentsInput(
|
||
system_id=system_id,
|
||
env_key=env_key,
|
||
enabled=enabled,
|
||
page=offset // limit + 1,
|
||
page_size=limit,
|
||
)
|
||
output = await use_cases.environment_service.list_environments(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.post("", response_model=dict)
|
||
async def create_environment(
|
||
body: CreateEnvironmentRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""创建环境。``created_by`` 由 ``current_user.uid`` 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = CreateEnvironmentInput(**body.model_dump(), created_by=current_user.uid)
|
||
output = await use_cases.environment_service.create_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
# ========== 静态路径端点(必须在 /{env_id} 之前声明) ==========
|
||
|
||
|
||
@environment_router.get("/active", response_model=dict)
|
||
async def get_active_environment(
|
||
system_id: int = Query(...),
|
||
env_key: str | None = Query(None, max_length=32),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取当前生效的环境配置。
|
||
|
||
指定 ``env_key`` 返回对应环境,否则返回默认环境,都没有返回 ``data=null``。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetActiveEnvironmentInput(system_id=system_id, env_key=env_key)
|
||
output = await use_cases.environment_service.get_active_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump() if output else None}
|
||
|
||
|
||
@environment_router.get("/by-key", response_model=dict)
|
||
async def get_environment_by_key(
|
||
system_id: int = Query(...),
|
||
env_key: str = Query(..., max_length=32),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""按 ``system_id`` + ``env_key`` 获取环境详情,不存在时抛 404。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetEnvironmentByKeyInput(system_id=system_id, env_key=env_key)
|
||
output = await use_cases.environment_service.get_environment_by_key(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.get("/stats", response_model=dict)
|
||
async def get_environment_stats(
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""统计指定系统下的环境数量(按启用状态、默认标记分组)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetEnvironmentStatsInput(system_id=system_id)
|
||
output = await use_cases.environment_service.get_environment_stats(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.get("/cross-system", response_model=dict)
|
||
async def list_cross_system_environments(
|
||
env_key: str = Query(..., max_length=32),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""跨系统查询同 ``env_key`` 的环境列表。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = CrossSystemEnvironmentsInput(env_key=env_key)
|
||
output = await use_cases.environment_service.list_cross_system_environments(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.post("/batch-test-connectivity", response_model=dict)
|
||
async def batch_test_environment_connectivity(
|
||
body: BatchTestConnectivityRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""批量测试环境连通性(并发上限 5)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = BulkTestEnvironmentsInput(
|
||
system_id=body.system_id,
|
||
env_keys=body.env_keys,
|
||
)
|
||
output = await use_cases.environment_service.bulk_test_environments(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.post("/batch-clone", response_model=dict)
|
||
async def batch_clone_environments(
|
||
body: BatchCloneEnvironmentsRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""批量克隆环境配置。``created_by`` 由 ``current_user.uid`` 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = BatchCloneEnvironmentsInput(
|
||
system_id=body.system_id,
|
||
source_env_ids=body.source_env_ids,
|
||
new_env_key_suffix=body.new_env_key_suffix,
|
||
new_name_suffix=body.new_name_suffix,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.environment_service.batch_clone_environments(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.post("/batch-enabled", response_model=dict)
|
||
async def batch_set_environments_enabled(
|
||
body: BatchEnableEnvironmentsRequest,
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""批量启用或禁用多个环境。``updated_by`` 由 ``current_user.uid`` 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = BatchEnableEnvironmentsInput(
|
||
system_id=body.system_id,
|
||
env_ids=body.env_ids,
|
||
enabled=body.enabled,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.environment_service.batch_set_environments_enabled(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.get("/{env_id}", response_model=dict)
|
||
async def get_environment(
|
||
env_id: int,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""获取环境详情。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetEnvironmentInput(id=env_id, system_id=system_id)
|
||
output = await use_cases.environment_service.get_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.put("/{env_id}", response_model=dict)
|
||
async def update_environment(
|
||
env_id: int,
|
||
body: UpdateEnvironmentRequest,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""更新环境。仅透传客户端显式设置的字段。``updated_by`` 由 ``current_user.uid`` 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = UpdateEnvironmentInput(
|
||
id=env_id,
|
||
system_id=system_id,
|
||
updated_by=current_user.uid,
|
||
**body.model_dump(exclude_unset=True),
|
||
)
|
||
output = await use_cases.environment_service.update_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.delete("/{env_id}", response_model=dict)
|
||
async def delete_environment(
|
||
env_id: int,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""删除环境。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = DeleteEnvironmentInput(id=env_id, system_id=system_id, deleted_by=current_user.uid)
|
||
output = await use_cases.environment_service.delete_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.patch("/{env_id}/default", response_model=dict)
|
||
async def set_default_environment(
|
||
env_id: int,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""设置默认环境。``updated_by`` 由 ``current_user.uid`` 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = SetDefaultEnvironmentInput(
|
||
id=env_id,
|
||
system_id=system_id,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.environment_service.set_default_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.patch("/{env_id}/enabled", response_model=dict)
|
||
async def set_environment_enabled(
|
||
env_id: int,
|
||
body: SetEnvironmentEnabledRequest,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""启用/禁用环境。按 ``enabled`` 值分派 ``enable_environment``/``disable_environment``。
|
||
|
||
``updated_by`` 由 ``current_user.uid`` 填充。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
if body.enabled:
|
||
input_dto = EnableEnvironmentInput(
|
||
id=env_id,
|
||
system_id=system_id,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.environment_service.enable_environment(input_dto)
|
||
else:
|
||
input_dto = DisableEnvironmentInput(
|
||
id=env_id,
|
||
system_id=system_id,
|
||
updated_by=current_user.uid,
|
||
)
|
||
output = await use_cases.environment_service.disable_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.post("/{env_id}/clone", response_model=dict)
|
||
async def clone_environment(
|
||
env_id: int,
|
||
body: CloneEnvironmentRequest,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""克隆环境。``created_by`` 由 ``current_user.uid`` 填充。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = CloneEnvironmentInput(
|
||
id=env_id,
|
||
system_id=system_id,
|
||
new_env_key=body.new_env_key,
|
||
new_name=body.new_name,
|
||
created_by=current_user.uid,
|
||
)
|
||
output = await use_cases.environment_service.clone_environment(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.post("/{env_id}/test-connectivity", response_model=dict)
|
||
async def test_environment_connection(
|
||
env_id: int,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> dict[str, Any]:
|
||
"""测试环境连通性。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = TestEnvironmentConnectionInput(id=env_id, system_id=system_id)
|
||
output = await use_cases.environment_service.test_environment_connection(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|
||
|
||
|
||
@environment_router.get("/{env_id}/export")
|
||
async def export_environment(
|
||
env_id: int,
|
||
system_id: int = Query(...),
|
||
include_secrets: bool = Query(False),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_admin_user),
|
||
) -> StreamingResponse:
|
||
"""导出环境配置为 JSON 文件下载流。
|
||
|
||
``include_secrets=false``(默认)返回脱敏配置;``include_secrets=true``
|
||
返回已解密的明文配置。由于 ``include_secrets=true`` 会泄露明文密钥,
|
||
本端点统一要求管理员权限,对齐写操作的权限梯度。
|
||
返回 ``StreamingResponse``(``application/octet-stream``),
|
||
对齐 ``system_router.export_systems`` 模式。
|
||
"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = ExportEnvironmentInput(
|
||
id=env_id,
|
||
system_id=system_id,
|
||
include_secrets=include_secrets,
|
||
)
|
||
output = await use_cases.environment_service.export_environment(input_dto)
|
||
payload = json.dumps(
|
||
output.model_dump(),
|
||
ensure_ascii=False,
|
||
default=str,
|
||
).encode("utf-8")
|
||
timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S")
|
||
filename = f"environment_{env_id}_{output.env_key}_{timestamp}.json"
|
||
|
||
async def _stream():
|
||
yield payload
|
||
|
||
return StreamingResponse(
|
||
_stream(),
|
||
media_type="application/octet-stream",
|
||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||
)
|
||
|
||
|
||
@environment_router.get("/{env_id}/health", response_model=dict)
|
||
async def get_environment_health(
|
||
env_id: int,
|
||
system_id: int = Query(...),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: User = Depends(get_required_user),
|
||
) -> dict[str, Any]:
|
||
"""查询环境最近健康状态(基于审计日志推导,不执行实时探测)。"""
|
||
use_cases = create_use_cases_from_db(db)
|
||
input_dto = GetEnvironmentHealthInput(id=env_id, system_id=system_id)
|
||
output = await use_cases.environment_service.get_environment_health(input_dto)
|
||
return {"success": True, "data": output.model_dump()}
|