完善远程部署回滚错误处理
This commit is contained in:
parent
e2b75d29ea
commit
0efeebfb88
@ -35,6 +35,8 @@ type Result struct {
|
||||
BackupID string `json:"backup_id,omitempty"`
|
||||
}
|
||||
|
||||
const deploymentRollbackTimeout = 30 * time.Second
|
||||
|
||||
type Report struct {
|
||||
Total int `json:"total"`
|
||||
Success int `json:"success"`
|
||||
@ -135,50 +137,57 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
backups := make([]backup, 0, len(paths))
|
||||
temporary := make([]string, 0, len(paths))
|
||||
installed := make([]string, 0, len(paths))
|
||||
rollback := func() {
|
||||
rollback := func() error {
|
||||
rollbackContext, cancel := context.WithTimeout(context.WithoutCancel(ctx), deploymentRollbackTimeout)
|
||||
defer cancel()
|
||||
var rollbackErrors []error
|
||||
for _, path := range temporary {
|
||||
_ = client.Remove(context.Background(), path)
|
||||
if err := client.Remove(rollbackContext, path); err != nil {
|
||||
rollbackErrors = append(rollbackErrors, fmt.Errorf("删除临时文件 %s 失败:%w", path, err))
|
||||
}
|
||||
}
|
||||
for _, path := range installed {
|
||||
_ = client.Remove(context.Background(), path)
|
||||
if err := client.Remove(rollbackContext, path); err != nil {
|
||||
rollbackErrors = append(rollbackErrors, fmt.Errorf("删除新证书文件 %s 失败:%w", path, err))
|
||||
}
|
||||
}
|
||||
for index := len(backups) - 1; index >= 0; index-- {
|
||||
item := backups[index]
|
||||
_ = client.Rename(context.Background(), item.saved, item.original)
|
||||
if err := client.Rename(rollbackContext, item.saved, item.original); err != nil {
|
||||
rollbackErrors = append(rollbackErrors, fmt.Errorf("恢复备份文件 %s 失败:%w", item.original, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(rollbackErrors...)
|
||||
}
|
||||
failed := func(message string) Result {
|
||||
if rollbackErr := rollback(); rollbackErr != nil {
|
||||
message += fmt.Sprintf(";自动回滚失败:%v", rollbackErr)
|
||||
}
|
||||
result.Message = message
|
||||
return result
|
||||
}
|
||||
for _, file := range paths {
|
||||
if err := client.MkdirAll(ctx, parentPath(file.path)); err != nil {
|
||||
result.Message = fmt.Sprintf("创建远程目录失败:%v", err)
|
||||
rollback()
|
||||
return result
|
||||
return failed(fmt.Sprintf("创建远程目录失败:%v", err))
|
||||
}
|
||||
exists, err := client.Exists(ctx, file.path)
|
||||
if err != nil {
|
||||
result.Message = fmt.Sprintf("检查远程文件失败:%v", err)
|
||||
rollback()
|
||||
return result
|
||||
return failed(fmt.Sprintf("检查远程文件失败:%v", err))
|
||||
}
|
||||
if exists {
|
||||
backupPath := file.path + ".scert-backup-" + backupID
|
||||
if err := client.Rename(ctx, file.path, backupPath); err != nil {
|
||||
result.Message = fmt.Sprintf("备份远程证书失败:%v", err)
|
||||
rollback()
|
||||
return result
|
||||
return failed(fmt.Sprintf("备份远程证书失败:%v", err))
|
||||
}
|
||||
backups = append(backups, backup{original: file.path, saved: backupPath})
|
||||
}
|
||||
temporaryPath := file.path + ".scert-tmp-" + backupID
|
||||
if err := client.Upload(ctx, temporaryPath, file.data, file.mode); err != nil {
|
||||
result.Message = fmt.Sprintf("上传远程证书失败:%v", err)
|
||||
rollback()
|
||||
return result
|
||||
return failed(fmt.Sprintf("上传远程证书失败:%v", err))
|
||||
}
|
||||
temporary = append(temporary, temporaryPath)
|
||||
if err := client.Rename(ctx, temporaryPath, file.path); err != nil {
|
||||
result.Message = fmt.Sprintf("替换远程证书失败:%v", err)
|
||||
rollback()
|
||||
return result
|
||||
return failed(fmt.Sprintf("替换远程证书失败:%v", err))
|
||||
}
|
||||
temporary = temporary[:len(temporary)-1]
|
||||
installed = append(installed, file.path)
|
||||
@ -188,9 +197,7 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
if err == nil {
|
||||
err = fmt.Errorf("退出码 %d", commandResult.ExitCode)
|
||||
}
|
||||
result.Message = fmt.Sprintf("reload 失败:%v", err)
|
||||
rollback()
|
||||
return result
|
||||
return failed(fmt.Sprintf("reload 失败:%v", err))
|
||||
}
|
||||
result.Status = "success"
|
||||
result.Message = "证书部署并 reload 成功"
|
||||
|
||||
@ -14,11 +14,14 @@ type memoryFile struct {
|
||||
}
|
||||
|
||||
type memoryClient struct {
|
||||
files map[string]memoryFile
|
||||
command []string
|
||||
runResult CommandResult
|
||||
runErr error
|
||||
uploadError string
|
||||
files map[string]memoryFile
|
||||
command []string
|
||||
runResult CommandResult
|
||||
runErr error
|
||||
uploadError string
|
||||
removeError string
|
||||
renameError string
|
||||
restoreError bool
|
||||
}
|
||||
|
||||
func newMemoryClient() *memoryClient {
|
||||
@ -41,6 +44,12 @@ func (c *memoryClient) Upload(_ context.Context, path string, data []byte, mode
|
||||
}
|
||||
|
||||
func (c *memoryClient) Rename(_ context.Context, source, target string) error {
|
||||
if c.renameError != "" && (strings.Contains(source, c.renameError) || strings.Contains(target, c.renameError)) {
|
||||
return errors.New("模拟重命名失败")
|
||||
}
|
||||
if c.restoreError && strings.Contains(source, ".scert-backup-") && !strings.Contains(target, ".scert-backup-") {
|
||||
return errors.New("模拟恢复备份失败")
|
||||
}
|
||||
value, ok := c.files[source]
|
||||
if !ok {
|
||||
return errors.New("源文件不存在")
|
||||
@ -51,6 +60,9 @@ func (c *memoryClient) Rename(_ context.Context, source, target string) error {
|
||||
}
|
||||
|
||||
func (c *memoryClient) Remove(_ context.Context, path string) error {
|
||||
if c.removeError != "" && strings.Contains(path, c.removeError) {
|
||||
return errors.New("模拟删除失败")
|
||||
}
|
||||
delete(c.files, path)
|
||||
return nil
|
||||
}
|
||||
@ -126,6 +138,22 @@ func TestDeployRollsBackWhenReloadFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployReportsRollbackFailure(t *testing.T) {
|
||||
client := newMemoryClient()
|
||||
client.runResult.ExitCode = 1
|
||||
client.restoreError = true
|
||||
server := testServer("server-1")
|
||||
client.files[server.CertificatePath] = memoryFile{data: []byte("old-cert"), mode: 0o644}
|
||||
service := NewService(memoryFactory{clients: map[string]*memoryClient{server.ID: client}})
|
||||
report := service.Deploy(context.Background(), []Target{{Server: server}}, testArtifact())
|
||||
if report.Status != "failed" || report.Failed != 1 {
|
||||
t.Fatalf("回滚失败结果错误:%+v", report)
|
||||
}
|
||||
if !strings.Contains(report.Results[0].Message, "自动回滚失败") || !strings.Contains(report.Results[0].Message, "恢复备份文件") {
|
||||
t.Fatalf("应报告自动回滚失败原因:%s", report.Results[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployReportsPartialSuccess(t *testing.T) {
|
||||
first := newMemoryClient()
|
||||
second := newMemoryClient()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user