diff --git a/common/libimage/runtime.go b/common/libimage/runtime.go index 3378e6120a..90ec713f0a 100644 --- a/common/libimage/runtime.go +++ b/common/libimage/runtime.go @@ -48,21 +48,6 @@ type RuntimeOptions struct { SystemContext *types.SystemContext } -// setRegistriesConfPath sets the registries.conf path for the specified context. -func setRegistriesConfPath(systemContext *types.SystemContext) { - if systemContext.SystemRegistriesConfPath != "" { - return - } - if envOverride, ok := os.LookupEnv("CONTAINERS_REGISTRIES_CONF"); ok { - systemContext.SystemRegistriesConfPath = envOverride - return - } - if envOverride, ok := os.LookupEnv("REGISTRIES_CONFIG_PATH"); ok { - systemContext.SystemRegistriesConfPath = envOverride - return - } -} - // Runtime is responsible for image management and storing them in a containers // storage. type Runtime struct { @@ -119,8 +104,6 @@ func RuntimeFromStore(store storage.Store, options *RuntimeOptions) (*Runtime, e systemContext.BigFilesTemporaryDir = tmpdir } - setRegistriesConfPath(&systemContext) - return &Runtime{ store: store, systemContext: systemContext, diff --git a/common/libimage/runtime_test.go b/common/libimage/runtime_test.go index 5c1d24106a..66ce029f6e 100644 --- a/common/libimage/runtime_test.go +++ b/common/libimage/runtime_test.go @@ -39,7 +39,7 @@ func testNewRuntime(t *testing.T, options ...testNewRuntimeOptions) *Runtime { // Make sure that the tests do not use the host's registries.conf. systemContext := &types.SystemContext{ SystemRegistriesConfPath: "testdata/registries.conf", - SystemRegistriesConfDirPath: "/dev/null", + SystemRegistriesConfDirPath: t.TempDir(), } if len(options) == 1 { diff --git a/common/pkg/auth/auth.go b/common/pkg/auth/auth.go index b6f7228546..1a04716150 100644 --- a/common/pkg/auth/auth.go +++ b/common/pkg/auth/auth.go @@ -393,7 +393,7 @@ func Logout(systemContext *types.SystemContext, opts *LogoutOptions, args []stri func defaultRegistryWhenUnspecified(systemContext *types.SystemContext) (string, error) { registriesFromFile, err := sysregistriesv2.UnqualifiedSearchRegistries(systemContext) if err != nil { - return "", fmt.Errorf("getting registry from registry.conf, please specify a registry: %w", err) + return "", fmt.Errorf("getting registry from registries.conf, please specify a registry: %w", err) } if len(registriesFromFile) == 0 { return "", errors.New("no registries found in registries.conf, a registry must be provided") diff --git a/image/docs/containers-registries.conf.5.md b/image/docs/containers-registries.conf.5.md index ed17fff840..d9706d0ea7 100644 --- a/image/docs/containers-registries.conf.5.md +++ b/image/docs/containers-registries.conf.5.md @@ -9,7 +9,46 @@ containers-registries.conf - Syntax of System Registry Configuration File The CONTAINERS-REGISTRIES configuration file is a system-wide configuration file for container image registries. The file format is TOML. -Container engines will use the `$HOME/.config/containers/registries.conf` if it exists, otherwise they will use `/etc/containers/registries.conf` +By default, the configuration is read from `$XDG_CONFIG_HOME/containers/registries.conf` (or from `$HOME/.config/containers/registries.conf` if `$XDG_CONFIG_HOME` is unset), if it exists; otherwise from `/etc/containers/registries.conf`; otherwise from `/usr/share/containers/registries.conf`. Applications may allow using a different configuration path instead. + +If `CONTAINERS_REGISTRIES_CONF` is set, it specifies the configuration file to use, +unless overridden by application-specific configuration. If the environment variable +is set then the following drop-in directories will not be read. + +In addition to registries.conf, drop-in files using the same format from the following directories are also read: + - `$XDG_CONFIG_HOME/containers/registries.conf.d` (or from `$HOME/.config/containers/registries.conf.d` if `$XDG_CONFIG_HOME` is unset) + - `/etc/containers/registries.conf.d` + - `/etc/containers/registries.rootful.conf.d` (only when running as uid 0) + - `/etc/containers/registries.rootless.conf.d` (only when running as uid > 0) + - `/etc/containers/registries.rootless.conf.d/$UID` (only when running as uid > 0) + - `/usr/share/containers/registries.rootful.conf.d` (only when running as uid 0) + - `/usr/share/containers/registries.rootless.conf.d` (only when running as uid > 0) + - `/usr/share/containers/registries.rootless.conf.d/$UID` (only when running as uid > 0) + +The files must be using the `.conf` suffix, directories or files with other suffixes will be ignored. +All files from these paths will be first collected and then sorted in alpha-numerical order. +If the same filename is used twice then only the first match from the directory list above is +being used. Then the files will be parsed in the sorted order. + +For example consider these files: + +- `/usr/share/containers/registries.rootless.conf.d/50-middle.conf` +- `/etc/containers/registries.rootless.conf.d/20-first.conf` +- `/etc/containers/registries.rootless.conf.d/70-last.conf` + +They will be read in the order of `20-first.conf`, `50-middle.conf`, `70-last.conf`, +the directory path itself does not matter for the order, only the basename. + +Specified fields in a conf file will overwrite any previous setting. +For instance, setting the `unqualified-search-registries` in +`/etc/containers/registries.conf.d/myregistries.conf` will overwrite previous +settings in `/etc/containers/registries.conf`. The `[[registry]]` tables merged +by overwriting existing items if the prefixes are identical while new ones are +added. + +If `CONTAINERS_REGISTRIES_CONF_OVERRIDE` is set, it specifies an additional path that is being read last, +unless overridden by application-specific configuration. + ### GLOBAL SETTINGS @@ -284,24 +323,6 @@ The format of `$image_reference` is `$repo{:$tag|@$digest}`. Additional Layer Stores can use this helper binary to access the private registry. -## VERSION 1 FORMAT - DEPRECATED -VERSION 1 format is still supported but it does not support -using registry mirrors, longest-prefix matches, or location rewriting. - -The TOML format is used to build a simple list of registries under three -categories: `registries.search`, `registries.insecure`, and `registries.block`. -You can list multiple registries using a comma separated list. - -Search registries are used when the caller of a container runtime does not fully specify the -container image that they want to execute. These registries are prepended onto the front -of the specified container image until the named image is found at a registry. - -Note that insecure registries can be used for any registry, not just the registries listed -under search. - -The `registries.insecure` and `registries.block` lists have the same meaning as the -`insecure` and `blocked` fields in the current version. - ### EXAMPLE The following example configuration defines two searchable registries, one insecure registry, and two blocked registries. diff --git a/image/docs/containers-registries.conf.d.5 b/image/docs/containers-registries.conf.d.5 new file mode 100644 index 0000000000..e2a02642c9 --- /dev/null +++ b/image/docs/containers-registries.conf.d.5 @@ -0,0 +1 @@ +.so man5/containers-registries.conf.5 diff --git a/image/docs/containers-registries.conf.d.5.md b/image/docs/containers-registries.conf.d.5.md deleted file mode 100644 index 563ccfd00c..0000000000 --- a/image/docs/containers-registries.conf.d.5.md +++ /dev/null @@ -1,37 +0,0 @@ -% CONTAINERS-REGISTRIES.CONF.D 5 -% Valentin Rothberg -% Mar 2020 - -# NAME -containers-registries.conf.d - directory for drop-in registries.conf files - -# DESCRIPTION -CONTAINERS-REGISTRIES.CONF.D is a system-wide directory for drop-in -configuration files in the `containers-registries.conf(5)` format. - -By default, the directory is located at `/etc/containers/registries.conf.d`. - -# CONFIGURATION PRECEDENCE - -Once the main configuration at `/etc/containers/registries.conf` is loaded, the -files in `/etc/containers/registries.conf.d` are loaded in alpha-numerical -order. Then the conf files in `$HOME/.config/containers/registries.conf.d` are loaded in alpha-numerical order, if they exist. If the `$HOME/.config/containers/registries.conf` is loaded, only the conf files under `$HOME/.config/containers/registries.conf.d` are loaded in alpha-numerical order. -Specified fields in a conf file will overwrite any previous setting. Note -that only files with the `.conf` suffix are loaded, other files and -sub-directories are ignored. - -For instance, setting the `unqualified-search-registries` in -`/etc/containers/registries.conf.d/myregistries.conf` will overwrite previous -settings in `/etc/containers/registries.conf`. The `[[registry]]` tables merged -by overwriting existing items if the prefixes are identical while new ones are -added. - -All drop-in configuration files must be specified in the version 2 of the -`containers-registries.conf(5)` format. - -# SEE ALSO -`containers-registries.conf(5)` - -# HISTORY - -Mar 2020, Originally compiled by Valentin Rothberg diff --git a/image/pkg/cli/environment/environment.go b/image/pkg/cli/environment/environment.go index 3551d98344..a934e74627 100644 --- a/image/pkg/cli/environment/environment.go +++ b/image/pkg/cli/environment/environment.go @@ -11,6 +11,10 @@ import ( // context, unless already set. Possible values are, in priority and only if // set, the CONTAINERS_REGISTRIES_CONF or REGISTRIES_CONFIG_PATH environment // variable. +// +// Deprecated: The registries.conf parsing code in pkg/sysregistriesv2 already +// reads CONTAINERS_REGISTRIES_CONF. REGISTRIES_CONFIG_PATH should not be used +// anymore. func UpdateRegistriesConf(sys *types.SystemContext) error { if sys == nil { return errors.New("internal error: UpdateRegistriesConf: nil argument") diff --git a/image/pkg/sysregistriesv2/paths_common.go b/image/pkg/sysregistriesv2/paths_common.go deleted file mode 100644 index c9e8ac5cbd..0000000000 --- a/image/pkg/sysregistriesv2/paths_common.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !freebsd - -package sysregistriesv2 - -// builtinRegistriesConfPath is the path to the registry configuration file. -// DO NOT change this, instead see systemRegistriesConfPath above. -const builtinRegistriesConfPath = "/etc/containers/registries.conf" - -// builtinRegistriesConfDirPath is the path to the registry configuration directory. -// DO NOT change this, instead see systemRegistriesConfDirectoryPath above. -const builtinRegistriesConfDirPath = "/etc/containers/registries.conf.d" diff --git a/image/pkg/sysregistriesv2/paths_freebsd.go b/image/pkg/sysregistriesv2/paths_freebsd.go deleted file mode 100644 index 7dada4b779..0000000000 --- a/image/pkg/sysregistriesv2/paths_freebsd.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build freebsd - -package sysregistriesv2 - -// builtinRegistriesConfPath is the path to the registry configuration file. -// DO NOT change this, instead see systemRegistriesConfPath above. -const builtinRegistriesConfPath = "/usr/local/etc/containers/registries.conf" - -// builtinRegistriesConfDirPath is the path to the registry configuration directory. -// DO NOT change this, instead see systemRegistriesConfDirectoryPath above. -const builtinRegistriesConfDirPath = "/usr/local/etc/containers/registries.conf.d" diff --git a/image/pkg/sysregistriesv2/shortnames_test.go b/image/pkg/sysregistriesv2/shortnames_test.go index 781126b381..dee605ac2c 100644 --- a/image/pkg/sysregistriesv2/shortnames_test.go +++ b/image/pkg/sysregistriesv2/shortnames_test.go @@ -117,7 +117,7 @@ func TestResolveShortNameAlias(t *testing.T) { } InvalidateCache() - conf, err := tryUpdatingCache(sys, newConfigWrapper(sys)) + conf, err := tryUpdatingCache(newConfigWrapper(sys)) require.NoError(t, err) assert.Len(t, conf.aliasCache.namedAliases, 4) assert.Len(t, conf.partialV2.Aliases, 0) // This is an implementation detail, not an API guarantee. @@ -172,7 +172,7 @@ func TestAliasesWithDropInConfigs(t *testing.T) { } InvalidateCache() - conf, err := tryUpdatingCache(sys, newConfigWrapper(sys)) + conf, err := tryUpdatingCache(newConfigWrapper(sys)) require.NoError(t, err) assert.Len(t, conf.aliasCache.namedAliases, 8) assert.Len(t, conf.partialV2.Aliases, 0) // This is an implementation detail, not an API guarantee. diff --git a/image/pkg/sysregistriesv2/system_registries_v2.go b/image/pkg/sysregistriesv2/system_registries_v2.go index bc063117c6..f489246052 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2.go +++ b/image/pkg/sysregistriesv2/system_registries_v2.go @@ -1,12 +1,8 @@ package sysregistriesv2 import ( - "errors" "fmt" - "io/fs" "maps" - "os" - "path/filepath" "reflect" "slices" "sort" @@ -17,23 +13,12 @@ import ( "github.com/sirupsen/logrus" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/types" + "go.podman.io/storage/pkg/configfile" "go.podman.io/storage/pkg/fileutils" - "go.podman.io/storage/pkg/homedir" "go.podman.io/storage/pkg/regexp" + "go.podman.io/storage/pkg/unshare" ) -// systemRegistriesConfPath is the path to the system-wide registry -// configuration file and is used to add/subtract potential registries for -// obtaining images. You can override this at build time with -// -ldflags '-X go.podman.io/image/v5/sysregistries.systemRegistriesConfPath=$your_path' -var systemRegistriesConfPath = builtinRegistriesConfPath - -// systemRegistriesConfDirPath is the path to the system-wide registry -// configuration directory and is used to add/subtract potential registries for -// obtaining images. You can override this at build time with -// -ldflags '-X go.podman.io/image/v5/sysregistries.systemRegistriesConfDirectoryPath=$your_path' -var systemRegistriesConfDirPath = builtinRegistriesConfDirPath - // AuthenticationFileHelper is a special key for credential helpers indicating // the usage of consulting containers-auth.json files instead of a credential // helper. @@ -73,12 +58,6 @@ type Endpoint struct { PullFromMirror string `toml:"pull-from-mirror,omitempty"` } -// userRegistriesFile is the path to the per user registry configuration file. -var userRegistriesFile = filepath.FromSlash(".config/containers/registries.conf") - -// userRegistriesDir is the path to the per user registry configuration file. -var userRegistriesDir = filepath.FromSlash(".config/containers/registries.conf.d") - // rewriteReference will substitute the provided reference `prefix` to the // endpoints `location` from the `ref` and creates a new named reference from it. // The function errors if the newly created reference is not parsable. @@ -185,11 +164,15 @@ func (r *Registry) PullSourcesFromReference(ref reference.Named) ([]PullSource, } // V1TOMLregistries is for backwards compatibility to sysregistries v1 +// +// Deprecated: This format is no longer accepted, use [V2RegistriesConf] instead. type V1TOMLregistries struct { Registries []string `toml:"registries"` } // V1TOMLConfig is for backwards compatibility to sysregistries v1 +// +// Deprecated: This format is no longer accepted, use [V2RegistriesConf] instead. type V1TOMLConfig struct { Search V1TOMLregistries `toml:"search"` Insecure V1TOMLregistries `toml:"insecure"` @@ -197,6 +180,9 @@ type V1TOMLConfig struct { } // V1RegistriesConf is the sysregistries v1 configuration format. +// +// Deprecated: This format is no longer accepted, use [V2RegistriesConf] instead. +// You can use [V1RegistriesConf.ConvertToV2] to convert the type. type V1RegistriesConf struct { V1TOMLConfig `toml:"registries"` } @@ -529,95 +515,90 @@ func (config *V2RegistriesConf) postProcessRegistries() error { } // ConfigPath returns the path to the system-wide registry configuration file. +// This may return an empty string if it fails to resolve a home directory. // // Deprecated: This API implies configuration is read from files, and that there is only one. // Please use ConfigurationSourceDescription to obtain a string usable for error messages. func ConfigPath(ctx *types.SystemContext) string { - return newConfigWrapper(ctx).configPath + configWrapper := newConfigWrapper(ctx) + paths, err := configfile.GetSearchPaths(configWrapper.toConfigFileOptions()) + if err == nil && len(paths.MainFiles) > 0 { + for _, file := range paths.MainFiles { + if fileutils.Exists(file) == nil { + return file + } + } + // nothing exists return first file + return paths.MainFiles[0] + } + return "" } // ConfigDirPath returns the path to the directory for drop-in -// registry configuration files. +// registry configuration files. This may return an empty string if it fails to resolve a home directory. // // Deprecated: This API implies configuration is read from directories, and that there is only one. // Please use ConfigurationSourceDescription to obtain a string usable for error messages. func ConfigDirPath(ctx *types.SystemContext) string { configWrapper := newConfigWrapper(ctx) - if configWrapper.userConfigDirPath != "" { - return configWrapper.userConfigDirPath + paths, err := configfile.GetSearchPaths(configWrapper.toConfigFileOptions()) + if err == nil && len(paths.DropInDirectories) > 0 { + return paths.DropInDirectories[0] } - return configWrapper.configDirPath + return "" } // configWrapper is used to store the paths from ConfigPath and ConfigDirPath // and acts as a key to the internal cache. +// +// Note some env vars like CONTAINERS_REGISTRIES_CONF or $HOME will effect the +// parsing behavior but are not used for the cache key here, if these env change +// at runtime then the cache will result in correct results. This is a known trade +// off as regular callers are not expected to modify the envs at runtime. +// If they do so they should call [InvalidateCache]. type configWrapper struct { - // path to the registries.conf file - configPath string - // path to system-wide registries.conf.d directory, or "" if not used - configDirPath string - // path to user specified registries.conf.d directory, or "" if not used - userConfigDirPath string -} + // system context override to specific path + systemRegistriesConfPath string + // system context override to specific drop in directory + systemRegistriesConfDirPath string -// newConfigWrapper returns a configWrapper for the specified SystemContext. -func newConfigWrapper(ctx *types.SystemContext) configWrapper { - return newConfigWrapperWithHomeDir(ctx, homedir.Get()) + // system context override to root directory + rootForImplicitAbsolutePaths string } -// newConfigWrapperWithHomeDir is an internal implementation detail of newConfigWrapper, -// it exists only to allow testing it with an artificial home directory. -func newConfigWrapperWithHomeDir(ctx *types.SystemContext, homeDir string) configWrapper { - var wrapper configWrapper - userRegistriesFilePath := filepath.Join(homeDir, userRegistriesFile) - userRegistriesDirPath := filepath.Join(homeDir, userRegistriesDir) - - // decide configPath using per-user path or system file - if ctx != nil && ctx.SystemRegistriesConfPath != "" { - wrapper.configPath = ctx.SystemRegistriesConfPath - } else if err := fileutils.Exists(userRegistriesFilePath); err == nil { - // per-user registries.conf exists, not reading system dir - // return config dirs from ctx or per-user one - wrapper.configPath = userRegistriesFilePath - if ctx != nil && ctx.SystemRegistriesConfDirPath != "" { - wrapper.configDirPath = ctx.SystemRegistriesConfDirPath - } else { - wrapper.userConfigDirPath = userRegistriesDirPath - } - - return wrapper - } else if ctx != nil && ctx.RootForImplicitAbsolutePaths != "" { - wrapper.configPath = filepath.Join(ctx.RootForImplicitAbsolutePaths, systemRegistriesConfPath) - } else { - wrapper.configPath = systemRegistriesConfPath +func (c *configWrapper) toConfigFileOptions() *configfile.File { + return &configfile.File{ + Name: "registries", + Extension: "conf", + EnvironmentName: "CONTAINERS_REGISTRIES_CONF", + RootForImplicitAbsolutePaths: c.rootForImplicitAbsolutePaths, + CustomConfigFilePath: c.systemRegistriesConfPath, + CustomConfigFileDropInDirectory: c.systemRegistriesConfDirPath, + UserId: unshare.GetRootlessUID(), } +} - // potentially use both system and per-user dirs if not using per-user config file - if ctx != nil && ctx.SystemRegistriesConfDirPath != "" { - // dir explicitly chosen: use only that one - wrapper.configDirPath = ctx.SystemRegistriesConfDirPath - } else if ctx != nil && ctx.RootForImplicitAbsolutePaths != "" { - wrapper.configDirPath = filepath.Join(ctx.RootForImplicitAbsolutePaths, systemRegistriesConfDirPath) - wrapper.userConfigDirPath = userRegistriesDirPath - } else { - wrapper.configDirPath = systemRegistriesConfDirPath - wrapper.userConfigDirPath = userRegistriesDirPath +// newConfigWrapper returns a configWrapper for the specified SystemContext. +func newConfigWrapper(ctx *types.SystemContext) configWrapper { + var wrapper configWrapper + if ctx != nil { + wrapper.systemRegistriesConfPath = ctx.SystemRegistriesConfPath + wrapper.systemRegistriesConfDirPath = ctx.SystemRegistriesConfDirPath + wrapper.rootForImplicitAbsolutePaths = ctx.RootForImplicitAbsolutePaths } return wrapper } -// ConfigurationSourceDescription returns a string containers paths of registries.conf and registries.conf.d +// ConfigurationSourceDescription returns a string of one or more paths to registries.conf and registries.conf.d. +// This may return an empty string in case it fails to resolve all paths. func ConfigurationSourceDescription(ctx *types.SystemContext) string { - wrapper := newConfigWrapper(ctx) - configSources := []string{wrapper.configPath} - if wrapper.configDirPath != "" { - configSources = append(configSources, wrapper.configDirPath) - } - if wrapper.userConfigDirPath != "" { - configSources = append(configSources, wrapper.userConfigDirPath) + configWrapper := newConfigWrapper(ctx) + paths, err := configfile.GetSearchPaths(configWrapper.toConfigFileOptions()) + if err != nil { + return "" } - return strings.Join(configSources, ", ") + return fmt.Sprintf("%q", slices.Concat(paths.MainFiles, paths.DropInDirectories)) } // configMutex is used to synchronize concurrent accesses to configCache. @@ -647,58 +628,7 @@ func getConfig(ctx *types.SystemContext) (*parsedConfig, error) { } configMutex.Unlock() - return tryUpdatingCache(ctx, wrapper) -} - -// dropInConfigs returns a slice of drop-in-configs from the registries.conf.d -// directory. -func dropInConfigs(wrapper configWrapper) ([]string, error) { - var ( - configs []string - dirPaths []string - ) - if wrapper.configDirPath != "" { - dirPaths = append(dirPaths, wrapper.configDirPath) - } - if wrapper.userConfigDirPath != "" { - dirPaths = append(dirPaths, wrapper.userConfigDirPath) - } - for _, dirPath := range dirPaths { - 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 == nil: - // this should only happen when err != nil but let's be sure - return nil - 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") { - configs = append(configs, path) - } - return nil - } - }, - ) - - if err != nil && !os.IsNotExist(err) { - // Ignore IsNotExist errors: most systems won't have a registries.conf.d - // directory. - return nil, fmt.Errorf("reading registries.conf.d: %w", err) - } - } - - return configs, nil + return tryUpdatingCache(wrapper) } // TryUpdatingCache loads the configuration from the provided `SystemContext` @@ -707,7 +637,7 @@ func dropInConfigs(wrapper configWrapper) ([]string, error) { // It returns the resulting configuration; this is DEPRECATED and may not correctly // reflect any future data handled by this package. func TryUpdatingCache(ctx *types.SystemContext) (*V2RegistriesConf, error) { - config, err := tryUpdatingCache(ctx, newConfigWrapper(ctx)) + config, err := tryUpdatingCache(newConfigWrapper(ctx)) if err != nil { return nil, err } @@ -716,47 +646,31 @@ func TryUpdatingCache(ctx *types.SystemContext) (*V2RegistriesConf, error) { // tryUpdatingCache implements TryUpdatingCache with an additional configWrapper // argument to avoid redundantly calculating the config paths. -func tryUpdatingCache(ctx *types.SystemContext, wrapper configWrapper) (*parsedConfig, error) { +func tryUpdatingCache(wrapper configWrapper) (*parsedConfig, error) { configMutex.Lock() defer configMutex.Unlock() - // load the config - config, err := loadConfigFile(wrapper.configPath, false) - if err != nil { - // Continue with an empty []Registry if we use the default config, which - // implies that the config path of the SystemContext isn't set. - // - // Note: if ctx.SystemRegistriesConfPath points to the default config, - // we will still return an error. - if os.IsNotExist(err) && (ctx == nil || ctx.SystemRegistriesConfPath == "") { - config = &parsedConfig{} - config.partialV2 = V2RegistriesConf{Registries: []Registry{}} - config.aliasCache, err = newShortNameAliasCache("", &shortNameAliasConf{}) - if err != nil { - return nil, err // Should never happen - } - } else { - return nil, fmt.Errorf("loading registries configuration %q: %w", wrapper.configPath, err) - } + config := &parsedConfig{ + partialV2: V2RegistriesConf{Registries: []Registry{}}, } - // Load the configs from the conf directory path. - dinConfigs, err := dropInConfigs(wrapper) + var err error + config.aliasCache, err = newShortNameAliasCache("", &shortNameAliasConf{}) if err != nil { - return nil, err + return nil, err // Should never happen } - for _, path := range dinConfigs { - // Enforce v2 format for drop-in-configs. - dropIn, err := loadConfigFile(path, true) + + for item, err := range configfile.Read(wrapper.toConfigFileOptions()) { if err != nil { - if errors.Is(err, fs.ErrNotExist) { - // file must have been removed between the directory listing - // and the open call, ignore that as it is a expected race - continue - } - return nil, fmt.Errorf("loading drop-in registries configuration %q: %w", path, err) + return nil, err } - config.updateWithConfigurationFrom(dropIn) + logrus.Debugf("Loading registries configuration %q", item.Name) + + parsed, err := loadConfigFile(item) + if err != nil { + return nil, fmt.Errorf("loading registries configuration %q: %w", item.Name, err) + } + config.updateWithConfigurationFrom(parsed) } if config.shortNameMode == types.ShortNameModeInvalid { @@ -947,42 +861,26 @@ func findRegistryWithParsedConfig(config *parsedConfig, ref string) (*Registry, } // loadConfigFile loads and unmarshals a single config file. -// Use forceV2 if the config must in the v2 format. -func loadConfigFile(path string, forceV2 bool) (*parsedConfig, error) { - logrus.Debugf("Loading registries configuration %q", path) - +func loadConfigFile(item *configfile.Item) (*parsedConfig, error) { // tomlConfig allows us to unmarshal either V1 or V2 simultaneously. type tomlConfig struct { V2RegistriesConf - V1RegistriesConf // for backwards compatibility with sysregistries v1 + V1RegistriesConf // to detect no-longer-supported use of the v1 format } // Load the tomlConfig. Note that `DecodeFile` will overwrite set fields. var combinedTOML tomlConfig - meta, err := toml.DecodeFile(path, &combinedTOML) + meta, err := toml.NewDecoder(item.Reader).Decode(&combinedTOML) if err != nil { return nil, err } if keys := meta.Undecoded(); len(keys) > 0 { - logrus.Debugf("Failed to decode keys %q from %q", keys, path) + logrus.Debugf("Failed to decode keys %q from %q", keys, item.Name) } if combinedTOML.V1RegistriesConf.hasSetField() { - // Enforce the v2 format if requested. - if forceV2 { - return nil, &InvalidRegistries{s: "registry must be in v2 format but is in v1"} - } - - // Convert a v1 config into a v2 config. - if combinedTOML.V2RegistriesConf.hasSetField() { - return nil, &InvalidRegistries{s: fmt.Sprintf("mixing sysregistry v1/v2 is not supported: %#v", combinedTOML)} - } - converted, err := combinedTOML.V1RegistriesConf.ConvertToV2() - if err != nil { - return nil, err - } - combinedTOML.V1RegistriesConf = V1RegistriesConf{} - combinedTOML.V2RegistriesConf = *converted + // V1 format is no longer supported, produce hard error so callers know they must update the config. + return nil, &InvalidRegistries{s: "registries.conf must be in v2 format but is in v1"} } res := parsedConfig{partialV2: combinedTOML.V2RegistriesConf} @@ -992,7 +890,7 @@ func loadConfigFile(path string, forceV2 bool) (*parsedConfig, error) { return nil, err } - res.unqualifiedSearchRegistriesOrigin = path + res.unqualifiedSearchRegistriesOrigin = item.Name if len(res.partialV2.ShortNameMode) > 0 { mode, err := parseShortNameMode(res.partialV2.ShortNameMode) @@ -1016,7 +914,7 @@ func loadConfigFile(path string, forceV2 bool) (*parsedConfig, error) { } // Parse and validate short-name aliases. - cache, err := newShortNameAliasCache(path, &res.partialV2.shortNameAliasConf) + cache, err := newShortNameAliasCache(item.Name, &res.partialV2.shortNameAliasConf) if err != nil { return nil, fmt.Errorf("validating short-name aliases: %w", err) } diff --git a/image/pkg/sysregistriesv2/system_registries_v2_test.go b/image/pkg/sysregistriesv2/system_registries_v2_test.go index f5b08094ad..72007b0597 100644 --- a/image/pkg/sysregistriesv2/system_registries_v2_test.go +++ b/image/pkg/sysregistriesv2/system_registries_v2_test.go @@ -238,66 +238,60 @@ func TestRefMatchingPrefix(t *testing.T) { } func TestNewConfigWrapper(t *testing.T) { - const nondefaultPath = "/this/is/not/the/default/registries.conf" - const variableReference = "$HOME" - const rootPrefix = "/root/prefix" - tempHome := t.TempDir() - userRegistriesFile := filepath.FromSlash(".config/containers/registries.conf") - userRegistriesFilePath := filepath.Join(tempHome, userRegistriesFile) - - for _, c := range []struct { - sys *types.SystemContext - userfilePresent bool - expected string + for _, tt := range []struct { + name string + sys *types.SystemContext + want configWrapper }{ - // The common case - {nil, false, systemRegistriesConfPath}, - // There is a context, but it does not override the path. - {&types.SystemContext{}, false, systemRegistriesConfPath}, - // Path overridden - {&types.SystemContext{SystemRegistriesConfPath: nondefaultPath}, false, nondefaultPath}, - // Root overridden { - &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, - false, - filepath.Join(rootPrefix, systemRegistriesConfPath), + name: "nil context", + sys: nil, + want: configWrapper{}, }, - // Root and path overrides present simultaneously, { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - SystemRegistriesConfPath: nondefaultPath, + name: "empty context", + sys: &types.SystemContext{}, + want: configWrapper{}, + }, + { + name: "RootForImplicitAbsolutePaths", + sys: &types.SystemContext{ + RootForImplicitAbsolutePaths: "root", }, - false, - nondefaultPath, + want: configWrapper{rootForImplicitAbsolutePaths: "root"}, }, - // User registries file overridden - {&types.SystemContext{}, true, userRegistriesFilePath}, - // Context and user User registries file preset simultaneously - {&types.SystemContext{SystemRegistriesConfPath: nondefaultPath}, true, nondefaultPath}, - // Root and user registries file overrides present simultaneously, { - &types.SystemContext{ - RootForImplicitAbsolutePaths: rootPrefix, - SystemRegistriesConfPath: nondefaultPath, + name: "SystemRegistriesConfPath", + sys: &types.SystemContext{ + SystemRegistriesConfPath: "somepath", + }, + want: configWrapper{systemRegistriesConfPath: "somepath"}, + }, + { + name: "SystemRegistriesConfDirPath", + sys: &types.SystemContext{ + SystemRegistriesConfDirPath: "somepath", + }, + want: configWrapper{systemRegistriesConfDirPath: "somepath"}, + }, + { + name: "all fields", + sys: &types.SystemContext{ + RootForImplicitAbsolutePaths: "a", + SystemRegistriesConfPath: "b", + SystemRegistriesConfDirPath: "c", + }, + want: configWrapper{ + rootForImplicitAbsolutePaths: "a", + systemRegistriesConfPath: "b", + systemRegistriesConfDirPath: "c", }, - true, - nondefaultPath, }, - // No environment expansion happens in the overridden paths - {&types.SystemContext{SystemRegistriesConfPath: variableReference}, false, variableReference}, } { - if c.userfilePresent { - err := os.MkdirAll(filepath.Dir(userRegistriesFilePath), os.ModePerm) - require.NoError(t, err) - f, err := os.Create(userRegistriesFilePath) - require.NoError(t, err) - f.Close() - } else { - os.Remove(userRegistriesFilePath) - } - path := newConfigWrapperWithHomeDir(c.sys, tempHome).configPath - assert.Equal(t, c.expected, path) + t.Run(tt.name, func(t *testing.T) { + got := newConfigWrapper(tt.sys) + assert.Equal(t, tt.want, got) + }) } } @@ -461,38 +455,12 @@ func TestUnmarshalConfig(t *testing.T) { assert.Equal(t, 4, len(registries)) } -func TestV1BackwardsCompatibility(t *testing.T) { - sys := &types.SystemContext{ - SystemRegistriesConfPath: "testdata/v1-compatibility.conf", - SystemRegistriesConfDirPath: "testdata/this-does-not-exist", - } - - registries, err := GetRegistries(sys) - assert.Nil(t, err) - assert.Equal(t, 4, len(registries)) - - unqRegs, err := UnqualifiedSearchRegistries(sys) - assert.Nil(t, err) - assert.Equal(t, []string{"registry-a.com", "registry-c.com", "registry-d.com"}, unqRegs) - - // check if merging works - reg, err := FindRegistry(sys, "registry-b.com/bar/foo/barfoo:latest") - assert.Nil(t, err) - assert.NotNil(t, reg) - assert.True(t, reg.Insecure) - assert.True(t, reg.Blocked) - - for _, c := range []string{"testdata/v1-invalid-block.conf", "testdata/v1-invalid-insecure.conf", "testdata/v1-invalid-search.conf"} { - _, err := GetRegistries(&types.SystemContext{ - SystemRegistriesConfPath: c, - SystemRegistriesConfDirPath: "testdata/this-does-not-exist", - }) - assert.Error(t, err, c) - } -} - -func TestMixingV1andV2(t *testing.T) { +func TestV1SyntaxErrors(t *testing.T) { for _, c := range []string{ + "testdata/v1-compatibility.conf", + "testdata/v1-invalid-block.conf", + "testdata/v1-invalid-insecure.conf", + "testdata/v1-invalid-search.conf", "testdata/mixing-v1-v2.conf", "testdata/mixing-v1-v2-empty.conf", } { @@ -500,7 +468,7 @@ func TestMixingV1andV2(t *testing.T) { SystemRegistriesConfPath: c, SystemRegistriesConfDirPath: "testdata/this-does-not-exist", }) - assert.ErrorContains(t, err, "mixing sysregistry v1/v2 is not supported", c) + assert.ErrorContains(t, err, "registries.conf must be in v2 format but is in v1", c) } } diff --git a/storage/pkg/configfile/parse.go b/storage/pkg/configfile/parse.go index b80ed3fbf5..ad09f98fee 100644 --- a/storage/pkg/configfile/parse.go +++ b/storage/pkg/configfile/parse.go @@ -57,6 +57,25 @@ type File struct { // NOTE: This does NOT affect paths starting by $HOME or environment variables paths. RootForImplicitAbsolutePaths string + // CustomConfigFilePath is the path to a specific file that will be parsed as main file instead + // of the default location files. Unlike the regular parsing logic if set this file must exists + // or ErrNotExist will be returned. Note when just using this option without also + // CustomConfigFileDropInDirectory it means the regular drop in directories are still searched + // assuming DoNotLoadDropInFiles is not set. + // This has higher priority over the EnvironmentName variable, so if set the env is ignored. + // RootForImplicitAbsolutePaths will not be used for this path. + // Optional. + CustomConfigFilePath string + + // CustomConfigFileDropInDirectory is the path to a specific drop in directory that will be searched + // instead of the default location. Note when just using this option without also + // CustomConfigFilePath it means the regular main file location is still being read assuming + // DoNotLoadMainFiles is not set. + // This has higher priority over the EnvironmentName + "_OVERRIDE" variable, so if set the env is ignored. + // RootForImplicitAbsolutePaths will not be used for this path. + // Optional. + CustomConfigFileDropInDirectory string + // DoNotLoadMainFiles should be set if only the Drop In files should be loaded. DoNotLoadMainFiles bool @@ -90,19 +109,40 @@ type Item struct { Name string } -func getConfName(name, extension string, noExtension bool) string { - if noExtension { - return name +type SearchPaths struct { + // MainFiles are the main config file paths, ordered from highest priority to lower ones. + // For example: $HOME/..., then /etc/..., then /usr/... + // Can be empty if there are no main files for the given config. + MainFiles []string + // DropInDirectories is the list of drop in directories read by this config file, again + // ordered from highest priority to lower ones. + // Can be empty if there are no drop in directories for the given config. + DropInDirectories []string + // ModuleDirectories is the list of module directories checked by this config file, again + // ordered from highest priority to lower ones. + // Will be empty if no modules were request for the given conf. + ModuleDirectories []string + // The file path from conf.EnvironmentName + "_OVERRIDE" env if it must be parsed for the given config. + // Can be empty. + ExtraOverrideFile string +} + +func (f *File) getConfName() string { + if f.DoNotUseExtensionForConfigName { + return f.Name } - return name + "." + extension + return f.Name + "." + f.Extension } -// Read parses all config files with the specified options and returns an iterator which returns all files as Item in the right order. -// If an error is returned by the iterator then this must be treated as fatal error and must fail the config file parsing. -// Expected ENOENT errors are already ignored in this function and must not be handled again by callers. -// The given File options must not be nil and populated with valid options. -func Read(conf *File) iter.Seq2[*Item, error] { - configFileName := getConfName(conf.Name, conf.Extension, conf.DoNotUseExtensionForConfigName) +// GetSearchPaths returns the list of files which will be tried to be parsed. +// See the doc of [SearchPaths] for more information. +func GetSearchPaths(conf *File) (SearchPaths, error) { + paths, _, err := getSearchPaths(conf) + return paths, err +} + +func getSearchPaths(conf *File) (SearchPaths, bool, error) { + configFileName := conf.getConfName() // Note this can be empty which is a valid case and should be simply ignored then. defaultConfig := systemConfigPath @@ -122,13 +162,95 @@ func Read(conf *File) iter.Seq2[*Item, error] { } } + // userConfig can be empty as well + userConfig, err := UserConfigPath() + if err != nil { + return SearchPaths{}, false, err + } + if userConfig != "" { + userConfig = filepath.Join(userConfig, configFileName) + } + + // main files + ignoreENOENT := true + shouldLoadDropIns := true + var mainFiles []string + if !conf.DoNotLoadMainFiles { + if conf.CustomConfigFilePath != "" { + mainFiles = append(mainFiles, conf.CustomConfigFilePath) + ignoreENOENT = false + // Only consider the env if no custom path was explicitly set. + // As this path often comes from cli options it is important it wins over the env value. + } else if path := os.Getenv(conf.EnvironmentName); path != "" && conf.EnvironmentName != "" { + mainFiles = append(mainFiles, path) + ignoreENOENT = false + // Also when the env is set skip the loading of drop in files, modules and _OVERRIDE env are still read though. + shouldLoadDropIns = false + } else { + // default search paths + if userConfig != "" { + mainFiles = append(mainFiles, userConfig) + } + if overrideConfig != "" { + mainFiles = append(mainFiles, overrideConfig) + } + if defaultConfig != "" { + mainFiles = append(mainFiles, defaultConfig) + } + } + } + + // drop in dirs + var dropInDirs []string + var extraOverrideFilePath string + if !conf.DoNotLoadDropInFiles { + if shouldLoadDropIns { + if conf.CustomConfigFileDropInDirectory != "" { + dropInDirs = append(dropInDirs, conf.CustomConfigFileDropInDirectory) + } else { + // default search paths + dropInDirs = getDropInPaths(defaultConfig, overrideConfig, userConfig, "."+conf.Extension, conf.UserId) + } + } + + if conf.EnvironmentName != "" && conf.CustomConfigFileDropInDirectory == "" { + if path := os.Getenv(conf.EnvironmentName + "_OVERRIDE"); path != "" { + extraOverrideFilePath = path + } + } + } + + // modules + var modDirs []string + if len(conf.Modules) > 0 { + modDirs = moduleDirectories(defaultConfig, overrideConfig, userConfig) + } + + return SearchPaths{ + MainFiles: mainFiles, + DropInDirectories: dropInDirs, + ModuleDirectories: modDirs, + ExtraOverrideFile: extraOverrideFilePath, + }, + ignoreENOENT, + nil +} + +// Read parses all config files with the specified options and returns an iterator which returns all files as Item in the right order. +// If an error is returned by the iterator then this must be treated as fatal error and must fail the config file parsing. +// Expected ENOENT errors are already ignored in this function and must not be handled again by callers. +// The given File options must not be nil and populated with valid options. +func Read(conf *File) iter.Seq2[*Item, error] { return func(yield func(*Item, error) bool) { + paths, ignoreMainENOENT, err := getSearchPaths(conf) + if err != nil { + yield(nil, err) + return + } + usedPaths := make([]string, 0, 8) foundAny := false - shouldLoadMainFile := !conf.DoNotLoadMainFiles - shouldLoadDropIns := !conf.DoNotLoadDropInFiles - yieldAndClose := func(f *os.File) bool { foundAny = true ok := yield(&Item{ @@ -146,61 +268,31 @@ func Read(conf *File) iter.Seq2[*Item, error] { return ok } - if conf.EnvironmentName != "" { - if path := os.Getenv(conf.EnvironmentName); path != "" { - usedPaths = append(usedPaths, path) - f, err := os.Open(path) - // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. - if err != nil { - yield(nil, err) - return - } - if !yieldAndClose(f) { - return - } - // Also when the env is set skip the loading of the main and drop in files, modules and _OVERRIDE env are still read though. - shouldLoadMainFile = false - shouldLoadDropIns = false + for _, path := range paths.MainFiles { + if path == "" { + continue } - } - - // userConfig can be empty as well - userConfig, err := UserConfigPath() - if err != nil { - // return error via iterator - yield(nil, err) - return - } - if userConfig != "" { - userConfig = filepath.Join(userConfig, configFileName) - } - - if shouldLoadMainFile { - for _, path := range []string{userConfig, overrideConfig, defaultConfig} { - if path == "" { + usedPaths = append(usedPaths, path) + f, err := os.Open(path) + if err != nil { + // only ignore ErrNotExist when needed, all other errors get return to the caller via yield + if ignoreMainENOENT && errors.Is(err, fs.ErrNotExist) { continue } - usedPaths = append(usedPaths, path) - f, err := os.Open(path) - // only ignore ErrNotExist, all other errors get return to the caller via yield - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - continue - } - yield(nil, err) - return - } + yield(nil, err) + return + } - if !yieldAndClose(f) { - return - } - // we only read the first file - break + if !yieldAndClose(f) { + return } + // we only read the first found file + break } - if shouldLoadDropIns { - files, err := readDropIns(defaultConfig, overrideConfig, userConfig, conf.Extension, conf.UserId) + if len(paths.DropInDirectories) > 0 { + suffix := "." + conf.Extension + files, err := readDropInsFromPaths(paths.DropInDirectories, suffix) if err != nil { // return error via iterator yield(nil, err) @@ -209,7 +301,7 @@ func Read(conf *File) iter.Seq2[*Item, error] { for _, file := range files { usedPaths = append(usedPaths, file) f, err := os.Open(file) - // only ignore ErrNotExist, all other errors get return to the caller via yield + // always ignore ErrNotExist, all other errors get return to the caller via yield if err != nil { if errors.Is(err, fs.ErrNotExist) { continue @@ -225,10 +317,9 @@ func Read(conf *File) iter.Seq2[*Item, error] { } if len(conf.Modules) > 0 { - dirs := moduleDirectories(defaultConfig, overrideConfig, userConfig) resolvedModules := make([]string, 0, len(conf.Modules)) for _, module := range conf.Modules { - f, err := resolveModule(module, dirs, &usedPaths) + f, err := resolveModule(module, paths.ModuleDirectories, &usedPaths) if err != nil { yield(nil, fmt.Errorf("could not resolve module: %w", err)) return @@ -241,24 +332,22 @@ func Read(conf *File) iter.Seq2[*Item, error] { conf.Modules = resolvedModules } - if conf.EnvironmentName != "" && !conf.DoNotLoadDropInFiles { + if paths.ExtraOverrideFile != "" { // The _OVERRIDE env must be appended after loading all files, even modules. - if path := os.Getenv(conf.EnvironmentName + "_OVERRIDE"); path != "" { - usedPaths = append(usedPaths, path) - f, err := os.Open(path) - // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. - if err != nil { - yield(nil, err) - return - } - if !yieldAndClose(f) { - return - } + usedPaths = append(usedPaths, paths.ExtraOverrideFile) + f, err := os.Open(paths.ExtraOverrideFile) + // Do not ignore ErrNotExist here, we want to hard error if users set a wrong path here. + if err != nil { + yield(nil, err) + return + } + if !yieldAndClose(f) { + return } } if conf.ErrorIfNotFound && !foundAny { - yield(nil, fmt.Errorf("%w: no %s file found; searched paths: %q", ErrConfigFileNotFound, configFileName, usedPaths)) + yield(nil, fmt.Errorf("%w: no %s file found; searched paths: %q", ErrConfigFileNotFound, conf.getConfName(), usedPaths)) return } } @@ -266,24 +355,27 @@ func Read(conf *File) iter.Seq2[*Item, error] { const dropInSuffix = ".d" -func readDropIns(defaultConfig, overrideConfig, userConfig, extension string, uid int) ([]string, error) { - dropInMap := make(map[string]string) +func getDropInPaths(defaultConfig, overrideConfig, userConfig, suffix string, uid int) []string { paths := make([]string, 0, 7) - suffix := "." + extension - - if defaultConfig != "" { - paths = append(paths, getDropInPaths(defaultConfig, suffix, uid)...) - } - if overrideConfig != "" { - paths = append(paths, getDropInPaths(overrideConfig, suffix, uid)...) - } if userConfig != "" { // the $HOME config only has one .d path not the rootful/rootless ones. paths = append(paths, userConfig+dropInSuffix) } + if overrideConfig != "" { + paths = append(paths, getDropInPathsUnderMain(overrideConfig, suffix, uid)...) + } + if defaultConfig != "" { + paths = append(paths, getDropInPathsUnderMain(defaultConfig, suffix, uid)...) + } + + return paths +} + +func readDropInsFromPaths(paths []string, suffix string) ([]string, error) { + dropInMap := make(map[string]string) - for _, path := range paths { + for _, path := range slices.Backward(paths) { entries, err := os.ReadDir(path) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -306,7 +398,7 @@ func readDropIns(defaultConfig, overrideConfig, userConfig, extension string, ui return files, nil } -func getDropInPaths(mainPath, suffix string, uid int) []string { +func getDropInPathsUnderMain(mainPath, suffix string, uid int) []string { paths := make([]string, 0, 3) paths = append(paths, mainPath+dropInSuffix) diff --git a/storage/pkg/configfile/parse_test.go b/storage/pkg/configfile/parse_test.go index 09b9446915..3c94751da1 100644 --- a/storage/pkg/configfile/parse_test.go +++ b/storage/pkg/configfile/parse_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func Test_getDropInPaths(t *testing.T) { +func Test_getDropInPathsUnderMain(t *testing.T) { tests := []struct { name string // Arguments for this function @@ -89,7 +89,7 @@ func Test_getDropInPaths(t *testing.T) { t.Parallel() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := getDropInPaths(tt.mainPath, tt.suffix, tt.uid) + got := getDropInPathsUnderMain(tt.mainPath, tt.suffix, tt.uid) assert.Equal(t, tt.want, got) }) } @@ -558,6 +558,206 @@ func Test_Read(t *testing.T) { // CONTAINERS_CONF, then modules, then CONTAINERS_CONF_OVERRIDE want: []string{"env1", "mod", "env2"}, }, + { + name: "CustomConfigFilePath with drop in", + arg: File{ + Name: "containers", + Extension: "conf", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + file := filepath.Join(t.TempDir(), "somepath") + err := os.WriteFile(file, []byte("custom"), 0o600) + require.NoError(t, err) + + tc.arg.CustomConfigFilePath = file + }, + want: []string{"custom", "drop in"}, + }, + { + name: "CustomConfigFilePath ENOENT", + arg: File{ + Name: "containers", + Extension: "conf", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + file := filepath.Join(t.TempDir(), "IDoNotExist") + + tc.arg.CustomConfigFilePath = file + }, + wantErr: fs.ErrNotExist, + }, + { + name: "CustomConfigFilePath must win over env", + arg: File{ + Name: "containers", + Extension: "conf", + EnvironmentName: "CONTAINERS_CONF", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + file := filepath.Join(t.TempDir(), "file") + err := os.WriteFile(file, []byte("explicit path"), 0o600) + require.NoError(t, err) + tc.arg.CustomConfigFilePath = file + + file1 := filepath.Join(t.TempDir(), "path1") + err = os.WriteFile(file1, []byte("env"), 0o600) + require.NoError(t, err) + t.Setenv("CONTAINERS_CONF", file1) + }, + want: []string{"explicit path", "drop in"}, + }, + { + name: "CustomConfigFileDropInDirectory with main file", + arg: File{ + Name: "containers", + Extension: "conf", + }, + + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "file.conf"), []byte("custom"), 0o600) + require.NoError(t, err) + + // write a second file without .conf which should not be parsed + err = os.WriteFile(filepath.Join(dir, "somefile"), []byte("somefile"), 0o600) + require.NoError(t, err) + + tc.arg.CustomConfigFileDropInDirectory = dir + }, + want: []string{"main", "custom"}, + }, + { + name: "CustomConfigFileDropInDirectory does not error with ENOENT", + arg: File{ + Name: "containers", + Extension: "conf", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + dir := filepath.Join(t.TempDir(), "dirDoesNotExist") + tc.arg.CustomConfigFileDropInDirectory = dir + }, + want: []string{"main"}, + }, + { + name: "CustomConfigFileDropInDirectory must win over env", + arg: File{ + Name: "containers", + Extension: "conf", + EnvironmentName: "CONTAINERS_CONF", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "file.conf"), []byte("explicit dir"), 0o600) + require.NoError(t, err) + + tc.arg.CustomConfigFileDropInDirectory = dir + + file2 := filepath.Join(t.TempDir(), "path1") + err = os.WriteFile(file2, []byte("env"), 0o600) + require.NoError(t, err) + t.Setenv("CONTAINERS_CONF_OVERRIDE", file2) + }, + want: []string{"main", "explicit dir"}, + }, + { + name: "CustomConfigFilePath and CustomConfigFileDropInDirectory", + arg: File{ + Name: "containers", + Extension: "conf", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + file := filepath.Join(t.TempDir(), "file") + err := os.WriteFile(file, []byte("custom main"), 0o600) + require.NoError(t, err) + tc.arg.CustomConfigFilePath = file + + dir := t.TempDir() + err = os.WriteFile(filepath.Join(dir, "file.conf"), []byte("custom dir"), 0o600) + require.NoError(t, err) + + tc.arg.CustomConfigFileDropInDirectory = dir + }, + want: []string{"custom main", "custom dir"}, + }, + { + name: "CustomConfigFilePath and CustomConfigFileDropInDirectory must win over envs", + arg: File{ + Name: "containers", + Extension: "conf", + EnvironmentName: "CONTAINERS_CONF", + }, + files: testfiles{ + usr: map[string]string{ + "containers.conf": "main", + "containers.conf.d/10-myconf.conf": "drop in", + }, + }, + setup: func(t *testing.T, tc *testcase) { + file := filepath.Join(t.TempDir(), "file") + err := os.WriteFile(file, []byte("explicit path"), 0o600) + require.NoError(t, err) + tc.arg.CustomConfigFilePath = file + + file1 := filepath.Join(t.TempDir(), "path1") + err = os.WriteFile(file1, []byte("main env"), 0o600) + require.NoError(t, err) + t.Setenv("CONTAINERS_CONF", file1) + + dir := t.TempDir() + err = os.WriteFile(filepath.Join(dir, "file.conf"), []byte("explicit dir"), 0o600) + require.NoError(t, err) + + tc.arg.CustomConfigFileDropInDirectory = dir + + file2 := filepath.Join(t.TempDir(), "path1") + err = os.WriteFile(file2, []byte("override env"), 0o600) + require.NoError(t, err) + t.Setenv("CONTAINERS_CONF_OVERRIDE", file2) + }, + want: []string{"explicit path", "explicit dir"}, + }, } for _, tt := range tests { @@ -741,3 +941,57 @@ func Test_ParseTOML(t *testing.T) { }) } } + +func TestGetSearchPaths(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", "/home") + tests := []struct { + name string + conf File + want SearchPaths + }{ + { + name: "basic containers.conf", + conf: File{ + Name: "containers", + Extension: "conf", + }, + want: SearchPaths{ + MainFiles: []string{ + "/home/containers/containers.conf", + adminOverrideConfigPath + "/containers.conf", + systemConfigPath + "/containers.conf", + }, + DropInDirectories: []string{ + "/home/containers/containers.conf.d", + adminOverrideConfigPath + "/containers.conf.d", + adminOverrideConfigPath + "/containers.rootful.conf.d", + systemConfigPath + "/containers.conf.d", + systemConfigPath + "/containers.rootful.conf.d", + }, + }, + }, + { + name: "basic policy.json", + conf: File{ + Name: "policy", + Extension: "json", + DoNotLoadDropInFiles: true, + }, + want: SearchPaths{ + MainFiles: []string{ + "/home/containers/policy.json", + adminOverrideConfigPath + "/policy.json", + systemConfigPath + "/policy.json", + }, + }, + }, + // TODO: add more test cases + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetSearchPaths(&tt.conf) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +}