docs: 分离 Database 资源与 Tenant 申请生命周期

This commit is contained in:
2026-09-24 16:21:17 +00:00
parent 9441b568da
commit 6db8a495fb
15 changed files with 627 additions and 1212 deletions
+150 -501
View File
@@ -1,558 +1,207 @@
# PostgreSQL Tenant Operator 系统规格说明书
# Ayatori Database 系统规格
| 项目 | 内容 |
| --- | --- |
| 状态 | Approved |
| 状态 | 资源模型与生命周期已批准;字段协议待 API 评审 |
| 目标 API | `database.ayatori.ddupan.top/v1alpha1` |
| 最后更新 | 2026-09-13 |
| 批准日期 | 2026-09-10 |
| 规范范围 | 首次注册外部 PostgreSQL 实例并创建一个应用租户 |
| 最后更新 | 2026-09-24 |
| 决策 | [ADR-0009](../decisions/0009-database-resource-and-claim.md) |
本文档定义系统对用户和外部依赖呈现的行为,是 API、测试和实现共同遵守的合同。
实现若需要改变本文合同,必须先修改规格并重新获得批准。
本文是当前行为合同,替代旧的 Tenant 同时承担申请与资源生命周期、PostgreSQL registry
持久所有权、任意 status 丢失自动恢复的设计。批准设计不表示实现已完成。
未决字段不能由实现自行补成新产品约定。
文中的“必须”“禁止”“应当”“可以”分别对应强制要求、强制限制、推荐行为和可选
行为。
## 1. 范围
## 1. 背景
在已存在的 PostgreSQL 实例上供应独立数据库、一个兼任 owner 的 login role、申请的扩展及
应用凭据;支持管理员显式导入已有数据库。Kubernetes API 管理声明与绑定,OpenBao 保存
应用凭据,ESO 向应用 namespace 投射 Secret。
homelab 中的大部分应用共享一个运行在独立 VM 上的 PostgreSQL DBMS。应用需要各自
独立的 database、作为 owner 的 login role 和密码,但不需要独立 PostgreSQL 实例。
目前这些资源依靠人工 SQL 和人工 Secret 管理,难以重复、审计和检测漂移。
不运行 PostgreSQL、VM、存储、备份或 OpenBao;不提供跨实例数据迁移、自动密码轮换、
多角色权限产品或跨系统事务。备份与数据恢复仍由管理员负责。
本系统使用 Kubernetes CRD 作为声明式 API,持续协调外部 PostgreSQL 与 OpenBao:
## 2. 资源与职责
```text
PostgreSQLInstance / PostgreSQLTenant
|
v
Ayatori Database controller
| |
v v
PostgreSQL catalog OpenBao KV v2
Instance
└─ Database × N 独立持久资源
└─ Tenant × 0..1 排他绑定的用户申请
```
## 2. 目标
v1alpha1 必须实现以下目标:
1. 注册一个已经存在的外部 PostgreSQL 实例并报告连接状态。
2. 为一个应用租户创建独立 database 和一个同时作为 database owner 的 login role。
3. 根据实例实际可安装扩展列表检查并安装租户申请的 PostgreSQL extension。
4. 首次生成高强度长期密码,并只把凭据明文写入 OpenBao KV v2。
5. 为 Kubernetes 应用创建 ExternalSecret,由 ESO 将凭据投射到同 namespace Secret。
6. 同时输出 OpenBao API URL,使 Kubernetes 外的应用可以直接读取凭据。
7. 同时输出 PostgreSQL DNS hostname 和 IP address,不假定所有消费者都能使用集群内
DNS。
8. 持续检测并修正由本系统管理的非破坏性漂移。
9. 通过 Kubernetes Condition 报告进度、成功和可操作的失败原因。
10. 重复 reconcile、controller 重启及外部依赖暂时失败不得重复创建或破坏资源。
11. 删除 Tenant CR 时默认保留外部资源;显式选择 `Delete` 时提供完整清理路径。
## 3. 非目标
v1alpha1 不负责:
- 创建、升级、备份或高可用运行 PostgreSQL DBMS/VM;
- 创建或运维 OpenBao;
- 直接写入包含凭据明文的 Kubernetes Secret;Secret 必须由 ESO 投射;
- 动态凭据、定时或自动密码轮换;
- Web UI、独立 REST API 或 Backstage 插件;
- 跨实例迁移 database;
- schema/table 级别租户、多 login role 或跨租户 grant;
- 删除不属于本系统管理的 database、role、extension 或 OpenBao Secret;
- 接管不是由本系统创建的外部资源;
- 提供生产环境 SLA。
## 4. 参与者与事实来源
| 对象 | 事实来源 | 说明 |
| 资源 | 职责 | 不承担的职责 |
| --- | --- | --- |
| 期望状态 | Kubernetes CR `spec` | 用户声明的合同 |
| 最近观察结果与当前阶段 | Kubernetes CR `status` | 可以丢失并重建,不是外部事实来源 |
| database/role/grant/extension | PostgreSQL catalog | 每轮 reconcile 必须重新读取 |
| 受管资源所有权与保留标记 | PostgreSQL controller registry | 与受管 DBMS 一起备份和恢复 |
| controller 工作流阶段 | Kubernetes CR `status.phase` | 状态机 checkpoint;可由外部事实保守重建 |
| 应用凭据 | OpenBao KV v2 | Kubernetes API 中不得出现明文 |
| Kubernetes 凭据投射 | External Secrets Operator | ExternalSecret 由本 controller 管理 |
| PostgreSQL 管理凭据 | controller namespace 的 Kubernetes Secret | 管理员维护 ExternalSecret,由 ESO 同步;Instance 只引用 Secret |
| Instance | 登记实例、管理连接、能力与供应前置条件 | 持有租户集合、保存所有权表 |
| Database | 描述外部数据库、管理范围、绑定与回收策略 | 充当第二套 registry 或通用资源框架 |
| Tenant | 声明需求或显式选择资源,申请使用并交付凭据 | 删除时隐式销毁独立资源记录 |
平台管理员管理 `PostgreSQLInstance`、controller 部署配置、OpenBao policy 和
PostgreSQL 管理 role。应用或 GitOps 流程在获得 namespace RBAC 后管理
`PostgreSQLTenant`。
Instance 保持 cluster-scoped,Tenant 保持 namespaced。Database 的工作名称是
PostgreSQLDatabase;scope、字段拼写和管理角色/凭据的具体边界待 API 评审。
单 database + 单 login owner 的首版使用场景不变,不新增独立 Role 或 Claim CRD。
## 5. 资源模型
Database 自身必须声明 `instanceRef`,手工登记时同时指定实际数据库名;无需先存在 Tenant,
即可通过 Instance 验证目标。动态申请由 Tenant 选择 Instance,供应时把该引用写入 Database;
选择已有 Database 的 Tenant 从资源获取 Instance,不重复指定另一份来源。资源与实例的归属
独立于使用绑定,Tenant 删除后仍保留;修改引用不是数据库迁移。
### 5.1 PostgreSQLInstance
参考 [Kubernetes PV/PVC](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) 的
资源/申请分离与绑定生命周期,不复制存储调度和 CSI 协议。资源与申请的关系不是 GC 所有关系。
`PostgreSQLInstance` 是 cluster-scoped 资源,表示一个已经存在、可由 controller
管理的 PostgreSQL server。
## 3. 身份与事实来源
它必须声明:
- Kubernetes spec 保存声明;受保护的资源绑定记录与 status 保存身份关联、操作进度和观察。
它们通过 API 持久化,不因 controller 重启而消失。
- PostgreSQL catalog 是 database、role、grant、extension 实际状态的来源。
- OpenBao 是应用凭据的事实来源;ESO 状态和目标 Secret 存在性说明投射结果。
- Instance、Database、Tenant 以 UID 区分对象身份;namespace/name 用于定位,
同名新 UID 不继承旧绑定。数据库 OID 仅供诊断,不是永久身份或删除授权。
- 不新增 PostgreSQL 所有权表、安装身份表或 Retain 墓碑;COMMENT 也不能授权认领。
- 记录操作意图不等于外部操作成功,phase 不等于外部所有权;执行前后仍须观察实际状态。
- PostgreSQL host、port 和管理连接使用的 database;
- PostgreSQL host address,供无法解析 DNS 的消费者使用;
- TLS mode;
- controller namespace 中 PostgreSQL 管理 Secret 的名称和字段名。
## 4. Instance 合同
可安装的 extension 集合由应用层从目标 PostgreSQL 查询,不由管理员在 Instance
中声明。v1alpha1 不实现 allowlist;该概念保留为后续可选策略。实际可用不代表安装
权限及前置条件已满足,安装仍需执行并回读;查询失败不得被解释为扩展不支持。
Instance 声明 host、hostaddr、port(默认 5432)、管理 database(默认 postgres)、
TLS mode(默认 verify-full)及 controller namespace 的管理 Secret 名称和字段映射。
禁止隐式 TLS 降级。凭据引用不接受自选 namespace 或 OpenBao path。
实例 Ready 不代表 PostgreSQL 数据有备份或高可用,只表示 controller 当前可以安全
建立管理连接、读取 server metadata、访问 controller registry 并使用所需管理能力。
管理员维护 ExternalSecret,由 ESO 同步管理 Secret;controller 只读,不修改管理密码,
不直接从 Bao 取管理凭据。有效用户名或密码变化时释放旧连接并重验;仅 metadata 或无关
字段变化不重建。中途凭据变化必须丢弃旧观察。已有有效管理凭据时,Bao/ESO 故障本身
不撤销 Instance Ready;首次缺少有效 Secret 时不能 Ready。
Instance 身份和 endpoint 以管理员声明为准。修改 endpoint 不验证是否仍是原物理
服务器或原 registry,不增加服务器/安装身份绑定检查;但旧配置观察失效,必须按
新配置重新检查连接和管理能力。新 CR 按新 Instance 处理,不自动接管旧 UID 的租户
资源。controller 不迁移旧服务器上的数据,也不清理旧目标,影响由管理员负责评估。
Ready 要求当前目标的连接、服务器 metadata 和所需管理能力检查通过,不要求创建、
迁移或读取 registry,也不证明备份或高可用。阶段简化为 Pending → Validating → Ready,
删除进入 Deleting。实际扩展可用列表不等于安装权限;查询失败不等于不支持。
Instance 开始受管时即添加 finalizer,成功保存后才参与供应,不等发现 Tenant 后
再补加。删除期间停止新供应;仍有引用它的 Tenant(包括正在删除的 Tenant)时保留
finalizer,无引用后才移除。不级联删除 Tenant 或任何外部数据库、角色、凭据。
引用检查失败不得当作无引用。首版不引入跨对象锁或准入控制:finalizer 不禁止同时
创建 Tenant CR,新 Tenant 遇到正在删除或已不存在的 Instance 时不得开始供应。
这不保证列表检查、CR 创建和在途外部操作之间的原子性;不是严格的跨对象事务。
endpoint 变更由管理员负责评估,不验证物理服务器连续性,不迁移或清理旧目标;
旧观察失效。新 UID 不接管旧资源。Instance 开始受管前保存 finalizer;删除时停止新供应,
只要有引用它的 Database(包括 Released/删除中)或尚未绑定的 Tenant 就等待。
查询失败不视为无引用;无引用才解除 finalizer,不级联删除任何业务资源。
引用检查不是跨对象事务,正在删除或不存在的 Instance 不允许开始新的供应/绑定。
### 5.2 PostgreSQLTenant
## 5. 动态供应与排他绑定
`PostgreSQLTenant` 是 namespaced 资源。v1alpha1 中,一个 Tenant 精确对应:
1. 校验 Tenant 请求、Instance 能力、名称和扩展要求。
2. 在首次外部写入前持久化独立 Database 记录、确定目标与管理范围。
3. 建立带 UID 的排他预留/绑定,避免两个 Tenant 同时使用同一 Database。
4. 按已确认步骤建立凭据、role、database、授权和扩展,逐步回读。
5. 验证应用登录与 ESO 投射后,Tenant 才可 Ready。
- 一个 `PostgreSQLInstance`;
- 一个 database;
- 一个同时作为 database owner、供应用使用的 `LOGIN` role;
- 零个或多个 extension;
- 一个 OpenBao KV v2 凭据位置;
- 一个同 namespace ExternalSecret 及其目标 Kubernetes Secret。
绑定前固定有效目标;绑定或开始外部供应后不得通过修改名称或引用实施隐式迁移。
每个 Database 最多一个使用者,每个 Tenant 最多一个 Database。
绑定 API 写入采用 resourceVersion 并发控制;双向记录不原子,单边完成不得授予使用权限。
具体字段、写入顺序与重启恢复协议须在 API 切片定义,并由真实 API server 验证。
Tenant 的 namespace 用于 Kubernetes RBAC 和身份识别,不代表 PostgreSQL schema。
同一 Instance 中的 database 和 role 名称全局唯一。
不同 Database 记录请求同一外部名称仍可能竞争,不能仅靠 Kubernetes 中的列表检查保证
PostgreSQL 名称唯一。后端创建时的重名失败报告 Conflict,失败方不得接管胜方资源。
不为此新增跨系统锁或 registry。管理员也不得把同一物理数据库登记成多个可绑定资源。
## 6. 标识与默认值
## 6. 显式导入
以下是 v1alpha1 的标识合同:
管理员创建资源声明,明确 Instance、已有数据库和允许管理的范围,构成导入授权。
初始检查只读验证存在性、owner、角色权限和扩展等是否匹配;不匹配报告清楚的差异,
不得通过重置密码、改变 owner 或撤销现有访问来“完成导入”。
| 字段 | 默认值 | 约束 |
| --- | --- | --- |
| Instance port | `5432` | 1–65535 |
| Instance host address | 无 | 必须是合法 IPv4 或 IPv6 address |
| 管理 database | `postgres` | 合法 PostgreSQL identifier |
| TLS mode | `verify-full` | 禁止隐式降级 |
| Tenant database | `metadata.name` | 同一 Instance 全局唯一 |
| login role | `metadata.name` | 同一 Instance 全局唯一 |
| deletion policy | `Retain` | `Retain` 或 `Delete` |
未显式导入的同名数据库一律 Conflict。导入资源默认 Retain,不隐含 Delete 授权。
Tenant 显式引用已登记资源仍需通过绑定资格与授权检查;知道资源名称不等于有权使用。
资源侧预留/授权的具体 API 和已有凭据的安全关联入口待细化,完成前不能宣称可用。
Tenant 的 `spec.instanceRef` 与 `metadata.name` 长度合计不得超过 241 个字符,确保
派生的 ExternalSecret/Secret 默认名称
`<instanceRef>-<metadata.name>-postgresql` 不超过 Kubernetes 253 字符限制。
## 7. Retain、重新绑定与 Delete
固定默认值由 CRD defaulting 写入。依赖 `metadata.name` 或 `instanceRef` 的 database、
login role、ExternalSecret/Secret 名称属于 controller 语义默认值:省略字段不会被 admission
回写,controller 必须始终计算同一个 effective value,并通过 status 的 database、
loginRole、credential reference 以及实际资源展示。
v1alpha1 不为此引入 mutating webhook。
回收策略属于 Database,默认 Retain;Tenant 删除是释放申请,不是独立资源的 GC 授权。
Database 不得设置会让它随 Tenant 消失的 ownerReference。
database 和 role 名称必须作为 PostgreSQL identifier 参数安全引用,禁止通过字符串
拼接执行。名称校验必须拒绝空字符串、NUL 和超过 PostgreSQL identifier 长度限制的
值,并统一限制为小写字母、数字和下划线。
### Retain
Tenant 首次成功后,`instanceRef`、database、login role 和凭据位置必须
不可变。修改这些字段不是 rename 或 migration,API 必须拒绝或报告明确的
`ImmutableField`。
- 保留 Database 对象、外部数据以及与资源关联的角色和凭据,不自动删除或重置。
- Tenant 释放后 Database 进入 Released,保留旧绑定身份用于诊断和防止自动复用。
Released 不是 Available,不再向原申请交付新状态,也不自动分配给同名新 Tenant。
- 保留策略不要求 PostgreSQL/Bao 在线才能完成申请释放,但必须先将释放关系安全记录到
Kubernetes;API 写入失败时不能宣称释放完成。仍有在途操作时不得跳过必要协调。
- 管理员检查数据、旧账号访问与凭据后,显式授权重新绑定。保留数据的复用可以不清空数据,
但必须由管理员确认新使用者应获得这些数据及旧使用者的权限处置。
- 删除旧投射 Secret 或解除绑定不等于撤销 PostgreSQL 访问;Retain 不承诺自动撤权。
ExternalSecret/Secret 的保留与清理细节需随凭据交付协议明确。
## 7. PostgreSQL 权限合同
### Delete
建议的 v1alpha1 权限模型如下:
必须由有权限的主体在资源侧明确授权,并核对 Database 身份、绑定、实际对象和管理范围。
在相关 finalizer 保护下清理投射、阻止新登录、处理已有连接、删除 database,再按已确认的
独占管理范围清理 role 与凭据;共享或未纳管的对象不得删除,禁止扩大 CASCADE 范围。
1. database 必须由 login role 拥有。
2. login role 必须是 `LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION`。
3. 必须撤销 `PUBLIC` 对租户 database 的连接权限,再显式允许 login role 连接。
4. controller 不得修改其他 database 或无关 role 的权限。
5. controller 只保证请求的 extension 存在;移除 extension 不得自动执行
`DROP EXTENSION`。
每步回读,失败保持进度与 finalizer;确认已删除的对象可幂等跳过,未知同名对象不能继续删除。
数据库被使用时,直接删除 Database 不得绕过绑定保护。资源已释放后才按策略处理。
具体 Tenant/Database finalizer 配合与投射清理顺序须经 API 设计及故障注入验收。
这意味着应用可以在自己的 database 内执行 schema migration,但不能创建其他
database、role 或访问其他租户。v1alpha1 不创建只有形式意义、却未隔离运行时权限的
额外 `NOLOGIN` owner。若以后应用能分别使用 migration 和 runtime 凭据,再通过新的
权限 profile 引入 owner/migrator/runtime 角色模型。
## 8. 幂等、失败与人工处理
## 8. OpenBao 凭据合同
普通依赖故障退避重试。已持久确认且仍与观察一致的步骤可以幂等继续;controller 重启
不重新生成密码,不重复创建已确认资源。
### 8.1 Controller 自身认证
外部创建成功但记录尚未保存,或超时导致结果不确定时,若不能可靠确认归属,报告
Conflict 并停止相关写入;不得仅凭名称相同、曾记录意图或字段相似自动认领。
失败恢复不承诺全部自动完成,也不实现队列模拟事务。
controller 必须使用 Kubernetes auth 登录 OpenBao。controller 使用的 OpenBao API
address、提供给消费者的 OpenBao API address、auth mount、auth role 和 KV v2 mount
属于部署配置,不属于任何 CR。两个 API address 可以相同;若 controller 使用集群内
地址而外部消费者不能解析,则必须单独配置 consumer address。生产部署的 KV mount
默认为 `kv`;开发环境可以配置为 OpenBao dev server 默认的 `secret`。长期 OpenBao
Token 禁止写入 Deployment、CR 或镜像。
status 缺失不假定发生于正常重启。Instance 可重新探测能力;Database/Tenant 缺少绑定或
操作确认时不能从外部同名对象推导所有权。按冲突/灾难恢复处理,不自动重建所有权表。
### 8.2 管理凭据
人工处理必须能看到:请求与资源身份、目标 Instance/database/role、失败步骤、已确认完成
与结果不确定的操作、冲突原因、下一步核实建议。保留现场,不自动删除疑似残留或改密。
管理员核实后可清理确定的残留再重试,或走显式导入;不能通过伪造 Ready/清空 status 强行继续。
`PostgreSQLInstance` 只引用 controller 自身 namespace 中 Kubernetes Secret 的名称及
用户名、密码字段名,不允许指定 namespace 或 Bao path。endpoint 仍由 Instance 声明。
平台管理员维护 ExternalSecret,将 OpenBao 管理凭据同步到该 Secret;controller 只读
Secret,不创建或修改管理 Secret、其 ExternalSecret 或上游管理凭据。
## 9. 权限、凭据与扩展
Instance 管理连接不直接访问 Bao,不负责管理密码轮换。已装配的凭据仍可访问
PostgreSQL 且满足 registry/权限要求时,Bao 或 ESO 暂时不可用不使 Instance NotReady。
首次装配无法取得有效 Secret 时不能 Ready。检测到所引用 Secret 的有效用户名或密码
变化时,controller 使用新值重建管理连接池并重新检查管理能力;仅 metadata 或无关
字段变化不触发重建。刷新不依赖 Instance generation 变化,新连接验证失败按实际
故障报告,不能用旧连接的成功结果证明新凭据可用。
controller 不修改 PostgreSQL 密码、不回写 Secret,也不修改 Bao 管理凭据;数据库侧
凭据变更由管理员负责。这是跟随已提供凭据的连接刷新,不是自动密码轮换。
Tenant 凭据管理仍直接依赖 Bao。
动态供应继续使用一个兼任 database owner 的 LOGIN role;应用角色不得具备 superuser、
CREATEDB、CREATEROLE 或 replication 权限。撤销 PUBLIC CONNECT,再授予目标角色;
不修改无关数据库和角色。identifier 匹配 `^[a-z][a-z0-9_]{0,62}$`,SQL 安全引用。
### 8.3 租户凭据
请求扩展按实例实际可用集合判断,成功后只追加,不自动 DROP EXTENSION。
可用列表查询失败按依赖错误处理;实际安装仍检查权限与结果。
Tenant 不声明凭据 path。controller 根据部署级 KV mount、base path 和 Tenant 的
namespace/name 推导唯一的 mount-relative path。base path 来自 controller 启动参数
`--openbao-tenant-base-path`,默认 `postgresql-tenants`。最终路径为
`<base-path>/<namespace>/<metadata.name>`。推导结果禁止以 `/` 开头,禁止包含空路径段、
`.`、`..`,也禁止把 KV v2 HTTP API 的 `data` 或 `metadata` 层编码进路径。
应用密码使用安全随机源,只写 OpenBao;已有可信凭据可复用,不因失败生成第二份密码。
新建时先安全保存并回读凭据,再创建角色;凭据写入本身结果不确定也适用人工冲突规则。
Kubernetes 应用由 ESO 投射同 namespace Secret,controller 不直接写明文 Secret。
新 Tenant 的凭据建立顺序必须可从任意中断点恢复:
凭据仍输出 username/password/database/host/hostaddr/port/sslmode 七键,不生成带密码 URI。
Tenant status 提供 Secret 引用与无认证信息的 OpenBao API URL。
mount/base path 属部署配置,Tenant 不得自选任意路径;原按 Tenant namespace/name 固定
推导路径的规则需修订为可支持资源保留与重新绑定的定位协议,本轮不定字段或新路径格式。
不得因换 Tenant、改部署参数或重新绑定就隐式搬迁凭据或改密。
1. 验证 Instance、名称、extension 和目标 OpenBao 路径;
2. 确认目标 database、role 和 OpenBao 记录不存在,或能够验证为同一 Tenant
已创建的部分状态;
3. 生成密码;
4. 先创建带 controller 所有权 metadata 的 OpenBao KV v2 记录;
5. 从 OpenBao 重新读取凭据;
6. 使用该凭据创建作为 database owner 的 login role 和其他 PostgreSQL 资源;
7. 用 login role 实际连接目标 database;
8. 全部验证成功后将 Tenant 标记 Ready。
TLS、OpenBao Kubernetes auth、controller/ESO 身份隔离、Secret 读取范围和防泄漏要求
见 [安全模型](security.md)。这些安全约束继续适用。
若第 4 步成功、后续 PostgreSQL 操作失败,下一轮必须读取同一份 OpenBao 凭据继续,
不得生成第二个密码。若 PostgreSQL 先存在而 OpenBao 记录不存在,controller 必须报告
Conflict,不得擅自重置已有 role 密码。
## 10. Conditions 与可观测性
controller 必须在 PostgreSQL 管理 database 的专用 registry schema 中持久保存可验证的
Instance UID、Tenant UID 与 namespace/name 关联,不能只依赖会丢失的 CR status 判断
资源所有权。registry 必须可回读且不得改变数据库授权语义;database 或 role COMMENT
不能作为权威所有权记录。
三类资源均提供唯一的 Ready Condition 及 observedGeneration,phase 只辅助表示阶段。
Ready=True 必须有当前目标的实际验证;Database 已绑定不等于 Tenant 凭据交付已经完成。
默认写入字段固定为:
至少区分 Reconciling、InvalidSpec、ImmutableField、DependencyUnavailable、
AuthenticationFailed、InsufficientPrivileges、InstanceNotReady、Conflict、
ProvisioningFailed、CredentialProjectionFailed。Released 应明确显示未可供重新绑定,
具体 Condition Reason 由 API 细化,不假定仅靠 phase 判断授权。
```text
username
password
database
host
hostaddr
port
sslmode
```
使用结构化日志、Events 和低基数失败分类 metrics;Condition 是面向使用者的主要诊断入口。
禁止在任何 CR、Event、日志、metric、trace 或测试输出中出现密码、Token、完整秘密响应。
这些字段是 controller 的规范化输出合同。controller 不生成包含密码的 URI、JDBC URL
或应用专用键名。应用通过 ExternalSecret template、Helm values 或自身配置把原子字段
映射为 `DATABASE_URL`、独立环境变量或配置文件;因此 URI escaping 和应用特有格式也
由消费方负责。`host` 是 DNS 名称,`hostaddr` 是可直接连接的 IP;消费者自行选择其
支持且可达的连接目标。PostgreSQL server 证书必须同时包含与 `host` 匹配的 DNS SAN
和与 `hostaddr` 匹配的 IP SAN,使两种目标都能在 `verify-full` 下独立完成身份验证。
## 11. 验收
### 8.4 凭据输出与 ExternalSecret
controller 必须根据部署级 base path 推导 Tenant 的 KV path,Tenant 不能选择 mount 或
任意远端路径。ExternalSecret 固定命名为
`<instanceRef>-<metadata.name>-postgresql`。Tenant 可以通过
`spec.credential.secretName` 指定目标 Kubernetes Secret 名称;省略时使用同一默认名。
自定义名称只需是合法 Kubernetes Secret 名称,不限制命名内容;两者均与 Tenant 位于
同一 namespace。
controller 必须创建同 namespace ExternalSecret,从固定的 ClusterSecretStore 读取七个
原子字段。ExternalSecret 及目标 Secret 的名称通过 Tenant status 暴露。controller
不得直接读取 OpenBao 密码后写入 Kubernetes Secret。
Tenant status 还必须提供完整、可由外部消费者使用的 OpenBao KV v2 API URL。URL 可以
包含 consumer API address、mount 和 secret path,但不得包含 Token、密码或其他认证
信息。默认 `kubectl get` 表格显示目标 Secret 名称;完整 OpenBao URL 通过
`kubectl get postgresqltenant <name> -o yaml` 获取,避免表格列过长。
OpenBao metadata 必须能够标识 Tenant UID、namespace/name 和 Instance,使 controller
区分自己的残留记录与外部记录。任何凭据值都不得进入日志、Event、Condition、metric
label、trace、CR spec/status 或测试快照。
## 9. Reconcile 行为
系统采用最终一致性模型。Kubernetes、PostgreSQL、OpenBao 和 ESO 可以短暂处于不同
阶段;controller 不尝试实现跨系统事务,而是以 Kubernetes CR `status.phase` 作为
工作流 checkpoint,通过幂等外部操作和每轮回读验证最终收敛。
两个 CR 的状态机权威记录都在 `status.phase`。controller 根据 phase 选择下一项候选
动作,但 phase 不能替代外部状态检查:执行前后仍须回读 PostgreSQL catalog、registry、
OpenBao 和 Kubernetes/ESO。外部写入成功但 status 更新失败时,下一轮必须识别已完成
事实并推进 phase,不得重复生成密码或报告虚假冲突。
status 丢失时,controller 必须从 registry 的所有权记录和各外部系统实际状态保守重建
phase。若 status 被伪造或领先于实际状态,controller 必须纠正到安全阶段并补齐资源,
不能跳过验证。registry 不保存或驱动协调 phase。
Instance phase 按当前 generation 表示连接与初始化进度:
```text
Pending -> Validating -> InitializingRegistry -> Ready
(any phase) --------------------------------> Deleting
```
spec generation 改变后可以从 `Ready` 回到 `Validating`。Tenant phase 如下:
```text
Pending -> Planned -> CredentialCreated -> RoleCreated -> DatabaseCreated
-> ExternalSecretCreated -> CredentialProjected -> Ready -> Deleting
```
失败不增加 `Failed` phase;phase 保留在无法推进的步骤,由 `Ready=False` 的 Reason 和
message 表达 `Conflict`、认证失败或依赖不可用。Retain 删除完成后 CR 已不存在,因此
没有持久的 `Retained` phase。
每轮 Tenant reconcile 必须按以下逻辑执行:
```text
读取 Tenant
-> 读取 Instance
-> 校验不可变字段与输入
-> 检查 Instance Ready
-> 读取 OpenBao 与 PostgreSQL 实际状态
-> 检测冲突或部分完成状态
-> 执行非破坏性补齐
-> 使用应用凭据验证登录
-> 创建并验证 ExternalSecret/Secret 投射
-> 回读实际状态
-> 更新 status
```
要求:
- 所有步骤必须幂等;
- 每个外部写入前必须先在 CR status 持久化足够的操作意图,写入后必须回读并推进
`status.phase`;
- 暂时性网络、锁和依赖错误必须重试;
- 输入错误、资源冲突和禁止操作不得忙循环重试,只在 generation 或依赖状态变化后
重试;
- 未知外部资源不得被修改、接管或删除;
- 用户从 `spec.extensions` 移除 extension 时不得执行卸载,必须报告该字段在 v1alpha1
中只允许追加;
- controller 重启不得影响已经签发的应用密码;
- `status` 丢失后必须可以从 registry、PostgreSQL、OpenBao 和 Kubernetes/ESO 重建。
## 10. Condition 合同
两个资源都必须提供唯一的 `Ready` Condition。可以增加辅助 Condition,但调用方只需
依赖 `Ready`。
| 状态 | 含义 |
| 场景 | 必须验证的结果 |
| --- | --- |
| `Ready=Unknown` | 正在首次观察或 reconcile,尚无结论 |
| `Ready=False` | 当前 generation 未达到合同要求 |
| `Ready=True` | 当前 generation 已回读验证成功 |
| Instance 登记与重验 | 无 registry 依赖;真实凭据/TLS/管理权限检查 |
| 动态供应与重复 reconcile | 独立资源记录、排他绑定、密码不变、实际登录与投射成功 |
| 显式导入 | 无数据/密码/owner 隐式修改;错误目标及未经授权申请被拒绝 |
| 同名未知资源 | Conflict,原数据库/角色/凭据不变 |
| 并发申请与单边绑定 | 最多一个使用者;失败方不能开始危险外部操作 |
| controller 重启 | 已确认步骤正常继续;不确定创建报告人工可诊断冲突 |
| Retain 与 namespace/Tenant 删除 | Database 不被 GC,外部数据保留,Released 不自动复用 |
| 人工重新绑定 | 旧 UID 不继承使用权;确认数据及凭据/旧访问处置后才能交付 |
| Delete 每步中断 | finalizer 保留,可重试,不误删未知/共享/未纳管对象 |
| 依赖稍后出现/权限恢复 | 安全重试,过期观察不授权写入 |
| status/备份恢复不一致 | 不凭同名推导归属,明确人工处理范围 |
| 泄漏与权限 | canary 不出现在输出;namespace 用户不能导入/回收他人资源 |
Condition 必须带正确的 `observedGeneration`。资源自身的
`status.observedGeneration` 只在当前 generation 完成一次有结论的 reconcile 后更新。
最低 Reason 集合:
| Reason | 适用资源 | 含义 |
| --- | --- | --- |
| `Reconciling` | 两者 | 尚在处理 |
| `Ready` | 两者 | 当前 generation 已验证 |
| `InvalidSpec` | 两者 | 输入不满足规格 |
| `DependencyUnavailable` | 两者 | PostgreSQL 或 OpenBao 暂时不可用 |
| `AuthenticationFailed` | Instance | 管理凭据或 TLS 验证失败 |
| `InsufficientPrivileges` | Instance | 管理 role 缺少必要权限 |
| `InstanceNotReady` | Tenant | 引用的 Instance 未 Ready |
| `Conflict` | Tenant | 目标名称或 OpenBao 路径已被其他主体占用 |
| `ProvisioningFailed` | Tenant | 可重试的创建/验证失败 |
| `CredentialProjectionFailed` | Tenant | ESO 或目标 Secret 未达到期望状态 |
Condition message 必须适合人类排障,但禁止包含连接串密码、Token 或完整 Secret 数据。
## 11. 删除与保留
### 11.1 Retain
`Retain` 是默认策略:
- 删除 Tenant CR 不得删除 database、role、extension 或 OpenBao 记录;
- controller 不得因外部依赖不可用而永久阻止 Retain CR 删除;
- 保留资源必须继续携带原 Tenant UID 和 namespace/name 的所有权记录,但在 CR 删除后
明确处于 unmanaged 状态;
- 重新创建同名 Tenant 会产生新的 UID,必须因已有资源不属于新 UID 而报告 Conflict;
- v1alpha1 不提供重新关联、import 或 adoption;恢复管理必须使用第 12 节的迁移流程,
或等待后续版本定义显式纳管协议。
### 11.2 Delete
用户在创建 Tenant 时显式设置 `deletionPolicy: Delete`,表示删除 CR 时授权永久清理
该 Tenant 的外部资源。controller 必须使用 finalizer,并按以下顺序处理:
1. 再次验证 database、role 和 OpenBao 记录都属于当前 Tenant UID;
2. 删除 ExternalSecret,并确认目标 Kubernetes Secret 已删除;
3. 禁止该 login role 建立新连接;
4. 终止该 database 的现有连接;
5. 删除 database,database 内 extension 随之删除;
6. 删除 login role;
7. 删除 OpenBao KV 记录及其可恢复版本;
8. 回读确认外部资源均不存在;
9. 删除 controller registry 记录;
10. 移除 finalizer,允许 Kubernetes 删除 CR。
任一步失败都必须保持 finalizer 并从安全检查开始重试。controller 禁止使用
`CASCADE` 删除无法证明属于该 Tenant 的依赖对象。若 Instance 或 OpenBao 永久丢失,
管理员可以在核实外部状态后手工移除 finalizer;该逃生操作必须在运维 runbook 中明确
标记为可能遗留资源。
v1alpha1 不自动检查备份,也不承诺恢复被 `Delete` 删除的数据。显式选择 Delete 的
用户承担数据销毁语义;默认 Retain 用于避免普通误删。
## 12. 现有环境迁移
v1alpha1 不接管现有 database 或 role,但必须提供可重复、可回滚的迁移 runbook。对每
个现有应用租户,推荐的停机迁移顺序是:
1. 盘点 database、role、owner、grant 和 extension,并完成可恢复备份;
2. 创建逻辑备份,必须使用可映射到新 owner 的格式,避免恢复旧 role ownership;
3. 停止应用写入并确认没有活动写事务;
4. 完成最终逻辑备份;
5. 将旧 database 和 role 重命名为带迁移时间戳的保留名称,释放最终名称;
6. 创建 `PostgreSQLTenant`,由 controller 创建最终 database、role 和 OpenBao 凭据;
7. 等待 Tenant Ready;
8. 以新 owner 恢复逻辑备份,并验证 row count、schema、extension 和应用权限;
9. 让 ESO 投射新凭据,重启或重新部署应用;
10. 验证应用读写后结束维护窗口;
11. 保留旧 database、role 和备份直到回滚窗口结束,再由管理员手工清理。
回滚时停止新应用写入、恢复原名称或连接配置,并重新使用旧凭据。迁移工具不得把旧
密码、管理凭据或 dump 文件提交到 Git。真实命令、锁定方式和各现有应用验证项见
[`migration.md`](migration.md),并必须在实现首个可用版本前通过临时 PostgreSQL 实例
演练。
## 13. 安全要求
1. 所有 PostgreSQL 与 OpenBao 网络访问必须支持超时和 context cancellation。
2. homelab 部署必须通过 Deployment 挂载的共享 CA bundle 验证 TLS server identity;
该 bundle 的信任根来自 OpenBao PKI,但不得包含 CA 私钥。Instance 默认使用
`verify-full`,其 host 必须与服务器证书名称匹配。开发环境可以显式使用 `disable`
明文连接。
3. PostgreSQL 管理 role 应使用满足本规格的最小权限,不应使用 PostgreSQL
superuser;若 extension 安装需要额外权限,必须单独记录例外。
4. controller 的 OpenBao policy 仅覆盖受管租户 KV 操作,不授予管理凭据路径权限。
管理凭据的 ESO 同步身份与应用凭据的 ESO 读取身份隔离。controller 只在自身
namespace 获得管理 Secret 读取权限,不因此扩大跨 namespace Secret data 访问范围。
5. namespace 用户不得修改 cluster-scoped Instance。
6. 所有 identifier、extension name 和引用字段必须在发起外部调用前校验。
7. controller 不得通过 shell 或 `psql` 子进程执行用户输入。
8. 错误包装、结构化日志和 tracing 必须经过 Secret 泄露测试。
详细威胁模型和部署 policy 见 [`security.md`](security.md)。
## 14. 可观测性要求
v1alpha1 至少必须提供:
- Kubernetes Events:开始 provisioning、成功及需要人工处理的失败;
- 结构化日志:resource namespace/name、Instance、generation、阶段和错误类别;
- controller-runtime 默认 reconcile metrics;
- 不包含 database、role、OpenBao path 等无界用户输入的低基数失败分类 metric。
日志和 metrics 的存在不能代替 Condition;Condition 是 API 使用者判断状态的主要方式。
## 15. 验收标准
实现 v1alpha1 第一条完整纵向切片前,测试必须覆盖:
1. 有效 Instance 可以建立 TLS 管理连接并变为 Ready。
2. PostgreSQL 管理能力不可用时 Instance Ready=False,恢复后自动变为 Ready;已有
管理凭据可正常使用时,Bao/ESO 故障不单独影响 Instance Ready。首次装配缺少有效
管理 Secret 时不能 Ready;Tenant 的 Bao 操作失败按其自身依赖故障报告。
3. 有效 Tenant 创建 database、作为 owner 的 login、grant、extension 和 OpenBao
记录。
4. 应用凭据可以实际连接且不能创建其他 database/role。
5. 相同 generation 重复 reconcile 不改变密码、不重复创建资源。
6. controller 在每个外部写入步骤后中断,重启后都能继续并得到相同最终状态。
7. 预先存在且不属于当前 Tenant UID 的 database、role 或 OpenBao path 导致
Conflict,且不修改已有资源。
8. 目标实例实际不支持的 extension 在供应外部写入前被拒绝;扩展列表查询失败时
按依赖故障处理,不报告为不支持。安装结果仍须回读验证。
9. status 被清空后可以从两个外部事实来源重建。
10. 删除 Retain Tenant 后外部资源仍存在且不再受管;重新创建同名 Tenant 报告
Conflict。
11. 日志、Event、Condition、metric 和 CR 中不存在生成的密码或管理凭据。
12. 两个 namespace 对同一 Instance 申请相同名称时,只有第一个成功,第二个报告
Conflict。
13. 删除 Delete Tenant 时,任一步骤失败都可重试,且最终删除 database、login role、
OpenBao KV 历史和 finalizer。
14. 使用迁移 runbook 可以把一个现有 database 转移到新建的受管 database,并在回滚
窗口内恢复旧服务。
15. Tenant 只有在 ExternalSecret Ready、目标 Secret 存在且应用凭据实际可登录后才
Ready。
16. Tenant status 同时提供 Kubernetes Secret reference 和不含认证信息的 OpenBao API
URL。
17. DNS 不可用时,使用输出的 `hostaddr` 可以连接 PostgreSQL;server 证书同时覆盖
`host` 的 DNS SAN 和 `hostaddr` 的 IP SAN,两种连接目标均可通过 `verify-full`。
18. 两个 CR 的 `status.phase` 都能反映当前协调步骤;清空 status 后可以从外部事实重建,
且伪造或过期 phase 不会使 controller 跳过验证或外部操作。
单元测试验证纯决策逻辑,adapter 集成测试使用 Docker PostgreSQL/OpenBao,controller
集成测试使用 envtest,完整网络路径使用 Kind E2E。
## 16. 已确认决策
- v1alpha1 使用一个同时作为 database owner 的 login role,不创建额外 NOLOGIN owner。
- v1alpha1 不接管任意现有资源,但必须提供并演练 dump/restore 迁移路径。
- v1alpha1 同时实现默认 `Retain` 和显式 `Delete`;Delete 必须有 finalizer、所有权验证
和完整清理路径。
- OpenBao KV v2 mount 和 base path 是 controller 部署配置,mount 默认 `kv`,base path
由 `--openbao-tenant-base-path` 配置并默认 `postgresql-tenants`;Tenant 不能选择 mount
或任意远端 path,controller 根据 namespace/name 推导记录路径。
- 租户 KV 记录固定写入 `username/password/database/host/hostaddr/port/sslmode` 七个
原子字段;
controller 不生成连接 URI,应用负责映射和拼装自身配置。
- PostgreSQL TLS 使用 controller Deployment 挂载的共享 CA bundle。OpenBao PKI 是
CA 权威并继续签发、续期 PostgreSQL server 证书;controller 只消费公开 trust
bundle,不接触 CA 私钥。bundle 可以由 ConfigMap 或现有证书同步机制投射,不允许
Tenant 或 Instance 选择其他 CA;开发环境可以显式使用 `sslMode: disable`。
- 每个 PostgreSQLInstance 在其管理 database 中维护 controller 专用 registry schema。
registry 是受管资源所有权、安装身份和 Retain 后 unmanaged 标记的权威记录;两个
CR 的 `status.phase` 是 controller 状态机的权威 checkpoint,Instance status 不聚合
Tenant 清单。
- PostgreSQL database 和 role identifier 必须匹配 `^[a-z][a-z0-9_]{0,62}$`,不支持
需要双引号的大小写或特殊字符名称。
- External Secrets Operator 是 v1alpha1 的运行依赖。controller 管理同 namespace
ExternalSecret,但不直接写明文 Secret;Tenant status 同时输出目标 Secret reference
和供非 Kubernetes 消费者使用的 OpenBao API URL。
- PostgreSQLInstance 同时声明 DNS `host` 和 IP `hostaddr`;PostgreSQL server 证书必须
同时包含对应 DNS SAN 和 IP SAN,消费者自行选择连接目标。
## 17. 批准状态
2026-09-14 确认 extension 判定修订:v1alpha1 使用实例实际可安装列表,不实现管理员
allowlist;后续可按需引入策略。现有 allowedExtensions 字段尚待 API 实现移除。
2026-09-13 已确认管理连接修订:Instance 引用 controller namespace 内的管理 Secret,
管理员维护 ExternalSecret,由 ESO 同步;controller 不再从 Bao 直接读取管理凭据。
此项是已批准行为,现有 API types 与实现尚待后续修改。
具体设计决策和本文整体已于 2026-09-10 获得批准,可以进入 API reference、测试和
实现阶段。同日确认状态机修订:两个 CR 的 `status.phase` 是 controller 工作流的权威
checkpoint;PostgreSQL registry 只承担所有权、安装身份和保留状态。
## 18. 与当前脚手架的已知差异
当前 API skeleton 至少需要以下调整:
- 删除 Tenant 自选 OpenBao path 的能力,改由部署级 mount、base path 和 Tenant
identity 推导,并修正当前包含 `kv/` 前缀的示例;
- 增加 controller 部署级 OpenBao KV mount 和 TLS 配置;
- 增加部署级 OpenBao consumer address、ClusterSecretStore 和 KV base path 配置;
- 删除独立 `ownerRole` 字段,使 login role 成为 database owner;
- 为 Instance 增加 `hostaddr`,为 Tenant 增加目标 Secret 配置及 Secret/Bao URL 输出
status;
- 按已确认的 identifier 合同收紧校验;
- 增加 PostgreSQL controller registry,记录基于 UID 的所有权、安装身份和保留状态;
- 修正凭据 type 中遗留的 rotation 注释;
- 使 Condition、不可变字段和 extension 追加语义具备 API 校验或明确的 reconcile
结果。
这些是规格批准后的实现工作,不属于本规格本身。
纯规则用单元测试;schema/CEL/status/watch/resourceVersion/绑定事件链用 envtest;
真实 PostgreSQL/OpenBao 验证后端行为;GC、ESO 与完整交付用具备相应控制器的测试集群。
envtest 不运行 GC/ESO,不能据此宣称这两类验收完成。详细测试与实现差距见
[开发文档](development.md)。导入不是数据迁移,dump/restore 仍是可选路径,见
[迁移文档](migration.md)。