# DataOps Platform 本地隔离测试环境

本目录启动平台 PostgreSQL、两个外部验收数据源、Neo4j、MinIO、n8n、Kestra、
独立 DataOps Runner、Flask 后端和 Vue 前端。所有数据使用 `dataops-test-*`
Docker 卷，不连接生产服务。

## 启动

```bash
cp deploy/docker/.env.example deploy/docker/.env.local
docker compose --env-file deploy/docker/.env.local \
  -f deploy/docker/docker-compose.yml up -d --build
docker compose -f deploy/docker/docker-compose.yml ps
```

不需要验证 DeepSeek 或 n8n 管理 API 时，可以不创建 `.env.local`，直接启动。基础设施和页面仍可使用。

## 本地地址

| 能力 | 地址 / 凭据 |
|---|---|
| 前端 | <http://localhost:18183> |
| 后端健康 | <http://localhost:15500/api/system/health> |
| Neo4j Browser | <http://localhost:17474>，`neo4j` / `Passw0rd` |
| MinIO Console | <http://localhost:19001>，`dataops-test` / `dataops-test-password` |
| n8n | <http://localhost:15678> |
| Kestra | <http://127.0.0.1:18080>，默认仅供本机测试 |
| DataOps Runner | <http://127.0.0.1:15600/health>，仅供本机健康检查 |
| PostgreSQL | `localhost:15432/dataops`，`dataops` / `dataops-test-password` |
| 外部 PostgreSQL 数据源 | `localhost:25432/acceptance`，`source_reader` / `source-test-password` |
| 外部 MySQL 数据源 | `localhost:23306/acceptance`，`source_reader` / `source-test-password` |
| 平台本地登录 | 首次迁移后按下方命令显式创建；默认验收账号为 `admin` |

平台 PostgreSQL 仅保存平台自身状态；`source-postgres` 和 `source-mysql`
模拟由“数据源”定义接入的业务数据库。三者的端口、账号和 Docker 卷相互隔离。

## n8n 首次配置

1. 打开本地 n8n，完成 owner 初始化。
2. 在 n8n 设置中生成 API Key。
3. 把 Key 写入未跟踪的 `deploy/docker/.env.local`：`N8N_API_KEY=...`。
4. 执行 `docker compose --env-file deploy/docker/.env.local -f deploy/docker/docker-compose.yml up -d backend`。

未配置 Key 时，n8n 自身健康检查仍通过，平台 `/api/datafactory/health` 会返回明确的未配置/未授权状态，不会回退线上地址。

## Kestra 与 MCP

Kestra 固定使用开源版 `v1.3.20`，只连接独立的 `kestra` 元数据库，不持有业务
数据源连接信息；业务数据访问仍由 DataOps 数据源资源池统一治理。

Kestra MCP 使用官方 Python MCP 镜像和 stdio 传输，不提供 HTTP 端口，也不作为
常驻服务启动。AI/MCP 客户端可按需执行：

```bash
docker compose --profile kestra-mcp run --rm -T kestra-mcp
```

该入口默认关闭企业版、文件、KV 和命名空间工具，只保留流程、执行、日志、回填与
运行恢复相关能力。生产环境应通过密钥管理系统提供 `KESTRA_USERNAME` 和
`KESTRA_PASSWORD`，不要沿用本地测试默认值。

## DataOps MCP Gateway

AI 调度默认不直接使用 Kestra 原始 MCP，而是通过 DataOps MCP Gateway 的受治理
复合工具。先在 `.env.local` 中显式设置主体、角色、业务域、环境和每次会话唯一的
UUID 关联 ID，再按需启动 stdio 进程：

```dotenv
DATAOPS_MCP_SUBJECT=local-scheduling-agent
DATAOPS_MCP_ROLES=scheduler
DATAOPS_MCP_BUSINESS_DOMAINS=sales
DATAOPS_MCP_ENVIRONMENTS=test
DATAOPS_MCP_CORRELATION_ID=<每次会话新生成的 UUID>
```

