feat: 接入 Database 三资源 API 与分层绑定协调
Verify / test (pull_request) Successful in 11m39s
Verify / lint (pull_request) Successful in 12m51s
Verify / database-integration (pull_request) Successful in 13m12s

确定单库单账号、集群级 Database、资源侧先写绑定和凭据定位合同。领域层承载纯规则,service 协調流程,Kubernetes adapter 负责资源呈现与版本保护。

验证:全量 make test、三轮 race、真实 API server 并发与重启补写、最小 RBAC/watch、lint 和文档检查通过。供应、凭据交付及删除清理尚未实现,保留 DeletionPending/finalizer 边界。
This commit is contained in:
2026-09-25 04:09:43 +00:00
parent 347a667c0c
commit 7e9e8e828b
33 changed files with 3516 additions and 45 deletions
@@ -0,0 +1,226 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: postgresqldatabases.database.ayatori.ddupan.top
spec:
group: database.ayatori.ddupan.top
names:
kind: PostgreSQLDatabase
listKind: PostgreSQLDatabaseList
plural: postgresqldatabases
singular: postgresqldatabase
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .spec.instanceRef.name
name: Instance
type: string
- jsonPath: .spec.database
name: Database
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PostgreSQLDatabaseSpec 是一库、一个 login owner 及凭据的独立资源声明。
properties:
credentialRef:
description: |-
CredentialReference 定位已有 OpenBao KV v2 凭据,不包含任何秘密值。
只由资源管理员在导入时填写;controller 必须检查部署允许的 mount/path 范围。
properties:
mount:
maxLength: 253
minLength: 1
type: string
path:
description: Path 是 mount 内的逻辑路径,不含 KV v2 的 data/ API 前缀。
maxLength: 1024
minLength: 1
type: string
required:
- mount
- path
type: object
database:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role 使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
instanceRef:
description: InstanceReference 仅引用同 API group 的集群级 PostgreSQLInstance。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
required:
- name
type: object
loginRole:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role 使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
reclaimPolicy:
default: Retain
description: ReclaimPolicy 控制资源释放后的处置,只有资源管理者可以修改。
enum:
- Retain
- Delete
type: string
source:
description: Source 明确区分创建与只读导入,不从后端同名对象推断。
enum:
- Provision
- Import
type: string
tenantRef:
description: TenantRef 由 controller 先写入;Released 时仍保留旧身份。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
namespace:
maxLength: 63
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
type: string
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
maxLength: 128
minLength: 1
type: string
required:
- name
- namespace
- uid
type: object
required:
- database
- instanceRef
- loginRole
- source
type: object
x-kubernetes-validations:
- message: only imported databases require an existing credentialRef
rule: (self.source == 'Import') == has(self.credentialRef)
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
instanceUID:
description: InstanceUID 记录观察时的实例身份,不把同名新实例视为原目标。
type: string
observedGeneration:
format: int64
type: integer
phase:
description: Phase 暂不冻结供应子阶段枚举;它不是操作授权或绑定的替代记录。
type: string
type: object
required:
- spec
type: object
x-kubernetes-validations:
- message: managed database target cannot change after observation or binding
starts
rule: '!(has(oldSelf.spec.tenantRef) || (has(oldSelf.status) && has(oldSelf.status.instanceUID)))
|| (self.spec.instanceRef == oldSelf.spec.instanceRef && self.spec.database
== oldSelf.spec.database && self.spec.loginRole == oldSelf.spec.loginRole
&& self.spec.source == oldSelf.spec.source && has(self.spec.credentialRef)
== has(oldSelf.spec.credentialRef) && (!has(oldSelf.spec.credentialRef)
|| self.spec.credentialRef == oldSelf.spec.credentialRef))'
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,191 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: postgresqlinstances.database.ayatori.ddupan.top
spec:
group: database.ayatori.ddupan.top
names:
kind: PostgreSQLInstance
listKind: PostgreSQLInstanceList
plural: postgresqlinstances
singular: postgresqlinstance
scope: Cluster
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
adminCredentialRef:
description: AdminCredentialReference 只能读取 controller namespace 的
Secret。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
passwordKey:
default: password
maxLength: 253
minLength: 1
pattern: ^[-._a-zA-Z0-9]+$
type: string
usernameKey:
default: username
maxLength: 253
minLength: 1
pattern: ^[-._a-zA-Z0-9]+$
type: string
required:
- name
type: object
endpoint:
description: PostgreSQLEndpoint 显式区分证书主机名与实际连接 IP,不进行 DNS 推导。
properties:
database:
default: postgres
description: PostgreSQLIdentifier 是第一版受管 database 与 login role
使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
host:
maxLength: 253
minLength: 1
type: string
hostaddr:
maxLength: 45
type: string
x-kubernetes-validations:
- message: hostaddr must be a single IPv4 or IPv6 address
rule: isIP(self)
port:
default: 5432
format: int32
maximum: 65535
minimum: 1
type: integer
sslMode:
default: verify-full
enum:
- disable
- require
- verify-ca
- verify-full
type: string
required:
- host
- hostaddr
type: object
required:
- adminCredentialRef
- endpoint
type: object
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
observedGeneration:
format: int64
type: integer
phase:
enum:
- Pending
- Validating
- Ready
- Deleting
type: string
postgresqlVersion:
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,223 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: postgresqltenants.database.ayatori.ddupan.top
spec:
group: database.ayatori.ddupan.top
names:
kind: PostgreSQLTenant
listKind: PostgreSQLTenantList
plural: postgresqltenants
singular: postgresqltenant
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.databaseRef.name
name: Database
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: PostgreSQLTenantSpec 显式选择动态申请或已有 Database,不重复声明来源。
properties:
databaseRef:
description: DatabaseReference 是 Tenant 对已有集群级 PostgreSQLDatabase
的选择。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
required:
- name
type: object
extensions:
description: Extensions 保留后端扩展名称的原样拼写,不按 SQL identifier 限制。
items:
type: string
type: array
x-kubernetes-list-type: set
provision:
description: DatabaseProvisionRequest 仅用于动态申请,省略名称时由 controller 按
Tenant 名称解析。
properties:
database:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role
使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
instanceRef:
description: InstanceReference 仅引用同 API group 的集群级 PostgreSQLInstance。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group
引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
required:
- name
type: object
loginRole:
description: PostgreSQLIdentifier 是第一版受管 database 与 login role
使用的名称。
maxLength: 63
pattern: ^[a-z][a-z0-9_]{0,62}$
type: string
required:
- instanceRef
type: object
secretName:
description: SecretName 指定 Tenant namespace 内的投射目标,省略时使用合同约定的默认名称。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
type: object
x-kubernetes-validations:
- message: exactly one of provision and databaseRef is required
rule: has(self.provision) != has(self.databaseRef)
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
credentialURL:
description: CredentialURL 只含 OpenBao API 位置,禁止嵌入认证信息。
type: string
databaseRef:
description: DatabaseRef 只有在资源侧确认绑定后才写入。
properties:
name:
description: ObjectName 定位集群级资源,不携带 namespace 或隐式跨 API group 引用。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
uid:
description: |-
UID is a type that holds unique ID values, including UUIDs. Because we
don't ONLY use UUIDs, this is an alias to string. Being a type captures
intent and helps make sure that UIDs and names do not get conflated.
maxLength: 128
minLength: 1
type: string
required:
- name
- uid
type: object
observedGeneration:
format: int64
type: integer
phase:
type: string
secretName:
description: SecretName 是已观察到的同 namespace 投射目标,不包含凭据值。
maxLength: 253
minLength: 1
pattern: ^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$
type: string
type: object
required:
- spec
type: object
x-kubernetes-validations:
- message: binding target cannot change after binding starts
rule: '!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase
in [''Binding'', ''Bound'', ''Deleting'']) || ((has(self.spec.provision)
== has(oldSelf.spec.provision)) && (!has(oldSelf.spec.provision) || self.spec.provision
== oldSelf.spec.provision) && (has(self.spec.databaseRef) == has(oldSelf.spec.databaseRef))
&& (!has(oldSelf.spec.databaseRef) || self.spec.databaseRef == oldSelf.spec.databaseRef))'
- message: binding progress cannot return to an unbound state
rule: '!has(oldSelf.status) || !has(oldSelf.status.phase) || !(oldSelf.status.phase
in [''Binding'', ''Bound'', ''Deleting'']) || (has(self.status) && has(self.status.phase)
&& self.status.phase in [''Binding'', ''Bound'', ''Deleting''])'
served: true
storage: true
subresources:
status: {}
+3
View File
@@ -2,6 +2,9 @@
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/database.ayatori.ddupan.top_postgresqlinstances.yaml
- bases/database.ayatori.ddupan.top_postgresqldatabases.yaml
- bases/database.ayatori.ddupan.top_postgresqltenants.yaml
- bases/execution.ayatori.ddupan.top_jobs.yaml
- bases/execution.ayatori.ddupan.top_jobclasses.yaml
- bases/execution.ayatori.ddupan.top_kubernetesexecutionparameters.yaml
+46 -6
View File
@@ -1,11 +1,51 @@
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: ayatori
app.kubernetes.io/managed-by: kustomize
name: manager-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqldatabases
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqldatabases/finalizers
- postgresqltenants/finalizers
verbs:
- update
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqldatabases/status
- postgresqltenants/status
verbs:
- get
- patch
- update
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqlinstances
verbs:
- get
- list
- watch
- apiGroups:
- database.ayatori.ddupan.top
resources:
- postgresqltenants
verbs:
- get
- list
- patch
- update
- watch
@@ -0,0 +1,15 @@
# 管理员登记已有数据库;不会因创建 CR 就修改数据库或凭据。
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLDatabase
metadata:
name: imported-app
spec:
instanceRef:
name: shared-postgres
database: existing_app
loginRole: existing_app
source: Import
credentialRef:
mount: secret
path: existing/app/postgresql
reclaimPolicy: Retain
@@ -0,0 +1,11 @@
# Instance 观察 controller 尚未接入;管理 Secret 由管理员在 controller namespace 提供。
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLInstance
metadata:
name: shared-postgres
spec:
endpoint:
host: postgres.example.test
hostaddr: 192.0.2.10
adminCredentialRef:
name: shared-postgres-admin
@@ -0,0 +1,25 @@
# 二选一:动态申请或显式引用已有 Database;当前只有绑定协调,没有供应/交付 controller。
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLTenant
metadata:
name: new-app
namespace: default
spec:
provision:
instanceRef:
name: shared-postgres
database: new_app
loginRole: new_app
extensions:
- pgcrypto
secretName: new-app-postgresql
---
apiVersion: database.ayatori.ddupan.top/v1alpha1
kind: PostgreSQLTenant
metadata:
name: existing-app
namespace: default
spec:
databaseRef:
name: imported-app
secretName: existing-app-postgresql
+3
View File
@@ -1,5 +1,8 @@
## Append samples of your project ##
resources:
- database_v1alpha1_postgresqlinstance.yaml
- database_v1alpha1_postgresqldatabase.yaml
- database_v1alpha1_postgresqltenant.yaml
- execution_v1alpha1_job.yaml
- execution_v1alpha1_jobclass.yaml
- execution_v1alpha1_kubernetesexecutionparameters.yaml