Scert/backend/internal/deployment/recovery_reconciliation.go

114 lines
5.0 KiB
Go

package deployment
import (
"context"
"encoding/hex"
"time"
)
func isRecoveryVerification(action RecoveryAction) bool {
return action == RecoveryActionVerifyCommit || action == RecoveryActionVerifyRollback
}
// Reconciles 仅允许已持久化结束的不确定原动作;不接管运行中处置或再次执行文件动作。
func (i RecoveryDispositionIntent) Reconciles(source RecoveryDisposition) bool {
if i.SourceDispositionID == "" || i.SourceDispositionID != source.Intent.ID || source.Intent.Validate() != nil ||
source.Status != RecoveryResultUncertain || source.ResultCode != RecoveryResultUncertain ||
i.TransactionID != source.Intent.TransactionID || i.ServerID != source.Intent.ServerID || i.ServerDigest != source.Intent.ServerDigest {
return false
}
return (i.Action == RecoveryActionVerifyCommit && source.Intent.Action == RecoveryActionCommit) ||
(i.Action == RecoveryActionVerifyRollback && source.Intent.Action == RecoveryActionRollback)
}
// PrepareRecoveryReconciliation 只供受保护本机通道调用,必须明确指定原处置编号并重新确认。
func (s *Service) PrepareRecoveryReconciliation(ctx context.Context, sourceID, operator string, resolve func(context.Context, string) (Server, error)) (RecoveryConfirmation, string, error) {
review, err := s.prepareRecoveryReconciliation(ctx, sourceID, operator, resolve)
if err != nil {
return RecoveryConfirmation{}, "", err
}
return s.newRecoveryConfirmation(ctx, review)
}
func (s *Service) prepareRecoveryReconciliation(ctx context.Context, sourceID, operator string, resolve func(context.Context, string) (Server, error)) (RecoveryReview, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if s == nil || !validDispositionID(sourceID) || !validReviewOperator(operator) {
return RecoveryReview{}, ErrRecoveryReviewInvalid
}
journal, ok := s.journal.(RecoveryDispositionJournal)
if !ok {
return RecoveryReview{}, ErrRecoveryDispositionUnavailable
}
source, err := journal.GetRecoveryDisposition(ctx, sourceID)
if err != nil || source.Intent.ID != sourceID || source.Intent.Validate() != nil || source.Summary().Validate() != nil ||
source.Status != RecoveryResultUncertain || source.ResultCode != RecoveryResultUncertain {
return RecoveryReview{}, ErrRecoveryActionRejected
}
var action RecoveryAction
switch source.Intent.Action {
case RecoveryActionCommit:
action = RecoveryActionVerifyCommit
case RecoveryActionRollback:
action = RecoveryActionVerifyRollback
default:
return RecoveryReview{}, ErrRecoveryActionRejected
}
review, err := s.prepareRecoveryReview(ctx, source.Intent.TransactionID, operator, action, resolve)
if err != nil {
return RecoveryReview{}, err
}
review.summary.SourceDispositionID = sourceID
intent := RecoveryDispositionIntent{SourceDispositionID: sourceID, TransactionID: review.summary.TransactionID,
ServerID: review.summary.ServerID, ServerDigest: hex.EncodeToString(review.serverDigest[:]), Action: action}
if !intent.Reconciles(source) || !verificationPhase(review.summary.Phase, action) || s.VerifyRecoveryTarget == nil {
return RecoveryReview{}, ErrRecoveryActionRejected
}
return review, nil
}
func verificationPhase(phase string, action RecoveryAction) bool {
return (action == RecoveryActionVerifyCommit && phase == TransactionPhaseCommitVerified) ||
(action == RecoveryActionVerifyRollback && phase == TransactionPhaseRollbackVerified)
}
// validateRecoveryCompletion 不删除任何远端文件,残留锁或备份均要求继续保留占用。
func validateRecoveryCompletion(ctx context.Context, client Client, record TransactionRecord, committed bool) error {
for _, lock := range record.LockPaths {
if exists, err := client.Exists(ctx, lock); err != nil || exists {
return ErrRecoveryActionRejected
}
}
for _, backup := range record.Backups {
if committed {
if err := checkFileDigest(ctx, client, backup.Original, record.Evidence.Files[backup.Original]); err != nil {
return err
}
} else if backup.OriginalExists {
if err := checkFileDigest(ctx, client, backup.Original, backup.OriginalDigest); err != nil {
return err
}
} else if exists, err := client.Exists(ctx, backup.Original); err != nil || exists {
return ErrRecoveryActionRejected
}
for _, path := range []string{backup.Saved, backup.Original + ".scert-tmp-" + record.ID} {
if exists, err := client.Exists(ctx, path); err != nil || exists {
return ErrRecoveryActionRejected
}
}
}
return ctx.Err()
}
func (s *Service) verifyRecoveryCompletion(ctx context.Context, record TransactionRecord, server Server, client Client, action RecoveryAction) error {
committed := action == RecoveryActionVerifyCommit
if err := validateRecoveryCompletion(ctx, client, record, committed); err != nil {
return err
}
if err := s.VerifyRecoveryTarget(ctx, server, record.Evidence.Baseline, committed); err != nil {
return err
}
// HTTPS 检查结束后重新读取现场,避免检查期间发生的文件变化被忽略。
return validateRecoveryCompletion(ctx, client, record, committed)
}