Fix config loading missing values (#699)

This commit is contained in:
James Read 2025-11-07 00:40:36 +00:00 committed by GitHub
commit 50abb53ace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 218 additions and 399 deletions

View File

@ -481,16 +481,20 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
ret.StartOffset = page.start
return connect.NewResponse(ret), nil
}
// Newest-first slicing: compute reversed indices
startIdx := page.total - page.end
endIdx := page.total - page.start
if startIdx < 0 { startIdx = 0 }
if endIdx > int64(len(filtered)) { endIdx = int64(len(filtered)) }
for _, le := range filtered[startIdx:endIdx] {
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
}
// Entries older than the returned newest page
ret.CountRemaining = page.start
// Newest-first slicing: compute reversed indices
startIdx := page.total - page.end
endIdx := page.total - page.start
if startIdx < 0 {
startIdx = 0
}
if endIdx > int64(len(filtered)) {
endIdx = int64(len(filtered))
}
for _, le := range filtered[startIdx:endIdx] {
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
}
// Entries older than the returned newest page
ret.CountRemaining = page.start
ret.PageSize = page.size
ret.TotalCount = page.total
ret.StartOffset = page.start

View File

@ -7,212 +7,212 @@ import (
// Action represents the core functionality of OliveTin - commands that show up
// as buttons in the UI.
type Action struct {
ID string
Title string
Icon string
Shell string
Exec []string
ShellAfterCompleted string
Timeout int
Acls []string
Entity string
Hidden bool
ExecOnStartup bool
ExecOnCron []string
ExecOnFileCreatedInDir []string
ExecOnFileChangedInDir []string
ExecOnCalendarFile string
Triggers []string
MaxConcurrent int
MaxRate []RateSpec
Arguments []ActionArgument
PopupOnStart string
SaveLogs SaveLogsConfig
ID string `koanf:"id"`
Title string `koanf:"title"`
Icon string `koanf:"icon"`
Shell string `koanf:"shell"`
Exec []string `koanf:"exec"`
ShellAfterCompleted string `koanf:"shellAfterCompleted"`
Timeout int `koanf:"timeout"`
Acls []string `koanf:"acls"`
Entity string `koanf:"entity"`
Hidden bool `koanf:"hidden"`
ExecOnStartup bool `koanf:"execOnStartup"`
ExecOnCron []string `koanf:"execOnCron"`
ExecOnFileCreatedInDir []string `koanf:"execOnFileCreatedInDir"`
ExecOnFileChangedInDir []string `koanf:"execOnFileChangedInDir"`
ExecOnCalendarFile string `koanf:"execOnCalendarFile"`
Triggers []string `koanf:"triggers"`
MaxConcurrent int `koanf:"maxConcurrent"`
MaxRate []RateSpec `koanf:"maxRate"`
Arguments []ActionArgument `koanf:"arguments"`
PopupOnStart string `koanf:"popupOnStart"`
SaveLogs SaveLogsConfig `koanf:"saveLogs"`
}
// ActionArgument objects appear on Actions.
type ActionArgument struct {
Name string
Title string
Description string
Type string
Default string
Choices []ActionArgumentChoice
Entity string
RejectNull bool
Suggestions map[string]string
Name string `koanf:"name"`
Title string `koanf:"title"`
Description string `koanf:"description"`
Type string `koanf:"type"`
Default string `koanf:"default"`
Choices []ActionArgumentChoice `koanf:"choices"`
Entity string `koanf:"entity"`
RejectNull bool `koanf:"rejectNull"`
Suggestions map[string]string `koanf:"suggestions"`
}
// ActionArgumentChoice represents a predefined choice for an argument.
type ActionArgumentChoice struct {
Value string
Title string
Value string `koanf:"value"`
Title string `koanf:"title"`
}
// RateSpec allows you to set a max frequency for an action.
type RateSpec struct {
Limit int
Duration string
Limit int `koanf:"limit"`
Duration string `koanf:"duration"`
}
// Entity represents a "thing" that can have multiple actions associated with it.
// for example, a media player with a start and stop action.
type EntityFile struct {
File string
Name string
Icon string
File string `koanf:"file"`
Name string `koanf:"name"`
Icon string `koanf:"icon"`
}
// PermissionsList defines what users can do with an action.
type PermissionsList struct {
View bool `mapstructure:"view"`
Exec bool `mapstructure:"exec"`
Logs bool `mapstructure:"logs"`
Kill bool `mapstructure:"kill"`
View bool `koanf:"view"`
Exec bool `koanf:"exec"`
Logs bool `koanf:"logs"`
Kill bool `koanf:"kill"`
}
// AccessControlList defines what permissions apply to a user or user group.
type AccessControlList struct {
Name string
AddToEveryAction bool
MatchUsergroups []string
MatchUsernames []string
Permissions PermissionsList
Policy ConfigurationPolicy
Name string `koanf:"name"`
AddToEveryAction bool `koanf:"addToEveryAction"`
MatchUsergroups []string `koanf:"matchUsergroups"`
MatchUsernames []string `koanf:"matchUsernames"`
Permissions PermissionsList `koanf:"permissions"`
Policy ConfigurationPolicy `koanf:"policy"`
}
// ConfigurationPolicy defines global settings which are overridden with an ACL.
type ConfigurationPolicy struct {
ShowDiagnostics bool `mapstructure:"showDiagnostics"`
ShowLogList bool `mapstructure:"showLogList"`
ShowDiagnostics bool `koanf:"showDiagnostics"`
ShowLogList bool `koanf:"showLogList"`
}
type PrometheusConfig struct {
Enabled bool `mapstructure:"enabled"`
DefaultGoMetrics bool `mapstructure:"defaultGoMetrics"`
Enabled bool `koanf:"enabled"`
DefaultGoMetrics bool `koanf:"defaultGoMetrics"`
}
// Config is the global config used through the whole app.
type Config struct {
UseSingleHTTPFrontend bool `mapstructure:"useSingleHTTPFrontend"`
ThemeName string `mapstructure:"themeName"`
ThemeCacheDisabled bool `mapstructure:"themeCacheDisabled"`
ListenAddressSingleHTTPFrontend string `mapstructure:"listenAddressSingleHTTPFrontend"`
ListenAddressWebUI string `mapstructure:"listenAddressWebUI"`
ListenAddressRestActions string `mapstructure:"listenAddressRestActions"`
ListenAddressPrometheus string `mapstructure:"listenAddressPrometheus"`
ExternalRestAddress string `mapstructure:"externalRestAddress"`
LogLevel string `mapstructure:"logLevel"`
LogDebugOptions LogDebugOptions `mapstructure:"logDebugOptions"`
LogHistoryPageSize int64 `mapstructure:"logHistoryPageSize"`
Actions []*Action `mapstructure:"actions"`
Entities []*EntityFile `mapstructure:"entities"`
Dashboards []*DashboardComponent `mapstructure:"dashboards"`
CheckForUpdates bool `mapstructure:"checkForUpdates"`
PageTitle string `mapstructure:"pageTitle"`
ShowFooter bool `mapstructure:"showFooter"`
ShowNavigation bool `mapstructure:"showNavigation"`
ShowNewVersions bool `mapstructure:"showNewVersions"`
EnableCustomJs bool `mapstructure:"enableCustomJs"`
AuthJwtCookieName string `mapstructure:"authJwtCookieName"`
AuthJwtHeader string `mapstructure:"authJwtHeader"`
AuthJwtAud string `mapstructure:"authJwtAud"`
AuthJwtDomain string `mapstructure:"authJwtDomain"`
AuthJwtCertsURL string `mapstructure:"authJwtCertsUrl"`
AuthJwtHmacSecret string `mapstructure:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
AuthJwtClaimUsername string `mapstructure:"authJwtClaimUsername"`
AuthJwtClaimUserGroup string `mapstructure:"authJwtClaimUserGroup"`
AuthJwtPubKeyPath string `mapstructure:"authJwtPubKeyPath"` // will read pub key from file on disk
AuthHttpHeaderUsername string `mapstructure:"authHttpHeaderUsername"`
AuthHttpHeaderUserGroup string `mapstructure:"authHttpHeaderUserGroup"`
AuthHttpHeaderUserGroupSep string `mapstructure:"authHttpHeaderUserGroupSep"`
AuthLocalUsers AuthLocalUsersConfig `mapstructure:"authLocalUsers"`
AuthLoginUrl string `mapstructure:"authLoginUrl"`
AuthRequireGuestsToLogin bool `mapstructure:"authRequireGuestsToLogin"`
AuthOAuth2RedirectURL string `mapstructure:"authOAuth2RedirectUrl"`
AuthOAuth2Providers map[string]*OAuth2Provider `mapstructure:"authOAuth2Providers"`
DefaultPermissions PermissionsList `mapstructure:"defaultPermissions"`
DefaultPolicy ConfigurationPolicy `mapstructure:"defaultPolicy"`
AccessControlLists []*AccessControlList `mapstructure:"accessControlLists"`
WebUIDir string `mapstructure:"webUIDir"`
CronSupportForSeconds bool `mapstructure:"cronSupportForSeconds"`
SectionNavigationStyle string `mapstructure:"sectionNavigationStyle"`
DefaultPopupOnStart string `mapstructure:"defaultPopupOnStart"`
InsecureAllowDumpOAuth2UserData bool `mapstructure:"insecureAllowDumpOAuth2UserData"`
InsecureAllowDumpVars bool `mapstructure:"insecureAllowDumpVars"`
InsecureAllowDumpSos bool `mapstructure:"insecureAllowDumpSos"`
InsecureAllowDumpActionMap bool `mapstructure:"insecureAllowDumpActionMap"`
InsecureAllowDumpJwtClaims bool `mapstructure:"insecureAllowDumpJwtClaims"`
Prometheus PrometheusConfig `mapstructure:"prometheus"`
SaveLogs SaveLogsConfig `mapstructure:"saveLogs"`
DefaultIconForActions string `mapstructure:"defaultIconForActions"`
DefaultIconForDirectories string `mapstructure:"defaultIconForDirectories"`
DefaultIconForBack string `mapstructure:"defaultIconForBack"`
AdditionalNavigationLinks []*NavigationLink `mapstructure:"additionalNavigationLinks"`
ServiceHostMode string `mapstructure:"serviceHostMode"`
StyleMods []string `mapstructure:"styleMods"`
BannerMessage string `mapstructure:"bannerMessage"`
BannerCSS string `mapstructure:"bannerCss"`
Include string `mapstructure:"include"`
UseSingleHTTPFrontend bool `koanf:"useSingleHTTPFrontend"`
ThemeName string `koanf:"themeName"`
ThemeCacheDisabled bool `koanf:"themeCacheDisabled"`
ListenAddressSingleHTTPFrontend string `koanf:"listenAddressSingleHTTPFrontend"`
ListenAddressWebUI string `koanf:"listenAddressWebUI"`
ListenAddressRestActions string `koanf:"listenAddressRestActions"`
ListenAddressPrometheus string `koanf:"listenAddressPrometheus"`
ExternalRestAddress string `koanf:"externalRestAddress"`
LogLevel string `koanf:"logLevel"`
LogDebugOptions LogDebugOptions `koanf:"logDebugOptions"`
LogHistoryPageSize int64 `koanf:"logHistoryPageSize"`
Actions []*Action `koanf:"actions"`
Entities []*EntityFile `koanf:"entities"`
Dashboards []*DashboardComponent `koanf:"dashboards"`
CheckForUpdates bool `koanf:"checkForUpdates"`
PageTitle string `koanf:"pageTitle"`
ShowFooter bool `koanf:"showFooter"`
ShowNavigation bool `koanf:"showNavigation"`
ShowNewVersions bool `koanf:"showNewVersions"`
EnableCustomJs bool `koanf:"enableCustomJs"`
AuthJwtCookieName string `koanf:"authJwtCookieName"`
AuthJwtHeader string `koanf:"authJwtHeader"`
AuthJwtAud string `koanf:"authJwtAud"`
AuthJwtDomain string `koanf:"authJwtDomain"`
AuthJwtCertsURL string `koanf:"authJwtCertsUrl"`
AuthJwtHmacSecret string `koanf:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
AuthJwtClaimUsername string `koanf:"authJwtClaimUsername"`
AuthJwtClaimUserGroup string `koanf:"authJwtClaimUserGroup"`
AuthJwtPubKeyPath string `koanf:"authJwtPubKeyPath"` // will read pub key from file on disk
AuthHttpHeaderUsername string `koanf:"authHttpHeaderUsername"`
AuthHttpHeaderUserGroup string `koanf:"authHttpHeaderUserGroup"`
AuthHttpHeaderUserGroupSep string `koanf:"authHttpHeaderUserGroupSep"`
AuthLocalUsers AuthLocalUsersConfig `koanf:"authLocalUsers"`
AuthLoginUrl string `koanf:"authLoginUrl"`
AuthRequireGuestsToLogin bool `koanf:"authRequireGuestsToLogin"`
AuthOAuth2RedirectURL string `koanf:"authOAuth2RedirectUrl"`
AuthOAuth2Providers map[string]*OAuth2Provider `koanf:"authOAuth2Providers"`
DefaultPermissions PermissionsList `koanf:"defaultPermissions"`
DefaultPolicy ConfigurationPolicy `koanf:"defaultPolicy"`
AccessControlLists []*AccessControlList `koanf:"accessControlLists"`
WebUIDir string `koanf:"webUIDir"`
CronSupportForSeconds bool `koanf:"cronSupportForSeconds"`
SectionNavigationStyle string `koanf:"sectionNavigationStyle"`
DefaultPopupOnStart string `koanf:"defaultPopupOnStart"`
InsecureAllowDumpOAuth2UserData bool `koanf:"insecureAllowDumpOAuth2UserData"`
InsecureAllowDumpVars bool `koanf:"insecureAllowDumpVars"`
InsecureAllowDumpSos bool `koanf:"insecureAllowDumpSos"`
InsecureAllowDumpActionMap bool `koanf:"insecureAllowDumpActionMap"`
InsecureAllowDumpJwtClaims bool `koanf:"insecureAllowDumpJwtClaims"`
Prometheus PrometheusConfig `koanf:"prometheus"`
SaveLogs SaveLogsConfig `koanf:"saveLogs"`
DefaultIconForActions string `koanf:"defaultIconForActions"`
DefaultIconForDirectories string `koanf:"defaultIconForDirectories"`
DefaultIconForBack string `koanf:"defaultIconForBack"`
AdditionalNavigationLinks []*NavigationLink `koanf:"additionalNavigationLinks"`
ServiceHostMode string `koanf:"serviceHostMode"`
StyleMods []string `koanf:"styleMods"`
BannerMessage string `koanf:"bannerMessage"`
BannerCSS string `koanf:"bannerCss"`
Include string `koanf:"include"`
sourceFiles []string
}
type AuthLocalUsersConfig struct {
Enabled bool `mapstructure:"enabled"`
Users []*LocalUser `mapstructure:"users"`
Enabled bool `koanf:"enabled"`
Users []*LocalUser `koanf:"users"`
}
type LocalUser struct {
Username string `mapstructure:"username"`
Usergroup string `mapstructure:"usergroup"`
Password string `mapstructure:"password"`
Username string `koanf:"username"`
Usergroup string `koanf:"usergroup"`
Password string `koanf:"password"`
}
type OAuth2Provider struct {
Name string
Title string
ClientID string
ClientSecret string
Icon string
Scopes []string
AuthUrl string
TokenUrl string
WhoamiUrl string
UsernameField string
UserGroupField string
InsecureSkipVerify bool
CallbackTimeout int
CertBundlePath string
Name string `koanf:"name"`
Title string `koanf:"title"`
ClientID string `koanf:"clientId"`
ClientSecret string `koanf:"clientSecret"`
Icon string `koanf:"icon"`
Scopes []string `koanf:"scopes"`
AuthUrl string `koanf:"authUrl"`
TokenUrl string `koanf:"tokenUrl"`
WhoamiUrl string `koanf:"whoamiUrl"`
UsernameField string `koanf:"usernameField"`
UserGroupField string `koanf:"userGroupField"`
InsecureSkipVerify bool `koanf:"insecureSkipVerify"`
CallbackTimeout int `koanf:"callbackTimeout"`
CertBundlePath string `koanf:"certBundlePath"`
}
type NavigationLink struct {
Title string `mapstructure:"title"`
Url string `mapstructure:"url"`
Target string `mapstructure:"target"`
Title string `koanf:"title"`
Url string `koanf:"url"`
Target string `koanf:"target"`
}
type SaveLogsConfig struct {
ResultsDirectory string `mapstructure:"resultsDirectory"`
OutputDirectory string `mapstructure:"outputDirectory"`
ResultsDirectory string `koanf:"resultsDirectory"`
OutputDirectory string `koanf:"outputDirectory"`
}
type LogDebugOptions struct {
SingleFrontendRequests bool
SingleFrontendRequestHeaders bool
AclCheckStarted bool
AclMatched bool
AclNotMatched bool
AclNoneMatched bool
SingleFrontendRequests bool `koanf:"singleFrontendRequests"`
SingleFrontendRequestHeaders bool `koanf:"singleFrontendRequestHeaders"`
AclCheckStarted bool `koanf:"aclCheckStarted"`
AclMatched bool `koanf:"aclMatched"`
AclNotMatched bool `koanf:"aclNotMatched"`
AclNoneMatched bool `koanf:"aclNoneMatched"`
}
type DashboardComponent struct {
Title string
Type string
Entity string
Icon string
CssClass string
Contents []*DashboardComponent
Title string `koanf:"title"`
Type string `koanf:"type"`
Entity string `koanf:"entity"`
Icon string `koanf:"icon"`
CssClass string `koanf:"cssClass"`
Contents []*DashboardComponent `koanf:"contents"`
}
func DefaultConfig() *Config {

View File

@ -34,90 +34,32 @@ func AddListener(l func()) {
listeners = append(listeners, l)
}
// AppendSourceWithIncludes loads base config and any included configs
func AppendSourceWithIncludes(cfg *Config, k *koanf.Koanf, configPath string) {
// Load base config first
AppendSource(cfg, k, configPath)
// Load included configs if specified
if cfg.Include != "" {
LoadIncludedConfigs(cfg, k, configPath)
}
}
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
}
loadCollectionsFallbacks(k, cfg)
applyConfigOverrides(k, cfg)
afterLoadFinalize(cfg, configPath)
}
func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
if err := k.Unmarshal(".", cfg); err != nil {
err := k.UnmarshalWithConf("", cfg, koanf.UnmarshalConf{
Tag: "koanf",
})
if err != nil {
log.Errorf("Error unmarshalling config: %v", err)
return false
}
return true
}
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))
}
}
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))
}
}
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))
}
}
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)))
@ -130,39 +72,19 @@ func afterLoadFinalize(cfg *Config, configPath string) {
}
}
func applyConfigOverrides(k *koanf.Koanf, cfg *Config) {
// Override fields that should be read from config
// mapstructure tags should make most of this unnecessary, but keep for safety
boolVal(k, "showFooter", &cfg.ShowFooter)
boolVal(k, "showNavigation", &cfg.ShowNavigation)
boolVal(k, "checkForUpdates", &cfg.CheckForUpdates)
boolVal(k, "useSingleHTTPFrontend", &cfg.UseSingleHTTPFrontend)
stringVal(k, "logLevel", &cfg.LogLevel)
stringVal(k, "pageTitle", &cfg.PageTitle)
boolVal(k, "authRequireGuestsToLogin", &cfg.AuthRequireGuestsToLogin)
stringVal(k, "include", &cfg.Include)
// loadIncludedConfigsFromDir loads configuration files from an include directory and merges them
func loadIncludedConfigsFromDir(k *koanf.Koanf, baseConfigPath string) {
relativeIncludePath := k.String("include")
// Handle nested defaultPolicy struct
if k.Exists("defaultPolicy") {
boolVal(k, "defaultPolicy.showDiagnostics", &cfg.DefaultPolicy.ShowDiagnostics)
boolVal(k, "defaultPolicy.showLogList", &cfg.DefaultPolicy.ShowLogList)
}
// Handle nested prometheus struct
if k.Exists("prometheus") {
boolVal(k, "prometheus.enabled", &cfg.Prometheus.Enabled)
boolVal(k, "prometheus.defaultGoMetrics", &cfg.Prometheus.DefaultGoMetrics)
}
}
// LoadIncludedConfigs loads configuration files from an include directory and merges them
func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
if cfg.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 {
@ -171,11 +93,10 @@ func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
sort.Strings(yamlFiles)
for _, filename := range yamlFiles {
loadAndMergeIncludedFile(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) {
@ -209,152 +130,45 @@ func listYamlFiles(includePath string) ([]string, bool) {
return yamlFiles, true
}
func loadAndMergeIncludedFile(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)
includeK := koanf.New(".")
if err := includeK.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
}
tempCfg := &Config{}
if err := includeK.Unmarshal(".", tempCfg); err != nil {
log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
return
}
loadCollectionsFallbacks(includeK, tempCfg)
mergeConfig(cfg, tempCfg)
log.Infof("Successfully loaded and merged %s", filename)
log.WithFields(log.Fields{
"filePath": filePath,
}).Info("Successfully loaded included config file")
}
func mergeConfig(base *Config, overlay *Config) {
mergeSlices(base, overlay)
overrideSimple(base, overlay)
overrideNested(base, overlay)
overrideStrings(base, overlay)
}
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
func mergeSlices(base *Config, overlay *Config) {
if len(overlay.Actions) > 0 {
base.Actions = append(base.Actions, overlay.Actions...)
}
if len(overlay.Dashboards) > 0 {
base.Dashboards = append(base.Dashboards, overlay.Dashboards...)
log.Debugf("Merged %d dashboards from include", len(overlay.Dashboards))
}
if len(overlay.Entities) > 0 {
base.Entities = append(base.Entities, overlay.Entities...)
log.Debugf("Merged %d entities from include", len(overlay.Entities))
}
if len(overlay.AccessControlLists) > 0 {
base.AccessControlLists = append(base.AccessControlLists, overlay.AccessControlLists...)
log.Debugf("Merged %d access control lists from include", len(overlay.AccessControlLists))
}
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))
}
if len(overlay.StyleMods) > 0 {
base.StyleMods = append(base.StyleMods, overlay.StyleMods...)
}
if len(overlay.AdditionalNavigationLinks) > 0 {
base.AdditionalNavigationLinks = append(base.AdditionalNavigationLinks, overlay.AdditionalNavigationLinks...)
}
}
func overrideSimple(base *Config, overlay *Config) {
if overlay.LogLevel != "" {
base.LogLevel = overlay.LogLevel
}
if overlay.PageTitle != "" {
base.PageTitle = overlay.PageTitle
}
if overlay.ShowFooter != base.ShowFooter {
base.ShowFooter = overlay.ShowFooter
}
if overlay.ShowNavigation != base.ShowNavigation {
base.ShowNavigation = overlay.ShowNavigation
}
if overlay.CheckForUpdates != base.CheckForUpdates {
base.CheckForUpdates = overlay.CheckForUpdates
}
if overlay.UseSingleHTTPFrontend != base.UseSingleHTTPFrontend {
base.UseSingleHTTPFrontend = overlay.UseSingleHTTPFrontend
}
if overlay.AuthRequireGuestsToLogin != base.AuthRequireGuestsToLogin {
base.AuthRequireGuestsToLogin = overlay.AuthRequireGuestsToLogin
}
if overlay.AuthLocalUsers.Enabled {
base.AuthLocalUsers.Enabled = overlay.AuthLocalUsers.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.DefaultPolicy.ShowLogList {
base.DefaultPolicy.ShowLogList = true
}
if overlay.Prometheus.Enabled {
base.Prometheus.Enabled = true
}
if overlay.Prometheus.DefaultGoMetrics {
base.Prometheus.DefaultGoMetrics = true
}
}
func overrideStrings(base *Config, overlay *Config) {
overrideString(&base.BannerMessage, overlay.BannerMessage)
overrideString(&base.BannerCSS, overlay.BannerCSS)
overrideString(&base.LogLevel, overlay.LogLevel)
overrideString(&base.PageTitle, overlay.PageTitle)
overrideString(&base.SectionNavigationStyle, overlay.SectionNavigationStyle)
overrideString(&base.DefaultPopupOnStart, overlay.DefaultPopupOnStart)
}
func overrideString(base *string, overlay string) {
if overlay != "" {
*base = overlay
}
}
func getActionTitles(actions []*Action) []string {
titles := make([]string, len(actions))
for i, action := range actions {
titles[i] = action.Title
}
return titles
return nil
}
var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`)
// Helper functions to reduce repetitive if/set chains
func stringVal(k *koanf.Koanf, key string, dest *string) {
if k.Exists(key) {
*dest = k.String(key)
}
}
func boolVal(k *koanf.Koanf, key string, dest *bool) {
if k.Exists(key) {
*dest = k.Bool(key)
}
}
func int64Val(k *koanf.Koanf, key string, dest *int64) {
if k.Exists(key) {
*dest = k.Int64(key)
}
}
func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) {
log.Debugf("envDecodeHookFunc called: from=%v, to=%v, data=%v", from, to, data)
if from.Kind() != reflect.String {

View File

@ -146,7 +146,7 @@ func initConfig(configDir string) {
)
}
var firstConfigPath string
var baseConfigPath string
for _, directory := range directories {
configPath := getConfigPath(directory)
@ -159,17 +159,20 @@ func initConfig(configDir string) {
log.WithFields(log.Fields{
"configPath": configPath,
"found": found,
}).Debug("Checking config path")
}).Debug("Checking base config path")
if !found {
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 {
@ -183,15 +186,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)
}