Files
ayatori/internal/database/application/credential_preparation.go
T
panxiao81 7834cab97f
Verify / test (pull_request) Successful in 13m29s
Verify / lint (pull_request) Successful in 14m13s
Verify / database-integration (pull_request) Successful in 15m48s
refactor: 集中显式注入并归位凭据领域规则
2026-09-27 19:16:48 +00:00

154 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package application
import (
"context"
"errors"
"fmt"
credentialdomain "git.ddupan.top/panxiao81/ayatori/internal/database/domain/credential"
)
var (
ErrCredentialLocation = errors.New("credential location is outside the configured scope")
ErrCredentialUnavailable = errors.New("credential backend unavailable")
ErrCredentialNotFound = errors.New("application credential not found")
ErrCredentialConflict = errors.New("credential creation requires manual conflict resolution")
ErrCredentialUncertain = errors.New("credential creation outcome is uncertain; manual resolution required")
)
// CredentialStore 只表达本用例需要的凭据操作,不提供覆盖或删除。
// version=0 的读取只用于观察是否已有值,成功不能作为认领依据。
type CredentialStore interface {
ProvisionLocation(string) (credentialdomain.Location, error)
ReadCredential(context.Context, credentialdomain.Location, int64) (credentialdomain.ApplicationCredential, error)
CreateCredential(context.Context, credentialdomain.Location, credentialdomain.ApplicationCredential) (int64, error)
}
// CredentialRecord 是同一轮观察的事实,状态中永远不保存密码。
type CredentialRecord struct {
credentialdomain.Target
Revision string
TenantGeneration int64
InstanceGeneration int64
Status credentialdomain.State
}
// CredentialResources 的写入必须检查 Database UID/resourceVersion,保留其他状态。
// CheckCurrent 在外部操作前后回读本轮三个资源,拒绝陈旧快照;它不是跨系统事务。
type CredentialResources interface {
Load(context.Context, string) (*CredentialRecord, error)
Save(context.Context, *CredentialRecord, credentialdomain.State) (*CredentialRecord, error)
CheckCurrent(context.Context, *CredentialRecord) error
}
type CredentialPreparation struct {
Resources CredentialResources
Store CredentialStore
}
func (s CredentialPreparation) 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, canContinue := record.Status.Resume(); !canContinue {
if state == record.Status {
return nil
}
return s.report(ctx, record, state.Phase, state.Message)
}
if issue := record.Check(); issue != nil {
return s.report(ctx, record, issue.Phase, issue.Message)
}
location, err := s.Store.ProvisionLocation(record.Database.Identity.UID)
if err != nil {
return s.report(ctx, record, credentialdomain.Unavailable, "凭据存储位置配置无效,未执行外部写入")
}
if issue := record.Status.CheckLocation(location); issue != nil {
return s.report(ctx, record, issue.Phase, issue.Message)
}
if record.Status.Location == nil {
status := record.Status
status.Location = &location
status = status.WithPhase(credentialdomain.Pinned, "凭据位置已固定,等待创建")
record, err = s.Resources.Save(ctx, record, status)
if err != nil {
return err
}
}
if record.Status.Confirmed() {
return s.observe(ctx, record)
}
return s.create(ctx, record)
}
func (s CredentialPreparation) create(ctx context.Context, record *CredentialRecord) error {
_, err := s.Store.ReadCredential(ctx, *record.Status.Location, 0)
if err == nil || errors.Is(err, credentialdomain.ErrApplicationCredentialInvalid) {
return s.report(ctx, record, credentialdomain.Conflict, "固定位置已有未确认的凭据;请人工核实,未认领或覆盖")
}
if !errors.Is(err, ErrCredentialNotFound) {
return s.report(ctx, record, credentialdomain.Unavailable, "创建前无法确认凭据位置是否为空,等待依赖恢复")
}
credential, err := credentialdomain.GenerateApplicationCredential(record.Database.LoginRole, record.Database.Name, record.Instance.Endpoint)
if err != nil {
return s.report(ctx, record, credentialdomain.InvalidTarget, "应用凭据目标无效,未执行外部写入")
}
status := record.Status
status = status.WithPhase(credentialdomain.Creating, "凭据创建已开始;尚无成功确认时不得重入创建")
record, err = s.Resources.Save(ctx, record, status)
if err != nil {
return err
}
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
return err
}
version, err := s.Store.CreateCredential(ctx, *record.Status.Location, credential)
if errors.Is(err, ErrCredentialUnavailable) {
// 适配器只在明确未执行写入(认证拒绝或请求前取消)时返回此错误。
return s.report(ctx, record, credentialdomain.Unavailable, "凭据创建在执行前被拒绝,等待认证或权限恢复")
}
if err != nil {
return s.report(ctx, record, credentialdomain.Conflict,
"凭据创建冲突或结果不确定;请核对固定位置的版本历史,未认领、覆盖或重新生成密码")
}
confirmed, issue := record.Status.Created(version)
if issue != nil {
return s.report(ctx, record, issue.Phase, issue.Message)
}
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
return err
}
_, err = s.Resources.Save(ctx, record, confirmed)
return err
}
func (s CredentialPreparation) observe(ctx context.Context, record *CredentialRecord) error {
credential, err := s.Store.ReadCredential(ctx, *record.Status.Location, record.Status.Version)
if errors.Is(err, ErrCredentialConflict) || errors.Is(err, ErrCredentialNotFound) {
return s.report(ctx, record, credentialdomain.Conflict, "已确认凭据消失、版本变化或内容无效;请人工核实,未生成替代密码")
}
if err != nil {
return s.report(ctx, record, credentialdomain.Unavailable, "已确认凭据暂时无法读取;保留确认版本,等待依赖恢复")
}
if issue := record.CheckCredential(credential); issue != nil {
return s.report(ctx, record, issue.Phase, issue.Message)
}
if err := s.Resources.CheckCurrent(ctx, record); err != nil {
return err
}
status := record.Status
status.Phase = credentialdomain.Prepared
status.Message = "已确认凭据可读取;尚未验证 PostgreSQL 资源或完成 Tenant 交付"
_, err = s.Resources.Save(ctx, record, status)
return err
}
func (s CredentialPreparation) report(ctx context.Context, record *CredentialRecord, phase credentialdomain.Phase, message string) error {
status := record.Status
status.Phase = phase
status.Message = fmt.Sprintf("Database %s:%s", record.Database.Identity.Name, message)
_, err := s.Resources.Save(ctx, record, status)
return err
}