完善DNS临时凭据清理重试
在单个和批量DNS临时凭据删除失败时保留可重试会话,保持正式配置凭据保护语义,补充回归测试并更新整改清单。
This commit is contained in:
parent
debcc67fb7
commit
4ec81eecab
@ -243,7 +243,7 @@ func (m *DNSVerificationManager) Discard(token string) error {
|
||||
}
|
||||
now := m.now().UTC()
|
||||
m.mu.Lock()
|
||||
expiredRefs := m.clearExpiredLocked(now)
|
||||
expiredRefs := m.clearExpiredExceptLocked(now, token)
|
||||
session, ok := m.sessions[token]
|
||||
if ok {
|
||||
delete(m.sessions, token)
|
||||
@ -254,6 +254,7 @@ func (m *DNSVerificationManager) Discard(token string) error {
|
||||
return errors.New("DNS 验证令牌无效或已经过期")
|
||||
}
|
||||
if err := m.deleteCredential(session.CredentialRef); err != nil {
|
||||
m.restoreSession(token, session)
|
||||
m.logCleanupFailure()
|
||||
return errors.New("DNS 临时凭据回收失败")
|
||||
}
|
||||
@ -277,15 +278,21 @@ func (m *DNSVerificationManager) DiscardAllExcept(protectedReferences ...string)
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
references := make([]string, 0, len(m.sessions))
|
||||
sessions := make(map[string]dnsVerificationSession, len(m.sessions))
|
||||
for token, session := range m.sessions {
|
||||
delete(m.sessions, token)
|
||||
if reference := strings.TrimSpace(session.CredentialRef); reference != "" && !protected[reference] {
|
||||
references = append(references, reference)
|
||||
if reference := strings.TrimSpace(session.CredentialRef); reference != "" && protected[reference] {
|
||||
continue
|
||||
}
|
||||
sessions[token] = session
|
||||
}
|
||||
m.mu.Unlock()
|
||||
m.deleteCredentials(references)
|
||||
for token, session := range sessions {
|
||||
if err := m.deleteCredential(session.CredentialRef); err != nil {
|
||||
m.restoreSession(token, session)
|
||||
m.logCleanupFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) validateSession(token, providerID, credentialRef, testDomain string, consume bool) error {
|
||||
@ -378,8 +385,15 @@ func (m *DNSVerificationManager) releaseVerificationSlot() {
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) clearExpiredLocked(now time.Time) []string {
|
||||
return m.clearExpiredExceptLocked(now, "")
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) clearExpiredExceptLocked(now time.Time, excludedToken string) []string {
|
||||
var references []string
|
||||
for token, session := range m.sessions {
|
||||
if token == excludedToken {
|
||||
continue
|
||||
}
|
||||
if !session.ExpiresAt.After(now) {
|
||||
delete(m.sessions, token)
|
||||
references = append(references, session.CredentialRef)
|
||||
@ -388,6 +402,17 @@ func (m *DNSVerificationManager) clearExpiredLocked(now time.Time) []string {
|
||||
return references
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) restoreSession(token string, session dnsVerificationSession) {
|
||||
if m == nil || strings.TrimSpace(token) == "" {
|
||||
return
|
||||
}
|
||||
m.mu.Lock()
|
||||
if _, exists := m.sessions[token]; !exists {
|
||||
m.sessions[token] = session
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *DNSVerificationManager) deleteCredentials(references []string) {
|
||||
for _, reference := range references {
|
||||
if err := m.deleteCredential(reference); err != nil {
|
||||
|
||||
@ -61,6 +61,11 @@ type cancellingSecretWriter struct {
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type retryableSecretWriter struct {
|
||||
memorySecretWriter
|
||||
failDelete bool
|
||||
}
|
||||
|
||||
func (s *cancellingSecretWriter) Put(ctx context.Context, reference string, value map[string]string) error {
|
||||
if err := s.memorySecretWriter.Put(ctx, reference, value); err != nil {
|
||||
return err
|
||||
@ -69,6 +74,13 @@ func (s *cancellingSecretWriter) Put(ctx context.Context, reference string, valu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *retryableSecretWriter) Delete(ctx context.Context, reference string) error {
|
||||
if s.failDelete {
|
||||
return errors.New("delete failed")
|
||||
}
|
||||
return s.memorySecretWriter.Delete(ctx, reference)
|
||||
}
|
||||
|
||||
func (m *memorySecretWriter) Put(_ context.Context, reference string, value map[string]string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@ -320,6 +332,58 @@ func TestDNSVerificationManagerCleansCredentialWhenContextCancelsAfterStore(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSVerificationManagerRetainsTokenWhenDiscardCleanupFails(t *testing.T) {
|
||||
store := &retryableSecretWriter{failDelete: true}
|
||||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||||
result, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||||
Credentials: map[string]string{"api_token": "secret"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := manager.Discard(result.VerificationToken); err == nil {
|
||||
t.Fatal("临时凭据回收失败时应返回错误")
|
||||
}
|
||||
if _, err := manager.Resolve(result.VerificationToken); err != nil {
|
||||
t.Fatalf("回收失败后应保留验证令牌以便重试:%v", err)
|
||||
}
|
||||
if len(store.values) != 1 {
|
||||
t.Fatalf("回收失败后不应丢失临时凭据:%v", store.values)
|
||||
}
|
||||
|
||||
store.failDelete = false
|
||||
if err := manager.Discard(result.VerificationToken); err != nil {
|
||||
t.Fatalf("临时凭据回收重试失败:%v", err)
|
||||
}
|
||||
if len(store.values) != 0 {
|
||||
t.Fatalf("回收重试后仍有临时凭据:%v", store.values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSVerificationManagerRetriesDiscardAllCleanupFailures(t *testing.T) {
|
||||
store := &retryableSecretWriter{failDelete: true}
|
||||
manager := NewDNSVerificationManager(fakeDNSTester{}, store)
|
||||
for range 2 {
|
||||
if _, err := manager.Verify(t.Context(), DNSVerificationInput{
|
||||
ProviderID: "cloudflare", TestDomain: "example.com",
|
||||
Credentials: map[string]string{"api_token": "secret"},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
manager.DiscardAll()
|
||||
if len(manager.sessions) != 2 || len(store.values) != 2 {
|
||||
t.Fatalf("批量回收失败后应保留可重试会话:sessions=%d values=%d", len(manager.sessions), len(store.values))
|
||||
}
|
||||
store.failDelete = false
|
||||
manager.DiscardAll()
|
||||
if len(manager.sessions) != 0 || len(store.values) != 0 {
|
||||
t.Fatalf("批量回收重试后仍有临时状态:sessions=%d values=%d", len(manager.sessions), len(store.values))
|
||||
}
|
||||
}
|
||||
|
||||
type countingDNSTester struct {
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
|
||||
@ -3758,3 +3758,14 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
|
||||
- [~] 进行中:DNS 清理失败与初始化回滚矩阵、其他 P0-10 任务链、跨数据库实机验证和统一复审。
|
||||
- [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。
|
||||
- [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。
|
||||
|
||||
## 本轮开发进度(2026-09-30,P0-10 DNS 临时凭据清理失败可重试)
|
||||
|
||||
总体验收继续保持 **P0-03:完成,待复审**,本轮继续先开发、后统一测试。
|
||||
|
||||
- [x] 已完成,等待复审:单个 DNS 临时凭据回收失败时保留验证会话,下一次清理可以重试。
|
||||
- [x] 已完成,等待复审:批量 DNS 临时凭据回收失败时仅恢复失败会话,已成功回收的会话不重复恢复。
|
||||
- [x] 已完成:新增清理失败重试回归测试;后端全量测试、静态检查和差异检查均通过。
|
||||
- [~] 进行中:DNS 过期清理与初始化回滚矩阵、其他 P0-10 任务链、跨数据库实机验证和统一复审。
|
||||
- [ ] 未开始:P0-12、真实数据库清理初始化、密码 SSH、远端多服务器部署及最终生产门禁。
|
||||
- [!] 不得标记 `[✓]`,总体验收仍为 **P0-03:完成,待复审**。
|
||||
|
||||
@ -1063,3 +1063,14 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全
|
||||
- [~] 继续执行:DNS 清理失败、令牌生命周期、初始化回滚、任务链、三数据库实机矩阵和统一复审。
|
||||
- [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
|
||||
- [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。
|
||||
|
||||
## 本轮执行记录(2026-09-30,P0-10 DNS 临时凭据清理失败可重试)
|
||||
|
||||
本轮按“先开发、后统一测试”执行,总体验收仍保持 **P0-03:完成,待复审**。
|
||||
|
||||
- [x] 单个 DNS 临时凭据回收失败后保留验证令牌,修复为可重试清理。
|
||||
- [x] 批量 DNS 临时凭据回收失败后仅恢复失败会话,受保护正式凭据仍不受影响。
|
||||
- [x] `go test ./...`、`go vet ./...`、`git diff --check` 已通过;本轮仅完成本地开发与自动化回归。
|
||||
- [~] 继续执行:DNS 过期清理、初始化回滚、令牌生命周期、任务链、三数据库实机矩阵和统一复审。
|
||||
- [ ] 尚未执行:P0-12、真实生产数据库清理、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
|
||||
- [!] 不新增 `[✓]`;总体验收继续为 **P0-03:完成,待复审**。
|
||||
|
||||
@ -1711,3 +1711,14 @@ P0-11 生产验收门禁
|
||||
- [~] 进行中:P0-10 DNS 清理失败、令牌消费、任务链、跨数据库实机矩阵、崩溃恢复、重试幂等和统一复审仍需继续。
|
||||
- [ ] 未开始:P0-12 备份恢复、浏览器清缓存、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁仍未完成。
|
||||
- [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。
|
||||
|
||||
## 本轮开发进度(2026-09-30,P0-10 DNS 临时凭据清理失败可重试)
|
||||
|
||||
总体验收位置继续保持 **P0-03:完成,待复审**;本轮仅修复 DNS 临时凭据删除失败后的重试边界,不代表 P0-10 统一复审通过或生产部署验收通过。
|
||||
|
||||
- [x] 已完成,等待复审:单个 DNS 验证会话清理失败时保留令牌和临时凭据,后续可再次调用清理,不再形成不可恢复的残留状态。
|
||||
- [x] 已完成,等待复审:批量清理对删除失败的会话逐项恢复,成功删除的会话不恢复;受保护正式凭据仍只保留正式配置,不保留验证令牌。
|
||||
- [x] 已完成:新增单个和批量清理失败重试回归测试;后端 `go test ./...`、`go vet ./...` 和 `git diff --check` 通过。
|
||||
- [~] 进行中:DNS 过期清理、初始化回滚、令牌消费、其他 P0-10 任务链、跨数据库实机矩阵和统一复审。
|
||||
- [ ] 未开始:P0-12 备份恢复、密码 SSH、远端部署、真实 HTTPS/DNS/ACME 和最终生产门禁。
|
||||
- [!] 总体验收不得升级:仍为 **P0-03:完成,待复审**,本轮没有新增 `[✓]` 复审通过项。
|
||||
|
||||
@ -637,3 +637,13 @@ P0-06 与 P0-07 均维持 `[~]`,六阶段中的隔离、生产和最终验收
|
||||
- [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。
|
||||
- [~] DNS 清理失败、初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。
|
||||
- [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。
|
||||
|
||||
## 本轮规格执行记录(2026-09-30,P0-10 DNS 临时凭据清理失败可重试)
|
||||
|
||||
本轮仅补齐 DNS 临时凭据清理失败后的可重试要求,总体验收继续保持 **P0-03:完成,待复审**。
|
||||
|
||||
- [x] 单个临时凭据删除失败时必须保留验证会话,允许后续重试,不能静默丢失清理入口。
|
||||
- [x] 批量删除失败时必须逐项恢复失败会话,成功删除的临时凭据不得恢复;正式配置依赖的凭据不得删除。
|
||||
- [x] 已新增对应回归测试,并通过后端全量测试、静态检查和差异检查。
|
||||
- [~] DNS 过期清理、初始化回滚、令牌消费及 P0-10 其他任务链继续开发和统一复审。
|
||||
- [!] 本地自动化结果不得替代真实 DNS/ACME、密码 SSH、远端多服务器部署和最终生产门禁;不得新增 `[✓]`。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user