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