From 25f155861eb400c7e8d22542a79f9e9d59dd86f2 Mon Sep 17 00:00:00 2001 From: Harshal Patil Date: Thu, 11 Apr 2024 15:13:10 -0400 Subject: [PATCH] Add support for drop-in config Signed-off-by: Harshal Patil --- store.go | 2 +- types/options.go | 72 +++++++++++++++++++++++++++++++++++----- types/options_darwin.go | 2 -- types/options_freebsd.go | 5 --- types/options_linux.go | 5 --- types/options_test.go | 61 ++++++++++++++++++++++++++++++++-- types/options_windows.go | 5 --- types/utils.go | 2 +- 8 files changed, 124 insertions(+), 30 deletions(-) diff --git a/store.go b/store.go index 253218e5ad..39b2d72e62 100644 --- a/store.go +++ b/store.go @@ -3710,7 +3710,7 @@ func DefaultConfigFile() (string, error) { // the configuration in storeOptions. // Deprecated: Use types.ReloadConfigurationFile, which can return an error. func ReloadConfigurationFile(configFile string, storeOptions *types.StoreOptions) { - _ = types.ReloadConfigurationFile(configFile, storeOptions) + _ = types.ReloadConfigurationFile(configFile, storeOptions, true) } // GetDefaultMountOptions returns the default mountoptions defined in container/storage diff --git a/types/options.go b/types/options.go index 03e5f7ab64..314cae0413 100644 --- a/types/options.go +++ b/types/options.go @@ -49,6 +49,12 @@ var ( defaultConfigFile = SystemConfigFile // DefaultStoreOptions is a reasonable default set of options. defaultStoreOptions StoreOptions + + // defaultOverrideConfigFile path to override the default system wide storage.conf file + defaultOverrideConfigFile = "/etc/containers/storage.conf" + + // defaultDropInConfigDir path to the folder containing drop in config files + defaultDropInConfigDir = defaultOverrideConfigFile + ".d" ) func loadDefaultStoreOptions() { @@ -114,11 +120,53 @@ func loadDefaultStoreOptions() { // loadStoreOptions returns the default storage ops for containers func loadStoreOptions() (StoreOptions, error) { - storageConf, err := DefaultConfigFile() + baseConf, err := DefaultConfigFile() + if err != nil { + return defaultStoreOptions, err + } + + // Load the base config file + baseOptions, err := loadStoreOptionsFromConfFile(baseConf) if err != nil { return defaultStoreOptions, err } - return loadStoreOptionsFromConfFile(storageConf) + + if _, err := os.Stat(defaultDropInConfigDir); err == nil { + // The directory exists, so merge the configuration from this directory + err = mergeConfigFromDirectory(&baseOptions, defaultDropInConfigDir) + if err != nil { + return defaultStoreOptions, err + } + } else if !os.IsNotExist(err) { + // There was an error other than the directory not existing + return defaultStoreOptions, err + } + + return baseOptions, nil +} + +func mergeConfigFromDirectory(baseOptions *StoreOptions, configDir string) error { + return filepath.Walk(configDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + // Only consider files with .conf extension + if filepath.Ext(path) != ".conf" { + return nil + } + + // Load drop-in options from the current file + err = ReloadConfigurationFile(path, baseOptions, false) + if err != nil { + return err + } + + return nil + }) } // usePerUserStorage returns whether the user private storage must be used. @@ -399,7 +447,7 @@ func ReloadConfigurationFileIfNeeded(configFile string, storeOptions *StoreOptio return nil } - if err := ReloadConfigurationFile(configFile, storeOptions); err != nil { + if err := ReloadConfigurationFile(configFile, storeOptions, true); err != nil { return err } @@ -412,7 +460,7 @@ func ReloadConfigurationFileIfNeeded(configFile string, storeOptions *StoreOptio // ReloadConfigurationFile parses the specified configuration file and overrides // the configuration in storeOptions. -func ReloadConfigurationFile(configFile string, storeOptions *StoreOptions) error { +func ReloadConfigurationFile(configFile string, storeOptions *StoreOptions, initializeOptions bool) error { config := new(TomlConfig) meta, err := toml.DecodeFile(configFile, &config) @@ -428,8 +476,11 @@ func ReloadConfigurationFile(configFile string, storeOptions *StoreOptions) erro } } - // Clear storeOptions of previous settings - *storeOptions = StoreOptions{} + if initializeOptions { + // Clear storeOptions of previous settings + *storeOptions = StoreOptions{} + } + if config.Storage.Driver != "" { storeOptions.GraphDriverName = config.Storage.Driver } @@ -519,8 +570,13 @@ func ReloadConfigurationFile(configFile string, storeOptions *StoreOptions) erro storeOptions.PullOptions = config.Storage.Options.PullOptions } - storeOptions.DisableVolatile = config.Storage.Options.DisableVolatile - storeOptions.TransientStore = config.Storage.TransientStore + if config.Storage.Options.DisableVolatile { + storeOptions.DisableVolatile = config.Storage.Options.DisableVolatile + } + + if config.Storage.TransientStore { + storeOptions.TransientStore = config.Storage.TransientStore + } storeOptions.GraphDriverOptions = append(storeOptions.GraphDriverOptions, cfg.GetGraphDriverOptions(storeOptions.GraphDriverName, config.Storage.Options)...) diff --git a/types/options_darwin.go b/types/options_darwin.go index 3eecc2b827..6084cbd7d5 100644 --- a/types/options_darwin.go +++ b/types/options_darwin.go @@ -8,8 +8,6 @@ const ( SystemConfigFile = "/usr/share/containers/storage.conf" ) -var defaultOverrideConfigFile = "/etc/containers/storage.conf" - // canUseRootlessOverlay returns true if the overlay driver can be used for rootless containers func canUseRootlessOverlay(home, runhome string) bool { return false diff --git a/types/options_freebsd.go b/types/options_freebsd.go index be2bc2f27d..86595afbe6 100644 --- a/types/options_freebsd.go +++ b/types/options_freebsd.go @@ -8,11 +8,6 @@ const ( SystemConfigFile = "/usr/local/share/containers/storage.conf" ) -// defaultConfigFile path to the system wide storage.conf file -var ( - defaultOverrideConfigFile = "/usr/local/etc/containers/storage.conf" -) - // canUseRootlessOverlay returns true if the overlay driver can be used for rootless containers func canUseRootlessOverlay(home, runhome string) bool { return false diff --git a/types/options_linux.go b/types/options_linux.go index a28e82883c..e2d60964c8 100644 --- a/types/options_linux.go +++ b/types/options_linux.go @@ -16,11 +16,6 @@ const ( SystemConfigFile = "/usr/share/containers/storage.conf" ) -// defaultConfigFile path to the system wide storage.conf file -var ( - defaultOverrideConfigFile = "/etc/containers/storage.conf" -) - // canUseRootlessOverlay returns true if the overlay driver can be used for rootless containers func canUseRootlessOverlay(home, runhome string) bool { // we check first for fuse-overlayfs since it is cheaper. diff --git a/types/options_test.go b/types/options_test.go index 26facc7a86..9567de899a 100644 --- a/types/options_test.go +++ b/types/options_test.go @@ -143,7 +143,7 @@ func TestSetRemapUIDsGIDsOpts(t *testing.T) { }, } - err := ReloadConfigurationFile("./storage_test.conf", &remapOpts) + err := ReloadConfigurationFile("./storage_test.conf", &remapOpts, true) require.NoError(t, err) if !reflect.DeepEqual(uidmap, remapOpts.UIDMap) { t.Errorf("Failed to set UIDMap: Expected %v Actual %v", uidmap, remapOpts.UIDMap) @@ -185,7 +185,7 @@ remap-group = "%s" mappings, err := idtools.NewIDMappings(user, user) require.NoError(t, err) - err = ReloadConfigurationFile(configPath, &remapOpts) + err = ReloadConfigurationFile(configPath, &remapOpts, true) require.NoError(t, err) if !reflect.DeepEqual(mappings.UIDs(), remapOpts.UIDMap) { t.Errorf("Failed to set UIDMap: Expected %v Actual %v", mappings.UIDs(), remapOpts.UIDMap) @@ -199,10 +199,65 @@ func TestReloadConfigurationFile(t *testing.T) { content := bytes.NewBufferString("") logrus.SetOutput(content) var storageOpts StoreOptions - err := ReloadConfigurationFile("./storage_broken.conf", &storageOpts) + err := ReloadConfigurationFile("./storage_broken.conf", &storageOpts, true) require.NoError(t, err) assert.Equal(t, storageOpts.RunRoot, "/run/containers/test") logrus.SetOutput(os.Stderr) assert.Equal(t, strings.Contains(content.String(), "Failed to decode the keys [\\\"foo\\\" \\\"storage.options.graphroot\\\"] from \\\"./storage_broken.conf\\\"\""), true) } + +func TestMergeConfigFromDirectory(t *testing.T) { + tempDir, err := os.MkdirTemp("", "testConfigDir") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + // Creating a mix of files with .txt and .conf extensions + fileNames := []string{"config1.conf", "config2.conf", "ignore.txt", "config3.conf", "config4.txt"} + contents := []string{ + `[storage] +runroot = 'temp/run1' +graphroot = 'temp/graph1'`, + `[storage] +runroot = 'temp/run2' +graphroot = 'temp/graph2'`, + `[storage] +runroot = 'should/ignore' +graphroot = 'should/ignore'`, + `[storage] +runroot = 'temp/run3'`, + `[storage] +runroot = 'temp/run4' +graphroot = 'temp/graph4'`, + } + for i, fileName := range fileNames { + filePath := filepath.Join(tempDir, fileName) + if err := os.WriteFile(filePath, []byte(contents[i]), 0o666); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + } + + // Set base options + baseOptions := StoreOptions{ + RunRoot: "initial/run", + GraphRoot: "initial/graph", + TransientStore: true, + } + + // Expected results after merging configurations from only .conf files + expectedOptions := StoreOptions{ + RunRoot: "temp/run3", // Last .conf file (config3.conf) read overrides earlier values + GraphRoot: "temp/graph2", + TransientStore: true, + } + + // Run the merging function + err = mergeConfigFromDirectory(&baseOptions, tempDir) + if err != nil { + t.Fatalf("Error merging config from directory: %v", err) + } + + assert.DeepEqual(t, expectedOptions, baseOptions) +} diff --git a/types/options_windows.go b/types/options_windows.go index c1bea9fac0..6084cbd7d5 100644 --- a/types/options_windows.go +++ b/types/options_windows.go @@ -8,11 +8,6 @@ const ( SystemConfigFile = "/usr/share/containers/storage.conf" ) -// defaultConfigFile path to the system wide storage.conf file -var ( - defaultOverrideConfigFile = "/etc/containers/storage.conf" -) - // canUseRootlessOverlay returns true if the overlay driver can be used for rootless containers func canUseRootlessOverlay(home, runhome string) bool { return false diff --git a/types/utils.go b/types/utils.go index b313a47288..c0911d5f2e 100644 --- a/types/utils.go +++ b/types/utils.go @@ -66,7 +66,7 @@ func reloadConfigurationFileIfNeeded(configFile string, storeOptions *StoreOptio return } - ReloadConfigurationFile(configFile, storeOptions) + ReloadConfigurationFile(configFile, storeOptions, true) prevReloadConfig.storeOptions = storeOptions prevReloadConfig.mod = mtime