```bash
docker compose --env-file deploy/docker/.env.local \
  -f deploy/docker/docker-compose.yml --profile dataops-mcp \
  run --rm -T dataops-context-mcp

docker compose --env-file deploy/docker/.env.local \
  -f deploy/docker/docker-compose.yml --profile dataops-mcp \
  run --rm -T dataops-scheduling-mcp
```

两个入口均使用 stdio、不开放 HTTP 端口、容器根文件系统只读并以非 root 用户
运行。Scheduling 入口连接平台 PostgreSQL、Kestra 和 Runner 任务令牌签发器；
Context 入口只读取有界控制面信息。身份范围或安全依赖缺失时进程拒绝启动。

V53 真实链路验收命令：

```bash
RUN_V53_MCP_CONTAINER=1 RUN_V53_MCP_L3=1 \
DATAOPS_MCP_PERSISTENCE_INTEGRATION=1 \
DATAOPS_MCP_TEST_DATABASE_URL=postgresql://dataops:dataops-test-password@127.0.0.1:15432/dataops \
PYTHONPATH=. .venv/bin/python -m pytest -q \
  tests/integration/test_dataops_mcp_container.py \
  tests/integration/test_mcp_gateway_persistence.py \
  tests/integration/test_v53_mcp_kestra_runner_l3.py
```

## 独立 DataOps Runner

Kestra 不直接连接业务数据库。每个 DAG 节点只调用内部域名
`http://dataops-runner:5600/v1/tasks/execute`，并携带由 DataOps 控制面签发的
短时、单任务绑定令牌。令牌包含任务、流程版本、节点摘要和关联 ID，不包含数据库
密码、密文或连接串；同一 `task_uid` 或令牌只能领取一次。

Runner 独立于 Flask 后端运行，复用现有 Neo4j 数据源定义和平台 PostgreSQL 加密
凭据仓库。默认 2 个 Worker，每个业务数据源池为 1 个基础连接加 1 个临时连接，
每个 Worker 最多保留 4 个空闲池，因此理论业务库连接上限为 16，低于默认预算
32。容器根文件系统只读，限制为 1 CPU、512 MiB 内存和 128 个进程。

首批节点执行策略：

- `sql.query`：只接受单条只读查询，使用绑定参数，底层事务强制只读并限制返回行数。
- `sql.execute`：要求受信任写授权和幂等策略；记录目标、幂等键和提交结果。提交结果
  不确定时标记为 `unknown`，不会作为普通失败自动重放。
- `python`：不接受任意源码，只执行镜像内预先注册的处理器；子进程设置 CPU、内存、
  文件句柄、时间和网络限制。当前默认处理器白名单为空。
- `http`：只允许 HTTPS 和部署方显式配置的主机白名单，禁止内联授权头、URL 凭据、
  重定向和超大响应。

本地 Compose 提供测试专用签名密钥；其他环境必须通过密钥系统提供至少 32 字节的
`RUNNER_TASK_TOKEN_SECRET`。允许的 HTTP 主机使用逗号分隔的
`RUNNER_HTTP_ALLOWED_HOSTS`。这两个值都不应写入 Kestra 流程定义。

## 首位管理员

平台不提供自助注册。首次完成数据库迁移后，由部署人员显式创建唯一的首位管理员；
后续账号只能由管理员在用户管理页面创建。密码不会写入 SQL、镜像或日志。

```bash
docker compose -f deploy/docker/docker-compose.yml exec -T \
  -e BOOTSTRAP_ADMIN_USERNAME=admin \
  -e BOOTSTRAP_ADMIN_PASSWORD='AdminPass123' \
  backend python -m app.commands.bootstrap_admin --if-missing
```

`AdminPass123` 仅是全本地隔离栈的验收样例。非本地环境必须通过密钥注入替换，且
重复执行命令只会跳过，不能覆盖已有管理员。

## 日常命令

```bash
# 状态
docker compose -f deploy/docker/docker-compose.yml ps

# 日志
docker compose -f deploy/docker/docker-compose.yml logs -f backend runner kestra n8n

# 停止但保留数据
docker compose -f deploy/docker/docker-compose.yml down

# 完全重置本地测试数据（不可恢复）
docker compose -f deploy/docker/docker-compose.yml down -v
```

