docs: 完善 v1alpha1 设计与运维合同

This commit is contained in:
2026-09-10 05:23:33 +00:00
parent 4823eeb767
commit a723372c6e
10 changed files with 732 additions and 91 deletions
+13 -8
View File
@@ -30,14 +30,17 @@ kubeconfig 或本地生成的二进制。
## 开发循环
1. 从最新 `main` 创建分支。
2.用测试描述预期的 reconcile 行为,再实现最小改动。
3. 修改 API type 或 Kubebuilder marker 后运行:
2.新增或修改规格,覆盖范围、非目标、外部行为、校验、失败语义、安全边界和验收
标准;获得人工批准前不得实现行为。
3. 从已批准验收标准派生失败测试,再实现最小纵向切片。若实现暴露规格歧义,返回
规格阶段重新审批。
4. 修改 API type 或 Kubebuilder marker 后运行:
```sh
make manifests generate
```
4. 提交前运行:
5. 提交前运行:
```sh
make test
@@ -45,9 +48,11 @@ kubeconfig 或本地生成的二进制。
git diff --exit-code
```
5. 推送分支并创建 PR。PR 说明应包含动机、行为变化、验证方式,以及对数据库或
OpenBao 的风险
6. CI 通过后 squash merge,删除已合并分支。
6. 达到一个小而完整、可独立 review 的边界时,先请求批准再创建 commit;一个 PR
可以包含多个这样的 commit
7. 推送分支后,在创建 PR 前再次请求批准。PR 说明应包含规格链接、动机、行为变化、
验证方式,以及对 PostgreSQL、OpenBao、ESO 和凭据的风险。
8. CI 通过后按仓库策略合并,删除已合并分支。
`make test-e2e` 会创建并删除名为
`postgresql-tenant-operator-test-e2e` 的 Kind 集群,只能在隔离环境运行,不能指向
@@ -74,8 +79,8 @@ Docker daemon,供 Kind 创建临时节点容器。建议在 Gitea 中保护 `m
- 外部调用必须有超时、可重试,并区分永久错误与暂时错误。
- 日志、Event、Condition message 和测试输出不得包含密码或管理凭据。
- 默认删除策略是 `Retain`;任何实际销毁路径都必须有 finalizer 和独立测试。
- controller 测试优先使用 fake PostgreSQL/OpenBao clientAPI 交互使用 envtest真实
组件集成留给 Kind e2e
- controller 测试优先使用 fake PostgreSQL/OpenBao/ESO clientAPI 交互使用 envtest
PostgreSQL/OpenBao adapter 使用 Docker 集成测试,完整网络和 ESO 投射留给 Kind E2E
## API 变更
+15 -10
View File
@@ -9,9 +9,11 @@ OpenBao 凭据生命周期暴露为 Kubernetes API。
## 目标
- 通过 `PostgreSQLInstance` 注册可管理的外部 PostgreSQL 实例。
- 通过 namespaced `PostgreSQLTenant` 声明 database、ownerlogin 和扩展。
- 通过 namespaced `PostgreSQLTenant` 声明 database、作为 ownerlogin role 和扩展。
- 生成的密码只写入 OpenBao,不写入 CR、Event 或日志。
- 使用 `status.conditions` 暴露持续 reconcile 的结果
- 通过 ExternalSecret 将七个原子连接字段投射到 Kubernetes Secret
- 为非 Kubernetes 消费者输出不含认证信息的 OpenBao API URL。
- 使用 `status.phase` 展示进度,以 `status.conditions` 暴露可依赖的 reconcile 结果。
- 默认使用 `Retain` 删除策略,避免删除 CR 时意外删除数据。
- 允许 GitOps、Terraform、`kubectl` 和未来的 Backstage 使用同一套 API。
@@ -26,16 +28,19 @@ metadata:
spec:
instanceRef: shared
database: netbox
ownerRole: netbox_owner
loginRole: netbox
extensions: [pg_trgm]
credential:
openBaoPath: kv/k8s/netbox/database
secretName: netbox-postgresql
deletionPolicy: Retain
```
更完整的资源见 [`config/samples`](config/samples),初始架构和安全边界见
[`docs/architecture.md`](docs/architecture.md)。
上例描述获批后的目标 API`config/samples` 当前仍随旧 API 骨架保留,将在实现 API
合同的同一改动中重新生成。系统架构见
[`docs/architecture.md`](docs/architecture.md)。API、部署、安全、迁移和运维合同见
[`docs/api-reference.md`](docs/api-reference.md)、
[`docs/deployment.md`](docs/deployment.md)、[`docs/security.md`](docs/security.md)、
[`docs/migration.md`](docs/migration.md) 和 [`docs/operations.md`](docs/operations.md)。
系统已批准的规范性行为、验收标准和设计决策见
[`docs/specification.md`](docs/specification.md)。
@@ -65,10 +70,10 @@ make run
## 路线
1. 完成 CRD 校验、默认值和状态约定。
2. 抽象 PostgreSQL 与 OpenBao client,用 fake 实现测试 reconciliation
3. 实现 database、owner role、login role、grantextension 的幂等收敛。
4. 实现带 finalizer 的 `Retain` / `Delete` 删除流程。
1. 按已批准规格完成 CRD 校验、默认值和状态约定。
2. 实现 PostgreSQL registry、OpenBao 和 ESO adapter 的测试替身及集成测试
3. 实现凭据、login owner、database、grantextension 和 Secret 投射的幂等收敛。
4. 实现并演练带 finalizer 的 `Retain` / `Delete` 删除流程和迁移 runbook
代码和 Gitea Actions 工作流预期托管在
`git.ddupan.top/panxiao81/postgresql-tenant-operator`
+168
View File
@@ -0,0 +1,168 @@
# v1alpha1 API 合同
| 项目 | 内容 |
| --- | --- |
| 状态 | Review |
| API group | `database.ddupan.top` |
| version | `v1alpha1` |
| 最后更新 | 2026-09-10 |
本文把已批准的系统规格映射为 CRD 字段合同。批准后,API types、生成 CRD、sample 和
测试必须与本文一致。当前代码仍是旧骨架,不能作为本页的事实来源。
## 通用约定
- PostgreSQL identifier 匹配 `^[a-z][a-z0-9_]{0,62}$`
- 所有引用名称使用 Kubernetes DNS label/name 的相应校验。
- 默认值由 CRD defaulting 提供;需要读取旧值的校验由 CEL 或 webhook 完成。
- `status` 由 controller 独占写入,禁止出现密码、Token、管理用户名或完整连接串。
- 两个 Kind 都只承诺一个 `Ready` Condition;调用方不得依赖内部协调阶段。
## PostgreSQLInstance
cluster-scopedshort name 为 `pginstance`
### Spec
| JSON path | 类型 | 必填/默认 | 合同 |
| --- | --- | --- | --- |
| `spec.endpoint.host` | string | 必填 | PostgreSQL DNS 名;必须被服务端证书 DNS SAN 覆盖 |
| `spec.endpoint.hostaddr` | string | 必填 | 单个 IPv4/IPv6;必须被服务端证书 IP SAN 覆盖 |
| `spec.endpoint.port` | int32 | `5432` | 165535 |
| `spec.endpoint.database` | string | `postgres` | 管理连接 database;合法 PostgreSQL identifier |
| `spec.endpoint.sslMode` | enum | `verify-full` | `disable``require``verify-ca``verify-full` |
| `spec.adminCredentialRef.path` | string | 必填 | 部署级 KV mount 内的 mount-relative path |
| `spec.adminCredentialRef.usernameKey` | string | `username` | OpenBao record 中的键名 |
| `spec.adminCredentialRef.passwordKey` | string | `password` | OpenBao record 中的键名 |
| `spec.allowedExtensions` | set[string] | 空集合 | 合法 extension 名称的 allowlist |
`adminCredentialRef.path` 不以 `/` 开头,不含空段、`.``..`,也不包含 KV v2 API 的
`data`/`metadata` 层。它只定位既有管理凭据;controller 不创建或修改该记录。
Instance endpoint、管理凭据引用和 allowlist 可以修改。修改后 controller 重新验证;
删除 allowlist 项目不会自动从已有 Tenant database 删除 extension。
### Status
| JSON path | 类型 | 含义 |
| --- | --- | --- |
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
| `status.phase` | enum | `Pending``Validating``InitializingRegistry``Ready``Deleting` |
| `status.postgresqlVersion` | string | 从 server 回读的版本,不用于客户端解析 |
| `status.conditions[]` | `metav1.Condition` | 至少包含唯一的 `Ready` |
print columns`Endpoint=.spec.endpoint.host``Phase``Ready``Age`
Instance `Ready=True` 要求管理凭据可读、TLS/认证成功、server metadata 可读、registry
可访问且权限预检成功。它不代表数据库已经备份或高可用。
## PostgreSQLTenant
namespacedshort name 为 `pgtenant`
### Spec
| JSON path | 类型 | 必填/默认 | 合同 |
| --- | --- | --- | --- |
| `spec.instanceRef` | string | 必填 | cluster-scoped Instance 名称 |
| `spec.database` | string | `metadata.name` | 合法 PostgreSQL identifier |
| `spec.loginRole` | string | `metadata.name` | database owner 兼应用 login |
| `spec.extensions` | set[string] | 空集合 | 必须属于 Instance allowlist;成功创建后只允许追加 |
| `spec.credential.secretName` | string | `<name>-postgresql` | 同 namespace ESO target Secret 名称 |
| `spec.deletionPolicy` | enum | `Retain` | `Retain``Delete` |
Tenant 不声明 OpenBao mount 或 path。controller 使用部署级 mount/base path 和
`namespace/name` 推导稳定路径,并用 UID metadata 验证所有权。
`instanceRef``database``loginRole``credential.secretName` 在首次成功创建外部
状态后不可变。`extensions` 只允许集合不变或追加;移除返回 `ImmutableField`,不会执行
`DROP EXTENSION``deletionPolicy` 在对象进入删除前可以修改;删除开始后以 finalizer
首次观察到的值为准,避免清理过程中改变授权范围。
### Status
| JSON path | 类型 | 含义 |
| --- | --- | --- |
| `status.observedGeneration` | int64 | 最近完成有结论协调的 generation |
| `status.phase` | enum | controller 状态机的权威 checkpoint |
| `status.databaseOID` | uint32 | 回读的 database OID,仅供诊断 |
| `status.credential.secretRef.name` | string | 同 namespace 目标 Secret 名称 |
| `status.credential.openBaoURL` | string | 完整 KV v2 API URL,不含认证信息 |
| `status.conditions[]` | `metav1.Condition` | 至少包含唯一的 `Ready` |
Secret reference 不重复 namespace,因为它必定与 Tenant 同 namespace。OpenBao URL 格式
`<consumer-address>/v1/<mount>/data/<derived-path>`;不得包含 Token、用户名、密码或
query credential。
Tenant phase 枚举为 `Pending``Planned``CredentialCreated``RoleCreated`
`DatabaseCreated``ExternalSecretCreated``CredentialProjected``Ready``Deleting`
它不包含 `Failed``Retained`;失败类型由 Condition Reason 表达。
print columns`Instance``Database``Phase``Secret``Ready``Age`。完整 OpenBao URL 只在
YAML/JSON status 中输出。
两个 Kind 的 `status.phase` 都是 controller 状态机的权威 checkpoint。controller 用它
选择下一候选动作,但必须在动作前后核对外部事实,不能仅凭 phase 跳过幂等检查。status
丢失或领先于实际状态时必须保守重建/纠正。自动化就绪判断仍应读取 `Ready` Condition
phase 用于进度展示、恢复和排障。
## Condition
每种类型最多一个 Condition;更新必须保留正确的 `lastTransitionTime` 语义。
| Reason | Kind | 可重试性 |
| --- | --- | --- |
| `Reconciling` | 两者 | 正常进行中 |
| `Ready` | 两者 | 已收敛 |
| `InvalidSpec` | 两者 | 修改 spec 前不会恢复 |
| `ImmutableField` | Tenant | 恢复原值或重新迁移 |
| `DependencyUnavailable` | 两者 | 自动重试 |
| `AuthenticationFailed` | Instance | 修复凭据/TLS 后重试 |
| `InsufficientPrivileges` | Instance | 修复管理 role 后重试 |
| `InstanceNotReady` | Tenant | Instance 恢复后重试 |
| `Conflict` | Tenant | 人工解除名称/所有权冲突 |
| `ProvisioningFailed` | Tenant | 按错误类别退避重试 |
| `CredentialProjectionFailed` | Tenant | ESO/Secret 恢复后重试 |
`Ready=True` 必须使用 Reason `Ready`。处理中为 `Unknown/Reconciling`;已知未满足合同为
`False`。Condition message 可以包含资源名和错误类别,禁止包含凭据值或完整 Secret。
## 删除语义
- `Retain` 不需要等待外部依赖;删除 CR 后外部记录保留原 UID 并标记 unmanaged。
- `Delete` 添加 finalizer,严格按规格的所有权验证和清理顺序执行;失败保持 finalizer。
- controller 不为 `PostgreSQLInstance` 级联删除 Tenant 或外部资源;存在引用时 Instance
删除应被 finalizer 阻止,直到 Tenant 被删除或管理员使用运维逃生流程。
## 示例
```yaml
apiVersion: database.ddupan.top/v1alpha1
kind: PostgreSQLInstance
metadata:
name: shared
spec:
endpoint:
host: postgresql.home.arpa
hostaddr: 192.0.2.10
port: 5432
database: postgres
sslMode: verify-full
adminCredentialRef:
path: infrastructure/postgresql/shared/admin
allowedExtensions: [pg_trgm]
---
apiVersion: database.ddupan.top/v1alpha1
kind: PostgreSQLTenant
metadata:
name: netbox
namespace: netbox
spec:
instanceRef: shared
database: netbox
loginRole: netbox
extensions: [pg_trgm]
credential:
secretName: netbox-postgresql
deletionPolicy: Retain
```
+76 -41
View File
@@ -1,59 +1,94 @@
# 初始架构
# 系统架构
## 职责边界
本文是已批准 [`specification.md`](specification.md) 的架构视图。规范定义外部行为,
本文解释组件边界;二者冲突时以规范为准。当前仓库仍处于 API 骨架阶段。
Kubernetes API 保存期望状态和最近一次观察结果;PostgreSQL catalog 是 database、
role 和权限的事实来源;OpenBao 是凭据的事实来源。controller 不把明文密码写入
Kubernetes API、Event 或日志。
## 组件与数据流
```text
Git / kubectl / Terraform / Backstage
|
v
Kubernetes API (CRD)
|
v
postgresql-tenant-operator
| |
v v
external PostgreSQL OpenBao
GitOps / kubectl / Terraform / Backstage
|
v
Kubernetes API (CRD)
|
v
postgresql-tenant-operator
| | |
v v v
PostgreSQL DBMS OpenBao KV ExternalSecret
catalog+registry |
v
Kubernetes Secret
```
- Kubernetes `spec` 保存期望状态;`status.phase` 保存 controller 状态机 checkpoint
其他 status 字段保存可重建的观察结果。整个 status 都必须能由外部事实保守恢复。
- PostgreSQL catalog 保存 database、role、grant 和 extension 的实际状态。
- 两个 CR 的 `status.phase` 是 controller 状态机的权威 checkpoint。
- PostgreSQL 管理 database 中的 controller registry 只负责所有权、安装身份和保留标记。
- OpenBao KV v2 是应用凭据的事实来源。
- External Secrets OperatorESO)读取 OpenBao,并创建应用使用的 Kubernetes Secret。
controller 不运行 PostgreSQL/OpenBao,不管理 VM、存储、备份或 OpenBao PKI,也不直接
把明文凭据写入 Kubernetes API。
## 资源模型
### PostgreSQLInstance
`PostgreSQLInstance` 是 cluster-scoped,由平台管理员创建,描述外部 PostgreSQL 的
DNS host、IP host address、端口、管理 database、TLS 模式、OpenBao 管理凭据引用和
extension allowlist。
Cluster-scoped,由平台管理员创建。它描述服务器端点、管理凭据的 OpenBao 引用,
以及租户可以申请的 extension 白名单。
`PostgreSQLTenant` 是 namespaced。一个 Tenant 对应一个 database、一个同时作为 owner
的 login role、一组只允许追加的 extension、一个由 controller 推导的 OpenBao KV
记录,以及同 namespace 的 ExternalSecret 和目标 Secret。
### PostgreSQLTenant
Tenant namespace 只提供 Kubernetes RBAC 和身份边界。database 与 role 名称在一个
Instance 内仍然全局唯一。
Namespaced,描述一个应用租户,包括 database、无登录 owner role、应用 login
role、extensions、凭据路径和删除策略。
## Reconcile 与所有权
第一版规定一个 tenant 拥有一个 database 和一个 login role。跨租户 grant、多个
login role 和定时轮换等需求出现后再扩展 API。
系统采用最终一致性,不在 Kubernetes、PostgreSQL、OpenBao 和 ESO 之间假装存在分布式
事务。每个外部写入前在 CR status 记录阶段,执行幂等操作,回读验证,再推进阶段:
## Reconcile 原则
```text
Planned -> CredentialCreated -> RoleCreated -> DatabaseCreated
-> ExternalSecretCreated -> CredentialProjected -> Ready
```
- 每轮从 PostgreSQL 和 OpenBao 读取实际状态,不把 `status` 当作事实来源。
- 所有操作幂等;任意步骤失败后可以从下一轮继续。
- 先验证 extension 白名单,再执行任何变更。
- controller 生成密码,调用方只能得到 OpenBao 路径和状态
- `metadata.generation` 只表示 spec 变更,不承载凭据版本语义。
controller 每轮同时读取 CR、registry、PostgreSQL catalog、OpenBao metadata 和 ESO
投射状态。`status.phase` 是状态机 checkpoint,但不能替代外部回读;丢失或与事实冲突
时必须保守重建/纠正。`metadata.generation` 只表示 spec 修改;Condition 的
`observedGeneration` 表示该版本是否已经完成一次有结论的协调
## 删除
所有权使用 Instance UID、Tenant UID 与 namespace/name 验证。database/role COMMENT
可以辅助排障,但不能代替 registry。未知资源只报告 `Conflict`,不得修改、接管或
删除。Retain 后用相同名称重建 CR 会获得新 UID,因此仍然冲突。
默认 `deletionPolicy: Retain`。删除 CR 时保留 PostgreSQL database、roles 和
OpenBao 数据。`Delete` 模式将在实现备份检查、活动连接处理和可测试的 finalizer
状态机后加入实际销毁逻辑。
## 创建与删除边界
## 暂不包含
创建时先校验全部输入和冲突,再生成一次密码并写入 OpenBao,随后创建 role、database、
extension 和 ExternalSecret。只有 ESO 已投射 Secret 且应用凭据实际登录成功,Tenant
才可 Ready。
- PostgreSQL 实例、VM 或存储的创建。
- Web UI 或独立 REST APIKubernetes API 已提供 get、list、watch 和 RBAC。
- PostgreSQL 高可用和备份编排。
- 凭据轮换;controller 只负责首次生成长期凭据并写入 OpenBao。只有出现能够
重新加载凭据并妥善处理现有连接的实际消费者后,才重新评估轮换协议。
- 自动将 OpenBao 数据投射为 Kubernetes Secret;这由 External Secrets Operator
负责。
`Retain` 是默认删除策略,只移除 Kubernetes 管理关系并保留外部资源。显式 `Delete`
使用 finalizer,在重新验证所有权后依次删除 ExternalSecret/Secret、连接、database、
role、OpenBao KV 历史和 registry。详细恢复与逃生步骤见
[`operations.md`](operations.md)。
## 网络与 TLS
Instance 同时公布 DNS `host` 和 IP `hostaddr`。PostgreSQL server 证书必须包含对应的
DNS SAN 和 IP SAN,消费者自行选择可达目标,并可使用 `verify-full` 验证。OpenBao PKI
持有 CA 私钥并签发服务端证书;controller 只挂载公开 CA bundle。
OpenBao 的 controller 内部地址和外部消费者地址可以不同。Tenant status 同时提供目标
Kubernetes Secret reference 和不含认证信息的 OpenBao KV v2 API URL。
## 文档入口
- API 字段与 Condition[`api-reference.md`](api-reference.md)
- 安装、依赖和配置:[`deployment.md`](deployment.md)
- 本地与 CI 测试:[`development.md`](development.md)
- 安全模型与最小权限:[`security.md`](security.md)
- 现有数据库迁移:[`migration.md`](migration.md)
- 日常排障和删除逃生:[`operations.md`](operations.md)
+112
View File
@@ -0,0 +1,112 @@
# 部署与配置
| 项目 | 内容 |
| --- | --- |
| 状态 | Review |
| 环境 | homelab Kubernetes + 外部 PostgreSQL/OpenBao |
| 最后更新 | 2026-09-10 |
本文定义 v1alpha1 的运行依赖、启动顺序和部署级配置。当前 manifests 尚未实现这些
配置,示例是后续实现合同,不可直接用于现有脚手架。
## 依赖与顺序
1. 准备 PostgreSQL VM、持久盘、备份和网络入口。
2. 用 OpenBao PKI 签发 PostgreSQL server 证书,包含 Instance `host` 的 DNS SAN 与
`hostaddr` 的 IP SAN;配置 PostgreSQL 强制 TLS。
3. 创建 PostgreSQL controller 管理 role 和管理 database 连接权限。
4. 在 OpenBao KV v2 写入管理 role 凭据。
5. 配置 OpenBao Kubernetes auth、controller policy 和面向 ESO 的读取 policy。
6. 在 Kubernetes 安装 ESO,创建可读取租户路径的 `ClusterSecretStore`
7. 创建公开 CA bundle ConfigMap,并挂载到 controller 和需要直接验证数据库的应用。
8. 部署 controller,再创建 Instance;等待 Ready 后才创建 Tenant。
任何一步都不得把真实密码、Token、kubeconfig 或 CA 私钥提交进 Git。
## Controller 配置合同
具体 CLI flag/env 名称将在实现时按下表确定;语义和作用域已经固定:
| 配置 | 必填/默认 | 说明 |
| --- | --- | --- |
| OpenBao internal API address | 必填 | controller 可访问的 HTTPS 地址 |
| OpenBao consumer API address | 默认同 internal | 写入 Tenant status,必须能被预期外部消费者解析 |
| OpenBao auth mount | 默认 `kubernetes` | Kubernetes auth mount 名称 |
| OpenBao auth role | 必填 | controller ServiceAccount 对应 role |
| OpenBao KV mount | 默认 `kv` | KV v2 mount;开发可显式用 `secret` |
| OpenBao tenant base path | 必填 | controller 专属 mount-relative 前缀 |
| ESO ClusterSecretStore name | 必填 | controller 创建的 ExternalSecret 固定引用 |
| CA bundle path | 必填(TLS | 只读 PEM trust bundle,不含私钥 |
| reconcile timeout | 有安全默认 | 单轮外部操作的总期限 |
Tenant 路径固定推导为 `<base-path>/<namespace>/<name>`。namespace/name 都已通过
Kubernetes 名称校验,因此不再允许 CR 提供任意路径。KV v2 API URL 使用 consumer
address 拼为 `<address>/v1/<mount>/data/<base-path>/<namespace>/<name>`
配置变化不得隐式迁移既有凭据。修改 KV mount/base path 或 consumer address 前必须
停止 controller、评估现有 Tenant,并走明确迁移;实现应把 mount/base path 视为安装
身份的一部分并在 registry 留存,以便检测错误配置。
## PostgreSQL 管理 role
生产部署禁止使用 superuser。管理 role 至少需要:
- 连接管理 database、读取必要 catalog
- 创建/修改受管 login role
- 创建 database 并指定 owner
- 撤销 `PUBLIC` CONNECT、授予租户 role CONNECT
- 连接租户 database 并创建 allowlist extension
- 创建和维护 controller 专属 registry schema/table
- `Delete` 时禁止连接、终止目标 database session、删除已验证归属的 database/role。
部分 PostgreSQL 操作天然要求较高权限,尤其终止其他 session 和安装某些 extension。
应优先使用 PostgreSQL 预定义角色、受控 SECURITY DEFINER 管理函数或限定数据库的
授权;任何不得不使用 superuser 的 extension 都必须按实例单独记录,不得扩大默认
controller 权限。最终可执行 SQL grant 将随 PostgreSQL adapter 集成测试固化。
## OpenBao 与 ESO
controller policy 分成两个范围:
- 只读 Instance 管理凭据路径;
- 在固定 tenant base path 下 create/read/update/delete KV v2 data 和 metadataDelete
必须能永久删除全部版本及 metadata。
ESO 使用独立身份,只需读取 tenant base path;它不应读取 PostgreSQL 管理凭据。
`ClusterSecretStore` 由平台管理员创建,controller 只引用,不创建或修改 Store。
controller 创建的 ExternalSecret 与 Tenant 同 namespace,并设置 ownerReference;目标
Secret 包含固定七键:`username``password``database``host``hostaddr``port`
`sslmode`
## Kubernetes RBAC
- controller 可读/写 Instance、Tenant 的 status/finalizer 和 Event。
- controller 可在 Tenant namespace 创建、读取、更新、删除 ExternalSecret,并只读检查
对应 Secret 是否完成投射。
- namespace 用户可以管理本 namespace Tenant,但不能管理 Instance、Store、controller
配置或其他 namespace 的 ExternalSecret。
- controller 无需读取目标 Secret 的 data;验证登录使用从 OpenBao 读取的应用凭据,
对 Secret 只检查存在性和 ESO 状态。
## 升级与回滚
v1alpha1 尚不承诺跨版本转换。升级前备份 CR、PostgreSQL registry 和 OpenBao metadata
先在隔离 Kind 环境运行 E2E。禁止在同一组 CR 上同时运行两个 controller 版本。若新版本
在执行任何破坏性迁移前失败,可回滚镜像;涉及 API/storage 或 registry schema 迁移时,
必须先写独立升级规格和回滚步骤。
## 上线验证
```text
PostgreSQL TLS 与备份验证
-> OpenBao auth/policy 验证
-> ClusterSecretStore Ready
-> controller Ready/leader elected
-> Instance Ready
-> 测试 Tenant Ready
-> DNS host 与 IP hostaddr 分别登录
-> 删除测试 Tenant 并验证所选策略
```
生产 homelab 上线前还必须完成 [`security.md`](security.md) 的权限检查和
[`operations.md`](operations.md) 的备份/逃生检查。
+34 -11
View File
@@ -1,6 +1,6 @@
# 开发与测试环境
本项目同时依赖 Kubernetes API、PostgreSQLOpenBao。日常开发不连接 homelab
本项目同时依赖 Kubernetes API、PostgreSQLOpenBao 和 ESO。日常开发不连接 homelab
中的真实服务:Kubernetes 使用 envtest 或一次性 Kind,另外两个依赖使用一次性
容器。这样既避免污染真实数据,也能把启动顺序固化为命令。
@@ -25,11 +25,11 @@ container 的 Docker/Dev Container 环境。
| 层次 | Kubernetes | PostgreSQL / OpenBao | 用途 |
| --- | --- | --- | --- |
| 单元测试 | fake client | fake client | SQL 计划、状态转换和错误分类 |
| controller 集成测试 | envtest | fake 或 Docker | CRD、watch、status、finalizer |
| controller 集成测试 | envtest | fake adapter | CRD、watch、status、finalizer、ExternalSecret 对象 |
| adapter 集成测试 | 不需要 | Docker Compose | 真实协议、权限和幂等行为 |
| E2E | 一次性 Kind | Kind 内测试实例 | 验证容器化 controller 与完整网络路径 |
| E2E | 一次性 Kind + ESO | Kind 内测试实例 | 凭据投射、TLS、完整网络和删除路径 |
envtest 只启动 API server 和 etcd,没有 kubelet、scheduler 或 controller-manager
envtest 只启动 API server 和 etcd,没有 kubelet、scheduler、ESO 或 controller-manager
因此不能用它验证 Deployment、Pod 调度或 Service 网络。此类行为必须留给 Kind
E2E。
@@ -64,7 +64,11 @@ make lint
`make test` 会下载与 `go.mod` 中 Kubernetes minor 版本匹配的 envtest 二进制,
启动临时 API server/etcd,测试结束后自动关闭。
### 2. 启动外部依赖
规格实现后,快速测试必须覆盖默认值/校验、Condition `observedGeneration`、两个 CR 的
status 状态机、不可变字段、extension 只追加、registry 所有权和外部错误分类。envtest 只断言 controller 创建了正确
的 ExternalSecret;它不能证明 ESO 已生成 Secret。
### 2. 启动 PostgreSQL/OpenBao adapter 依赖
只有开发 PostgreSQL/OpenBao adapter 或完整 reconcile 时才需要:
@@ -99,6 +103,9 @@ POSTGRES_DEV_PORT=25432 OPENBAO_DEV_PORT=28200 make dev-up
后续执行 `dev-smoke` 和 controller 时必须使用相同端口变量。
Compose 使用明文 PostgreSQL/OpenBao dev 模式,不覆盖生产 TLS 合同。DNS SAN、IP SAN、
Kubernetes auth、最小 policy 和 ESO 必须在 Kind E2E fixture 中验证。
### 3. 运行针对临时依赖的测试或 controller
adapter 集成测试加入后,统一通过独立 Make target 执行,不默认塞进快速单元测试。
@@ -112,7 +119,9 @@ make install
make run
```
此时 controller 运行在开发容器内,可以直接访问上面的回环端口。不要把包含
此时 controller 运行在开发容器内,可以直接访问上面的回环端口。若要验证 Tenant
Ready,专用 Kind 还必须安装 ESO、创建测试 ClusterSecretStore,并让 Kind workload
能够访问测试 OpenBao。不要把包含
`127.0.0.1` 端点的样例部署到 Kind 内;Pod 中的回环地址只指向 Pod 自身。
### 4. 清理
@@ -127,22 +136,24 @@ make cleanup-test-e2e
## E2E 顺序
CI 的 E2E 与本机 `make run` 不同:controller 会作为 Pod 运行在 Kind 中。因此完整
E2E fixture 必须把测试 PostgreSQLOpenBao部署进 Kind,并等待两者 Ready 后
E2E fixture 必须把测试 PostgreSQLOpenBao 和 ESO 部署进 Kind,并等待依赖 Ready 后
再创建 `PostgreSQLInstance``PostgreSQLTenant`
```text
创建 Kind
-> 安装 CRD
-> 部署 PostgreSQL/OpenBao fixture
-> 等待依赖 Ready并写入测试管理凭据
-> 部署 PostgreSQL/OpenBao fixture,签发含 DNS/IP SAN 的测试证书
-> 安装 ESO,配置 OpenBao auth/policy 和 ClusterSecretStore
-> 等待依赖 Ready 并写入测试管理凭据
-> 构建并加载 controller image
-> 部署 controller
-> 创建 Instance
-> 等待 Instance Ready
-> 创建 Tenant
-> 等待 Tenant Ready
-> 验证 PostgreSQL catalogOpenBao KV
-> 删除 Tenant并验证 Retain
-> 验证 registry、PostgreSQL catalogOpenBao KV、ExternalSecret 和 Secret
-> 分别使用 DNS host 与 IP hostaddr 登录
-> 删除 Tenant 并分别验证 Retain 与 Delete(含故障点重试)
-> 删除 Kind
```
@@ -150,6 +161,18 @@ E2E fixture 必须把测试 PostgreSQL 和 OpenBao也部署进 Kind,并等待
manager Deployment 和 metrics endpoint。上述真实依赖 fixture 应与第一个完整
reconcile 纵向切片一起实现,不能在文档中声称已经通过。
## 测试数据与泄漏检查
- 只使用显眼的固定 canary 测试密码,测试后扫描日志、Event、Condition、metrics 和
CR dump,出现 canary 即失败。
- 每个最终一致性阶段都注入一次中断,重启后验证密码不变且阶段只向前推进。
- 清空、落后或伪造超前的 `status.phase` 后验证它能从外部事实保守恢复/纠正,且不会
跳过任何回读。
- 为未知同名 database、role、Bao record 和伪造 COMMENT 分别构造 Conflict。
- Delete 在每个外部删除步骤失败后重试,确认未误删非当前 UID 资源。
- 迁移测试按 [`migration.md`](migration.md) 完整执行,不以单纯 `pg_restore` 成功代替
应用读写和回滚验证。
## 故障排查
查看依赖状态与日志:
+125
View File
@@ -0,0 +1,125 @@
# 现有数据库迁移 Runbook
| 项目 | 内容 |
| --- | --- |
| 状态 | Review;尚未在临时 PostgreSQL 演练 |
| 适用范围 | 任意既有数据库迁移为新建 v1alpha1 Tenant |
| 最后更新 | 2026-09-10 |
v1alpha1 不接管现有 database、role 或 OpenBao record。本流程通过逻辑 dump/restore 把
数据迁移到 controller 创建的新资源,保留旧资源作为限时回滚点。
以下命令是顺序模板,不可原样复制到真实环境。先把尖括号变量解析成明确值,确认当前
连接目标,再逐条执行。dump 可能包含敏感业务数据,必须放在加密临时存储且不得提交 Git。
## 前置条件
- 已验证 PostgreSQL/OpenBao 备份和恢复;记录恢复点。
- Instance 已 Ready,目标 namespace 存在,ESO ClusterSecretStore Ready。
- 最终 database/login role 当前由旧应用占用,但改名后的保留名称、新推导的 Bao path
均不存在。
- 已记录旧 database owner、grants、extensions、locale/encoding、连接配置和验证清单。
- 已确认应用可停止写入,并确定回滚窗口和负责人。
- 已确认旧 login role 不被其他 database/应用共享,且角色改名不会破坏未纳入本次维护
的依赖。
## 迁移顺序
### 1. 盘点与预演
```sh
pg_dump --schema-only --no-owner --no-privileges \
--dbname='<old-admin-connection>' > schema-preview.sql
```
检查不受 v1alpha1 管理的对象:额外 roles、跨库依赖、FDW、large objects、订阅、显式
tablespace、owner/grant 和不在 allowlist 的 extension。无法映射为单 database + 单 login
owner 的环境必须先人工简化,不能让 controller 猜测。
### 2. 创建一致性 dump
停止应用写入并确认活跃写事务结束,然后创建最终 custom-format dump
```sh
pg_dump --format=custom --no-owner --no-privileges \
--file='<secure-temp>/tenant.dump' \
--dbname='<old-admin-connection>'
pg_restore --list '<secure-temp>/tenant.dump'
```
不要删除旧 database/role。记录停写时间、dump checksum 和 PostgreSQL 版本。
### 3. 释放最终名称
保持应用停写,终止旧 database 的应用连接。连接其他管理 database,以管理员身份把旧
database 和旧 login role 改为明确的保留名称:
```sql
ALTER DATABASE <old_database> RENAME TO <old_database>_retained_<timestamp>;
ALTER ROLE <old_login_role> RENAME TO <old_login_role>_retained_<timestamp>;
```
identifier 必须由管理员工具安全引用,不能把未经校验的值直接拼入 SQL。PostgreSQL 在
角色改名时会清除以旧角色名加盐的 MD5 密码;使用 MD5 的旧环境必须在维护前准备安全的
密码重设/回滚方法。SCRAM verifier 不受角色名改动影响,但仍须实际验证回滚登录。
### 4. 创建受管空目标
应用 `PostgreSQLTenant`,使用未被占用的 database/loginRole,等待 Ready。确认:
- registry 记录 UID 正确;
- OpenBao metadata 属于该 Tenant
- ExternalSecret Ready 且目标 Secret 已投射;
- 新凭据可以通过 DNS host 和 IP hostaddr 分别登录空 database。
### 5. Restore
从 OpenBao 或目标 Secret 安全取得新应用凭据,不要把密码放进 shell history。以新 login
owner 连接目标 database
```sh
pg_restore --exit-on-error --no-owner --no-privileges \
--dbname='<new-application-connection>' \
'<secure-temp>/tenant.dump'
```
extension 应由 Tenant spec 创建。若 dump 仍包含 extension 定义,预演必须确认 restore
行为幂等;不在 allowlist 的 extension 必须在迁移前解决。
### 6. 验证并切换
- 对比关键 schema、表数、行数/校验和、sequence、function 和 migration version。
- 用新 login 验证读写、migration 和应用健康检查。
- 将应用配置切换到新 Secret 或 OpenBao URL,保持旧数据库只读/停写。
- 观察一个约定窗口,确认错误率、连接数和关键业务功能。
### 7. 收尾
回滚窗口结束后,按独立变更删除旧 database/role/旧凭据;它们不属于 controller,禁止
通过 Tenant `Delete` 清理。安全删除 dump 和临时凭据材料,并记录验证结果。
## 回滚
在新目标出现问题且旧资源仍保留时:
1. 立即停止新目标写入。
2. 评估切换后是否产生新数据;若有,先决定反向迁移或接受丢弃,不能盲目切回。
3. 将应用连接切回 retained database/role;若必须恢复原名称,先确保新受管目标已用
`Delete` 完整清理或改用不同名称,再安全地反向执行 rename。
4. 恢复旧凭据(MD5 环境可能需要重设),验证旧服务。
5. 保留失败 Tenant 供排障;选择 Retain 或 Delete 前明确其外部资源后果。
若已经删除旧资源,则只能使用已验证备份恢复,不再属于本 runbook 的快速回滚。
## 演练验收
发布首个可用版本前,必须在临时 PostgreSQL/OpenBao/Kind 环境执行本文并记录:
- 使用的 PostgreSQL major version 和命令版本;
- dump/restore 返回码和对象差异;
- DNS/IP TLS 登录结果;
- ESO 投射与应用启动结果;
- 回滚演练结果;
- 哪些命令或前置检查需要修订。
完成演练前,本文不得标记为 `Verified`
+76
View File
@@ -0,0 +1,76 @@
# 运维与故障处理
| 项目 | 内容 |
| --- | --- |
| 状态 | Review;命令待实现后演练 |
| 最后更新 | 2026-09-10 |
## 日常检查
先看 API 合同,而不是从日志猜状态:
```sh
kubectl get postgresqlinstances
kubectl get postgresqltenants -A
kubectl get postgresqltenant -n <namespace> <name> -o yaml
kubectl describe postgresqltenant -n <namespace> <name>
```
随后检查 controller 日志、ExternalSecret/Secret、OpenBao metadata、registry 和
PostgreSQL catalog。排障时不得把 Secret data 或带 Token 的请求粘贴到 issue/日志。
`status.phase` 是 controller 状态机 checkpoint,也用于定位当前步骤;`Ready`
Condition/Reason 用于判断对外结果。phase 不能替代外部事实,清空或不一致时应由
controller 自动重建/纠正。
## 常见 Reason
| Reason | 首要检查 |
| --- | --- |
| `InvalidSpec` / `ImmutableField` | API 字段、identifier、不可变/只追加约束 |
| `DependencyUnavailable` | 网络、DNS、服务状态和超时 |
| `AuthenticationFailed` | 管理凭据、CA、DNS/IP SAN、OpenBao auth |
| `InsufficientPrivileges` | PostgreSQL grants、OpenBao policy、Kubernetes RBAC |
| `InstanceNotReady` | 先恢复所引用 Instance |
| `Conflict` | registry UID、同名 DB/role、OpenBao metadata;禁止直接覆盖 |
| `CredentialProjectionFailed` | ClusterSecretStore、ExternalSecret Condition、目标 Secret |
| `ProvisioningFailed` | `status.phase` 及对应外部资源的回读结果 |
修复依赖后让正常 reconcile 自动重试。不要通过删除/重建 CR 规避 Conflict;新 UID 只会
使已有保留资源继续冲突。
## Retain 后的资源
Retain 删除完成后,database、role、OpenBao record 和 registry 所有权记录仍存在但标记
unmanaged。v1alpha1 不支持重新关联。需要恢复管理时,使用 [`migration.md`](migration.md)
把数据迁移到一个全新受管名称;不要手工把 registry UID 改成新 CR UID。
## Delete 卡住
1. 暂停应用写入并记录 Tenant UID、Instance UID、database、role 和 Bao path。
2. 从 registry 和 OpenBao metadata 独立确认所有权。
3. 检查删除阶段,修复 PostgreSQL/OpenBao/ESO 依赖,让 controller 继续。
4. 若依赖永久丢失,列出每个可能残留的 database、role、KV metadata 和 Secret。
5. 只有确认接受这些残留后,才人工移除 finalizer。
最终 finalizer 名称由 API 实现固定后补入命令。人工移除 finalizer不会执行剩余清理,
也不会把外部资源变成可由新 CR 接管的资源。
## 备份与恢复
- PostgreSQL VM/磁盘备份必须与数据库一致性策略配套;仅复制在线磁盘不自动等于有效
PostgreSQL 备份。
- PostgreSQL 备份必须包含管理 database 中的 controller registry。
- OpenBao 使用独立的受支持备份/快照流程,且恢复点应与 PostgreSQL 尽量接近。
- Kubernetes 侧备份 CR、controller 配置、ClusterSecretStore 和公开 CA bundle,不备份
明文 Secret 作为凭据事实来源。
- 定期在隔离环境执行恢复演练,验证 registry、KV metadata、应用登录及 Retain/Delete。
恢复后先停止 controller,核对 PostgreSQL/OpenBao 时间点与 UID 映射,再启动单副本
controller 观察;出现一侧存在、一侧缺失时不得手工生成新密码或改 registry,应先按
Conflict 处理并决定恢复哪一侧。
## 升级与紧急停止
有疑似越权删除或凭据泄漏时,先把 controller Deployment scale 到 0,保留 CR、registry
和日志证据,再撤销 OpenBao token/role 并限制 PostgreSQL 管理 role。恢复前在隔离环境
复现并确认不会扩大破坏。一般依赖故障无需 scale down,最终一致性会自动重试。
+68
View File
@@ -0,0 +1,68 @@
# 安全模型
| 项目 | 内容 |
| --- | --- |
| 状态 | Review |
| 最后更新 | 2026-09-10 |
## 保护目标
- 应用密码只存在于 OpenBao、ESO 投射的目标 Secret 和需要使用它的进程内存中。
- controller 只能修改其 registry 能证明归属当前 Tenant UID 的资源。
- namespace 租户不能越权管理 Instance、其他 namespace 或 controller 配置。
- PostgreSQL 和 OpenBao 的网络身份使用受信 CA 验证,不因 DNS 不可用而降级 TLS。
## 信任边界
Kubernetes 管理员、OpenBao 管理员和 PostgreSQL 管理员是平台信任主体。能读取 Tenant
目标 Secret 或对应 OpenBao path 的主体等同于持有数据库账号。database owner 可以
改变自己 database 内的对象,因此 COMMENT 不能作为 controller 所有权依据。
VM/磁盘备份会包含 PostgreSQL registry 和租户数据,但不应包含 OpenBao 中的密码;完整
灾难恢复必须同时保护 PostgreSQL 与 OpenBao,并控制两份备份的访问权限。
## 凭据处理
- controller 使用 Kubernetes auth 获取短期 OpenBao token,不配置长期静态 token。
- 管理凭据只从 Instance 引用读取,不复制到 CR/status/Event/metric/trace。
- 租户密码使用密码学安全随机源生成一次;中断恢复必须复用 OpenBao 现值。
- controller 创建 ExternalSecret,不直接创建含 data/stringData 的 Secret。
- 日志字段允许 namespace/name、UID、generation、阶段和错误类别;禁止记录请求/响应体、
DSN、Authorization header、密码或完整 OpenBao URL path 作为 metric label。
- panic、错误包装和测试失败输出必须经过凭据泄漏测试。
## TLS
- homelab 默认 `verify-full``disable` 只允许显式开发配置。
- server 证书同时覆盖 DNS `host` 和 IP `hostaddr`;消费者自行选择连接目标。
- OpenBao PKI 保管 CA 私钥并负责签发/续期。controller Deployment 只挂载公开 CA
bundle,挂载只读且使用最小文件权限。
- 证书轮换必须先发布同时信任新旧 CA 的 bundle,再轮换服务端证书,最后移除旧 CA。
## 最小权限
OpenBao controller identity 只能读取管理凭据范围并管理固定 tenant base pathESO
identity 只能读取 tenant base path。两者不得共用可访问管理凭据的 policy。
PostgreSQL 管理 role 不应是 superuser。若平台选择 SECURITY DEFINER 函数承载创建或
删除操作,函数必须固定 `search_path`、严格校验 identifier、拒绝任意 SQL,并仅向
controller role 授予 EXECUTE。controller 不调用 shell 或 `psql` 拼接用户输入。
Kubernetes RBAC 应把 cluster-scoped Instance 管理限制给平台管理员。Tenant editor
不自动获得 Secret read;是否读取目标 Secret 由 namespace 内独立 RBAC 决定。
## 删除保护
Delete 是明确的数据销毁授权,但仍必须在每一步校验 Instance UID、Tenant UID、名称和
OpenBao metadata。禁止对未知对象使用 `CASCADE`。删除 finalizer 卡住时只能按
[`operations.md`](operations.md) 核实外部状态后人工移除;该操作可能遗留资源。
## 发布前安全验收
- 使用错误 CA、错误 DNS 名和错误 IP 时连接失败;正确 DNS/IP SAN 均成功。
- namespace 用户不能修改 Instance 或跨 namespace Tenant/ExternalSecret。
- controller/ESO 的 OpenBao policy 互相隔离,越权请求被拒绝。
- 应用 login 不能创建 role/database,也不能连接其他租户 database。
- 日志、Event、Condition、metrics、CR 导出和测试 artifact 不含 canary password/token。
- 伪造 COMMENT、同名 database/role 或错误 UID metadata 均不能绕过 Conflict。
- Delete 只销毁 registry 可证明归属当前 Tenant 的资源。
+45 -21
View File
@@ -17,7 +17,7 @@
## 1. 背景
homelab 中的大部分应用共享一个运行在独立 VM 上的 PostgreSQL DBMS。应用需要各自
独立的 database、owner role、login role 和密码,但不需要独立 PostgreSQL 实例。
独立的 database、作为 owner login role 和密码,但不需要独立 PostgreSQL 实例。
目前这些资源依靠人工 SQL 和人工 Secret 管理,难以重复、审计和检测漂移。
本系统使用 Kubernetes CRD 作为声明式 API,持续协调外部 PostgreSQL 与 OpenBao
@@ -69,9 +69,10 @@ v1alpha1 不负责:
| 对象 | 事实来源 | 说明 |
| --- | --- | --- |
| 期望状态 | Kubernetes CR `spec` | 用户声明的合同 |
| 最近观察结果 | Kubernetes CR `status` | 可以丢失并重建,不是外部事实来源 |
| 最近观察结果与当前阶段 | Kubernetes CR `status` | 可以丢失并重建,不是外部事实来源 |
| database/role/grant/extension | PostgreSQL catalog | 每轮 reconcile 必须重新读取 |
| 受管资源所有权与协调阶段 | PostgreSQL controller registry | 与受管 DBMS 一起备份和恢复 |
| 受管资源所有权与保留标记 | PostgreSQL controller registry | 与受管 DBMS 一起备份和恢复 |
| controller 工作流阶段 | Kubernetes CR `status.phase` | 状态机 checkpoint;可由外部事实保守重建 |
| 应用凭据 | OpenBao KV v2 | Kubernetes API 中不得出现明文 |
| Kubernetes 凭据投射 | External Secrets Operator | ExternalSecret 由本 controller 管理 |
| PostgreSQL 管理凭据 | OpenBao KV v2 | 由 `PostgreSQLInstance` 引用 |
@@ -106,7 +107,7 @@ PostgreSQL 管理 role。应用或 GitOps 流程在获得 namespace RBAC 后管
- 一个 database
- 一个同时作为 database owner、供应用使用的 `LOGIN` role
- 零个或多个 extension
- 一个 OpenBao KV v2 凭据位置
- 一个 OpenBao KV v2 凭据位置
- 一个同 namespace ExternalSecret 及其目标 Kubernetes Secret。
Tenant 的 namespace 用于 Kubernetes RBAC 和身份识别,不代表 PostgreSQL schema。
@@ -233,19 +234,36 @@ label、trace、CR spec/status 或测试快照。
## 9. Reconcile 行为
系统采用最终一致性模型。Kubernetes、PostgreSQLOpenBao 可以短暂处于不同阶段;
controller 不尝试实现跨系统事务,而是通过 PostgreSQL 中持久化的协调阶段、幂等外部
操作和每轮回读验证最终收敛。
系统采用最终一致性模型。Kubernetes、PostgreSQLOpenBao 和 ESO 可以短暂处于不同
阶段;controller 不尝试实现跨系统事务,而是以 Kubernetes CR `status.phase` 作为
工作流 checkpoint,通过幂等外部操作和每轮回读验证最终收敛。
Tenant 的 registry 记录至少经历以下单向阶段:
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
Planned -> CredentialCreated -> RoleCreated -> DatabaseCreated
-> ExternalSecretCreated -> CredentialProjected -> Ready
Pending -> Validating -> InitializingRegistry -> Ready
(any phase) --------------------------------> Deleting
```
阶段用于恢复进度,但不能替代实际状态检查。controller 重启后必须同时检查 registry、
PostgreSQL catalog 和 OpenBao,再决定继续、保持 Ready 或报告 Conflict。
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 必须按以下逻辑执行:
@@ -266,7 +284,8 @@ PostgreSQL catalog 和 OpenBao,再决定继续、保持 Ready 或报告 Confli
要求:
- 所有步骤必须幂等;
- 每个外部写入前必须先持久化足够的操作意图,写入后必须回读并推进 registry 阶段;
- 每个外部写入前必须先在 CR status 持久化足够的操作意图,写入后必须回读并推进
`status.phase`
- 暂时性网络、锁和依赖错误必须重试;
- 输入错误、资源冲突和禁止操作不得忙循环重试,只在 generation 或依赖状态变化后
重试;
@@ -274,7 +293,7 @@ PostgreSQL catalog 和 OpenBao,再决定继续、保持 Ready 或报告 Confli
- 用户从 `spec.extensions` 移除 extension 时不得执行卸载,必须报告该字段在 v1alpha1
中只允许追加;
- controller 重启不得影响已经签发的应用密码;
- `status` 丢失后必须可以从 PostgreSQLOpenBao 重建。
- `status` 丢失后必须可以从 registry、PostgreSQLOpenBao 和 Kubernetes/ESO 重建。
## 10. Condition 合同
@@ -363,8 +382,9 @@ v1alpha1 不接管现有 database 或 role,但必须提供可重复、可回
11. 保留旧 database、role 和备份直到回滚窗口结束,再由管理员手工清理。
回滚时停止新应用写入、恢复原名称或连接配置,并重新使用旧凭据。迁移工具不得把旧
密码、管理凭据或 dump 文件提交到 Git。真实命令、锁定方式和各现有应用验证项在实现
首个可用版本前写入独立 `docs/migration.md`通过临时 PostgreSQL 实例演练。
密码、管理凭据或 dump 文件提交到 Git。真实命令、锁定方式和各现有应用验证项
[`migration.md`](migration.md),并必须在实现首个可用版本前通过临时 PostgreSQL 实例
演练。
## 13. 安全要求
@@ -382,7 +402,7 @@ v1alpha1 不接管现有 database 或 role,但必须提供可重复、可回
7. controller 不得通过 shell 或 `psql` 子进程执行用户输入。
8. 错误包装、结构化日志和 tracing 必须经过 Secret 泄露测试。
详细威胁模型和部署 policy 将在 `docs/security.md` 中定义
详细威胁模型和部署 policy 见 [`security.md`](security.md)
## 14. 可观测性要求
@@ -425,6 +445,8 @@ v1alpha1 至少必须提供:
URL。
17. DNS 不可用时,使用输出的 `hostaddr` 可以连接 PostgreSQLserver 证书同时覆盖
`host` 的 DNS SAN 和 `hostaddr` 的 IP SAN,两种连接目标均可通过 `verify-full`
18. 两个 CR 的 `status.phase` 都能反映当前协调步骤;清空 status 后可以从外部事实重建,
且伪造或过期 phase 不会使 controller 跳过验证或外部操作。
单元测试验证纯决策逻辑,adapter 集成测试使用 Docker PostgreSQL/OpenBaocontroller
集成测试使用 envtest,完整网络路径使用 Kind E2E。
@@ -445,8 +467,9 @@ v1alpha1 至少必须提供:
bundle,不接触 CA 私钥。bundle 可以由 ConfigMap 或现有证书同步机制投射,不允许
Tenant 或 Instance 选择其他 CA;开发环境可以显式使用 `sslMode: disable`
- 每个 PostgreSQLInstance 在其管理 database 中维护 controller 专用 registry schema。
registry 是受管资源所有权和协调阶段的权威记录;Tenant status 只是观察缓存,
Instance status 不聚合 Tenant 清单。
registry 是受管资源所有权、安装身份和 Retain 后 unmanaged 标记的权威记录;两个
CR 的 `status.phase` 是 controller 状态机的权威 checkpointInstance status 不聚合
Tenant 清单。
- PostgreSQL database 和 role identifier 必须匹配 `^[a-z][a-z0-9_]{0,62}$`,不支持
需要双引号的大小写或特殊字符名称。
- External Secrets Operator 是 v1alpha1 的运行依赖。controller 管理同 namespace
@@ -458,7 +481,8 @@ v1alpha1 至少必须提供:
## 17. 批准状态
具体设计决策和本文整体已于 2026-09-10 获得批准,可以进入 API reference、测试和
实现阶段。
实现阶段。同日确认状态机修订:两个 CR 的 `status.phase` 是 controller 工作流的权威
checkpointPostgreSQL registry 只承担所有权、安装身份和保留状态。
## 18. 与当前脚手架的已知差异
@@ -471,7 +495,7 @@ v1alpha1 至少必须提供:
- 删除独立 `ownerRole` 字段,使 login role 成为 database owner
- 为 Instance 增加 `hostaddr`,为 Tenant 增加目标 Secret 配置及凭据输出 status
- 按已确认的 identifier 合同收紧校验;
- 增加 PostgreSQL controller registry,记录基于 UID 的所有权和最终一致性协调阶段
- 增加 PostgreSQL controller registry,记录基于 UID 的所有权、安装身份和保留状态
- 修正凭据 type 中遗留的 rotation 注释;
- 使 Condition、不可变字段和 extension 追加语义具备 API 校验或明确的 reconcile
结果。