fix: Propperly merge included configs (handling actions)

This commit is contained in:
jamesread 2025-11-07 00:31:33 +00:00
parent 7dc99b1398
commit 1552c104e9
2 changed files with 60 additions and 37 deletions

View File

@ -3,6 +3,8 @@ package config
import (
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
@ -33,7 +35,11 @@ func AddListener(l func()) {
}
func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
log.Infof("Appending cfg source: %s", configPath)
log.WithFields(log.Fields{
"configPath": configPath,
}).Info("Appending cfg source")
loadIncludedConfigsFromDir(k, configPath)
if !unmarshalRoot(k, cfg) {
return
@ -42,14 +48,6 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
afterLoadFinalize(cfg, configPath)
}
func AppendSourceWithIncludes(cfg *Config, k *koanf.Koanf, configPath string) {
AppendSource(cfg, k, configPath)
if cfg.Include != "" {
LoadIncludedConfigs(cfg, k, configPath)
}
}
func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{
Tag: "koanf",
@ -74,14 +72,19 @@ func afterLoadFinalize(cfg *Config, configPath string) {
}
}
// LoadIncludedConfigs loads configuration files from an include directory and merges them
func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
if cfg.Include == "" {
// loadIncludedConfigsFromDir loads configuration files from an include directory and merges them
func loadIncludedConfigsFromDir(k *koanf.Koanf, baseConfigPath string) {
relativeIncludePath := k.String("include")
if relativeIncludePath == "" {
return
}
includePath := filepath.Join(filepath.Dir(baseConfigPath), cfg.Include)
log.Infof("Loading included configs from: %s", includePath)
includePath := filepath.Join(filepath.Dir(baseConfigPath), relativeIncludePath)
log.WithFields(log.Fields{
"includePath": includePath,
}).Infof("Loading included configs from dir")
yamlFiles, ok := listYamlFiles(includePath)
if !ok || len(yamlFiles) == 0 {
@ -90,11 +93,10 @@ func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
sort.Strings(yamlFiles)
for _, filename := range yamlFiles {
loadAndMergeIncludedFile(k, cfg, includePath, filename)
loadAndMergeIncludedFile(k, includePath, filename)
}
log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
cfg.Sanitize()
}
func listYamlFiles(includePath string) ([]string, bool) {
@ -128,24 +130,43 @@ func listYamlFiles(includePath string) ([]string, bool) {
return yamlFiles, true
}
func loadAndMergeIncludedFile(k *koanf.Koanf, cfg *Config, includePath, filename string) {
func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
filePath := filepath.Join(includePath, filename)
log.Infof("Loading included config file: %s", filePath)
if err := k.Load(file.Provider(filePath), yaml.Parser()); err != nil {
if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
log.Errorf("Error loading included config file %s: %v", filePath, err)
return
}
if err := k.Unmarshal(".", cfg); err != nil {
log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
return
}
log.Infof("Successfully loaded %s", filename)
log.WithFields(log.Fields{
"filePath": filePath,
}).Info("Successfully loaded included config file")
}
func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
// Handle actions merging - koanf provides []interface{} not []*Action
// Merge src (new) into dest (existing) by appending src's actions to dest's actions
if srcActions, ok := src["actions"]; ok {
if destActions, ok := dest["actions"]; ok {
// Both have actions - append src to dest
srcSlice, ok1 := srcActions.([]interface{})
destSlice, ok2 := destActions.([]interface{})
if ok1 && ok2 {
dest["actions"] = append(destSlice, srcSlice...)
} else {
// Fallback: if types don't match, just use src
dest["actions"] = srcActions
}
} else {
// dest doesn't have actions, so use src's actions
dest["actions"] = srcActions
}
}
// If src doesn't have actions, leave dest unchanged
return nil
}
/**
var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`)
func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) {
@ -168,4 +189,3 @@ func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error
log.Debugf("Environment variable interpolation result: %q -> %q", input, output)
return output, nil
}
*/

View File

@ -146,23 +146,28 @@ func initConfig(configDir string) {
)
}
var firstConfigPath string
var baseConfigPath string
for _, directory := range directories {
configPath := getConfigPath(directory)
log.Debugf("Checking config path: %s", configPath)
log.WithFields(log.Fields{
"configPath": configPath,
}).Debug("Checking base config file path")
if _, err := os.Stat(configPath); err != nil {
log.Debugf("Config file not found at %s: %v", configPath, err)
continue
}
if firstConfigPath == "" {
firstConfigPath = configPath
if baseConfigPath == "" {
baseConfigPath = configPath
}
log.Infof("Loading config from %s", configPath)
log.WithFields(log.Fields{
"configPath": configPath,
}).Info("Loading config from path")
f := file.Provider(configPath)
if err := k.Load(f, yaml.Parser()); err != nil {
@ -176,15 +181,13 @@ func initConfig(configDir string) {
k.Load(f, yaml.Parser())
config.AppendSource(cfg, k, configPath)
})
break
}
cfg = config.DefaultConfigWithBasePort(getBasePort())
if firstConfigPath != "" {
config.AppendSourceWithIncludes(cfg, k, firstConfigPath)
} else {
config.AppendSource(cfg, k, "base")
}
config.AppendSource(cfg, k, baseConfigPath)
}