## 数据库迁移与事件一致性

后端容器每次启动时都会先执行 `alembic upgrade head`，迁移成功后才启动
Gunicorn。初始化 SQL 只负责新测试卷的基础建库；后续结构变化统一由 Alembic
增量迁移管理。

```bash
# 查看当前版本
docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  alembic -c alembic.ini current

# 手工升级（正常启动时无需重复执行）
docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  alembic -c alembic.ini upgrade head

# 查看 Outbox 积压、失败数和最老事件年龄（不返回事件载荷）
curl -fsS http://localhost:15500/api/system/health

# 当前阶段尚未注册业务事件处理器；命令会安全退出并保留待处理事件
docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.process_outbox
```

治理对象使用 UUIDv7 作为跨 PostgreSQL、Neo4j、n8n 的稳定标识。核查或补齐
历史 Neo4j 节点前，应先执行只读报告，再显式选择回填：

```bash
docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.reconcile_governance_uids --ensure-constraints

docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.reconcile_governance_uids --backfill --ensure-constraints
```

## 冒烟验证

```bash
curl -fsS http://localhost:18183/ >/dev/null
curl -fsS http://localhost:18183/api/system/health
curl -fsS http://localhost:15678/healthz

docker compose -f deploy/docker/docker-compose.yml exec -T postgres \
  psql -U dataops -d dataops -c '\\dt public.*'

docker compose -f deploy/docker/docker-compose.yml run --rm minio-init
```

## 外部数据源连接池验收

两个外部数据源都会初始化 `acceptance_customers` 表及 Alpha、Beta 两条固定数据。
以下测试对 PostgreSQL 和 MySQL 分别执行 100 次并发读取，并验证连接复用、最大
并发连接数、空闲回收、重建和删除失效：

```bash
PYTHONPATH=. \
TEST_SOURCE_POSTGRES_URL=postgresql://source_reader:source-test-password@127.0.0.1:25432/acceptance \
TEST_SOURCE_MYSQL_URL=mysql+pymysql://source_reader:source-test-password@127.0.0.1:23306/acceptance \
.venv/bin/pytest -q tests/integration/test_datasource_pools.py
```

### 运行参数

连接池只管理通过数据源定义接入的外部业务数据库，不管理平台自身的 PostgreSQL。
中型单实例默认值为：基础连接 2、临时溢出 3、取连接等待 10 秒、连接回收
1800 秒、空闲池回收 900 秒、每个 Worker 最多保留 20 个空闲池、查询超时
30 秒。可通过 `.env.local` 中以下变量调整，服务端会执行上下限校验：

```dotenv
DATASOURCE_POOL_SIZE=2
DATASOURCE_MAX_OVERFLOW=3
DATASOURCE_POOL_TIMEOUT=10
DATASOURCE_POOL_RECYCLE=1800
DATASOURCE_POOL_IDLE_TTL=900
DATASOURCE_MAX_IDLE_POOLS=20
DATASOURCE_QUERY_TIMEOUT=30
DATASOURCE_CREDENTIAL_MASTER_KEY=<Base64 编码的 32 字节密钥>
DATASOURCE_CREDENTIAL_KEY_VERSION=v1
DATASOURCE_CERT_DIR=/etc/dataops-platform/datasource-certs
```

每个数据源只允许覆盖 `pool_size` 和 `max_overflow`，其余参数由平台统一控制。
修改连接定义或凭据版本后，旧池进入失效/排空流程，下一次访问按新版本惰性创建。

### 凭据迁移和一致性核查

先执行只读报告；确认单个 UID 后才允许加密迁移。迁移会先验证密文可解密，再从
Neo4j 删除旧明文：

```bash
docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.report_datasource_credentials

docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.migrate_datasource_credentials \
  --uid <数据源UID> --confirm-encrypt-and-remove-plaintext

docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.reconcile_datasource_credentials

docker compose -f deploy/docker/docker-compose.yml exec -T backend \
  python -m app.commands.reconcile_datasource_credentials \
  --repair --uid <数据源UID>
```

### 管理员诊断和安全失效

