diff --git a/pkg/config/config.go b/pkg/config/config.go index 94ca58bcd..2095230e2 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1249,28 +1249,6 @@ func ReadCustomConfig() (*Config, error) { return newConfig, nil } -// Write writes the configuration to the default file -func (c *Config) Write() error { - var err error - path, err := customConfigFile() - if err != nil { - return err - } - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - configFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) - if err != nil { - return err - } - defer configFile.Close() - enc := toml.NewEncoder(configFile) - if err := enc.Encode(c); err != nil { - return err - } - return nil -} - // Reload clean the cached config and reloads the configuration from containers.conf files // This function is meant to be used for long-running processes that need to reload potential changes made to // the cached containers.conf files. diff --git a/pkg/config/config_unix.go b/pkg/config/config_unix.go new file mode 100644 index 000000000..d14c12125 --- /dev/null +++ b/pkg/config/config_unix.go @@ -0,0 +1,33 @@ +//go:build !windows +// +build !windows + +package config + +import ( + "os" + "path/filepath" + + "github.com/BurntSushi/toml" +) + +// Write writes the configuration to the default file +func (c *Config) Write() error { + var err error + path, err := customConfigFile() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + configFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) + if err != nil { + return err + } + defer configFile.Close() + enc := toml.NewEncoder(configFile) + if err := enc.Encode(c); err != nil { + return err + } + return nil +} diff --git a/pkg/config/config_windows.go b/pkg/config/config_windows.go index b5838072e..fb3487d8f 100644 --- a/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -1,6 +1,16 @@ +//go:build windows +// +build windows + package config -import "os" +import ( + "io" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" + "github.com/containers/common/pkg/util" +) const ( // OverrideContainersConfig holds the default config path overridden by the root user @@ -29,3 +39,20 @@ func ifRootlessConfigPath() (string, error) { var defaultHelperBinariesDir = []string{ "C:\\Program Files\\RedHat\\Podman", } + +// Write writes the configuration to the default file +func (c *Config) Write() error { + var err error + path, err := customConfigFile() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + + return util.WriteFilePseudoAtomic(path, 0o644, func(writer io.Writer) error { + enc := toml.NewEncoder(writer) + return enc.Encode(c) + }) +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 98890a686..1a3527993 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -1,6 +1,11 @@ package util -import "regexp" +import ( + "io" + "os" + "path/filepath" + "regexp" +) // StringInSlice determines if a string is in a string slice, returns bool func StringInSlice(s string, sl []string) bool { @@ -22,3 +27,56 @@ func StringMatchRegexSlice(s string, re []string) bool { } return false } + +func pathNoExt(path string) string { + l := len(filepath.Ext(path)) + return path[:len(path)-l] +} + +// WriteFilePseudoAtomic writes a file in a loosely atomic manner. It does not +// perform cooperative fs locking, or even application level locking. It simply +// ensures that a written file is fully flushed to disk before its content is +// published under the specified path name. Concurrent calls will overwrite +// the same resulting path. +func WriteFilePseudoAtomic(path string, mode os.FileMode, writeOp func(io.Writer) error) error { + tmpPattern := pathNoExt(filepath.Base(path)) + "-*.tmp" + + // Create a unique tmp name in the same directory as the target file + // Note: We don't use O_SYNC since golang doesn't support it for Windows + // And even if it did use FILE_FLAG_WRITE_THROUGH, Windows IDE and SATA + // drives will ignore the corresponding Force Unit Access flag, so we need + // to manually flush the file instead + tmpFile, err := os.CreateTemp(filepath.Dir(path), tmpPattern) + if err != nil { + return err + } + defer tmpFile.Close() + + if err := tmpFile.Chmod(mode); err != nil { + return err + } + + // Perform caller provided write operation + if err := writeOp(tmpFile); err != nil { + return err + } + + // Ensure file is flushed to disk before renaming + // Uses fsync() on Linux/Unix, fcntl(F_FULLFSYNC) on Darwin, and + // FlushFileBuffers on Windows, all of which ensure state is written to + // media (excluding bugs and bad drivers) + if err := tmpFile.Sync(); err != nil { + return err + } + + // Eager cleanup is necessary on Windows (and harmless on other OSs) + // to prevent a lock being held + if err := tmpFile.Close(); err != nil { + return err + } + + // os.Rename is atomic on Linux/Unix + // Triggers MoveFileEx with MOVEFILE_REPLACE_EXISTING, ensuring the change + // is a metadata write (since tmp file is on the same volume) + return os.Rename(tmpFile.Name(), path) +}