From a723372c6ec8e4e0671d35f5af0c6282985a4547 Mon Sep 17 00:00:00 2001 From: panxiao81 Date: Thu, 10 Sep 2026 05:23:33 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=20v1alpha1=20?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E4=B8=8E=E8=BF=90=E7=BB=B4=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 21 ++++-- README.md | 25 ++++--- docs/api-reference.md | 168 ++++++++++++++++++++++++++++++++++++++++++ docs/architecture.md | 117 ++++++++++++++++++----------- docs/deployment.md | 112 ++++++++++++++++++++++++++++ docs/development.md | 45 ++++++++--- docs/migration.md | 125 +++++++++++++++++++++++++++++++ docs/operations.md | 76 +++++++++++++++++++ docs/security.md | 68 +++++++++++++++++ docs/specification.md | 66 +++++++++++------ 10 files changed, 732 insertions(+), 91 deletions(-) create mode 100644 docs/api-reference.md create mode 100644 docs/deployment.md create mode 100644 docs/migration.md create mode 100644 docs/operations.md create mode 100644 docs/security.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5361e3f..28e84be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 client;API 交互使用 envtest;真实 - 组件集成留给 Kind e2e。 +- controller 测试优先使用 fake PostgreSQL/OpenBao/ESO client;API 交互使用 envtest; + PostgreSQL/OpenBao adapter 使用 Docker 集成测试,完整网络和 ESO 投射留给 Kind E2E。 ## API 变更 diff --git a/README.md b/README.md index 94fad49..00fc719 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,11 @@ OpenBao 凭据生命周期暴露为 Kubernetes API。 ## 目标 - 通过 `PostgreSQLInstance` 注册可管理的外部 PostgreSQL 实例。 -- 通过 namespaced `PostgreSQLTenant` 声明 database、owner、login 和扩展。 +- 通过 namespaced `PostgreSQLTenant` 声明 database、作为 owner 的 login 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、grant 和 extension 的幂等收敛。 -4. 实现带 finalizer 的 `Retain` / `Delete` 删除流程。 +1. 按已批准规格完成 CRD 校验、默认值和状态约定。 +2. 实现 PostgreSQL registry、OpenBao 和 ESO adapter 的测试替身及集成测试。 +3. 实现凭据、login owner、database、grant、extension 和 Secret 投射的幂等收敛。 +4. 实现并演练带 finalizer 的 `Retain` / `Delete` 删除流程和迁移 runbook。 代码和 Gitea Actions 工作流预期托管在 `git.ddupan.top/panxiao81/postgresql-tenant-operator`。 diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..21d97b6 --- /dev/null +++ b/docs/api-reference.md @@ -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-scoped,short 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` | 1–65535 | +| `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 + +namespaced,short 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 | `-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 格式 +为 `/v1//data/`;不得包含 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 +``` diff --git a/docs/architecture.md b/docs/architecture.md index a92d123..382e1b7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 Operator(ESO)读取 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 API;Kubernetes 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) diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..172c871 --- /dev/null +++ b/docs/deployment.md @@ -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 路径固定推导为 `//`。namespace/name 都已通过 +Kubernetes 名称校验,因此不再允许 CR 提供任意路径。KV v2 API URL 使用 consumer +address 拼为 `
/v1//data///`。 + +配置变化不得隐式迁移既有凭据。修改 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 和 metadata,Delete + 必须能永久删除全部版本及 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) 的备份/逃生检查。 diff --git a/docs/development.md b/docs/development.md index 4cc9505..82d0b7b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,6 +1,6 @@ # 开发与测试环境 -本项目同时依赖 Kubernetes API、PostgreSQL 和 OpenBao。日常开发不连接 homelab +本项目同时依赖 Kubernetes API、PostgreSQL、OpenBao 和 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 必须把测试 PostgreSQL 和 OpenBao也部署进 Kind,并等待两者 Ready 后 +E2E fixture 必须把测试 PostgreSQL、OpenBao 和 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 catalog 与 OpenBao KV - -> 删除 Tenant并验证 Retain + -> 验证 registry、PostgreSQL catalog、OpenBao 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` 成功代替 + 应用读写和回滚验证。 + ## 故障排查 查看依赖状态与日志: diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 0000000..cb4d573 --- /dev/null +++ b/docs/migration.md @@ -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='' > 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='/tenant.dump' \ + --dbname='' +pg_restore --list '/tenant.dump' +``` + +不要删除旧 database/role。记录停写时间、dump checksum 和 PostgreSQL 版本。 + +### 3. 释放最终名称 + +保持应用停写,终止旧 database 的应用连接。连接其他管理 database,以管理员身份把旧 +database 和旧 login role 改为明确的保留名称: + +```sql +ALTER DATABASE RENAME TO _retained_; +ALTER ROLE RENAME TO _retained_; +``` + +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='' \ + '/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`。 diff --git a/docs/operations.md b/docs/operations.md new file mode 100644 index 0000000..e98d45b --- /dev/null +++ b/docs/operations.md @@ -0,0 +1,76 @@ +# 运维与故障处理 + +| 项目 | 内容 | +| --- | --- | +| 状态 | Review;命令待实现后演练 | +| 最后更新 | 2026-09-10 | + +## 日常检查 + +先看 API 合同,而不是从日志猜状态: + +```sh +kubectl get postgresqlinstances +kubectl get postgresqltenants -A +kubectl get postgresqltenant -n -o yaml +kubectl describe postgresqltenant -n +``` + +随后检查 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,最终一致性会自动重试。 diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..35779e4 --- /dev/null +++ b/docs/security.md @@ -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 path;ESO +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 的资源。 diff --git a/docs/specification.md b/docs/specification.md index 97cfb3b..f9876d2 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -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、PostgreSQL 和 OpenBao 可以短暂处于不同阶段; -controller 不尝试实现跨系统事务,而是通过 PostgreSQL 中持久化的协调阶段、幂等外部 -操作和每轮回读验证最终收敛。 +系统采用最终一致性模型。Kubernetes、PostgreSQL、OpenBao 和 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` 丢失后必须可以从 PostgreSQL 和 OpenBao 重建。 +- `status` 丢失后必须可以从 registry、PostgreSQL、OpenBao 和 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` 可以连接 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。 @@ -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 状态机的权威 checkpoint,Instance 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 工作流的权威 +checkpoint;PostgreSQL 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 结果。