仅管理员可读取 `/api/datasource/pools`、`/api/datasource/<UID>/pool`，或向
`/api/datasource/<UID>/pool/invalidate` 提交固定原因 `admin_reset`、
`configuration_changed`、`credential_rotated`。接口只返回池状态和聚合指标，
不返回主机、账号、密文、连接串或配置指纹。强制失效会记录安全审计事件。

### 故障与恢复演练

默认测试只会跳过，不控制 Docker。显式设置开关后，测试会停止外部 MySQL，
验证三次失败后只对该数据源熔断，并确认外部 PostgreSQL和平台 PostgreSQL继续
可用；随后恢复 MySQL、执行半开探测并验证凭据版本轮换。`finally` 清理保证
MySQL 一定被重新启动：

```bash
RUN_DATASOURCE_OUTAGE_TEST=1 PYTHONPATH=. \
.venv/bin/pytest -q tests/integration/test_datasource_pool_failures.py
```

## Runner 增量验收

启动 V52 所需的最小服务并验证 PostgreSQL/MySQL 受治理读取、重复令牌拒绝以及
Kestra 到 Runner 的完整调用链：

```bash
docker compose -f deploy/docker/docker-compose.yml up -d --build --wait \
  postgres neo4j source-postgres source-mysql kestra runner

RUN_RUNNER_INTEGRATION=1 PYTHONPATH=. .venv/bin/pytest -q \
  tests/integration/test_runner_datasource_pool.py \
  tests/integration/test_kestra_runner_execution.py
```

Runner 停止或单个业务数据源故障不会停止后端、平台 PostgreSQL、Kestra 或 n8n。
回滚 V52 运行路径时只需停止 Runner，并继续使用原 Flask 数据源池和 n8n 流程。

若修改了 `database/*.sql`，已有 PostgreSQL 卷不会自动重放初始化脚本。已存在的
测试环境应新增 Alembic 迁移；只有明确需要丢弃全部本地数据时才执行 `down -v`。

## 2026-07-16 已验证基线

| 检查 | 结果 |
|---|---|
| Compose 服务 | backend、frontend、PostgreSQL、Neo4j、MinIO、n8n 均 healthy；`minio-init` 正常退出 0 |
| HTTP | 前端、前端代理的后端健康接口、n8n `/healthz` 均返回 200 |
| 平台健康 | PostgreSQL 与 Neo4j 检查为 true，总状态 healthy |
| 本地认证 | 新 RBAC 管理员 `admin` / `AdminPass123`；Argon2id 哈希，不提供注册接口 |
| PostgreSQL | 6 张当前业务表和本地 users 表已创建 |
| Neo4j | `RETURN 1` 成功 |
| MinIO | `dataops-bucket` 创建成功 |
| 隔离扫描 | 活跃源码、构建产物、渲染配置与运行日志未发现历史生产服务地址或密钥 |
| 数据库迁移 | Alembic 基线、治理 UID 与 Outbox 三组迁移可在空库和既有本地库重复执行 |
| 跨存储一致性 | 业务事务与事件同事务回滚；下游中断后可重试恢复；重复投递不重复执行处理器 |

镜像基线：

- backend：本仓库构建，Python 3.11，image ID `215df46dacc2`。
- frontend：本仓库构建，Node 24 + Nginx 1.27，image ID `42de39689f54`。
- PostgreSQL：`pgvector/pgvector:pg16`（PostgreSQL 16 + vector 扩展）。
- Neo4j：`neo4j:5.26-community`。
- MinIO：`minio/minio:RELEASE.2025-04-22T22-12-26Z`。
- n8n：`n8nio/n8n:1.100.1`。

已知非阻塞技术债：Vue 2 构建仍有 console、CSS 顺序和大包体警告，npm audit 报告 72 个传递依赖问题；这些与 Vue 3/依赖治理阶段统一处理，不在本次业务边界清理中升级。n8n 管理 API 需要完成本地 owner 初始化并配置本地 API Key 后才能验收。未配置 `VUE_APP_N8N_CHAT_URL` 和预览服务时，聊天助手与文件在线预览失败关闭，不会连接历史线上服务。
