Skip to content

Commit

Permalink
containers.conf: implement modules
Browse files Browse the repository at this point in the history
Add a new concept to containers.conf called "modules".  A "module" is
a containers.conf file located at a specific directory.  More than one
modules 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.

Also move some functions from config.go for locality.

Signed-off-by: Valentin Rothberg <[email protected]>
  • Loading branch information
vrothberg committed Aug 8, 2023
1 parent 1790204 commit 56397b6
Show file tree
Hide file tree
Showing 21 changed files with 445 additions and 125 deletions.
29 changes: 20 additions & 9 deletions docs/containers.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,26 @@ Note, container engines also use other configuration files for configuring the e
container images.
* `policy.conf` for controlling which images can be pulled to the system.

## ENVIRONMENT VARIABLES
If the `CONTAINERS_CONF` environment variable is set, all system and user
config files are ignored and only the specified config file will be loaded.

If the `CONTAINERS_CONF_OVERRIDE` path environment variable is set, the config
file will be loaded last even when `CONTAINERS_CONF` is set.

The values of both environment variables may be absolute or relative paths, for
instance, `CONTAINERS_CONF=/tmp/my_containers.conf`.

## MODULES
A module is a containers.conf file located directly in or a sub-directory of the following three directories:
- __$HOME/.config/containers/containers.conf.modules__
- __/etc/containers/containers.conf.modules__
- __/usr/share/containers/containers.conf.modules__

Files in those locations are not loaded by default but only on-demand. They are loaded after all system and user configuration files but before `CONTAINERS_CONF_OVERRIDE` hence allowing for overriding system and user configs.

Modules are currently supported by podman(1). The `podman --module` flag allows for loading a module and can be specified multiple times. If the specified value is an absolute path, the config file will be loaded directly. Relative paths are resolved relative to the three module directories mentioned above and in the specified order such that modules in `$HOME` allow for overriding those in `/etc` and `/usr/share`. Modules in `$HOME` (or `$XDG_CONFIG_HOME` if specified) are only used for rootless users.

# FORMAT
The [TOML format][toml] is used as the encoding of the configuration file.
Every option is nested under its table. No bare options are used. The format of
Expand Down Expand Up @@ -871,15 +891,6 @@ __/etc/containers/containers.conf.d__ which will be loaded in alphanumeric order
Rootless users can further override fields in the config by creating a config
file stored in the __$HOME/.config/containers/containers.conf__ file or __.conf__ files in __$HOME/.config/containers/containers.conf.d__.

If the `CONTAINERS_CONF` environment variable is set, all system and user
config files are ignored and only the specified config file will be loaded.

If the `CONTAINERS_CONF_OVERRIDE` path environment variable is set, the config
file will be loaded last even when `CONTAINERS_CONF` is set.

The values of both environment variables may be absolute or relative paths, for
instance, `CONTAINERS_CONF=/tmp/my_containers.conf`.

Fields specified in a containers.conf file override the default options, as
well as options in previously loaded containers.conf files.

Expand Down
94 changes: 0 additions & 94 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package config
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"

"github.com/BurntSushi/toml"
Expand Down Expand Up @@ -707,98 +705,6 @@ func (c *EngineConfig) ImagePlatformToRuntime(os string, arch string) string {
return c.OCIRuntime
}

// 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"); 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.
Expand Down
88 changes: 88 additions & 0 deletions pkg/config/modules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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"
)

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

// Return the directories to load modules from:
// 1) XDG_CONFIG_HOME/HOME if rootless
// 2) /etc/
// 3) /usr/share
func moduleDirectories() ([]string, error) {
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
}
Loading

0 comments on commit 56397b6

Please sign in to comment.