完善证书恢复目录回滚
This commit is contained in:
parent
4402beeee1
commit
ca7c2786d8
@ -404,48 +404,113 @@ func removeDirectoryWithRollback(target, timestamp string) (func() error, error)
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return nil, errors.New("证书恢复目标必须是普通目录")
|
||||
}
|
||||
backupPath := target + ".before-restore-" + timestamp
|
||||
if err := os.Rename(target, backupPath); err != nil {
|
||||
return nil, fmt.Errorf("保留现有证书目录失败:%w", err)
|
||||
}
|
||||
return func() error { return os.Rename(backupPath, target) }, nil
|
||||
return restoreDirectoryContentsWithRollback("", target, timestamp)
|
||||
}
|
||||
|
||||
func restoreDirectoryWithRollback(source, target, timestamp string) (func() error, error) {
|
||||
if pathsOverlap(source, target) {
|
||||
return nil, errors.New("证书恢复源目录和目标目录不能互相包含")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
|
||||
return nil, fmt.Errorf("创建证书恢复父目录失败:%w", err)
|
||||
}
|
||||
backupPath := ""
|
||||
return restoreDirectoryContentsWithRollback(source, target, timestamp)
|
||||
}
|
||||
|
||||
func restoreDirectoryContentsWithRollback(source, target, timestamp string) (func() error, error) {
|
||||
createdTarget := false
|
||||
if info, err := os.Lstat(target); err == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
||||
return nil, errors.New("证书恢复目标必须是普通目录")
|
||||
}
|
||||
backupPath = target + ".before-restore-" + timestamp
|
||||
if err := os.Rename(target, backupPath); err != nil {
|
||||
return nil, fmt.Errorf("保留现有证书目录失败:%w", err)
|
||||
} else if errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o750); err != nil {
|
||||
return nil, fmt.Errorf("创建证书恢复父目录失败:%w", err)
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.Mkdir(target, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("创建证书恢复父目录失败:%w", err)
|
||||
}
|
||||
createdTarget = true
|
||||
} else {
|
||||
return nil, fmt.Errorf("检查证书恢复目标失败:%w", err)
|
||||
}
|
||||
if err := copyDirectoryExclusive(source, target); err != nil {
|
||||
_ = os.RemoveAll(target)
|
||||
if backupPath != "" {
|
||||
_ = os.Rename(backupPath, target)
|
||||
}
|
||||
return nil, fmt.Errorf("恢复证书目录失败:%w", err)
|
||||
|
||||
transaction, err := os.MkdirTemp(target, ".scert-restore-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保留现有证书目录失败:%w", err)
|
||||
}
|
||||
return func() error {
|
||||
if err := os.RemoveAll(target); err != nil {
|
||||
oldDirectory := filepath.Join(transaction, "old")
|
||||
if err := os.Mkdir(oldDirectory, 0o700); err != nil {
|
||||
_ = os.RemoveAll(transaction)
|
||||
return nil, fmt.Errorf("保留现有证书目录失败:%w", err)
|
||||
}
|
||||
|
||||
rollback := func() error {
|
||||
var rollbackErr error
|
||||
rollbackErr = errors.Join(rollbackErr, clearDirectoryChildren(target, filepath.Base(transaction)))
|
||||
rollbackErr = errors.Join(rollbackErr, moveDirectoryChildren(oldDirectory, target))
|
||||
if createdTarget {
|
||||
if removeErr := os.Remove(target); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
|
||||
rollbackErr = errors.Join(rollbackErr, removeErr)
|
||||
}
|
||||
}
|
||||
rollbackErr = errors.Join(rollbackErr, os.RemoveAll(transaction))
|
||||
return rollbackErr
|
||||
}
|
||||
|
||||
if err := moveDirectoryChildren(target, oldDirectory, filepath.Base(transaction)); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("保留现有证书目录失败:%w", err), rollback())
|
||||
}
|
||||
|
||||
if source != "" {
|
||||
newDirectory := filepath.Join(transaction, "new")
|
||||
if err := copyDirectoryExclusive(source, newDirectory); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("恢复证书目录失败:%w", err), rollback())
|
||||
}
|
||||
if err := moveDirectoryChildren(newDirectory, target); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("恢复证书目录失败:%w", err), rollback())
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(transaction); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("恢复证书目录失败:%w", err), rollback())
|
||||
}
|
||||
return func() error { return nil }, nil
|
||||
}
|
||||
|
||||
func moveDirectoryChildren(source, target string, excluded ...string) error {
|
||||
entries, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
excludedNames := make(map[string]struct{}, len(excluded))
|
||||
for _, name := range excluded {
|
||||
excludedNames[name] = struct{}{}
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if _, skip := excludedNames[entry.Name()]; skip {
|
||||
continue
|
||||
}
|
||||
if err := os.Rename(filepath.Join(source, entry.Name()), filepath.Join(target, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
if backupPath != "" {
|
||||
return os.Rename(backupPath, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clearDirectoryChildren(directory, excluded string) error {
|
||||
entries, err := os.ReadDir(directory)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
return err
|
||||
}
|
||||
var clearErr error
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == excluded {
|
||||
continue
|
||||
}
|
||||
clearErr = errors.Join(clearErr, os.RemoveAll(filepath.Join(directory, entry.Name())))
|
||||
}
|
||||
return clearErr
|
||||
}
|
||||
|
||||
func copyDirectoryExclusive(source, target string) error {
|
||||
|
||||
@ -180,7 +180,7 @@ func TestCreateBackupRequiresSecretsWhenInitialized(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreAdditionalAssetsKeepsRollbackCopies(t *testing.T) {
|
||||
func TestRestoreAdditionalAssetsPreservesCertificateRoot(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
cfg := config.Default(filepath.Join(base, "target"))
|
||||
input := filepath.Join(base, "backup")
|
||||
@ -209,6 +209,9 @@ func TestRestoreAdditionalAssetsKeepsRollbackCopies(t *testing.T) {
|
||||
if err := restoreAdditionalAssets(cfg, input); err != nil {
|
||||
t.Fatalf("恢复敏感配置和证书失败:%v", err)
|
||||
}
|
||||
if info, err := os.Stat(cfg.Paths.Certificate); err != nil || !info.IsDir() {
|
||||
t.Fatalf("恢复缺失状态失败:%v", err)
|
||||
}
|
||||
for path, expected := range map[string]string{
|
||||
filepath.Join(cfg.Paths.Config, "master.key"): "new-master",
|
||||
filepath.Join(cfg.Paths.Config, "secrets.json"): "new-secrets",
|
||||
@ -219,16 +222,21 @@ func TestRestoreAdditionalAssetsKeepsRollbackCopies(t *testing.T) {
|
||||
t.Fatalf("恢复结果不正确 %s:data=%q err=%v", path, data, err)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(cfg.Paths.Certificate, "old.example.com")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("备份中不存在的资产仍保留在当前路径:%s err=%v", filepath.Join(cfg.Paths.Certificate, "old.example.com"), err)
|
||||
}
|
||||
for _, pattern := range []string{
|
||||
filepath.Join(cfg.Paths.Config, "master.key.before-restore-*"),
|
||||
filepath.Join(cfg.Paths.Config, "secrets.json.before-restore-*"),
|
||||
cfg.Paths.Certificate + ".before-restore-*",
|
||||
} {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil || len(matches) != 1 {
|
||||
t.Fatalf("恢复前副本不正确 %s:matches=%v err=%v", pattern, matches, err)
|
||||
}
|
||||
}
|
||||
if matches, err := filepath.Glob(filepath.Join(cfg.Paths.Certificate, ".scert-restore-*")); err != nil || len(matches) != 0 {
|
||||
t.Fatalf("恢复前副本不正确 %s:matches=%v err=%v", filepath.Join(cfg.Paths.Certificate, ".scert-restore-*"), matches, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreAdditionalAssetsRestoresAbsentState(t *testing.T) {
|
||||
@ -259,22 +267,66 @@ func TestRestoreAdditionalAssetsRestoresAbsentState(t *testing.T) {
|
||||
for _, path := range []string{
|
||||
filepath.Join(cfg.Paths.Config, "master.key"),
|
||||
filepath.Join(cfg.Paths.Config, "secrets.json"),
|
||||
cfg.Paths.Certificate,
|
||||
filepath.Join(cfg.Paths.Certificate, "current.pem"),
|
||||
} {
|
||||
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("备份中不存在的资产仍保留在当前路径:%s err=%v", path, err)
|
||||
}
|
||||
}
|
||||
if info, err := os.Stat(cfg.Paths.Certificate); err != nil || !info.IsDir() {
|
||||
t.Fatalf("恢复缺失状态失败:%v", err)
|
||||
}
|
||||
for _, pattern := range []string{
|
||||
filepath.Join(cfg.Paths.Config, "master.key.before-restore-*"),
|
||||
filepath.Join(cfg.Paths.Config, "secrets.json.before-restore-*"),
|
||||
cfg.Paths.Certificate + ".before-restore-*",
|
||||
} {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil || len(matches) != 1 {
|
||||
t.Fatalf("缺失状态恢复未保留恢复前副本 %s:matches=%v err=%v", pattern, matches, err)
|
||||
}
|
||||
}
|
||||
if matches, err := filepath.Glob(filepath.Join(cfg.Paths.Certificate, ".scert-restore-*")); err != nil || len(matches) != 0 {
|
||||
t.Fatalf("恢复前副本不正确 %s:matches=%v err=%v", filepath.Join(cfg.Paths.Certificate, ".scert-restore-*"), matches, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreDirectoryWorksWhenCertificateParentIsNotWritable(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Windows 不提供 POSIX 目录权限语义")
|
||||
}
|
||||
base := t.TempDir()
|
||||
parent := filepath.Join(base, "certificate-parent")
|
||||
target := filepath.Join(parent, "ssl")
|
||||
source := filepath.Join(base, "restore-source")
|
||||
if err := os.MkdirAll(target, 0o750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(source, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(target, "old.pem"), []byte("old"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(source, "new.pem"), []byte("new"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(parent, 0o555); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(parent, 0o755)
|
||||
|
||||
if _, err := restoreDirectoryWithRollback(source, target, "permission"); err != nil {
|
||||
t.Fatalf("恢复敏感配置和证书失败:%v", err)
|
||||
}
|
||||
if data, err := os.ReadFile(filepath.Join(target, "new.pem")); err != nil || string(data) != "new" {
|
||||
t.Fatalf("恢复结果不正确 %s:data=%q err=%v", filepath.Join(target, "new.pem"), data, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(target, "old.pem")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("恢复缺失状态失败:%v", err)
|
||||
}
|
||||
if matches, err := filepath.Glob(filepath.Join(target, ".scert-restore-*")); err != nil || len(matches) != 0 {
|
||||
t.Fatalf("恢复缺失状态失败:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRestoreCompatibilityRejectsCrossDatabase(t *testing.T) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user