package types import ( "errors" "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/BurntSushi/toml" cfg "github.com/containers/storage/pkg/config" "github.com/containers/storage/pkg/fileutils" "github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/unshare" "github.com/sirupsen/logrus" ) // TOML-friendly explicit tables used for conversions. type TomlConfig struct { Storage struct { Driver string `toml:"driver,omitempty"` DriverPriority []string `toml:"driver_priority,omitempty"` RunRoot string `toml:"runroot,omitempty"` ImageStore string `toml:"imagestore,omitempty"` GraphRoot string `toml:"graphroot,omitempty"` RootlessStoragePath string `toml:"rootless_storage_path,omitempty"` TransientStore bool `toml:"transient_store,omitempty"` Options cfg.OptionsConfig `toml:"options,omitempty"` } `toml:"storage"` } const ( overlayDriver = "overlay" overlay2 = "overlay2" storageConfEnv = "CONTAINERS_STORAGE_CONF" ) var ( defaultStoreOptionsOnce sync.Once loadDefaultStoreOptionsErr error once sync.Once storeOptions StoreOptions storeError error defaultConfigFileSet bool // defaultConfigFile path to the system wide storage.conf file defaultConfigFile = SystemConfigFile // DefaultStoreOptions is a reasonable default set of options. defaultStoreOptions StoreOptions ) func loadDefaultStoreOptions() { defaultStoreOptions.GraphDriverName = "" setDefaults := func() { // reload could set values to empty for run and graph root if config does not contains anything if defaultStoreOptions.RunRoot == "" { defaultStoreOptions.RunRoot = defaultRunRoot } if defaultStoreOptions.GraphRoot == "" { defaultStoreOptions.GraphRoot = defaultGraphRoot } } setDefaults() if path, ok := os.LookupEnv(storageConfEnv); ok { defaultOverrideConfigFile = path if err := ReloadConfigurationFileIfNeeded(path, &defaultStoreOptions); err != nil { loadDefaultStoreOptionsErr = err return } setDefaults() return } if path, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok { homeConfigFile := filepath.Join(path, "containers", "storage.conf") if err := fileutils.Exists(homeConfigFile); err == nil { // user storage.conf in XDG_CONFIG_HOME if it exists defaultOverrideConfigFile = homeConfigFile } else { if !os.IsNotExist(err) { loadDefaultStoreOptionsErr = err return } } } err := fileutils.Exists(defaultOverrideConfigFile) if err == nil { // The DefaultConfigFile() function returns the path // of the used storage.conf file, by returning defaultConfigFile // If override exists containers/storage uses it by default. defaultConfigFile = defaultOverrideConfigFile if err := ReloadConfigurationFileIfNeeded(defaultOverrideConfigFile, &defaultStoreOptions); err != nil { loadDefaultStoreOptionsErr = err return } setDefaults() return } if !os.IsNotExist(err) { logrus.Warningf("Attempting to use %s, %v", defaultConfigFile, err) } if err := ReloadConfigurationFileIfNeeded(defaultConfigFile, &defaultStoreOptions); err != nil && !errors.Is(err, os.ErrNotExist) { loadDefaultStoreOptionsErr = err return } setDefaults() } // loadStoreOptions returns the default storage ops for containers func loadStoreOptions() (StoreOptions, error) { storageConf, err := DefaultConfigFile() if err != nil { return defaultStoreOptions, err } return loadStoreOptionsFromConfFile(storageConf) } // usePerUserStorage returns whether the user private storage must be used. // We cannot simply use the unshare.IsRootless() condition, because // that checks only if the current process needs a user namespace to // work and it would break cases where the process is already created // in a user namespace (e.g. nested Podman/Buildah) and the desired // behavior is to use system paths instead of user private paths. func usePerUserStorage() bool { return unshare.IsRootless() && unshare.GetRootlessUID() != 0 } // loadStoreOptionsFromConfFile is an internal implementation detail of DefaultStoreOptions to allow testing. // Everyone but the tests this is intended for should only call loadStoreOptions, never this function. func loadStoreOptionsFromConfFile(storageConf string) (StoreOptions, error) { var ( defaultRootlessRunRoot string defaultRootlessGraphRoot string err error ) defaultStoreOptionsOnce.Do(loadDefaultStoreOptions) if loadDefaultStoreOptionsErr != nil { return StoreOptions{}, loadDefaultStoreOptionsErr } storageOpts := defaultStoreOptions if usePerUserStorage() { storageOpts, err = getRootlessStorageOpts(storageOpts) if err != nil { return storageOpts, err } } err = fileutils.Exists(storageConf) if err != nil && !os.IsNotExist(err) { return storageOpts, err } if err == nil && !defaultConfigFileSet { defaultRootlessRunRoot = storageOpts.RunRoot defaultRootlessGraphRoot = storageOpts.GraphRoot storageOpts = StoreOptions{} reloadConfigurationFileIfNeeded(storageConf, &storageOpts) if usePerUserStorage() { // If the file did not specify a graphroot or runroot, // set sane defaults so we don't try and use root-owned // directories if storageOpts.RunRoot == "" { storageOpts.RunRoot = defaultRootlessRunRoot } if storageOpts.GraphRoot == "" { if storageOpts.RootlessStoragePath != "" { storageOpts.GraphRoot = storageOpts.RootlessStoragePath } else { storageOpts.GraphRoot = defaultRootlessGraphRoot } } } } if storageOpts.RunRoot == "" { return storageOpts, fmt.Errorf("runroot must be set") } rootlessUID := unshare.GetRootlessUID() runRoot, err := expandEnvPath(storageOpts.RunRoot, rootlessUID) if err != nil { return storageOpts, err } storageOpts.RunRoot = runRoot if storageOpts.GraphRoot == "" { return storageOpts, fmt.Errorf("graphroot must be set") } graphRoot, err := expandEnvPath(storageOpts.GraphRoot, rootlessUID) if err != nil { return storageOpts, err } storageOpts.GraphRoot = graphRoot if storageOpts.RootlessStoragePath != "" { storagePath, err := expandEnvPath(storageOpts.RootlessStoragePath, rootlessUID) if err != nil { return storageOpts, err } storageOpts.RootlessStoragePath = storagePath } if storageOpts.ImageStore != "" && storageOpts.ImageStore == storageOpts.GraphRoot { return storageOpts, fmt.Errorf("imagestore %s must either be not set or be a different than graphroot", storageOpts.ImageStore) } return storageOpts, nil } // UpdateOptions should be called iff container engine received a SIGHUP, // otherwise use DefaultStoreOptions func UpdateStoreOptions() (StoreOptions, error) { storeOptions, storeError = loadStoreOptions() return storeOptions, storeError } // DefaultStoreOptions returns the default storage ops for containers func DefaultStoreOptions() (StoreOptions, error) { once.Do(func() { storeOptions, storeError = loadStoreOptions() }) return storeOptions, storeError } // StoreOptions is used for passing initialization options to GetStore(), for // initializing a Store object and the underlying storage that it controls. type StoreOptions struct { // RunRoot is the filesystem path under which we can store run-time // information, such as the locations of active mount points, that we // want to lose if the host is rebooted. RunRoot string `json:"runroot,omitempty"` // GraphRoot is the filesystem path under which we will store the // contents of layers, images, and containers. GraphRoot string `json:"root,omitempty"` // Image Store is the alternate location of image store if a location // separate from the container store is required. ImageStore string `json:"imagestore,omitempty"` // RootlessStoragePath is the storage path for rootless users // default $HOME/.local/share/containers/storage RootlessStoragePath string `toml:"rootless_storage_path"` // If the driver is not specified, the best suited driver will be picked // either from GraphDriverPriority, if specified, or from the platform // dependent priority list (in that order). GraphDriverName string `json:"driver,omitempty"` // GraphDriverPriority is a list of storage drivers that will be tried // to initialize the Store for a given RunRoot and GraphRoot unless a // GraphDriverName is set. // This list can be used to define a custom order in which the drivers // will be tried. GraphDriverPriority []string `json:"driver-priority,omitempty"` // GraphDriverOptions are driver-specific options. GraphDriverOptions []string `json:"driver-options,omitempty"` // UIDMap and GIDMap are used for setting up a container's root filesystem // for use inside of a user namespace where UID mapping is being used. UIDMap []idtools.IDMap `json:"uidmap,omitempty"` GIDMap []idtools.IDMap `json:"gidmap,omitempty"` // RootAutoNsUser is the user used to pick a subrange when automatically setting // a user namespace for the root user. RootAutoNsUser string `json:"root_auto_ns_user,omitempty"` // AutoNsMinSize is the minimum size for an automatic user namespace. AutoNsMinSize uint32 `json:"auto_userns_min_size,omitempty"` // AutoNsMaxSize is the maximum size for an automatic user namespace. AutoNsMaxSize uint32 `json:"auto_userns_max_size,omitempty"` // PullOptions specifies options to be handed to pull managers // This API is experimental and can be changed without bumping the major version number. PullOptions map[string]string `toml:"pull_options"` // DisableVolatile doesn't allow volatile mounts when it is set. DisableVolatile bool `json:"disable-volatile,omitempty"` // If transient, don't persist containers over boot (stores db in runroot) TransientStore bool `json:"transient_store,omitempty"` } // isRootlessDriver returns true if the given storage driver is valid for containers running as non root func isRootlessDriver(driver string) bool { validDrivers := map[string]bool{ "btrfs": true, "overlay": true, "overlay2": true, "vfs": true, } return validDrivers[driver] } // getRootlessStorageOpts returns the storage opts for containers running as non root func getRootlessStorageOpts(systemOpts StoreOptions) (StoreOptions, error) { var opts StoreOptions rootlessUID := unshare.GetRootlessUID() dataDir, err := homedir.GetDataHome() if err != nil { return opts, err } rootlessRuntime, err := homedir.GetRuntimeDir() if err != nil { return opts, err } opts.RunRoot = filepath.Join(rootlessRuntime, "containers") if err := os.MkdirAll(opts.RunRoot, 0o700); err != nil { return opts, fmt.Errorf("unable to make rootless runtime: %w", err) } opts.PullOptions = systemOpts.PullOptions if systemOpts.RootlessStoragePath != "" { opts.GraphRoot, err = expandEnvPath(systemOpts.RootlessStoragePath, rootlessUID) if err != nil { return opts, err } } else { opts.GraphRoot = filepath.Join(dataDir, "containers", "storage") } if driver := systemOpts.GraphDriverName; isRootlessDriver(driver) { opts.GraphDriverName = driver } if driver := os.Getenv("STORAGE_DRIVER"); driver != "" { opts.GraphDriverName = driver } if opts.GraphDriverName == overlay2 { logrus.Warnf("Switching default driver from overlay2 to the equivalent overlay driver") opts.GraphDriverName = overlayDriver } // If the configuration file was explicitly set, then copy all the options // present. if defaultConfigFileSet { opts.GraphDriverOptions = systemOpts.GraphDriverOptions opts.ImageStore = systemOpts.ImageStore } else if opts.GraphDriverName == overlayDriver { for _, o := range systemOpts.GraphDriverOptions { if strings.Contains(o, "ignore_chown_errors") { opts.GraphDriverOptions = append(opts.GraphDriverOptions, o) break } } } if opts.GraphDriverName == "" { if len(systemOpts.GraphDriverPriority) == 0 { dirEntries, err := os.ReadDir(opts.GraphRoot) if err == nil { for _, entry := range dirEntries { if strings.HasSuffix(entry.Name(), "-images") { opts.GraphDriverName = strings.TrimSuffix(entry.Name(), "-images") break } } } if opts.GraphDriverName == "" { if canUseRootlessOverlay(opts.GraphRoot, opts.RunRoot) { opts.GraphDriverName = overlayDriver } else { opts.GraphDriverName = "vfs" } } } else { opts.GraphDriverPriority = systemOpts.GraphDriverPriority } } if os.Getenv("STORAGE_OPTS") != "" { opts.GraphDriverOptions = append(opts.GraphDriverOptions, strings.Split(os.Getenv("STORAGE_OPTS"), ",")...) } return opts, nil } var prevReloadConfig = struct { storeOptions *StoreOptions mod time.Time mutex sync.Mutex configFile string }{} // SetDefaultConfigFilePath sets the default configuration to the specified path func SetDefaultConfigFilePath(path string) error { defaultConfigFile = path defaultConfigFileSet = true return ReloadConfigurationFileIfNeeded(defaultConfigFile, &defaultStoreOptions) } func ReloadConfigurationFileIfNeeded(configFile string, storeOptions *StoreOptions) error { prevReloadConfig.mutex.Lock() defer prevReloadConfig.mutex.Unlock() fi, err := os.Stat(configFile) if err != nil { return err } mtime := fi.ModTime() if prevReloadConfig.storeOptions != nil && prevReloadConfig.mod == mtime && prevReloadConfig.configFile == configFile { *storeOptions = *prevReloadConfig.storeOptions return nil } if err := ReloadConfigurationFile(configFile, storeOptions); err != nil { return err } cOptions := *storeOptions prevReloadConfig.storeOptions = &cOptions prevReloadConfig.mod = mtime prevReloadConfig.configFile = configFile return nil } // ReloadConfigurationFile parses the specified configuration file and overrides // the configuration in storeOptions. func ReloadConfigurationFile(configFile string, storeOptions *StoreOptions) error { config := new(TomlConfig) meta, err := toml.DecodeFile(configFile, &config) if err == nil { keys := meta.Undecoded() if len(keys) > 0 { logrus.Warningf("Failed to decode the keys %q from %q", keys, configFile) } } else { if !os.IsNotExist(err) { logrus.Warningf("Failed to read %s %v\n", configFile, err.Error()) return err } } // Clear storeOptions of previous settings *storeOptions = StoreOptions{} if config.Storage.Driver != "" { storeOptions.GraphDriverName = config.Storage.Driver } if os.Getenv("STORAGE_DRIVER") != "" { config.Storage.Driver = os.Getenv("STORAGE_DRIVER") storeOptions.GraphDriverName = config.Storage.Driver } if storeOptions.GraphDriverName == overlay2 { logrus.Warnf("Switching default driver from overlay2 to the equivalent overlay driver") storeOptions.GraphDriverName = overlayDriver } storeOptions.GraphDriverPriority = config.Storage.DriverPriority if storeOptions.GraphDriverName == "" && len(storeOptions.GraphDriverPriority) == 0 { logrus.Warnf("The storage 'driver' option should be set in %s. A driver was picked automatically.", configFile) } if config.Storage.RunRoot != "" { storeOptions.RunRoot = config.Storage.RunRoot } if config.Storage.GraphRoot != "" { storeOptions.GraphRoot = config.Storage.GraphRoot } if config.Storage.ImageStore != "" { storeOptions.ImageStore = config.Storage.ImageStore } if config.Storage.RootlessStoragePath != "" { storeOptions.RootlessStoragePath = config.Storage.RootlessStoragePath } for _, s := range config.Storage.Options.AdditionalImageStores { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.imagestore=%s", config.Storage.Driver, s)) } for _, s := range config.Storage.Options.AdditionalLayerStores { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.additionallayerstore=%s", config.Storage.Driver, s)) } if config.Storage.Options.Size != "" { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.size=%s", config.Storage.Driver, config.Storage.Options.Size)) } if config.Storage.Options.MountProgram != "" { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.mount_program=%s", config.Storage.Driver, config.Storage.Options.MountProgram)) } if config.Storage.Options.SkipMountHome != "" { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.skip_mount_home=%s", config.Storage.Driver, config.Storage.Options.SkipMountHome)) } if config.Storage.Options.IgnoreChownErrors != "" { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.ignore_chown_errors=%s", config.Storage.Driver, config.Storage.Options.IgnoreChownErrors)) } if config.Storage.Options.ForceMask != 0 { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.force_mask=%o", config.Storage.Driver, config.Storage.Options.ForceMask)) } if config.Storage.Options.MountOpt != "" { storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, fmt.Sprintf("%s.mountopt=%s", config.Storage.Driver, config.Storage.Options.MountOpt)) } storeOptions.RootAutoNsUser = config.Storage.Options.RootAutoUsernsUser if config.Storage.Options.AutoUsernsMinSize > 0 { storeOptions.AutoNsMinSize = config.Storage.Options.AutoUsernsMinSize } if config.Storage.Options.AutoUsernsMaxSize > 0 { storeOptions.AutoNsMaxSize = config.Storage.Options.AutoUsernsMaxSize } if config.Storage.Options.PullOptions != nil { storeOptions.PullOptions = config.Storage.Options.PullOptions } storeOptions.DisableVolatile = config.Storage.Options.DisableVolatile storeOptions.TransientStore = config.Storage.TransientStore storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, cfg.GetGraphDriverOptions(storeOptions.GraphDriverName, config.Storage.Options)...) if opts, ok := os.LookupEnv("STORAGE_OPTS"); ok { storeOptions.GraphDriverOptions = strings.Split(opts, ",") } if len(storeOptions.GraphDriverOptions) == 1 && storeOptions.GraphDriverOptions[0] == "" { storeOptions.GraphDriverOptions = nil } return nil } func Options() (StoreOptions, error) { defaultStoreOptionsOnce.Do(loadDefaultStoreOptions) return defaultStoreOptions, loadDefaultStoreOptionsErr } // Save overwrites the tomlConfig in storage.conf with the given conf func Save(conf TomlConfig) error { configFile, err := DefaultConfigFile() if err != nil { return err } if err = os.Remove(configFile); !os.IsNotExist(err) && err != nil { return err } f, err := os.Create(configFile) if err != nil { return err } return toml.NewEncoder(f).Encode(conf) } // StorageConfig is used to retrieve the storage.conf toml in order to overwrite it func StorageConfig() (*TomlConfig, error) { config := new(TomlConfig) configFile, err := DefaultConfigFile() if err != nil { return nil, err } _, err = toml.DecodeFile(configFile, &config) if err != nil { return nil, err } return config, nil }