|
| 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