完善首次部署恢复事务核验
保留首次部署新证书现场并在 HTTPS 核验成功后安全收尾,补充恢复场景测试。
This commit is contained in:
parent
7469a622b1
commit
0e579fe60a
@ -666,6 +666,14 @@ func (s *Service) recoverTransaction(ctx context.Context, record *TransactionRec
|
||||
return fmt.Errorf("连接服务器失败:%w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
if record.Phase == TransactionPhaseReloadAttempted && transactionTargetsWereAbsent(*record) && transactionFilesMatchExpected(recoveryContext, client, *record) {
|
||||
// 首次部署没有可恢复的旧文件,只能在目标文件和远端站点均确认本次证书后收尾。
|
||||
if err := s.recoverCommittedTransaction(recoveryContext, record, server, client); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("首次部署结果核验失败,保留现场等待人工核查:%w", err)
|
||||
}
|
||||
}
|
||||
if record.Phase == TransactionPhaseAcquiring || record.Phase == TransactionPhaseRollbackVerified || record.Phase == TransactionPhaseCommitVerified {
|
||||
return releaseTransactionLocks(recoveryContext, client, *record, true, true)
|
||||
}
|
||||
@ -746,6 +754,30 @@ func (s *Service) recoverCommittedTransaction(ctx context.Context, record *Trans
|
||||
return releaseTransactionLocks(ctx, client, *record, true, false)
|
||||
}
|
||||
|
||||
func transactionTargetsWereAbsent(record TransactionRecord) bool {
|
||||
if len(record.Backups) == 0 || len(record.Backups) != len(record.Paths) {
|
||||
return false
|
||||
}
|
||||
for _, backup := range record.Backups {
|
||||
if backup.OriginalExists {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func transactionFilesMatchExpected(ctx context.Context, client Client, record TransactionRecord) bool {
|
||||
if record.Evidence == nil || len(record.Paths) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, path := range record.Paths {
|
||||
expected := record.Evidence.Files[path]
|
||||
if expected == "" || checkFileDigest(ctx, client, path, expected) != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func restoreTransactionFiles(ctx context.Context, client Client, record TransactionRecord) error {
|
||||
if err := validateRollbackFiles(ctx, client, record); err != nil {
|
||||
return err
|
||||
|
||||
@ -213,3 +213,67 @@ func TestRecoverPendingAcquiringPreservesLocksWithoutOwner(t *testing.T) {
|
||||
t.Fatal("无归属锁必须保留事务等待人工核查")
|
||||
}
|
||||
}
|
||||
func TestRecoverPendingCommitsVerifiedFirstDeployment(t *testing.T) {
|
||||
server, client, record := recoveryPreflightFixture()
|
||||
for index := range record.Backups {
|
||||
record.Backups[index].OriginalExists = false
|
||||
record.Backups[index].OriginalDigest = ""
|
||||
delete(client.files, record.Backups[index].Saved)
|
||||
}
|
||||
journal := &memoryTransactionJournal{pending: []TransactionRecord{record}}
|
||||
service := NewServiceWithJournal(fixedClientFactory{client}, journal)
|
||||
service.VerifyRecoveryTarget = func(_ context.Context, _ Server, _ RecoveryBaseline, committed bool) error {
|
||||
if !committed {
|
||||
t.Fatal("首次部署现场应按新证书提交核验")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := service.RecoverPending(t.Context(), func(context.Context, string) (Server, error) { return server, nil }); err != nil {
|
||||
t.Fatalf("首次部署提交恢复失败:%v", err)
|
||||
}
|
||||
if len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusCommitted {
|
||||
t.Fatalf("首次部署提交状态不正确:%+v", journal.finished)
|
||||
}
|
||||
for index, target := range record.Paths {
|
||||
if _, ok := client.files[target+".scert-lock"]; ok {
|
||||
t.Fatalf("提交完成后不应残留部署锁:%s", target)
|
||||
}
|
||||
if _, ok := client.files[record.TemporaryPaths[index]]; ok {
|
||||
t.Fatalf("提交完成后不应残留临时文件:%s", record.TemporaryPaths[index])
|
||||
}
|
||||
if string(client.files[target].data) != "new" {
|
||||
t.Fatalf("提交完成后目标文件被错误修改:%s", target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoverPendingKeepsFirstDeploymentForManualReviewWhenVerificationFails(t *testing.T) {
|
||||
server, client, record := recoveryPreflightFixture()
|
||||
for index := range record.Backups {
|
||||
record.Backups[index].OriginalExists = false
|
||||
record.Backups[index].OriginalDigest = ""
|
||||
delete(client.files, record.Backups[index].Saved)
|
||||
}
|
||||
journal := &memoryTransactionJournal{pending: []TransactionRecord{record}}
|
||||
service := NewServiceWithJournal(fixedClientFactory{client}, journal)
|
||||
service.VerifyRecoveryTarget = func(context.Context, Server, RecoveryBaseline, bool) error {
|
||||
return errors.New("站点尚未确认新证书")
|
||||
}
|
||||
if err := service.RecoverPending(t.Context(), func(context.Context, string) (Server, error) { return server, nil }); err == nil {
|
||||
t.Fatal("首次部署站点未确认时必须保留人工核查状态")
|
||||
}
|
||||
if len(journal.finished) != 0 || len(journal.updated) != 1 || journal.updated[0].Status != TransactionStatusRecoveryRequired {
|
||||
t.Fatalf("失败核验状态不正确:%+v", journal)
|
||||
}
|
||||
for index, target := range record.Paths {
|
||||
if _, ok := client.files[target+".scert-lock"]; !ok {
|
||||
t.Fatalf("失败核验时不得释放部署锁:%s", target)
|
||||
}
|
||||
if string(client.files[target].data) != "new" {
|
||||
t.Fatalf("失败核验时不得删除新目标文件:%s", target)
|
||||
}
|
||||
if _, ok := client.files[record.TemporaryPaths[index]]; !ok {
|
||||
t.Fatalf("失败核验时应保留临时现场:%s", record.TemporaryPaths[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user