243 lines
6.7 KiB
Go
243 lines
6.7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/containers/storage/pkg/fileutils"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
cachedConfigError error
|
|
cachedConfigMutex sync.Mutex
|
|
cachedConfig *Config
|
|
)
|
|
|
|
const (
|
|
containersConfEnv = "CONTAINERS_CONF"
|
|
containersConfOverrideEnv = containersConfEnv + "_OVERRIDE"
|
|
)
|
|
|
|
// Options to use when loading a Config via New().
|
|
type Options struct {
|
|
// Attempt to load the following config modules.
|
|
Modules []string
|
|
|
|
// Set the loaded config as the default one which can later on be
|
|
// accessed via Default().
|
|
SetDefault bool
|
|
|
|
// Additional configs to load. An internal only field to make the
|
|
// behavior observable and testable in unit tests.
|
|
additionalConfigs []string
|
|
}
|
|
|
|
// New returns a Config as described in the containers.conf(5) man page.
|
|
func New(options *Options) (*Config, error) {
|
|
if options == nil {
|
|
options = &Options{}
|
|
} else if options.SetDefault {
|
|
cachedConfigMutex.Lock()
|
|
defer cachedConfigMutex.Unlock()
|
|
}
|
|
return newLocked(options)
|
|
}
|
|
|
|
// Default returns the default container config. If no default config has been
|
|
// set yet, a new config will be loaded by New() and set as the default one.
|
|
// All callers are expected to use the returned Config read only. Changing
|
|
// data may impact other call sites.
|
|
func Default() (*Config, error) {
|
|
cachedConfigMutex.Lock()
|
|
defer cachedConfigMutex.Unlock()
|
|
if cachedConfig != nil || cachedConfigError != nil {
|
|
return cachedConfig, cachedConfigError
|
|
}
|
|
cachedConfig, cachedConfigError = newLocked(&Options{SetDefault: true})
|
|
return cachedConfig, cachedConfigError
|
|
}
|
|
|
|
// A helper function for New() expecting the caller to hold the
|
|
// cachedConfigMutex if options.SetDefault is set..
|
|
func newLocked(options *Options) (*Config, error) {
|
|
// Start with the built-in defaults
|
|
config, err := defaultConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Now, gather the system configs and merge them as needed.
|
|
configs, err := systemConfigs()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("finding config on system: %w", err)
|
|
}
|
|
|
|
for _, path := range configs {
|
|
// Merge changes in later configs with the previous configs.
|
|
// Each config file that specified fields, will override the
|
|
// previous fields.
|
|
if err = readConfigFromFile(path, config, true); err != nil {
|
|
return nil, fmt.Errorf("reading system config %q: %w", path, err)
|
|
}
|
|
logrus.Debugf("Merged system config %q", path)
|
|
logrus.Tracef("%+v", config)
|
|
}
|
|
|
|
modules, err := options.modules()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.loadedModules = modules
|
|
|
|
options.additionalConfigs = append(options.additionalConfigs, modules...)
|
|
|
|
// The _OVERRIDE variable _must_ always win. That's a contract we need
|
|
// to honor (for the Podman CI).
|
|
if path := os.Getenv(containersConfOverrideEnv); path != "" {
|
|
if err := fileutils.Exists(path); err != nil {
|
|
return nil, fmt.Errorf("%s file: %w", containersConfOverrideEnv, err)
|
|
}
|
|
options.additionalConfigs = append(options.additionalConfigs, path)
|
|
}
|
|
|
|
// If the caller specified a config path to use, then we read it to
|
|
// override the system defaults.
|
|
for _, add := range options.additionalConfigs {
|
|
if add == "" {
|
|
continue
|
|
}
|
|
// readConfigFromFile reads in container config in the specified
|
|
// file and then merge changes with the current default.
|
|
if err := readConfigFromFile(add, config, false); err != nil {
|
|
return nil, fmt.Errorf("reading additional config %q: %w", add, err)
|
|
}
|
|
logrus.Debugf("Merged additional config %q", add)
|
|
logrus.Tracef("%+v", config)
|
|
}
|
|
config.addCAPPrefix()
|
|
|
|
if err := config.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := config.setupEnv(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options.SetDefault {
|
|
cachedConfig = config
|
|
cachedConfigError = nil
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// NewConfig creates a new Config. It starts with an empty config and, if
|
|
// specified, merges the config at `userConfigPath` path.
|
|
//
|
|
// Deprecated: use new instead.
|
|
func NewConfig(userConfigPath string) (*Config, error) {
|
|
return New(&Options{additionalConfigs: []string{userConfigPath}})
|
|
}
|
|
|
|
// Returns the list of configuration files, if they exist in order of hierarchy.
|
|
// The files are read in order and each new file can/will override previous
|
|
// file settings.
|
|
func systemConfigs() (configs []string, finalErr error) {
|
|
if path := os.Getenv(containersConfEnv); path != "" {
|
|
if err := fileutils.Exists(path); err != nil {
|
|
return nil, fmt.Errorf("%s file: %w", containersConfEnv, err)
|
|
}
|
|
return append(configs, path), nil
|
|
}
|
|
|
|
configs = append(configs, DefaultContainersConfig)
|
|
|
|
var err error
|
|
path, err := overrideContainersConfigPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configs = append(configs, path)
|
|
|
|
configs, err = addConfigs(path+".d", configs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path, err = userConfigPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
configs = append(configs, path)
|
|
configs, err = addConfigs(path+".d", configs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return configs, nil
|
|
}
|
|
|
|
// addConfigs will search one level in the config dirPath for config files
|
|
// If the dirPath does not exist, addConfigs will return nil
|
|
func addConfigs(dirPath string, configs []string) ([]string, error) {
|
|
newConfigs := []string{}
|
|
|
|
err := filepath.WalkDir(dirPath,
|
|
// WalkFunc to read additional configs
|
|
func(path string, d fs.DirEntry, err error) error {
|
|
switch {
|
|
case err != nil:
|
|
// return error (could be a permission problem)
|
|
return err
|
|
case d.IsDir():
|
|
if path != dirPath {
|
|
// make sure to not recurse into sub-directories
|
|
return filepath.SkipDir
|
|
}
|
|
// ignore directories
|
|
return nil
|
|
default:
|
|
// only add *.conf files
|
|
if strings.HasSuffix(path, ".conf") {
|
|
newConfigs = append(newConfigs, path)
|
|
}
|
|
return nil
|
|
}
|
|
},
|
|
)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
err = nil
|
|
}
|
|
sort.Strings(newConfigs)
|
|
return append(configs, newConfigs...), err
|
|
}
|
|
|
|
// readConfigFromFile reads the specified config file at `path` and attempts to
|
|
// unmarshal its content into a Config. The config param specifies the previous
|
|
// default config. If the path, only specifies a few fields in the Toml file
|
|
// the defaults from the config parameter will be used for all other fields.
|
|
func readConfigFromFile(path string, config *Config, ignoreErrNotExist bool) error {
|
|
logrus.Tracef("Reading configuration file %q", path)
|
|
meta, err := toml.DecodeFile(path, config)
|
|
if err != nil {
|
|
if ignoreErrNotExist && errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("decode configuration %v: %w", path, err)
|
|
}
|
|
keys := meta.Undecoded()
|
|
if len(keys) > 0 {
|
|
logrus.Debugf("Failed to decode the keys %q from %q.", keys, path)
|
|
}
|
|
|
|
return nil
|
|
}
|