diff --git a/docs/config.md b/docs/config.md index 2aa2189..2c4cee1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -71,12 +71,10 @@ plugin: # ignored if plugin.dir is set baseDir: "/path/to/base/dir" -# Default configuration regardless of engine version -default: - # List of plugins to install - plugins: - - store-dynamodb - - store-redis +# List of plugins to install +plugins: + - store-dynamodb + - store-redis # Map of environment variables to set env: diff --git a/internal/plugin/configs_test.go b/internal/plugin/configs_test.go index fd865fa..a0426f3 100644 --- a/internal/plugin/configs_test.go +++ b/internal/plugin/configs_test.go @@ -62,8 +62,8 @@ func TestEnsureConfiguredPlugins(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup viper configuration - viper.Set(defaultPluginsConfigKey, tt.configuredPlugins) - defer viper.Set(defaultPluginsConfigKey, nil) // Clean up + viper.Set(pluginsConfigKey, tt.configuredPlugins) + defer viper.Set(pluginsConfigKey, nil) // Clean up // Test EnsureConfiguredPlugins count, err := EnsureConfiguredPlugins(tt.engineType, tt.version) @@ -78,3 +78,65 @@ func TestEnsureConfiguredPlugins(t *testing.T) { }) } } + +func TestEnsureConfiguredPlugins_DeprecatedKey(t *testing.T) { + configDir, err := os.MkdirTemp(os.TempDir(), "imposter-plugin-configs-deprecated-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(configDir) + config.DirPath = configDir + + tests := []struct { + name string + deprecatedPlugins []string + expectedCount int + }{ + { + name: "deprecated key with plugins", + deprecatedPlugins: []string{"store-redis"}, + expectedCount: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + viper.Set(defaultPluginsConfigKey, tt.deprecatedPlugins) + defer viper.Set(defaultPluginsConfigKey, nil) + + count, err := EnsureConfiguredPlugins(engine.EngineTypeDockerCore, "4.9.1") + if err != nil { + t.Errorf("EnsureConfiguredPlugins() error = %v", err) + return + } + if count != tt.expectedCount { + t.Errorf("EnsureConfiguredPlugins() count = %v, expectedCount %v", count, tt.expectedCount) + } + }) + } +} + +func TestEnsureConfiguredPlugins_BothKeys(t *testing.T) { + configDir, err := os.MkdirTemp(os.TempDir(), "imposter-plugin-configs-both-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(configDir) + config.DirPath = configDir + + viper.Set(pluginsConfigKey, []string{"store-redis"}) + viper.Set(defaultPluginsConfigKey, []string{"store-redis"}) + defer func() { + viper.Set(pluginsConfigKey, nil) + viper.Set(defaultPluginsConfigKey, nil) + }() + + count, err := EnsureConfiguredPlugins(engine.EngineTypeDockerCore, "4.9.1") + if err != nil { + t.Errorf("EnsureConfiguredPlugins() error = %v", err) + return + } + if count != 1 { + t.Errorf("EnsureConfiguredPlugins() count = %v, expected 1 (merged and deduplicated)", count) + } +} diff --git a/internal/plugin/defaults.go b/internal/plugin/defaults.go index c0c79a3..5a677e7 100644 --- a/internal/plugin/defaults.go +++ b/internal/plugin/defaults.go @@ -8,6 +8,9 @@ import ( "path/filepath" ) +const pluginsConfigKey = "plugins" + +// Deprecated: use pluginsConfigKey instead. const defaultPluginsConfigKey = "default.plugins" // addDefaultPlugins adds the provided plugins to the list of default @@ -45,9 +48,20 @@ func ListDefaultPlugins() ([]string, error) { v, err := parseConfigFile() if err != nil { return []string{}, err - } else { - return v.GetStringSlice(defaultPluginsConfigKey), nil } + return getConfiguredPlugins(v), nil +} + +// getConfiguredPlugins reads plugins from both the top-level "plugins" +// key and the deprecated "default.plugins" key, merging and deduplicating. +func getConfiguredPlugins(v *viper.Viper) []string { + plugins := v.GetStringSlice(pluginsConfigKey) + deprecated := v.GetStringSlice(defaultPluginsConfigKey) + if len(deprecated) > 0 { + logger.Warnf("'default.plugins' config key is deprecated; use top-level 'plugins' instead") + plugins = stringutil.CombineUnique(plugins, deprecated) + } + return plugins } func writeDefaultPlugins(plugins []string) error { @@ -55,7 +69,12 @@ func writeDefaultPlugins(plugins []string) error { if err != nil { return err } - v.Set(defaultPluginsConfigKey, plugins) + v.Set(pluginsConfigKey, plugins) + + // clear deprecated nested key so it is not written back + if v.IsSet(defaultPluginsConfigKey) { + v.Set(defaultPluginsConfigKey, []string{}) + } configDir, err := config.GetGlobalConfigDir() if err != nil { diff --git a/internal/plugin/defaults_test.go b/internal/plugin/defaults_test.go index b15b036..84f8b3f 100644 --- a/internal/plugin/defaults_test.go +++ b/internal/plugin/defaults_test.go @@ -29,7 +29,23 @@ func TestListDefaultPlugins(t *testing.T) { expectError: false, }, { - name: "config with plugins", + name: "top-level plugins", + configContent: `plugins: + - store-redis + - js-graal +`, + expectedPlugins: []string{"store-redis", "js-graal"}, + expectError: false, + }, + { + name: "top-level empty plugins list", + configContent: `plugins: [] +`, + expectedPlugins: []string{}, + expectError: false, + }, + { + name: "deprecated default.plugins format", configContent: `default: plugins: - store-redis @@ -39,13 +55,25 @@ func TestListDefaultPlugins(t *testing.T) { expectError: false, }, { - name: "config with empty plugins list", + name: "deprecated default.plugins empty list", configContent: `default: plugins: [] `, expectedPlugins: []string{}, expectError: false, }, + { + name: "both formats merges and deduplicates", + configContent: `plugins: + - store-redis +default: + plugins: + - js-graal + - store-redis +`, + expectedPlugins: []string{"store-redis", "js-graal"}, + expectError: false, + }, } for _, tt := range tests { @@ -321,18 +349,17 @@ func TestParseConfigFile(t *testing.T) { config.DirPath = configDir // Test with non-existent config file - viper, err := parseConfigFile() + v, err := parseConfigFile() if err != nil { t.Errorf("parseConfigFile() with non-existent file error = %v", err) } - if viper == nil { + if v == nil { t.Error("parseConfigFile() returned nil viper instance") } - // Test with existing config file - configContent := `default: - plugins: - - test-plugin + // Test with existing config file using top-level plugins + configContent := `plugins: + - test-plugin ` configFilePath := filepath.Join(configDir, "config.yaml") err = os.WriteFile(configFilePath, []byte(configContent), 0644) @@ -340,17 +367,36 @@ func TestParseConfigFile(t *testing.T) { t.Fatal(err) } - viper, err = parseConfigFile() + v, err = parseConfigFile() if err != nil { t.Errorf("parseConfigFile() with existing file error = %v", err) } - if viper == nil { + if v == nil { t.Error("parseConfigFile() returned nil viper instance") } - // Verify the config was parsed correctly - plugins := viper.GetStringSlice("default.plugins") + plugins := v.GetStringSlice("plugins") if len(plugins) != 1 || plugins[0] != "test-plugin" { t.Errorf("parseConfigFile() parsed plugins incorrectly: got %v, expected [test-plugin]", plugins) } + + // Test with deprecated default.plugins format + configContent = `default: + plugins: + - legacy-plugin +` + err = os.WriteFile(configFilePath, []byte(configContent), 0644) + if err != nil { + t.Fatal(err) + } + + v, err = parseConfigFile() + if err != nil { + t.Errorf("parseConfigFile() with deprecated format error = %v", err) + } + + legacyPlugins := v.GetStringSlice("default.plugins") + if len(legacyPlugins) != 1 || legacyPlugins[0] != "legacy-plugin" { + t.Errorf("parseConfigFile() parsed deprecated plugins incorrectly: got %v, expected [legacy-plugin]", legacyPlugins) + } } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index d739d9c..f957f0d 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -46,19 +46,24 @@ func EnsureConfiguredPlugins(engineType engine.EngineType, version string) (int, // this includes the config from the current configuration context, // not just the global CLI config file, so it includes any // configuration in the working directory - plugins := viper.GetStringSlice(defaultPluginsConfigKey) + plugins := viper.GetStringSlice(pluginsConfigKey) + deprecated := viper.GetStringSlice(defaultPluginsConfigKey) + if len(deprecated) > 0 { + logger.Warnf("'default.plugins' config key is deprecated; use top-level 'plugins' instead") + plugins = append(plugins, deprecated...) + } + + var expanded []string for _, plugin := range plugins { // work-around for https://github.com/spf13/viper/issues/380 if strings.Contains(plugin, ",") { - for _, p := range strings.Split(plugin, ",") { - plugins = append(plugins, p) - } + expanded = append(expanded, strings.Split(plugin, ",")...) } else { - plugins = append(plugins, plugin) + expanded = append(expanded, plugin) } } - plugins = stringutil.Unique(plugins) + plugins = stringutil.Unique(expanded) logger.Tracef("found %d configured plugin(s): %v", len(plugins), plugins) return EnsurePlugins(plugins, engineType, version, false)