Skip to content

Commit 4bebc96

Browse files
committed
Add atomicdir.Sync function
The function can be used to atomically sync a directory with the desired state. This uses atomicdir.swap implemented earlier.
1 parent 339f6f0 commit 4bebc96

File tree

2 files changed

+430
-0
lines changed

2 files changed

+430
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package atomicdir
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"k8s.io/klog/v2"
9+
)
10+
11+
// Sync can be used to atomically synchronize target directory with the given file content map.
12+
// This is done by populating a temporary directory, then atomically swapping it with the target directory.
13+
// This effectively means that any extra files in the target directory are pruned.
14+
//
15+
// The first return value indicates whether the state has been synchronized.
16+
// This can be the case even though an error is returned since that can be related to cleaning up.
17+
func Sync(targetDir string, files map[string][]byte, filePerm os.FileMode) error {
18+
return sync(&realFS, targetDir, files, filePerm)
19+
}
20+
21+
type fileSystem struct {
22+
MkdirAll func(path string, perm os.FileMode) error
23+
MkdirTemp func(dir, pattern string) (string, error)
24+
RemoveAll func(path string) error
25+
WriteFile func(name string, data []byte, perm os.FileMode) error
26+
SwapDirectories func(dirA, dirB string) error
27+
}
28+
29+
var realFS = fileSystem{
30+
MkdirAll: os.MkdirAll,
31+
MkdirTemp: os.MkdirTemp,
32+
RemoveAll: os.RemoveAll,
33+
WriteFile: os.WriteFile,
34+
SwapDirectories: swap,
35+
}
36+
37+
// sync prepares a tmp directory and writes all files into that directory.
38+
// Then it atomically swap the tmp directory for the target one.
39+
// This is currently implemented as really atomically swapping directories.
40+
//
41+
// The same goal of atomic swap could be implemented using symlinks much like AtomicWriter does in
42+
// https://github.com/kubernetes/kubernetes/blob/v1.34.0/pkg/volume/util/atomic_writer.go#L58
43+
// The reason we don't do that is that we already have a directory populated and watched that needs to we swapped.
44+
// In other words, it's for compatibility reasons. And if we were to migrate to the symlink approach,
45+
// we would anyway need to atomically turn the current data directory into a symlink.
46+
// This would all just increase complexity and require atomic swap on the OS level anyway.
47+
func sync(fs *fileSystem, targetDir string, files map[string][]byte, filePerm os.FileMode) (retErr error) {
48+
klog.Infof("Ensuring content directory %q exists ...", targetDir)
49+
if err := fs.MkdirAll(targetDir, 0755); err != nil && !os.IsExist(err) {
50+
return fmt.Errorf("failed creating content directory: %w", err)
51+
}
52+
53+
klog.Infof("Creating temporary directory to swap for %q ...", targetDir)
54+
tmpDir, err := fs.MkdirTemp(filepath.Dir(targetDir), filepath.Base(targetDir)+"-*")
55+
if err != nil {
56+
return fmt.Errorf("failed creating temporary directory: %w", err)
57+
}
58+
defer func() {
59+
if err := fs.RemoveAll(tmpDir); err != nil {
60+
if retErr != nil {
61+
retErr = fmt.Errorf("failed removing temporary directory %q: %w; previous error: %w", tmpDir, err, retErr)
62+
}
63+
retErr = fmt.Errorf("failed removing temporary directory %q: %w", tmpDir, err)
64+
}
65+
}()
66+
67+
for filename, content := range files {
68+
fullFilename := filepath.Join(tmpDir, filename)
69+
klog.Infof("Writing file %q ...", fullFilename)
70+
71+
if err := fs.WriteFile(fullFilename, content, filePerm); err != nil {
72+
return fmt.Errorf("failed writing %q: %w", fullFilename, err)
73+
}
74+
}
75+
76+
klog.Infof("Atomically swapping target directory %q with temporary directory %q ...", targetDir, tmpDir)
77+
if err := fs.SwapDirectories(targetDir, tmpDir); err != nil {
78+
return fmt.Errorf("failed swapping target directory %q with temporary directory %q: %w", targetDir, tmpDir, err)
79+
}
80+
return
81+
}

0 commit comments

Comments
 (0)