add --module flag

Support a new concept in containers.conf called "modules".  A "module"
is a containers.conf file located at a specific directory.  More than
one module can be loaded in the specified order, following existing
override semantics.

There are three directories to load modules from:
 - $CONFIG_HOME/containers/containers.conf.modules
 - /etc/containers/containers.conf.modules
 - /usr/share/containers/containers.conf.modules

With CONFIG_HOME pointing to $HOME/.config or, if set, $XDG_CONFIG_HOME.
Absolute paths will be loaded as is, relative paths will be resolved
relative to the three directories above allowing for admin configs
(/etc/) to override system configs (/usr/share/) and user configs
($CONFIG_HOME) to override admin configs.

Pulls in containers/common/pull/1599.

Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2023-08-09 15:50:15 +02:00
parent 9cd4286922
commit d5841ed528
65 changed files with 1253 additions and 756 deletions

View File

@ -3,14 +3,11 @@ package config
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"github.com/BurntSushi/toml"
"github.com/containers/common/libnetwork/types"
@ -81,6 +78,8 @@ type Config struct {
ConfigMaps ConfigMapConfig `toml:"configmaps"`
// Farms defines configurations for the buildfarm farms
Farms FarmConfig `toml:"farms"`
loadedModules []string // only used at runtime to store which modules were loaded
}
// ContainersConfig represents the "containers" TOML config table
@ -708,166 +707,6 @@ func (c *EngineConfig) ImagePlatformToRuntime(os string, arch string) string {
return c.OCIRuntime
}
// NewConfig creates a new Config. It starts with an empty config and, if
// specified, merges the config at `userConfigPath` path. Depending if we're
// running as root or rootless, we then merge the system configuration followed
// by merging the default config (hard-coded default in memory).
// Note that the OCI runtime is hard-set to `crun` if we're running on a system
// with cgroupv2v2. Other OCI runtimes are not yet supporting cgroupv2v2. This
// might change in the future.
func NewConfig(userConfigPath string) (*Config, error) {
// Generate the default config for the system
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); err != nil {
return nil, fmt.Errorf("reading system config %q: %w", path, err)
}
logrus.Debugf("Merged system config %q", path)
logrus.Tracef("%+v", config)
}
// If the caller specified a config path to use, then we read it to
// override the system defaults.
if userConfigPath != "" {
var err error
// readConfigFromFile reads in container config in the specified
// file and then merge changes with the current default.
if err = readConfigFromFile(userConfigPath, config); err != nil {
return nil, fmt.Errorf("reading user config %q: %w", userConfigPath, err)
}
logrus.Debugf("Merged user config %q", userConfigPath)
logrus.Tracef("%+v", config)
}
config.addCAPPrefix()
if err := config.Validate(); err != nil {
return nil, err
}
if err := config.setupEnv(); err != nil {
return nil, err
}
return config, nil
}
// 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) error {
logrus.Tracef("Reading configuration file %q", path)
meta, err := toml.DecodeFile(path, config)
if err != 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
}
// 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
}
// 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("CONTAINERS_CONF_OVERRIDE"); path != "" {
if _, err := os.Stat(path); err != nil {
return nil, fmt.Errorf("CONTAINERS_CONF_OVERRIDE file: %w", err)
}
// Add the override config last to make sure it can override any
// previous settings.
defer func() {
if finalErr == nil {
configs = append(configs, path)
}
}()
}
if path := os.Getenv("CONTAINERS_CONF"); path != "" {
if _, err := os.Stat(path); err != nil {
return nil, fmt.Errorf("CONTAINERS_CONF file: %w", err)
}
return append(configs, path), nil
}
if _, err := os.Stat(DefaultContainersConfig); err == nil {
configs = append(configs, DefaultContainersConfig)
}
if _, err := os.Stat(OverrideContainersConfig); err == nil {
configs = append(configs, OverrideContainersConfig)
}
var err error
configs, err = addConfigs(OverrideContainersConfig+".d", configs)
if err != nil {
return nil, err
}
path, err := ifRootlessConfigPath()
if err != nil {
return nil, err
}
if path != "" {
if _, err := os.Stat(path); err == nil {
configs = append(configs, path)
}
configs, err = addConfigs(path+".d", configs)
if err != nil {
return nil, err
}
}
return configs, nil
}
// CheckCgroupsAndAdjustConfig checks if we're running rootless with the systemd
// cgroup manager. In case the user session isn't available, we're switching the
// cgroup manager to cgroupfs. Note, this only applies to rootless.
@ -1190,37 +1029,6 @@ func rootlessConfigPath() (string, error) {
return filepath.Join(home, UserOverrideContainersConfig), nil
}
var (
configErr error
configMutex sync.Mutex
config *Config
)
// Default returns the default container config.
// Configuration files will be read in the following files:
// * /usr/share/containers/containers.conf
// * /etc/containers/containers.conf
// * $HOME/.config/containers/containers.conf # When run in rootless mode
// Fields in latter files override defaults set in previous files and the
// default config.
// None of these files are required, and not all fields need to be specified
// in each file, only the fields you want to override.
// The system defaults container config files can be overwritten using the
// CONTAINERS_CONF environment variable. This is usually done for testing.
func Default() (*Config, error) {
configMutex.Lock()
defer configMutex.Unlock()
if config != nil || configErr != nil {
return config, configErr
}
return defConfig()
}
func defConfig() (*Config, error) {
config, configErr = NewConfig("")
return config, configErr
}
func Path() string {
if path := os.Getenv("CONTAINERS_CONF"); path != "" {
return path
@ -1289,9 +1097,7 @@ func (c *Config) Write() error {
// This function is meant to be used for long-running processes that need to reload potential changes made to
// the cached containers.conf files.
func Reload() (*Config, error) {
configMutex.Lock()
defer configMutex.Unlock()
return defConfig()
return New(&Options{SetDefault: true})
}
func (c *Config) ActiveDestination() (uri, identity string, machine bool, err error) {

View File

@ -157,9 +157,11 @@ const (
DefaultVolumePluginTimeout = 5
)
// DefaultConfig defines the default values from containers.conf.
func DefaultConfig() (*Config, error) {
defaultEngineConfig, err := defaultConfigFromMemory()
// defaultConfig returns Config with builtin defaults and minimal adjustments
// to the current host only. It does not read any config files from the host or
// the environment.
func defaultConfig() (*Config, error) {
defaultEngineConfig, err := defaultEngineConfig()
if err != nil {
return nil, err
}
@ -266,9 +268,9 @@ func defaultFarmConfig() FarmConfig {
}
}
// defaultConfigFromMemory returns a default engine configuration. Note that the
// defaultEngineConfig eturns a default engine configuration. Note that the
// config is different for root and rootless. It also parses the storage.conf.
func defaultConfigFromMemory() (*EngineConfig, error) {
func defaultEngineConfig() (*EngineConfig, error) {
c := new(EngineConfig)
tmp, err := defaultTmpDir()
if err != nil {
@ -653,3 +655,16 @@ func useUserConfigLocations() bool {
// GetRootlessUID == -1 on Windows, so exclude negative range
return unshare.GetRootlessUID() > 0
}
// getDefaultImage returns the default machine image stream
// On Windows this refers to the Fedora major release number
func getDefaultMachineImage() string {
return "testing"
}
// getDefaultMachineUser returns the user to use for rootless podman
// This is only for the apple, hyperv, and qemu implementations.
// WSL's user will be hardcoded in podman to "user"
func getDefaultMachineUser() string {
return "core"
}

View File

@ -17,17 +17,6 @@ func getDefaultCgroupsMode() string {
return "enabled"
}
// getDefaultMachineImage returns the default machine image stream
// On Linux/Mac, this returns the FCOS stream
func getDefaultMachineImage() string {
return "testing"
}
// getDefaultMachineUser returns the user to use for rootless podman
func getDefaultMachineUser() string {
return "core"
}
// getDefaultProcessLimits returns the nproc for the current process in ulimits format
// Note that nfile sometimes cannot be set to unlimited, and the limit is hardcoded
// to (oldMaxSize) 1048576 (2^20), see: http://stackoverflow.com/a/1213069/1811501

View File

@ -5,17 +5,6 @@ package config
import "os"
// getDefaultMachineImage returns the default machine image stream
// On Linux/Mac, this returns the FCOS stream
func getDefaultMachineImage() string {
return "testing"
}
// getDefaultMachineUser returns the user to use for rootless podman
func getDefaultMachineUser() string {
return "core"
}
// isCgroup2UnifiedMode returns whether we are running in cgroup2 mode.
func isCgroup2UnifiedMode() (isUnified bool, isUnifiedErr error) {
return false, nil

View File

@ -2,19 +2,6 @@ package config
import "os"
// getDefaultImage returns the default machine image stream
// On Windows this refers to the Fedora major release number
func getDefaultMachineImage() string {
return "testing"
}
// getDefaultMachineUser returns the user to use for rootless podman
// This is only for the hyperv and qemu implementations. WSL's user
// will be hardcoded in podman to "user"
func getDefaultMachineUser() string {
return "core"
}
// isCgroup2UnifiedMode returns whether we are running in cgroup2 mode.
func isCgroup2UnifiedMode() (isUnified bool, isUnifiedErr error) {
return false, nil

View File

@ -0,0 +1,95 @@
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/containers/storage/pkg/homedir"
"github.com/containers/storage/pkg/unshare"
"github.com/hashicorp/go-multierror"
)
// The subdirectory for looking up containers.conf modules.
const moduleSubdir = "containers/containers.conf.modules"
// Moving the base paths into variables allows for overriding them in units
// tests.
var (
moduleBaseEtc = "/etc/"
moduleBaseUsr = "/usr/share"
)
// LoadedModules returns absolute paths to loaded containers.conf modules.
func (c *Config) LoadedModules() []string {
// Required for conmon's callback to Podman's cleanup.
// Absolute paths make loading the modules a bit faster.
return c.loadedModules
}
// Find the specified modules in the options. Return an error if a specific
// module cannot be located on the host.
func (o *Options) modules() ([]string, error) {
if len(o.Modules) == 0 {
return nil, nil
}
dirs, err := ModuleDirectories()
if err != nil {
return nil, err
}
configs := make([]string, 0, len(o.Modules))
for _, path := range o.Modules {
resolved, err := resolveModule(path, dirs)
if err != nil {
return nil, fmt.Errorf("could not resolve module %q: %w", path, err)
}
configs = append(configs, resolved)
}
return configs, nil
}
// ModuleDirectories return the directories to load modules from:
// 1) XDG_CONFIG_HOME/HOME if rootless
// 2) /etc/
// 3) /usr/share
func ModuleDirectories() ([]string, error) { // Public API for shell completions in Podman
modules := []string{
filepath.Join(moduleBaseEtc, moduleSubdir),
filepath.Join(moduleBaseUsr, moduleSubdir),
}
if !unshare.IsRootless() {
return modules, nil
}
// Prepend the user modules dir.
configHome, err := homedir.GetConfigHome()
if err != nil {
return nil, err
}
return append([]string{filepath.Join(configHome, moduleSubdir)}, modules...), nil
}
// Resolve the specified path to a module.
func resolveModule(path string, dirs []string) (string, error) {
if filepath.IsAbs(path) {
_, err := os.Stat(path)
return path, err
}
// Collect all errors to avoid suppressing important errors (e.g.,
// permission errors).
var multiErr error
for _, d := range dirs {
candidate := filepath.Join(d, path)
_, err := os.Stat(candidate)
if err == nil {
return candidate, nil
}
multiErr = multierror.Append(multiErr, err)
}
return "", multiErr
}

240
vendor/github.com/containers/common/pkg/config/new.go generated vendored Normal file
View File

@ -0,0 +1,240 @@
package config
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/BurntSushi/toml"
"github.com/sirupsen/logrus"
)
var (
cachedConfigError error
cachedConfigMutex sync.Mutex
cachedConfig *Config
)
const (
// FIXME: update code base and tests to use the two constants below.
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); 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 := os.Stat(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); 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 := os.Stat(path); err != nil {
return nil, fmt.Errorf("%s file: %w", containersConfEnv, err)
}
return append(configs, path), nil
}
if _, err := os.Stat(DefaultContainersConfig); err == nil {
configs = append(configs, DefaultContainersConfig)
}
if _, err := os.Stat(OverrideContainersConfig); err == nil {
configs = append(configs, OverrideContainersConfig)
}
var err error
configs, err = addConfigs(OverrideContainersConfig+".d", configs)
if err != nil {
return nil, err
}
path, err := ifRootlessConfigPath()
if err != nil {
return nil, err
}
if path != "" {
if _, err := os.Stat(path); err == nil {
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) error {
logrus.Tracef("Reading configuration file %q", path)
meta, err := toml.DecodeFile(path, config)
if err != 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
}