feat: 接通 PostgreSQL 角色与数据库创建闭环
This commit is contained in:
@@ -106,14 +106,14 @@ func (t Target) Check() *Issue {
|
||||
return &Issue{Unavailable, "等待 Database 与 Tenant 双向绑定完成"}
|
||||
}
|
||||
if *database.Tenant != t.Tenant.Identity || *t.Tenant.Database != database.Identity {
|
||||
return &Issue{Conflict, "双向绑定的名称或 UID 不匹配,未创建凭据"}
|
||||
return &Issue{Conflict, "双向绑定的名称或 UID 不匹配,未继续供应"}
|
||||
}
|
||||
if t.Tenant.Deleting || t.Tenant.Phase != binding.Bound || !t.DatabaseProtected || !t.TenantProtected {
|
||||
return &Issue{Stopped, "Tenant 未完成绑定、正在删除或缺少 finalizer 保护,未创建凭据"}
|
||||
return &Issue{Stopped, "Tenant 未完成绑定、正在删除或缺少 finalizer 保护,未继续供应"}
|
||||
}
|
||||
request, err := t.Tenant.Request.Resolve(t.Tenant.Identity)
|
||||
if err != nil || (request.Provision != nil && !database.MatchesProvision(request, t.Tenant.Identity)) || request.Name != database.Identity.Name {
|
||||
return &Issue{Conflict, "Tenant 申请与 Database 目标不一致,未创建凭据"}
|
||||
return &Issue{Conflict, "Tenant 申请与 Database 目标不一致,未继续供应"}
|
||||
}
|
||||
if t.Instance == nil || database.InstanceUID == "" {
|
||||
return &Issue{Unavailable, "等待 Instance 与已记录的实例身份"}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Package provisioning 保存 PostgreSQL 资源供应的确认与恢复规则,不依赖后端或 API。
|
||||
package provisioning
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
Pending Phase = "Pending"
|
||||
CreatingRole Phase = "CreatingRole"
|
||||
CreatingDatabase Phase = "CreatingDatabase"
|
||||
Available Phase = "Available"
|
||||
Conflict Phase = "Conflict"
|
||||
Unavailable Phase = "Unavailable"
|
||||
Stopped Phase = "Stopped"
|
||||
)
|
||||
|
||||
// OID 是已成功创建并回读的对象身份,不是从名称推导出的管理授权。
|
||||
// 仅保存在 CR;不建立 PostgreSQL registry,也不承诺备份还原后的自动认领。
|
||||
type State struct {
|
||||
RoleOID uint32
|
||||
DatabaseOID uint32
|
||||
Phase Phase
|
||||
Message string
|
||||
}
|
||||
|
||||
type Observation struct {
|
||||
RoleOID uint32
|
||||
DatabaseOID uint32
|
||||
RoleSafe bool
|
||||
OwnerOID uint32
|
||||
PublicConnect bool
|
||||
AllowConnections bool
|
||||
}
|
||||
|
||||
func (s State) WithPhase(phase Phase, message string) State {
|
||||
s.Phase, s.Message = phase, message
|
||||
return s
|
||||
}
|
||||
|
||||
func (s State) Resume() (State, bool) {
|
||||
if s.Phase == Conflict {
|
||||
return s, false
|
||||
}
|
||||
if (s.Phase == CreatingRole && s.RoleOID == 0) || (s.Phase == CreatingDatabase && s.DatabaseOID == 0) {
|
||||
return s.WithPhase(Conflict, "外部创建未留下成功确认;请核对目标角色和数据库,不自动认领或重复创建"), false
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
// Check 既阻止未知同名对象,也拒绝已确认对象消失、被重建或权限漂移。
|
||||
func (s State) Check(o Observation) error {
|
||||
if s.RoleOID != o.RoleOID {
|
||||
return fmt.Errorf("角色身份不匹配:记录 OID=%d,观察 OID=%d", s.RoleOID, o.RoleOID)
|
||||
}
|
||||
if s.DatabaseOID != o.DatabaseOID {
|
||||
return fmt.Errorf("数据库身份不匹配:记录 OID=%d,观察 OID=%d", s.DatabaseOID, o.DatabaseOID)
|
||||
}
|
||||
if o.RoleOID != 0 && !o.RoleSafe {
|
||||
return fmt.Errorf("已确认角色的登录属性、特权或成员关系发生变化")
|
||||
}
|
||||
if o.DatabaseOID != 0 && o.OwnerOID != s.RoleOID {
|
||||
return fmt.Errorf("数据库 owner 与已确认角色不匹配")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package provisioning
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResume(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
state State
|
||||
resume bool
|
||||
}{
|
||||
{"初次", State{}, true},
|
||||
{"角色创建不确定", State{Phase: CreatingRole}, false},
|
||||
{"库创建不确定", State{RoleOID: 11, Phase: CreatingDatabase}, false},
|
||||
{"已确认角色", State{RoleOID: 11, Phase: Pending}, true},
|
||||
{"已确认资源暂不可用", State{RoleOID: 11, DatabaseOID: 22, Phase: Unavailable}, true},
|
||||
{"冲突保持", State{RoleOID: 11, Phase: Conflict, Message: "首次诊断"}, false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
state, resume := test.state.Resume()
|
||||
if resume != test.resume || state.RoleOID != test.state.RoleOID || state.DatabaseOID != test.state.DatabaseOID {
|
||||
t.Fatal("恢复决策丢失确认或错误放行")
|
||||
}
|
||||
if !resume && state.Phase != Conflict {
|
||||
t.Fatal("不确定创建未转冲突")
|
||||
}
|
||||
if test.state.Phase == Conflict && state != test.state {
|
||||
t.Fatal("冲突诊断被覆盖")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestObservedIdentity(t *testing.T) {
|
||||
state := State{RoleOID: 11, DatabaseOID: 22}
|
||||
good := Observation{RoleOID: 11, DatabaseOID: 22, RoleSafe: true, OwnerOID: 11}
|
||||
if err := state.Check(good); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, observed := range []Observation{
|
||||
{}, {RoleOID: 12, DatabaseOID: 22, RoleSafe: true, OwnerOID: 12},
|
||||
{RoleOID: 11, DatabaseOID: 23, RoleSafe: true, OwnerOID: 11},
|
||||
{RoleOID: 11, DatabaseOID: 22, RoleSafe: false, OwnerOID: 11},
|
||||
{RoleOID: 11, DatabaseOID: 22, RoleSafe: true, OwnerOID: 99},
|
||||
} {
|
||||
if state.Check(observed) == nil {
|
||||
t.Fatal("对象身份或权限漂移被接受")
|
||||
}
|
||||
}
|
||||
if (State{}).Check(good) == nil {
|
||||
t.Fatal("未确认同名对象被认领")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user