|
|
@@ -48,51 +48,77 @@ func AppendSourceWithIncludes(cfg *Config, k *koanf.Koanf, configPath string) {
|
|
|
func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
|
|
|
log.Infof("Appending cfg source: %s", configPath)
|
|
|
|
|
|
- // Unmarshal config - koanf will handle mapstructure tags automatically
|
|
|
- err := k.Unmarshal(".", cfg)
|
|
|
- if err != nil {
|
|
|
- log.Errorf("Error unmarshalling config: %v", err)
|
|
|
+ if !unmarshalRoot(k, cfg) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // Fallback for complex nested structures that might not unmarshal correctly
|
|
|
- // Only attempt manual unmarshaling if the automatic approach didn't populate the fields
|
|
|
- if len(cfg.Actions) == 0 && k.Exists("actions") {
|
|
|
- var actions []*Action
|
|
|
- if err := k.Unmarshal("actions", &actions); err == nil {
|
|
|
- cfg.Actions = actions
|
|
|
- log.Debugf("Manually loaded %d actions", len(actions))
|
|
|
- }
|
|
|
+ loadCollectionsFallbacks(k, cfg)
|
|
|
+
|
|
|
+ applyConfigOverrides(k, cfg)
|
|
|
+
|
|
|
+ afterLoadFinalize(cfg, configPath)
|
|
|
+}
|
|
|
+
|
|
|
+func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
|
|
|
+ if err := k.Unmarshal(".", cfg); err != nil {
|
|
|
+ log.Errorf("Error unmarshalling config: %v", err)
|
|
|
+ return false
|
|
|
}
|
|
|
+ return true
|
|
|
+}
|
|
|
|
|
|
- if len(cfg.Dashboards) == 0 && k.Exists("dashboards") {
|
|
|
- var dashboards []*DashboardComponent
|
|
|
- if err := k.Unmarshal("dashboards", &dashboards); err == nil {
|
|
|
- cfg.Dashboards = dashboards
|
|
|
- log.Debugf("Manually loaded %d dashboards", len(dashboards))
|
|
|
- }
|
|
|
+func loadCollectionsFallbacks(k *koanf.Koanf, cfg *Config) {
|
|
|
+ maybeUnmarshalActions(k, cfg)
|
|
|
+ maybeUnmarshalDashboards(k, cfg)
|
|
|
+ maybeUnmarshalEntities(k, cfg)
|
|
|
+ maybeUnmarshalAuthLocalUsers(k, cfg)
|
|
|
+}
|
|
|
+
|
|
|
+func maybeUnmarshalActions(k *koanf.Koanf, cfg *Config) {
|
|
|
+ if len(cfg.Actions) != 0 || !k.Exists("actions") {
|
|
|
+ return
|
|
|
}
|
|
|
+ var actions []*Action
|
|
|
+ if err := k.Unmarshal("actions", &actions); err == nil {
|
|
|
+ cfg.Actions = actions
|
|
|
+ log.Debugf("Manually loaded %d actions", len(actions))
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- if len(cfg.Entities) == 0 && k.Exists("entities") {
|
|
|
- var entities []*EntityFile
|
|
|
- if err := k.Unmarshal("entities", &entities); err == nil {
|
|
|
- cfg.Entities = entities
|
|
|
- log.Debugf("Manually loaded %d entities", len(entities))
|
|
|
- }
|
|
|
+func maybeUnmarshalDashboards(k *koanf.Koanf, cfg *Config) {
|
|
|
+ if len(cfg.Dashboards) != 0 || !k.Exists("dashboards") {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var dashboards []*DashboardComponent
|
|
|
+ if err := k.Unmarshal("dashboards", &dashboards); err == nil {
|
|
|
+ cfg.Dashboards = dashboards
|
|
|
+ log.Debugf("Manually loaded %d dashboards", len(dashboards))
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- if len(cfg.AuthLocalUsers.Users) == 0 && k.Exists("authLocalUsers") {
|
|
|
- var authLocalUsers AuthLocalUsersConfig
|
|
|
- if err := k.Unmarshal("authLocalUsers", &authLocalUsers); err == nil {
|
|
|
- cfg.AuthLocalUsers = authLocalUsers
|
|
|
- log.Debugf("Manually loaded local auth config")
|
|
|
- }
|
|
|
+func maybeUnmarshalEntities(k *koanf.Koanf, cfg *Config) {
|
|
|
+ if len(cfg.Entities) != 0 || !k.Exists("entities") {
|
|
|
+ return
|
|
|
}
|
|
|
+ var entities []*EntityFile
|
|
|
+ if err := k.Unmarshal("entities", &entities); err == nil {
|
|
|
+ cfg.Entities = entities
|
|
|
+ log.Debugf("Manually loaded %d entities", len(entities))
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // Map structure tags should handle these automatically, but we keep fallbacks
|
|
|
- // for fields that might not unmarshal correctly
|
|
|
- applyConfigOverrides(k, cfg)
|
|
|
+func maybeUnmarshalAuthLocalUsers(k *koanf.Koanf, cfg *Config) {
|
|
|
+ if len(cfg.AuthLocalUsers.Users) != 0 || !k.Exists("authLocalUsers") {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var authLocalUsers AuthLocalUsersConfig
|
|
|
+ if err := k.Unmarshal("authLocalUsers", &authLocalUsers); err == nil {
|
|
|
+ cfg.AuthLocalUsers = authLocalUsers
|
|
|
+ log.Debugf("Manually loaded local auth config")
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
+func afterLoadFinalize(cfg *Config, configPath string) {
|
|
|
metricConfigReloadedCount.Inc()
|
|
|
metricConfigActionCount.Set(float64(len(cfg.Actions)))
|
|
|
|
|
|
@@ -135,128 +161,112 @@ func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- configDir := filepath.Dir(baseConfigPath)
|
|
|
- includePath := filepath.Join(configDir, cfg.Include)
|
|
|
-
|
|
|
+ includePath := filepath.Join(filepath.Dir(baseConfigPath), cfg.Include)
|
|
|
log.Infof("Loading included configs from: %s", includePath)
|
|
|
|
|
|
- // Check if the include directory exists
|
|
|
+ yamlFiles, ok := listYamlFiles(includePath)
|
|
|
+ if !ok || len(yamlFiles) == 0 {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ sort.Strings(yamlFiles)
|
|
|
+ for _, filename := range yamlFiles {
|
|
|
+ loadAndMergeIncludedFile(cfg, includePath, filename)
|
|
|
+ }
|
|
|
+
|
|
|
+ log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
|
|
|
+ cfg.Sanitize()
|
|
|
+}
|
|
|
+
|
|
|
+func listYamlFiles(includePath string) ([]string, bool) {
|
|
|
dirInfo, err := os.Stat(includePath)
|
|
|
if err != nil {
|
|
|
log.Warnf("Include directory not found: %s", includePath)
|
|
|
- return
|
|
|
+ return nil, false
|
|
|
}
|
|
|
-
|
|
|
if !dirInfo.IsDir() {
|
|
|
log.Warnf("Include path is not a directory: %s", includePath)
|
|
|
- return
|
|
|
+ return nil, false
|
|
|
}
|
|
|
-
|
|
|
- // Read all .yml files from the directory
|
|
|
entries, err := os.ReadDir(includePath)
|
|
|
if err != nil {
|
|
|
log.Errorf("Error reading include directory: %v", err)
|
|
|
- return
|
|
|
+ return nil, false
|
|
|
}
|
|
|
-
|
|
|
- // Filter and sort .yml files
|
|
|
var yamlFiles []string
|
|
|
for _, entry := range entries {
|
|
|
- if !entry.IsDir() && (strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml")) {
|
|
|
- yamlFiles = append(yamlFiles, entry.Name())
|
|
|
+ if entry.IsDir() {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ name := entry.Name()
|
|
|
+ if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
|
|
+ yamlFiles = append(yamlFiles, name)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
if len(yamlFiles) == 0 {
|
|
|
log.Infof("No YAML files found in include directory: %s", includePath)
|
|
|
- return
|
|
|
}
|
|
|
+ return yamlFiles, true
|
|
|
+}
|
|
|
|
|
|
- // Sort files to ensure deterministic load order
|
|
|
- sort.Strings(yamlFiles)
|
|
|
-
|
|
|
- // Load each file and merge into config
|
|
|
- for _, filename := range yamlFiles {
|
|
|
- filePath := filepath.Join(includePath, filename)
|
|
|
- log.Infof("Loading included config file: %s", filePath)
|
|
|
-
|
|
|
- includeK := koanf.New(".")
|
|
|
- f := file.Provider(filePath)
|
|
|
-
|
|
|
- if err := includeK.Load(f, yaml.Parser()); err != nil {
|
|
|
- log.Errorf("Error loading included config file %s: %v", filePath, err)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // Unmarshal into a temporary config to process properly
|
|
|
- tempCfg := &Config{}
|
|
|
- if err := includeK.Unmarshal(".", tempCfg); err != nil {
|
|
|
- log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // Apply the same manual loading workarounds as in AppendSource
|
|
|
- if len(tempCfg.Actions) == 0 && includeK.Exists("actions") {
|
|
|
- var actions []*Action
|
|
|
- if err := includeK.Unmarshal("actions", &actions); err == nil {
|
|
|
- tempCfg.Actions = actions
|
|
|
- log.Debugf("Manually loaded %d actions from %s", len(actions), filename)
|
|
|
- }
|
|
|
- }
|
|
|
+func loadAndMergeIncludedFile(cfg *Config, includePath, filename string) {
|
|
|
+ filePath := filepath.Join(includePath, filename)
|
|
|
+ log.Infof("Loading included config file: %s", filePath)
|
|
|
|
|
|
- // Merge the temp config into the main config
|
|
|
- // Later files override earlier ones
|
|
|
- mergeConfig(cfg, tempCfg)
|
|
|
+ includeK := koanf.New(".")
|
|
|
+ if err := includeK.Load(file.Provider(filePath), yaml.Parser()); err != nil {
|
|
|
+ log.Errorf("Error loading included config file %s: %v", filePath, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
- log.Infof("Successfully loaded and merged %s", filename)
|
|
|
+ tempCfg := &Config{}
|
|
|
+ if err := includeK.Unmarshal(".", tempCfg); err != nil {
|
|
|
+ log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
- log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
|
|
|
+ loadCollectionsFallbacks(includeK, tempCfg)
|
|
|
|
|
|
- // Sanitize the merged config
|
|
|
- cfg.Sanitize()
|
|
|
+ mergeConfig(cfg, tempCfg)
|
|
|
+ log.Infof("Successfully loaded and merged %s", filename)
|
|
|
}
|
|
|
|
|
|
func mergeConfig(base *Config, overlay *Config) {
|
|
|
- // Merge Actions - overlay appends to base
|
|
|
+ mergeSlices(base, overlay)
|
|
|
+ overrideSimple(base, overlay)
|
|
|
+ overrideNested(base, overlay)
|
|
|
+ overrideStrings(base, overlay)
|
|
|
+}
|
|
|
+
|
|
|
+func mergeSlices(base *Config, overlay *Config) {
|
|
|
if len(overlay.Actions) > 0 {
|
|
|
base.Actions = append(base.Actions, overlay.Actions...)
|
|
|
}
|
|
|
-
|
|
|
- // Merge Dashboards - overlay appends to base
|
|
|
if len(overlay.Dashboards) > 0 {
|
|
|
base.Dashboards = append(base.Dashboards, overlay.Dashboards...)
|
|
|
log.Debugf("Merged %d dashboards from include", len(overlay.Dashboards))
|
|
|
}
|
|
|
-
|
|
|
- // Merge Entities - overlay appends to base
|
|
|
if len(overlay.Entities) > 0 {
|
|
|
base.Entities = append(base.Entities, overlay.Entities...)
|
|
|
log.Debugf("Merged %d entities from include", len(overlay.Entities))
|
|
|
}
|
|
|
-
|
|
|
- // Merge AccessControlLists - overlay appends to base
|
|
|
if len(overlay.AccessControlLists) > 0 {
|
|
|
base.AccessControlLists = append(base.AccessControlLists, overlay.AccessControlLists...)
|
|
|
log.Debugf("Merged %d access control lists from include", len(overlay.AccessControlLists))
|
|
|
}
|
|
|
-
|
|
|
- // Merge AuthLocalUsers.Users - overlay appends to base
|
|
|
if len(overlay.AuthLocalUsers.Users) > 0 {
|
|
|
base.AuthLocalUsers.Users = append(base.AuthLocalUsers.Users, overlay.AuthLocalUsers.Users...)
|
|
|
log.Debugf("Merged %d local users from include", len(overlay.AuthLocalUsers.Users))
|
|
|
}
|
|
|
-
|
|
|
- // Merge slices by appending
|
|
|
if len(overlay.StyleMods) > 0 {
|
|
|
base.StyleMods = append(base.StyleMods, overlay.StyleMods...)
|
|
|
}
|
|
|
-
|
|
|
if len(overlay.AdditionalNavigationLinks) > 0 {
|
|
|
base.AdditionalNavigationLinks = append(base.AdditionalNavigationLinks, overlay.AdditionalNavigationLinks...)
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- // Override simple fields (later files win)
|
|
|
+func overrideSimple(base *Config, overlay *Config) {
|
|
|
if overlay.LogLevel != "" {
|
|
|
base.LogLevel = overlay.LogLevel
|
|
|
}
|
|
|
@@ -278,28 +288,30 @@ func mergeConfig(base *Config, overlay *Config) {
|
|
|
if overlay.AuthRequireGuestsToLogin != base.AuthRequireGuestsToLogin {
|
|
|
base.AuthRequireGuestsToLogin = overlay.AuthRequireGuestsToLogin
|
|
|
}
|
|
|
-
|
|
|
- // Override nested structs
|
|
|
- if overlay.DefaultPolicy.ShowDiagnostics != base.DefaultPolicy.ShowDiagnostics {
|
|
|
- base.DefaultPolicy.ShowDiagnostics = overlay.DefaultPolicy.ShowDiagnostics
|
|
|
- }
|
|
|
- if overlay.DefaultPolicy.ShowLogList != base.DefaultPolicy.ShowLogList {
|
|
|
- base.DefaultPolicy.ShowLogList = overlay.DefaultPolicy.ShowLogList
|
|
|
+ if overlay.AuthLocalUsers.Enabled {
|
|
|
+ base.AuthLocalUsers.Enabled = overlay.AuthLocalUsers.Enabled
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- if overlay.Prometheus.Enabled != base.Prometheus.Enabled {
|
|
|
- base.Prometheus.Enabled = overlay.Prometheus.Enabled
|
|
|
+func overrideNested(base *Config, overlay *Config) {
|
|
|
+ // Only apply overrides when overlay explicitly enables the option.
|
|
|
+ // This mirrors the presence-check pattern used elsewhere to avoid
|
|
|
+ // unintentionally disabling an already-enabled base setting with a default false.
|
|
|
+ if overlay.DefaultPolicy.ShowDiagnostics {
|
|
|
+ base.DefaultPolicy.ShowDiagnostics = true
|
|
|
}
|
|
|
- if overlay.Prometheus.DefaultGoMetrics != base.Prometheus.DefaultGoMetrics {
|
|
|
- base.Prometheus.DefaultGoMetrics = overlay.Prometheus.DefaultGoMetrics
|
|
|
+ if overlay.DefaultPolicy.ShowLogList {
|
|
|
+ base.DefaultPolicy.ShowLogList = true
|
|
|
}
|
|
|
-
|
|
|
- // Override AuthLocalUsers.Enabled if set
|
|
|
- if overlay.AuthLocalUsers.Enabled {
|
|
|
- base.AuthLocalUsers.Enabled = overlay.AuthLocalUsers.Enabled
|
|
|
+ if overlay.Prometheus.Enabled {
|
|
|
+ base.Prometheus.Enabled = true
|
|
|
}
|
|
|
+ if overlay.Prometheus.DefaultGoMetrics {
|
|
|
+ base.Prometheus.DefaultGoMetrics = true
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- // Override string fields if non-empty
|
|
|
+func overrideStrings(base *Config, overlay *Config) {
|
|
|
overrideString(&base.BannerMessage, overlay.BannerMessage)
|
|
|
overrideString(&base.BannerCSS, overlay.BannerCSS)
|
|
|
overrideString(&base.LogLevel, overlay.LogLevel)
|