feat: 接入 Instance 原生管理观测与删除保护
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
)
|
||||
|
||||
const (
|
||||
instanceDependencyUnavailable = "DependencyUnavailable"
|
||||
instanceAuthenticationFailed = "AuthenticationFailed"
|
||||
)
|
||||
|
||||
// InstanceRecord 是 API 快照;Revision 仅用于持久化并发保护,不是领域版本。
|
||||
type InstanceRecord struct {
|
||||
Target instance.ObservationTarget
|
||||
Revision string
|
||||
Deleting bool
|
||||
}
|
||||
|
||||
type InstanceResources interface {
|
||||
LoadInstance(context.Context, string) (*InstanceRecord, error)
|
||||
ProtectInstance(context.Context, *InstanceRecord) (*InstanceRecord, error)
|
||||
// InstanceReferences 返回一个可定位的阻塞引用;空字符串表示没有引用。
|
||||
InstanceReferences(context.Context, string) (string, error)
|
||||
}
|
||||
|
||||
type InstanceObserver interface {
|
||||
ObserveManagement(context.Context, instance.ObservationTarget) (InstanceObservation, error)
|
||||
Forget(string)
|
||||
}
|
||||
|
||||
type InstanceResult struct {
|
||||
Record *InstanceRecord
|
||||
Snapshot instance.Snapshot
|
||||
Reason string
|
||||
Message string
|
||||
RemoveProtection bool
|
||||
}
|
||||
|
||||
// InstanceReconciliation 协调 API 保护、实时观察和领域判断,不拼装 Kubernetes status。
|
||||
type InstanceReconciliation struct {
|
||||
Resources InstanceResources
|
||||
Observer InstanceObserver
|
||||
}
|
||||
|
||||
func (s *InstanceReconciliation) Reconcile(ctx context.Context, name string) (InstanceResult, error) {
|
||||
record, err := s.Resources.LoadInstance(ctx, name)
|
||||
if err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
if record == nil {
|
||||
s.Observer.Forget(name)
|
||||
return InstanceResult{}, nil
|
||||
}
|
||||
if record.Deleting {
|
||||
return s.deleting(ctx, record)
|
||||
}
|
||||
record, err = s.Resources.ProtectInstance(ctx, record)
|
||||
if err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
// 每轮从无证据的领域对象开始;持久化 Ready 和连接存活不能替代本轮检查。
|
||||
aggregate, err := instance.Reconstitute(record.Target, instance.Snapshot{}, false)
|
||||
if err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
if err := aggregate.BeginValidation(); err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
observation, observationErr := s.Observer.ObserveManagement(ctx, record.Target)
|
||||
result := InstanceResult{Record: record}
|
||||
if observationErr != nil {
|
||||
result.Snapshot = aggregate.Snapshot()
|
||||
result.Snapshot.Readiness = instance.NotReady
|
||||
result.Snapshot.ObservedRevision = record.Target.Revision().Value()
|
||||
result.Reason, result.Message = observationFailure(observationErr)
|
||||
return result, nil
|
||||
}
|
||||
capabilities, err := observation.Capabilities()
|
||||
if err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
if err := aggregate.AssessManagement(capabilities); err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
result.Snapshot = aggregate.Snapshot()
|
||||
result.Snapshot.ReportedVersion = observation.Version()
|
||||
result.Reason, result.Message = managementResult(result.Snapshot.Failure)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InstanceReconciliation) deleting(ctx context.Context, record *InstanceRecord) (InstanceResult, error) {
|
||||
name := record.Target.Identity().Name()
|
||||
s.Observer.Forget(name)
|
||||
aggregate, err := instance.Reconstitute(record.Target, instance.Snapshot{}, true)
|
||||
if err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
if err := aggregate.BeginDeletion(); err != nil {
|
||||
return InstanceResult{}, err
|
||||
}
|
||||
result := InstanceResult{Record: record, Snapshot: aggregate.Snapshot(), Reason: "Deleting"}
|
||||
reference, err := s.Resources.InstanceReferences(ctx, name)
|
||||
if err != nil {
|
||||
result.Reason = instanceDependencyUnavailable
|
||||
result.Message = "无法确认 Database/Tenant 引用已解除;保留 Instance 删除保护并重试"
|
||||
return result, nil
|
||||
}
|
||||
if reference != "" {
|
||||
result.Reason = "InstanceInUse"
|
||||
result.Message = "仍被 " + reference + " 引用;先处理该资源,不会级联删除外部数据库"
|
||||
return result, nil
|
||||
}
|
||||
result.Message = "引用已解除,仅移除登记保护;不删除 PostgreSQL 或凭据"
|
||||
result.RemoveProtection = true
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func observationFailure(err error) (string, string) {
|
||||
switch {
|
||||
case errors.Is(err, ErrAuthentication):
|
||||
return instanceAuthenticationFailed, "管理连接认证或 TLS 校验失败;检查管理 Secret 和 CA/证书配置"
|
||||
case errors.Is(err, ErrCredentialsInvalid):
|
||||
return "InvalidCredentials", "管理 Secret 的用户名或密码字段缺失;检查引用字段映射"
|
||||
case errors.Is(err, ErrCredentialsChanged):
|
||||
return "CredentialsChanged", "观察期间管理凭据变化,已丢弃结果并关闭旧连接;等待重新验证"
|
||||
case errors.Is(err, ErrCredentialsUnavailable):
|
||||
return instanceDependencyUnavailable, "无法读取管理 Secret;检查其是否存在及 controller namespace 内的读取权限"
|
||||
default:
|
||||
return instanceDependencyUnavailable, "管理连接或能力查询失败;检查 PostgreSQL 可达性、catalog 读取权限和超时"
|
||||
}
|
||||
}
|
||||
|
||||
func managementResult(failure instance.Failure) (string, string) {
|
||||
switch failure {
|
||||
case instance.NoFailure:
|
||||
return "ManagementReady", "当前管理能力检查通过;具体资源授权和扩展安装仍需执行时验证"
|
||||
case instance.InsufficientPrivileges:
|
||||
return "InsufficientPrivileges", "原生管理要求非 superuser 且具备 CREATEDB/CREATEROLE;不会自动修改账号权限"
|
||||
case instance.DependencyUnavailable:
|
||||
return instanceDependencyUnavailable, "当前 PostgreSQL 不可写或所需管理能力暂不可用"
|
||||
case instance.AuthenticationFailed:
|
||||
return instanceAuthenticationFailed, "当前管理能力检查未通过认证"
|
||||
default:
|
||||
return "ObservationIncomplete", "管理能力检查尚有缺项,不能仅凭 metadata 查询成功标记 Ready"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user