清理证书部署成功后的旧备份
提交证书部署完成后的旧备份清理逻辑及对应测试,保留当前证书并对清理失败显式报错。
This commit is contained in:
parent
7adc163e8b
commit
f5ce3077d1
@ -11,6 +11,7 @@ type Client interface {
|
||||
Lock(ctx context.Context, path string) error
|
||||
Unlock(ctx context.Context, path string) error
|
||||
MkdirAll(ctx context.Context, path string) error
|
||||
List(ctx context.Context, path string) ([]string, error)
|
||||
Exists(ctx context.Context, path string) (bool, error)
|
||||
Upload(ctx context.Context, path string, data []byte, mode uint32) error
|
||||
Rename(ctx context.Context, source, target string) error
|
||||
|
||||
@ -178,8 +178,7 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
}
|
||||
backupID := s.backupID()
|
||||
result.BackupID = backupID
|
||||
type backup struct{ original, saved string }
|
||||
backups := make([]backup, 0, len(paths))
|
||||
backups := make([]backupFile, 0, len(paths))
|
||||
temporary := make([]string, 0, len(paths))
|
||||
installed := make([]string, 0, len(paths))
|
||||
rollback := func() error {
|
||||
@ -256,7 +255,7 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
if exists, err := client.Exists(ctx, backupPath); err != nil || exists {
|
||||
return failed(fmt.Sprintf("备份目标已存在或无法检查,已停止替换:%s:%v", backupPath, err))
|
||||
}
|
||||
backups = append(backups, backup{original: file.path, saved: backupPath})
|
||||
backups = append(backups, backupFile{original: file.path, saved: backupPath})
|
||||
if err := client.Rename(ctx, file.path, backupPath); err != nil {
|
||||
return failed(fmt.Sprintf("备份远程证书失败:%v", err))
|
||||
}
|
||||
@ -282,12 +281,50 @@ func (s *Service) deployOne(ctx context.Context, server Server, artifact Artifac
|
||||
}
|
||||
return failed(fmt.Sprintf("reload 失败:%v", err))
|
||||
}
|
||||
if err := cleanupBackups(ctx, client, backups, paths); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Message = fmt.Sprintf("\u8bc1\u4e66\u5df2\u90e8\u7f72\u5e76 reload \u6210\u529f\uff0c\u4f46\u65e7\u8bc1\u4e66\u5907\u4efd\u6e05\u7406\u5931\u8d25\uff1a%v", err)
|
||||
return result
|
||||
}
|
||||
result.Status = "success"
|
||||
result.Message = "证书部署、配置校验并 reload 成功"
|
||||
result.BackupID = backupID
|
||||
return result
|
||||
}
|
||||
|
||||
func cleanupBackups(ctx context.Context, client Client, backups []backupFile, paths []remoteFile) error {
|
||||
seen := make(map[string]struct{}, len(backups))
|
||||
for _, item := range backups {
|
||||
if _, exists := seen[item.saved]; exists {
|
||||
continue
|
||||
}
|
||||
seen[item.saved] = struct{}{}
|
||||
if err := client.Remove(ctx, item.saved); err != nil {
|
||||
return fmt.Errorf("\u5220\u9664\u672c\u6b21\u5907\u4efd %s \u5931\u8d25\uff1a%w", item.saved, err)
|
||||
}
|
||||
}
|
||||
for _, file := range paths {
|
||||
directory := parentPath(file.path)
|
||||
entries, err := client.List(ctx, directory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("\u5217\u51fa\u8bc1\u4e66\u76ee\u5f55 %s \u5931\u8d25\uff1a%w", directory, err)
|
||||
}
|
||||
prefix := file.path + ".scert-backup-"
|
||||
for _, entry := range entries {
|
||||
if !strings.HasPrefix(entry, prefix) {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[entry]; exists {
|
||||
continue
|
||||
}
|
||||
seen[entry] = struct{}{}
|
||||
if err := client.Remove(ctx, entry); err != nil {
|
||||
return fmt.Errorf("\u5220\u9664\u5386\u53f2\u5907\u4efd %s \u5931\u8d25\uff1a%w", entry, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func reconnectDeploymentClient(ctx context.Context, factory ClientFactory, server Server, client *Client) error {
|
||||
if state, ok := (*client).(interface{ Disconnected() bool }); ok && state.Disconnected() {
|
||||
_ = (*client).Close()
|
||||
@ -306,6 +343,11 @@ type remoteFile struct {
|
||||
mode uint32
|
||||
}
|
||||
|
||||
type backupFile struct {
|
||||
original string
|
||||
saved string
|
||||
}
|
||||
|
||||
func validateDeploymentPaths(files []remoteFile) error {
|
||||
seen := make(map[string]struct{}, len(files))
|
||||
for _, file := range files {
|
||||
|
||||
@ -38,6 +38,35 @@ func newMemoryClient() *memoryClient {
|
||||
|
||||
func (c *memoryClient) MkdirAll(context.Context, string) error { return nil }
|
||||
|
||||
func (c *memoryClient) List(_ context.Context, directory string) ([]string, error) {
|
||||
|
||||
entries := make([]string, 0)
|
||||
|
||||
prefix := strings.TrimSuffix(directory, "/") + "/"
|
||||
|
||||
for filePath := range c.files {
|
||||
|
||||
if !strings.HasPrefix(filePath, prefix) {
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(filePath, prefix)
|
||||
|
||||
if name == "" || strings.Contains(name, "/") {
|
||||
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
entries = append(entries, filePath)
|
||||
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (c *memoryClient) Lock(_ context.Context, path string) error {
|
||||
if _, exists := c.files[path]; exists {
|
||||
return errors.New("模拟锁已存在")
|
||||
@ -118,12 +147,15 @@ func (f memoryFactory) Connect(_ context.Context, server Server) (Client, error)
|
||||
return f.clients[server.ID], nil
|
||||
}
|
||||
|
||||
func TestDeployReplacesFilesAndKeepsBackup(t *testing.T) {
|
||||
func TestDeployReplacesFilesAndCleansBackups(t *testing.T) {
|
||||
client := newMemoryClient()
|
||||
server := testServer("server-1")
|
||||
client.files[server.CertificatePath] = memoryFile{data: []byte("old-cert"), mode: 0o644}
|
||||
client.files[server.CertificateChainPath] = memoryFile{data: []byte("old-chain"), mode: 0o644}
|
||||
client.files[server.PrivateKeyPath] = memoryFile{data: []byte("old-key"), mode: 0o600}
|
||||
client.files[server.CertificatePath+".scert-backup-old-1"] = memoryFile{data: []byte("stale-cert"), mode: 0o644}
|
||||
client.files[server.CertificateChainPath+".scert-backup-old-1"] = memoryFile{data: []byte("stale-chain"), mode: 0o644}
|
||||
client.files[server.PrivateKeyPath+".scert-backup-old-1"] = memoryFile{data: []byte("stale-key"), mode: 0o600}
|
||||
service := NewService(memoryFactory{clients: map[string]*memoryClient{server.ID: client}})
|
||||
service.now = func() time.Time { return time.Date(2026, 8, 27, 1, 2, 3, 4, time.UTC) }
|
||||
artifact := testArtifact(t)
|
||||
@ -143,11 +175,29 @@ func TestDeployReplacesFilesAndKeepsBackup(t *testing.T) {
|
||||
backupCount++
|
||||
}
|
||||
}
|
||||
if backupCount != 3 {
|
||||
if backupCount != 0 {
|
||||
t.Fatalf("成功部署后应保留三份远程备份:%d", backupCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployReportsBackupCleanupFailure(t *testing.T) {
|
||||
client := newMemoryClient()
|
||||
client.removeError = ".scert-backup-"
|
||||
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(t))
|
||||
if report.Status != "failed" || report.Failed != 1 {
|
||||
t.Fatalf("backup cleanup failure result is wrong: %+v", report)
|
||||
}
|
||||
if !strings.Contains(report.Results[0].Message, "\u65e7\u8bc1\u4e66\u5907\u4efd\u6e05\u7406\u5931\u8d25") {
|
||||
t.Fatalf("backup cleanup failure was not reported: %s", report.Results[0].Message)
|
||||
}
|
||||
if string(client.files[server.CertificatePath].data) == "old-cert" {
|
||||
t.Fatal("backup cleanup failure must not silently report success")
|
||||
}
|
||||
}
|
||||
func TestDeployRollsBackWhenReloadFails(t *testing.T) {
|
||||
client := newMemoryClient()
|
||||
client.runResults = []CommandResult{{ExitCode: 0}, {ExitCode: 1}}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@ -252,6 +253,20 @@ func (c *sshClientAdapter) MkdirAll(ctx context.Context, path string) error {
|
||||
return c.fileOperation(ctx, func() error { return c.sftp.MkdirAll(path) })
|
||||
}
|
||||
|
||||
func (c *sshClientAdapter) List(ctx context.Context, directory string) ([]string, error) {
|
||||
return sshOperation(ctx, c, func() ([]string, error) {
|
||||
entries, err := c.sftp.ReadDir(directory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
paths = append(paths, path.Join(directory, entry.Name()))
|
||||
}
|
||||
return paths, nil
|
||||
})
|
||||
}
|
||||
|
||||
// 远程原子建目录用于协作锁,跨进程共享;已有锁绝不自动抢占或清理。
|
||||
func (c *sshClientAdapter) Lock(ctx context.Context, path string) error {
|
||||
return c.fileOperation(ctx, func() error { return c.sftp.Mkdir(path) })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user