完善远程部署回滚错误处理
This commit is contained in:
parent
e2b75d29ea
commit
0efeebfb88
@ -35,6 +35,8 @@ type Result struct {
|
|||||||
BackupID string `json:"backup_id,omitempty"`
|
BackupID string `json:"backup_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deploymentRollbackTimeout = 30 * time.Second
|
||||||
|
|
||||||
type Report struct {
|
type Report struct {
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
Success int `json:"success"`
|
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))
|
backups := make([]backup, 0, len(paths))
|
||||||
temporary := make([]string, 0, len(paths))
|
temporary := make([]string, 0, len(paths))
|
||||||
installed := 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 {
|
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 {
|
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-- {
|
for index := len(backups) - 1; index >= 0; index-- {
|
||||||
item := backups[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 {
|
for _, file := range paths {
|
||||||
if err := client.MkdirAll(ctx, parentPath(file.path)); err != nil {
|
if err := client.MkdirAll(ctx, parentPath(file.path)); err != nil {
|
||||||
result.Message = fmt.Sprintf("创建远程目录失败:%v", err)
|
return failed(fmt.Sprintf("创建远程目录失败:%v", err))
|
||||||
rollback()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
exists, err := client.Exists(ctx, file.path)
|
exists, err := client.Exists(ctx, file.path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Message = fmt.Sprintf("检查远程文件失败:%v", err)
|
return failed(fmt.Sprintf("检查远程文件失败:%v", err))
|
||||||
rollback()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
backupPath := file.path + ".scert-backup-" + backupID
|
backupPath := file.path + ".scert-backup-" + backupID
|
||||||
if err := client.Rename(ctx, file.path, backupPath); err != nil {
|
if err := client.Rename(ctx, file.path, backupPath); err != nil {
|
||||||
result.Message = fmt.Sprintf("备份远程证书失败:%v", err)
|
return failed(fmt.Sprintf("备份远程证书失败:%v", err))
|
||||||
rollback()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
backups = append(backups, backup{original: file.path, saved: backupPath})
|
backups = append(backups, backup{original: file.path, saved: backupPath})
|
||||||
}
|
}
|
||||||
temporaryPath := file.path + ".scert-tmp-" + backupID
|
temporaryPath := file.path + ".scert-tmp-" + backupID
|
||||||
if err := client.Upload(ctx, temporaryPath, file.data, file.mode); err != nil {
|
if err := client.Upload(ctx, temporaryPath, file.data, file.mode); err != nil {
|
||||||
result.Message = fmt.Sprintf("上传远程证书失败:%v", err)
|
return failed(fmt.Sprintf("上传远程证书失败:%v", err))
|
||||||
rollback()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
temporary = append(temporary, temporaryPath)
|
temporary = append(temporary, temporaryPath)
|
||||||
if err := client.Rename(ctx, temporaryPath, file.path); err != nil {
|
if err := client.Rename(ctx, temporaryPath, file.path); err != nil {
|
||||||
result.Message = fmt.Sprintf("替换远程证书失败:%v", err)
|
return failed(fmt.Sprintf("替换远程证书失败:%v", err))
|
||||||
rollback()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
temporary = temporary[:len(temporary)-1]
|
temporary = temporary[:len(temporary)-1]
|
||||||
installed = append(installed, file.path)
|
installed = append(installed, file.path)
|
||||||
@ -188,9 +197,7 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
err = fmt.Errorf("退出码 %d", commandResult.ExitCode)
|
err = fmt.Errorf("退出码 %d", commandResult.ExitCode)
|
||||||
}
|
}
|
||||||
result.Message = fmt.Sprintf("reload 失败:%v", err)
|
return failed(fmt.Sprintf("reload 失败:%v", err))
|
||||||
rollback()
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
result.Status = "success"
|
result.Status = "success"
|
||||||
result.Message = "证书部署并 reload 成功"
|
result.Message = "证书部署并 reload 成功"
|
||||||
|
|||||||
@ -14,11 +14,14 @@ type memoryFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type memoryClient struct {
|
type memoryClient struct {
|
||||||
files map[string]memoryFile
|
files map[string]memoryFile
|
||||||
command []string
|
command []string
|
||||||
runResult CommandResult
|
runResult CommandResult
|
||||||
runErr error
|
runErr error
|
||||||
uploadError string
|
uploadError string
|
||||||
|
removeError string
|
||||||
|
renameError string
|
||||||
|
restoreError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMemoryClient() *memoryClient {
|
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 {
|
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]
|
value, ok := c.files[source]
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("源文件不存在")
|
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 {
|
func (c *memoryClient) Remove(_ context.Context, path string) error {
|
||||||
|
if c.removeError != "" && strings.Contains(path, c.removeError) {
|
||||||
|
return errors.New("模拟删除失败")
|
||||||
|
}
|
||||||
delete(c.files, path)
|
delete(c.files, path)
|
||||||
return nil
|
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) {
|
func TestDeployReportsPartialSuccess(t *testing.T) {
|
||||||
first := newMemoryClient()
|
first := newMemoryClient()
|
||||||
second := newMemoryClient()
|
second := newMemoryClient()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user