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
+136
View File
@@ -0,0 +1,136 @@
// Package binding 定义资源与申请的纯绑定规则,不访问 Kubernetes 或数据库。
package binding
import (
"fmt"
"regexp"
)
const (
Binding = "Binding"
Bound = "Bound"
Deleting = "Deleting"
Conflict = "Conflict"
DependencyUnavailable = "DependencyUnavailable"
)
type Identity struct {
Name string
UID string
}
type TenantIdentity struct {
Namespace string
Name string
UID string
}
// Request 保留用户输入;Resolve 产生默认值已确定的目标,不修改原请求。
type Request struct {
Provision *ProvisionRequest
ExistingDatabase string
}
type ProvisionRequest struct {
Instance string
Database string
LoginRole string
}
type Target struct {
Name string
Provision *ProvisionRequest
}
var identifier = regexp.MustCompile(`^[a-z][a-z0-9_]{0,62}$`)
func (r Request) Resolve(tenant TenantIdentity) (Target, error) {
if (r.Provision == nil) == (r.ExistingDatabase == "") {
return Target{}, fmt.Errorf("必须且只能选择动态申请或已有 Database")
}
if r.Provision == nil {
return Target{Name: r.ExistingDatabase}, nil
}
provision := *r.Provision
if provision.Database == "" {
provision.Database = tenant.Name
}
if provision.LoginRole == "" {
provision.LoginRole = tenant.Name
}
if !identifier.MatchString(provision.Database) || !identifier.MatchString(provision.LoginRole) {
return Target{}, fmt.Errorf("动态 database/loginRole 必须符合 PostgreSQL identifier 规则;省略时使用 Tenant 名称")
}
return Target{Name: DynamicDatabaseName(tenant.UID), Provision: &provision}, nil
}
func DynamicDatabaseName(tenantUID string) string { return "tenant-" + tenantUID }
type Tenant struct {
Identity TenantIdentity
Request Request
Phase string
Deleting bool
Database *Identity
}
// Database 是绑定所需的资源事实,不包含存储版本、Conditions 或客户端对象。
type Database struct {
Identity Identity
Instance string
InstanceUID string
Name string
LoginRole string
Source string
Tenant *TenantIdentity
Phase string
Deleting bool
Ready bool
}
type Instance struct {
Identity Identity
Deleting bool
Ready bool
}
type Issue struct {
Reason string
Message string
}
func (d Database) MatchesProvision(target Target, tenant TenantIdentity) bool {
return target.Provision != nil && d.Source == "Provision" && d.Instance == target.Provision.Instance &&
d.Name == target.Provision.Database && d.LoginRole == target.Provision.LoginRole &&
d.Tenant != nil && *d.Tenant == tenant
}
func (i Instance) Check(database *Database) *Issue {
if i.Deleting || !i.Ready {
return &Issue{DependencyUnavailable, "Instance 正在删除或尚无当前版本的 Ready 观察"}
}
if database != nil && database.InstanceUID != "" && database.InstanceUID != i.Identity.UID {
return &Issue{Conflict, "Instance UID 已变化;请核实实例身份,未迁移或接管资源"}
}
return nil
}
func (d Database) CanBind(tenant Tenant) *Issue {
if tenant.Database != nil && tenant.Database.UID != d.Identity.UID {
return &Issue{Conflict, fmt.Sprintf("Database %s 的 UID 与已记录绑定不同;请核实同名重建,未接管新对象", d.Identity.Name)}
}
if d.Deleting || d.Phase == "Released" || d.Phase == Deleting {
return &Issue{Conflict, "Database 正在删除或处于 Released;请由管理员核实并处理,未重新分配"}
}
if d.Tenant != nil {
if *d.Tenant != tenant.Identity {
return &Issue{Conflict, fmt.Sprintf("Database %s 已绑定 Tenant %s/%s(UID %s);未抢占",
d.Identity.Name, d.Tenant.Namespace, d.Tenant.Name, d.Tenant.UID)}
}
return nil
}
if d.Phase != "Available" || !d.Ready {
return &Issue{DependencyUnavailable, "Database 尚未完成验证并进入 Available,等待资源观察"}
}
return nil
}