Skip to content

Commit 6c2d3d0

Browse files
Merge pull request #2027 from tchap/sync-directory
OCPBUGS-33013: Add atomicdir.Sync function
2 parents d9058b4 + 0a7a574 commit 6c2d3d0

File tree

2 files changed

+520
-0
lines changed

2 files changed

+520
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 staging 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 staging directory needs to be explicitly specified. It is initially created using os.MkdirAll with targetDirPerm.
16+
// It is then populated using files with filePerm. Once the atomic swap is performed, the staging directory
17+
// (which is now the original target directory) is removed.
18+
func Sync(targetDir string, targetDirPerm os.FileMode, stagingDir string, files map[string][]byte, filePerm os.FileMode) error {
19+
return sync(&realFS, targetDir, targetDirPerm, stagingDir, files, filePerm)
20+
}
21+
22+
type fileSystem struct {
23+
MkdirAll func(path string, perm os.FileMode) 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+
RemoveAll: os.RemoveAll,
32+
WriteFile: os.WriteFile,
33+
SwapDirectories: swap,
34+
}
35+
36+
// sync prepares a tmp directory and writes all files into that directory.
37+
// Then it atomically swap the tmp directory for the target one.
38+
// This is currently implemented as really atomically swapping directories.
39+
//
40+
// The same goal of atomic swap could be implemented using symlinks much like AtomicWriter does in
41+
// https://github.com/kubernetes/kubernetes/blob/v1.34.0/pkg/volume/util/atomic_writer.go#L58
42+
// The reason we don't do that is that we already have a directory populated and watched that needs to we swapped.
43+
// In other words, it's for compatibility reasons. And if we were to migrate to the symlink approach,
44+
// we would anyway need to atomically turn the current data directory into a symlink.
45+
// This would all just increase complexity and require atomic swap on the OS level anyway.
46+
func sync(fs *fileSystem, targetDir string, targetDirPerm os.FileMode, stagingDir string, files map[string][]byte, filePerm os.FileMode) (retErr error) {
47+
klog.Infof("Ensuring target directory %q exists ...", targetDir)
48+
if err := fs.MkdirAll(targetDir, targetDirPerm); err != nil {
49+
return fmt.Errorf("failed creating target directory: %w", err)
50+
}
51+
52+
klog.Infof("Creating staging directory to swap for %q ...", targetDir)
53+
if err := fs.MkdirAll(stagingDir, targetDirPerm); err != nil {
54+
return fmt.Errorf("failed creating staging directory: %w", err)
55+
}
56+
defer func() {
57+
if err := fs.RemoveAll(stagingDir); err != nil {
58+
if retErr != nil {
59+
retErr = fmt.Errorf("failed removing staging directory %q: %w; previous error: %w", stagingDir, err, retErr)
60+
return
61+
}
62+
retErr = fmt.Errorf("failed removing staging directory %q: %w", stagingDir, err)
63+
}
64+
}()
65+
66+
for filename, content := range files {
67+
// Make sure filename is a plain filename, not a path.
68+
// This also ensures the staging directory cannot be escaped.
69+
if filename != filepath.Base(filename) {
70+
return fmt.Errorf("filename cannot be a path: %q", filename)
71+
}
72+
73+
fullFilename := filepath.Join(stagingDir, filename)
74+
klog.Infof("Writing file %q ...", fullFilename)
75+
76+
if err := fs.WriteFile(fullFilename, content, filePerm); err != nil {
77+
return fmt.Errorf("failed writing %q: %w", fullFilename, err)
78+
}
79+
}
80+
81+
klog.Infof("Atomically swapping target directory %q with staging directory %q ...", targetDir, stagingDir)
82+
if err := fs.SwapDirectories(targetDir, stagingDir); err != nil {
83+
return fmt.Errorf("failed swapping target directory %q with staging directory %q: %w", targetDir, stagingDir, err)
84+
}
85+
return
86+
}

0 commit comments

Comments
 (0)