feat: 接通 PostgreSQL 角色与数据库创建闭环
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/provisioning"
|
||||
)
|
||||
|
||||
type ProvisioningRecord struct {
|
||||
credential.Target
|
||||
Revision string
|
||||
TenantGeneration int64
|
||||
InstanceGeneration int64
|
||||
InstanceTarget instance.ObservationTarget
|
||||
Credentials credential.State
|
||||
State provisioning.State
|
||||
}
|
||||
|
||||
type ProvisioningResources interface {
|
||||
Load(context.Context, string) (*ProvisioningRecord, error)
|
||||
Save(context.Context, *ProvisioningRecord, provisioning.State) (*ProvisioningRecord, error)
|
||||
CheckCurrent(context.Context, *ProvisioningRecord) error
|
||||
}
|
||||
|
||||
type DatabaseProvisioning struct {
|
||||
Resources ProvisioningResources
|
||||
Credentials CredentialStore
|
||||
Backend ProvisioningBackend
|
||||
}
|
||||
|
||||
func (s DatabaseProvisioning) Reconcile(ctx context.Context, name string) error {
|
||||
record, err := s.Resources.Load(ctx, name)
|
||||
if err != nil || record == nil || !record.RequiresPreparation() {
|
||||
return err
|
||||
}
|
||||
if state, resume := record.State.Resume(); !resume {
|
||||
if state == record.State {
|
||||
return nil
|
||||
}
|
||||
_, err := s.Resources.Save(ctx, record, state)
|
||||
return err
|
||||
}
|
||||
if issue := record.Check(); issue != nil {
|
||||
phase := provisioning.Unavailable
|
||||
if issue.Phase == credential.Conflict {
|
||||
phase = provisioning.Conflict
|
||||
}
|
||||
if issue.Phase == credential.Stopped {
|
||||
phase = provisioning.Stopped
|
||||
}
|
||||
return s.report(ctx, record, phase, issue.Message)
|
||||
}
|
||||
if !record.Credentials.Confirmed() || record.Credentials.Location == nil || record.Credentials.Phase != credential.Prepared {
|
||||
return s.report(ctx, record, provisioning.Unavailable, "等待已确认且当前可用的应用凭据")
|
||||
}
|
||||
value, err := s.Credentials.ReadCredential(ctx, *record.Credentials.Location, record.Credentials.Version)
|
||||
if err != nil {
|
||||
return s.report(ctx, record, provisioning.Unavailable, "无法读取已确认凭据;未执行 PostgreSQL 写入")
|
||||
}
|
||||
if issue := record.CheckCredential(value); issue != nil {
|
||||
return s.report(ctx, record, provisioning.Conflict, issue.Message)
|
||||
}
|
||||
observed, err := s.Backend.InspectResources(ctx, record.InstanceTarget, record.Database.Name, record.Database.LoginRole)
|
||||
if err != nil {
|
||||
return s.report(ctx, record, provisioning.Unavailable, "无法验证当前 PostgreSQL 资源和管理权限")
|
||||
}
|
||||
if err := record.State.Check(observed); err != nil {
|
||||
return s.report(ctx, record, provisioning.Conflict, err.Error()+";请人工核对,未认领或覆盖")
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
if record.State.RoleOID == 0 {
|
||||
return s.createRole(ctx, record, value)
|
||||
}
|
||||
if record.State.DatabaseOID == 0 {
|
||||
return s.createDatabase(ctx, record)
|
||||
}
|
||||
if err := s.Backend.ConfigureAccess(ctx, record.InstanceTarget, record.Database.Name, record.Database.LoginRole, record.State); err != nil {
|
||||
return s.backendFailure(ctx, record, err, "数据库访问权限收敛")
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.report(ctx, record, provisioning.Available, "角色和数据库已确认,PUBLIC CONNECT 已撤销;扩展与 Tenant 交付尚未完成")
|
||||
}
|
||||
|
||||
func (s DatabaseProvisioning) createRole(ctx context.Context, record *ProvisioningRecord, value credential.ApplicationCredential) error {
|
||||
record, err := s.Resources.Save(ctx, record, record.State.WithPhase(provisioning.CreatingRole, "开始创建登录角色;尚未持久确认"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
oid, err := s.Backend.CreateLoginRole(ctx, record.InstanceTarget, value)
|
||||
if err != nil {
|
||||
return s.backendFailure(ctx, record, err, "登录角色创建")
|
||||
}
|
||||
if oid == 0 {
|
||||
return s.backendFailure(ctx, record, ErrResourceUncertain, "登录角色创建")
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
state := record.State.WithPhase(provisioning.Pending, "登录角色已创建并回读,等待创建数据库")
|
||||
state.RoleOID = oid
|
||||
_, err = s.Resources.Save(ctx, record, state)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s DatabaseProvisioning) createDatabase(ctx context.Context, record *ProvisioningRecord) error {
|
||||
record, err := s.Resources.Save(ctx, record, record.State.WithPhase(provisioning.CreatingDatabase, "开始创建数据库;尚未持久确认"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
oid, err := s.Backend.CreateOwnedDatabase(ctx, record.InstanceTarget, record.Database.Name, record.Database.LoginRole, record.State.RoleOID)
|
||||
if err != nil {
|
||||
return s.backendFailure(ctx, record, err, "数据库创建")
|
||||
}
|
||||
if oid == 0 {
|
||||
return s.backendFailure(ctx, record, ErrResourceUncertain, "数据库创建")
|
||||
}
|
||||
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
|
||||
return err
|
||||
}
|
||||
state := record.State.WithPhase(provisioning.Pending, "数据库已创建并回读,等待收紧访问权限;连接入口仍关闭")
|
||||
state.DatabaseOID = oid
|
||||
_, err = s.Resources.Save(ctx, record, state)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s DatabaseProvisioning) backendFailure(ctx context.Context, record *ProvisioningRecord, err error, step string) error {
|
||||
if errors.Is(err, ErrResourceUnavailable) {
|
||||
return s.report(ctx, record, provisioning.Unavailable, step+"被拒绝或暂不可用,保留已确认步骤并等待依赖恢复")
|
||||
}
|
||||
return s.report(ctx, record, provisioning.Conflict, step+"冲突或结果不确定;请核对目标与已确认 OID,未自动认领、改密或清理")
|
||||
}
|
||||
|
||||
func (s DatabaseProvisioning) report(ctx context.Context, record *ProvisioningRecord, phase provisioning.Phase, message string) error {
|
||||
message = fmt.Sprintf("Database %s / Instance %s / database %s / role %s:%s", record.Database.Identity.Name,
|
||||
record.Database.Instance, record.Database.Name, record.Database.LoginRole, message)
|
||||
_, err := s.Resources.Save(ctx, record, record.State.WithPhase(phase, message))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package application
|
||||
|
||||
import "context"
|
||||
|
||||
// DatabaseReconciliation 顺序编排两个已有用例,不复制它们的领域判断。
|
||||
// 启用资源创建时由同一个 controller 驱动,避免两个 worker 争写同一 Database status。
|
||||
type DatabaseReconciliation struct {
|
||||
Credentials *CredentialPreparation
|
||||
Provisioning *DatabaseProvisioning
|
||||
}
|
||||
|
||||
func (s DatabaseReconciliation) Reconcile(ctx context.Context, name string) error {
|
||||
if err := s.Credentials.Reconcile(ctx, name); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Provisioning.Reconcile(ctx, name)
|
||||
}
|
||||
@@ -35,6 +35,7 @@ var (
|
||||
// Database 与 Connector 复用原项目 internal/instance/service.go 的能力边界。
|
||||
// Metadata 只查询版本与可用扩展,不能产生领域 Ready。
|
||||
type Database interface {
|
||||
ResourceDatabase
|
||||
InspectMetadata(context.Context) (DatabaseMetadata, error)
|
||||
InspectManagement(context.Context) (DatabaseMetadata, error)
|
||||
Close()
|
||||
@@ -93,16 +94,45 @@ func (s *InstanceService) ObserveManagement(ctx context.Context, target instance
|
||||
}
|
||||
|
||||
func (s *InstanceService) observe(ctx context.Context, target instance.ObservationTarget, management bool) (InstanceObservation, error) {
|
||||
if err := target.Validate(); err != nil {
|
||||
return InstanceObservation{}, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
current, err := s.connection(ctx, target)
|
||||
if err != nil {
|
||||
return InstanceObservation{}, err
|
||||
}
|
||||
name := target.Identity().Name()
|
||||
var metadata DatabaseMetadata
|
||||
if management {
|
||||
metadata, err = current.database.InspectManagement(ctx)
|
||||
} else {
|
||||
metadata, err = current.database.InspectMetadata(ctx)
|
||||
metadata.Management = instance.ManagementChecks{}
|
||||
}
|
||||
if err != nil {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, err
|
||||
}
|
||||
if metadata.Version == "" {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, ErrObservation
|
||||
}
|
||||
if err := s.checkCredentials(ctx, target, current.credentials); err != nil {
|
||||
return InstanceObservation{}, err
|
||||
}
|
||||
return InstanceObservation{target: target, version: metadata.Version,
|
||||
extensions: instance.ObserveExtensionSupport(metadata.AvailableExtensions), management: metadata.Management}, nil
|
||||
}
|
||||
|
||||
// connection 由持有 mu 的调用者使用;不缓存观察结果或管理权限。
|
||||
func (s *InstanceService) connection(ctx context.Context, target instance.ObservationTarget) (*entry, error) {
|
||||
if err := target.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.closed {
|
||||
return InstanceObservation{}, ErrClosed
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return InstanceObservation{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 先读取有效凭据。读取失败时不得继续使用缓存中的旧连接。
|
||||
@@ -110,11 +140,11 @@ func (s *InstanceService) observe(ctx context.Context, target instance.Observati
|
||||
credentials, err := s.source.Read(ctx, target.Definition().AdminCredential())
|
||||
if err != nil {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, credentialError(err)
|
||||
return nil, credentialError(err)
|
||||
}
|
||||
if credentials.username == "" || credentials.password == "" {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, ErrCredentialsInvalid
|
||||
return nil, ErrCredentialsInvalid
|
||||
}
|
||||
|
||||
// 连接身份与有效值均未变化时复用 pgxpool;generation 本身不要求换池。
|
||||
@@ -127,7 +157,7 @@ func (s *InstanceService) observe(ctx context.Context, target instance.Observati
|
||||
if current == nil {
|
||||
database, err := s.connector.Connect(ctx, target.Definition().Endpoint(), credentials)
|
||||
if err != nil {
|
||||
return InstanceObservation{}, err
|
||||
return nil, err
|
||||
}
|
||||
current = &entry{
|
||||
target: target,
|
||||
@@ -137,39 +167,22 @@ func (s *InstanceService) observe(ctx context.Context, target instance.Observati
|
||||
s.entries[name] = current
|
||||
}
|
||||
|
||||
var metadata DatabaseMetadata
|
||||
if management {
|
||||
metadata, err = current.database.InspectManagement(ctx)
|
||||
} else {
|
||||
metadata, err = current.database.InspectMetadata(ctx)
|
||||
// 即使 adapter 误填权限,也不能把只读 metadata 入口升级为 Ready。
|
||||
metadata.Management = instance.ManagementChecks{}
|
||||
}
|
||||
if err != nil {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, err
|
||||
}
|
||||
if metadata.Version == "" {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, ErrObservation
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func (s *InstanceService) checkCredentials(ctx context.Context, target instance.ObservationTarget, credentials Credentials) error {
|
||||
name := target.Identity().Name()
|
||||
// 回读后再检查凭据,避免把轮换前取得的结果交给新凭据的调用链。
|
||||
latest, err := s.source.Read(ctx, target.Definition().AdminCredential())
|
||||
if err != nil {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, credentialError(err)
|
||||
return credentialError(err)
|
||||
}
|
||||
if latest != credentials {
|
||||
s.release(name)
|
||||
return InstanceObservation{}, ErrCredentialsChanged
|
||||
return ErrCredentialsChanged
|
||||
}
|
||||
return InstanceObservation{
|
||||
target: target,
|
||||
version: metadata.Version,
|
||||
extensions: instance.ObserveExtensionSupport(metadata.AvailableExtensions),
|
||||
management: metadata.Management,
|
||||
}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func credentialError(err error) error {
|
||||
|
||||
@@ -37,6 +37,7 @@ func (s *sourceStub) Read(context.Context, instance.CredentialReference) (Creden
|
||||
}
|
||||
|
||||
type databaseStub struct {
|
||||
ResourceDatabase
|
||||
closes int
|
||||
err error
|
||||
metadata DatabaseMetadata
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/instance"
|
||||
"git.ddupan.top/panxiao81/ayatori/internal/database/domain/provisioning"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrResourceConflict = errors.New("PostgreSQL resource conflict; manual resolution required")
|
||||
ErrResourceUncertain = errors.New("PostgreSQL creation outcome uncertain; manual resolution required")
|
||||
ErrResourceUnavailable = errors.New("PostgreSQL operation unavailable before creation")
|
||||
)
|
||||
|
||||
// ResourceDatabase 是供应实际需要的后端能力,不暴露 SQL 或任意回调给用例。
|
||||
type ResourceDatabase interface {
|
||||
InspectResources(context.Context, string, string) (provisioning.Observation, error)
|
||||
CreateLoginRole(context.Context, credential.ApplicationCredential) (uint32, error)
|
||||
CreateOwnedDatabase(context.Context, string, string, uint32) (uint32, error)
|
||||
ConfigureAccess(context.Context, string, string, provisioning.State) error
|
||||
}
|
||||
|
||||
type ProvisioningBackend interface {
|
||||
InspectResources(context.Context, instance.ObservationTarget, string, string) (provisioning.Observation, error)
|
||||
CreateLoginRole(context.Context, instance.ObservationTarget, credential.ApplicationCredential) (uint32, error)
|
||||
CreateOwnedDatabase(context.Context, instance.ObservationTarget, string, string, uint32) (uint32, error)
|
||||
ConfigureAccess(context.Context, instance.ObservationTarget, string, string, provisioning.State) error
|
||||
}
|
||||
|
||||
func (s *InstanceService) InspectResources(ctx context.Context, target instance.ObservationTarget, name, role string) (provisioning.Observation, error) {
|
||||
var result provisioning.Observation
|
||||
err := s.withManagementConnection(ctx, target, false, func(database Database) (err error) {
|
||||
result, err = database.InspectResources(ctx, name, role)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return provisioning.Observation{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InstanceService) CreateLoginRole(ctx context.Context, target instance.ObservationTarget, value credential.ApplicationCredential) (uint32, error) {
|
||||
var oid uint32
|
||||
err := s.withManagementConnection(ctx, target, true, func(database Database) (err error) {
|
||||
oid, err = database.CreateLoginRole(ctx, value)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
func (s *InstanceService) CreateOwnedDatabase(ctx context.Context, target instance.ObservationTarget, name, role string, roleOID uint32) (uint32, error) {
|
||||
var oid uint32
|
||||
err := s.withManagementConnection(ctx, target, true, func(database Database) (err error) {
|
||||
oid, err = database.CreateOwnedDatabase(ctx, name, role, roleOID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return oid, nil
|
||||
}
|
||||
|
||||
func (s *InstanceService) ConfigureAccess(ctx context.Context, target instance.ObservationTarget, name, role string, state provisioning.State) error {
|
||||
return s.withManagementConnection(ctx, target, false, func(database Database) error {
|
||||
return database.ConfigureAccess(ctx, name, role, state)
|
||||
})
|
||||
}
|
||||
|
||||
// 所有供应操作与观察共用同一个连接登记和 Secret 刷新边界。
|
||||
// 创建后凭据回读失败不能冒充明确未执行;用例必须保留不确定诊断。
|
||||
func (s *InstanceService) withManagementConnection(ctx context.Context, target instance.ObservationTarget, creating bool, operation func(Database) error) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
current, err := s.connection(ctx, target)
|
||||
if err != nil {
|
||||
return ErrResourceUnavailable
|
||||
}
|
||||
metadata, err := current.database.InspectManagement(ctx)
|
||||
checks := metadata.Management
|
||||
if err != nil || checks.Connection != instance.CheckPassed || checks.Roles != instance.CheckPassed || checks.Databases != instance.CheckPassed || checks.Grants != instance.CheckPassed {
|
||||
return ErrResourceUnavailable
|
||||
}
|
||||
if err := s.checkCredentials(ctx, target, current.credentials); err != nil {
|
||||
return ErrResourceUnavailable
|
||||
}
|
||||
if err := operation(current.database); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.checkCredentials(ctx, target, current.credentials); err != nil {
|
||||
if creating {
|
||||
return ErrResourceUncertain
|
||||
}
|
||||
return ErrResourceUnavailable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user