补齐旧 DNS 账户幂等导入
启动时登记旧 DNS 配置并保持用户修改,增加冲突保护和回归测试。
This commit is contained in:
parent
92b499979f
commit
920b318cf0
@ -14,6 +14,7 @@ import (
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/dnsprovider"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/firewall"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/httpapi"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/httpchallenge"
|
||||
@ -105,6 +106,25 @@ func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string
|
||||
return nil, fmt.Errorf("初始化超级管理员用户失败:%w", err)
|
||||
}
|
||||
}
|
||||
if cfg.DNS.CredentialRef != "" && cfg.DNS.Provider != "" {
|
||||
legacyProjectID := cfg.DNS.ProjectID
|
||||
if legacyProjectID == "" {
|
||||
legacyProjectID = domain.DefaultProjectID
|
||||
}
|
||||
legacyOwnerID := cfg.DNS.OwnerUserID
|
||||
if legacyOwnerID == "" {
|
||||
legacyOwnerID = "primary-super-admin"
|
||||
}
|
||||
legacyImporter := repository.NewSQLProjectMemberRepository(db.DB, runtimeDatabaseConfig.Driver)
|
||||
if err := legacyImporter.ImportLegacyDNSAccount(ctx, domain.DNSAccount{
|
||||
ID: "legacy-system-dns", Name: "旧系统 DNS 账户", ProjectID: legacyProjectID,
|
||||
OwnerUserID: legacyOwnerID, Provider: cfg.DNS.Provider, CredentialRef: cfg.DNS.CredentialRef,
|
||||
}); err != nil {
|
||||
_ = db.Close()
|
||||
_ = logger.Close()
|
||||
return nil, fmt.Errorf("导入旧 DNS 账户失败:%w", err)
|
||||
}
|
||||
}
|
||||
taskRepo := repository.NewSQLTaskRepository(db.DB, runtimeDatabaseConfig.Driver)
|
||||
transactionRepo := repository.NewSQLDeploymentTransactionRepository(db.DB, runtimeDatabaseConfig.Driver)
|
||||
deploymentService := deployment.NewServiceWithJournal(deployment.SSHFactory{Secrets: secretStore}, transactionRepo)
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/domain"
|
||||
@ -12,6 +13,8 @@ import (
|
||||
|
||||
var ErrDNSAccountConflict = errors.New("DNS 账户已变更,请刷新后重试")
|
||||
|
||||
var ErrLegacyDNSAccountConflict = errors.New("旧系统 DNS 账户标识已被其他账户占用")
|
||||
|
||||
// DNSAccountRepository 的公开读取不返回凭据引用;秘密读取只能使用独立的使用权限入口。
|
||||
// 删除及运行租约将在域名绑定接入后提供,当前不提供无引用保护的删除方法。
|
||||
type DNSAccountRepository interface {
|
||||
@ -26,13 +29,75 @@ type DNSAccountRepository interface {
|
||||
RecordDNSAccountVerification(context.Context, string, string, int64, bool) (domain.DNSAccount, error)
|
||||
}
|
||||
|
||||
// DNSLegacyAccountImporter 只负责把旧配置登记为可追踪的账户元数据。
|
||||
// 它不读取或复制秘密,也不把旧验证状态冒充为新账户已在线验证。
|
||||
type DNSLegacyAccountImporter interface {
|
||||
ImportLegacyDNSAccount(context.Context, domain.DNSAccount) error
|
||||
}
|
||||
|
||||
var _ DNSAccountRepository = (*SQLProjectMemberRepository)(nil)
|
||||
var _ DNSLegacyAccountImporter = (*SQLProjectMemberRepository)(nil)
|
||||
|
||||
const dnsAccountColumns = `dns_accounts.id, dns_accounts.name, dns_accounts.project_id,
|
||||
dns_accounts.owner_user_id, dns_accounts.provider, dns_accounts.credential_ref,
|
||||
dns_accounts.status, dns_accounts.verification_state, dns_accounts.revision,
|
||||
dns_accounts.created_at, dns_accounts.updated_at`
|
||||
|
||||
// ImportLegacyDNSAccount 将旧的单账户配置幂等登记到独立账户表。
|
||||
// 已存在同凭据引用时保持现有记录不变;同一固定 ID 被其他账户占用时拒绝启动迁移,避免覆盖用户修改。
|
||||
func (r *SQLProjectMemberRepository) ImportLegacyDNSAccount(ctx context.Context, item domain.DNSAccount) error {
|
||||
if err := r.ensureDB(); err != nil {
|
||||
return err
|
||||
}
|
||||
item.Name = strings.TrimSpace(item.Name)
|
||||
item.Status = domain.DNSAccountActive
|
||||
item.VerificationState = domain.DNSAccountUnverified
|
||||
item.Revision = 1
|
||||
if err := item.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var existingID string
|
||||
err = tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT id FROM dns_accounts WHERE credential_ref = ?`+r.resourceWriteLock()), item.CredentialRef).Scan(&existingID)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
var existingRef string
|
||||
err = tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT credential_ref FROM dns_accounts WHERE id = ?`+r.resourceWriteLock()), item.ID).Scan(&existingRef)
|
||||
if err == nil {
|
||||
if existingRef == item.CredentialRef {
|
||||
return nil
|
||||
}
|
||||
return ErrLegacyDNSAccountConflict
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
var projectID string
|
||||
if err := tx.QueryRowContext(ctx, Rebind(r.driver, `SELECT id FROM projects WHERE id = ?`), item.ProjectID).Scan(&projectID); err != nil {
|
||||
return fmt.Errorf("旧 DNS 账户所属项目不存在:%w", err)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
item.CreatedAt, item.UpdatedAt = now, now
|
||||
if _, err := tx.ExecContext(ctx, Rebind(r.driver, `INSERT INTO dns_accounts
|
||||
(id, name, project_id, owner_user_id, provider, credential_ref, status, verification_state, revision, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), item.ID, item.Name, item.ProjectID, item.OwnerUserID,
|
||||
item.Provider, item.CredentialRef, item.Status, item.VerificationState, item.Revision, item.CreatedAt, item.UpdatedAt); err != nil {
|
||||
return errors.New("登记旧 DNS 账户失败")
|
||||
}
|
||||
if err := writeDNSAccountAudit(ctx, tx, r.driver, item.OwnerUserID, item, "dns_account.import_legacy"); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func scanDNSAccount(row scanner) (domain.DNSAccount, error) {
|
||||
var item domain.DNSAccount
|
||||
err := row.Scan(&item.ID, &item.Name, &item.ProjectID, &item.OwnerUserID, &item.Provider,
|
||||
|
||||
@ -41,6 +41,54 @@ func TestDNSAccountRepository(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportLegacyDNSAccountIsIdempotentAndPreservesExistingChanges(t *testing.T) {
|
||||
cfg := config.Default(t.TempDir()).Database
|
||||
cfg.DSN = filepath.Join(t.TempDir(), "legacy-dns.db")
|
||||
db, err := database.Open(t.Context(), cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err := database.Migrate(t.Context(), db.DB, "sqlite"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo := NewSQLProjectMemberRepository(db.DB, "sqlite")
|
||||
item := domain.DNSAccount{
|
||||
ID: "legacy-system-dns", Name: "旧系统 DNS 账户", ProjectID: domain.DefaultProjectID,
|
||||
OwnerUserID: "primary-super-admin", Provider: "aliyun", CredentialRef: "secret://dns/legacy",
|
||||
}
|
||||
if err := repo.ImportLegacyDNSAccount(t.Context(), item); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var name, status, verification, reference string
|
||||
var revision int
|
||||
if err := db.DB.QueryRowContext(t.Context(), `SELECT name,status,verification_state,credential_ref,revision FROM dns_accounts WHERE id = ?`, item.ID).
|
||||
Scan(&name, &status, &verification, &reference, &revision); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if name != item.Name || status != domain.DNSAccountActive || verification != domain.DNSAccountUnverified || reference != item.CredentialRef || revision != 1 {
|
||||
t.Fatalf("旧账户初次导入状态错误:%s %s %s %s %d", name, status, verification, reference, revision)
|
||||
}
|
||||
if _, err := db.DB.ExecContext(t.Context(), `UPDATE dns_accounts SET name = ?, status = ?, verification_state = ?, revision = ? WHERE id = ?`, "用户修改后的账户", domain.DNSAccountDisabled, domain.DNSAccountVerified, 8, item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repo.ImportLegacyDNSAccount(t.Context(), item); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.DB.QueryRowContext(t.Context(), `SELECT name,status,verification_state,revision FROM dns_accounts WHERE id = ?`, item.ID).
|
||||
Scan(&name, &status, &verification, &revision); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if name != "用户修改后的账户" || status != domain.DNSAccountDisabled || verification != domain.DNSAccountVerified || revision != 8 {
|
||||
t.Fatal("重复导入覆盖了现有账户修改")
|
||||
}
|
||||
conflict := item
|
||||
conflict.CredentialRef = "secret://dns/other"
|
||||
if err := repo.ImportLegacyDNSAccount(t.Context(), conflict); !errors.Is(err, ErrLegacyDNSAccountConflict) {
|
||||
t.Fatalf("固定旧账户标识冲突未拒绝:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkDNSAccountRepository(t *testing.T, db, admin *sql.DB, driver string) {
|
||||
t.Helper()
|
||||
repo := NewSQLProjectMemberRepository(db, driver)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user