3k release: massively improve config loading, reduce log spam, etc (#700)
This commit is contained in:
commit
209856eda9
|
|
@ -8,11 +8,11 @@
|
||||||
|
|
||||||
<template #user-info>
|
<template #user-info>
|
||||||
<div class="flex-row user-info" style="gap: .5em;">
|
<div class="flex-row user-info" style="gap: .5em;">
|
||||||
<span id="link-login" v-if="!isLoggedIn"><router-link to="/login">Login</router-link></span>
|
<span id="link-login" v-if="!isLoggedIn && showLoginLink"><router-link to="/login">Login</router-link></span>
|
||||||
<router-link v-else to="/user" class="user-link">
|
<router-link v-else to="/user" class="user-link" v-if="isLoggedIn">
|
||||||
<span id="username-text">{{ username }}</span>
|
<span id="username-text">{{ username }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" />
|
<HugeiconsIcon :icon="UserCircle02Icon" width = "1.5em" height = "1.5em" v-if="isLoggedIn" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -71,7 +71,7 @@ import logoUrl from '../../OliveTinLogo.png';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const sidebar = ref(null);
|
const sidebar = ref(null);
|
||||||
const username = ref('guest');
|
const username = ref('notset');
|
||||||
const isLoggedIn = ref(false);
|
const isLoggedIn = ref(false);
|
||||||
const serverConnection = ref('Connected');
|
const serverConnection = ref('Connected');
|
||||||
const currentVersion = ref('?');
|
const currentVersion = ref('?');
|
||||||
|
|
@ -84,6 +84,7 @@ const showLogs = ref(true)
|
||||||
const showDiagnostics = ref(true)
|
const showDiagnostics = ref(true)
|
||||||
const initError = ref(false)
|
const initError = ref(false)
|
||||||
const initErrorMessage = ref('')
|
const initErrorMessage = ref('')
|
||||||
|
const showLoginLink = ref(true)
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
if (sidebar.value && showNavigation.value) {
|
if (sidebar.value && showNavigation.value) {
|
||||||
|
|
@ -102,6 +103,10 @@ function updateHeaderFromInit() {
|
||||||
showNavigation.value = window.initResponse.showNavigation
|
showNavigation.value = window.initResponse.showNavigation
|
||||||
showLogs.value = window.initResponse.showLogList
|
showLogs.value = window.initResponse.showLogList
|
||||||
showDiagnostics.value = window.initResponse.showDiagnostics
|
showDiagnostics.value = window.initResponse.showDiagnostics
|
||||||
|
|
||||||
|
if (!window.initResponse.authLocalLogin && window.initResponse.oAuth2Providers.length === 0) {
|
||||||
|
showLoginLink.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,15 +129,7 @@ async function requestInit() {
|
||||||
window.initErrorMessage = ''
|
window.initErrorMessage = ''
|
||||||
window.initCompleted = true
|
window.initCompleted = true
|
||||||
|
|
||||||
username.value = initResponse.authenticatedUser
|
window.updateHeaderFromInit()
|
||||||
isLoggedIn.value = initResponse.authenticatedUser !== '' && initResponse.authenticatedUser !== 'guest'
|
|
||||||
currentVersion.value = initResponse.currentVersion
|
|
||||||
bannerMessage.value = initResponse.bannerMessage || '';
|
|
||||||
bannerCss.value = initResponse.bannerCss || '';
|
|
||||||
showFooter.value = initResponse.showFooter
|
|
||||||
showNavigation.value = initResponse.showNavigation
|
|
||||||
showLogs.value = initResponse.showLogList
|
|
||||||
showDiagnostics.value = initResponse.showDiagnostics
|
|
||||||
|
|
||||||
if (showNavigation.value && sidebar.value) {
|
if (showNavigation.value && sidebar.value) {
|
||||||
for (const rootDashboard of initResponse.rootDashboards) {
|
for (const rootDashboard of initResponse.rootDashboards) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
make:
|
||||||
|
haproxy -f ./haproxy.conf
|
||||||
|
|
@ -1,15 +1,32 @@
|
||||||
|
global
|
||||||
|
stats socket /var/run/haproxy/admin.sock mode 660 level admin
|
||||||
|
log stdout local0
|
||||||
|
|
||||||
|
|
||||||
frontend cleartext_frontend
|
frontend cleartext_frontend
|
||||||
bind 0.0.0.0:80
|
bind 0.0.0.0:8080
|
||||||
|
|
||||||
|
timeout client 10s
|
||||||
|
|
||||||
|
stats enable
|
||||||
|
stats uri /stats
|
||||||
|
stats refresh 30s
|
||||||
|
|
||||||
|
log global
|
||||||
option httplog
|
option httplog
|
||||||
|
option dontlognull
|
||||||
|
log-format "[%t] %ci:%cp -> BACKEND:%b/%s | %HM %HU | Status:%ST | Bytes:%B"
|
||||||
|
mode http
|
||||||
|
|
||||||
use_backend be_olivetin_webs if { hdr(Host) -i olivetin.example.com && path_beg /websocket }
|
use_backend be_olivetin if { hdr(Host) -i -m beg olivetin.example.com }
|
||||||
use_backend be_olivetin_http if { hdr(Host) -i olivetin.example.com }
|
|
||||||
|
|
||||||
backend be_olivetin_http
|
backend be_olivetin
|
||||||
server olivetinServer 127.0.0.1:1337 check
|
mode http
|
||||||
|
timeout connect 10s
|
||||||
backend be_olivetin_webs
|
timeout server 10s
|
||||||
timeout tunnel 1h
|
timeout tunnel 1h
|
||||||
option http-server-close
|
option http-server-close
|
||||||
server olivetinServer 127.0.0.1:1337
|
http-request set-header X-Forwarded-User "Alice"
|
||||||
|
http-request set-header X-Forwarded-Group "Group1,Group2"
|
||||||
|
server olivetinServer 127.0.0.1:1337 check
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -208,12 +208,19 @@ func UserFromContext[T any](ctx context.Context, req *connect.Request[T], cfg *c
|
||||||
} else {
|
} else {
|
||||||
buildUserAcls(cfg, &user)
|
buildUserAcls(cfg, &user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path := ""
|
||||||
|
if req != nil {
|
||||||
|
path = req.Spec().Procedure
|
||||||
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
"usergroupLine": user.UsergroupLine,
|
"usergroupLine": user.UsergroupLine,
|
||||||
"provider": user.Provider,
|
"provider": user.Provider,
|
||||||
"acls": user.Acls,
|
"acls": user.Acls,
|
||||||
}).Debugf("UserFromContext")
|
"path": path,
|
||||||
|
}).Debugf("Authenticated API request")
|
||||||
return &user
|
return &user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -481,16 +481,20 @@ func (api *oliveTinAPI) GetActionLogs(ctx ctx.Context, req *connect.Request[apiv
|
||||||
ret.StartOffset = page.start
|
ret.StartOffset = page.start
|
||||||
return connect.NewResponse(ret), nil
|
return connect.NewResponse(ret), nil
|
||||||
}
|
}
|
||||||
// Newest-first slicing: compute reversed indices
|
// Newest-first slicing: compute reversed indices
|
||||||
startIdx := page.total - page.end
|
startIdx := page.total - page.end
|
||||||
endIdx := page.total - page.start
|
endIdx := page.total - page.start
|
||||||
if startIdx < 0 { startIdx = 0 }
|
if startIdx < 0 {
|
||||||
if endIdx > int64(len(filtered)) { endIdx = int64(len(filtered)) }
|
startIdx = 0
|
||||||
for _, le := range filtered[startIdx:endIdx] {
|
}
|
||||||
ret.Logs = append(ret.Logs, api.internalLogEntryToPb(le, user))
|
if endIdx > int64(len(filtered)) {
|
||||||
}
|
endIdx = int64(len(filtered))
|
||||||
// Entries older than the returned newest page
|
}
|
||||||
ret.CountRemaining = page.start
|
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.PageSize = page.size
|
||||||
ret.TotalCount = page.total
|
ret.TotalCount = page.total
|
||||||
ret.StartOffset = page.start
|
ret.StartOffset = page.start
|
||||||
|
|
@ -662,7 +666,9 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
|
||||||
AuthenticatedUser: user,
|
AuthenticatedUser: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("EventStream: client connected: %v", client.AuthenticatedUser.Username)
|
log.WithFields(log.Fields{
|
||||||
|
"authenticatedUser": user.Username,
|
||||||
|
}).Debugf("EventStream: client connected")
|
||||||
|
|
||||||
api.streamingClientsMutex.Lock()
|
api.streamingClientsMutex.Lock()
|
||||||
api.streamingClients[client] = struct{}{}
|
api.streamingClients[client] = struct{}{}
|
||||||
|
|
@ -814,7 +820,7 @@ func (api *oliveTinAPI) buildRootDashboards(user *acl.AuthenticatedUser, dashboa
|
||||||
func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) {
|
func (api *oliveTinAPI) addDefaultDashboardIfNeeded(rootDashboards *[]string, rr *DashboardRenderRequest) {
|
||||||
defaultDashboard := buildDefaultDashboard(rr)
|
defaultDashboard := buildDefaultDashboard(rr)
|
||||||
if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
|
if defaultDashboard != nil && len(defaultDashboard.Contents) > 0 {
|
||||||
log.Infof("defaultDashboard: %+v", defaultDashboard.Contents)
|
log.Tracef("defaultDashboard: %+v", defaultDashboard.Contents)
|
||||||
*rootDashboards = append(*rootDashboards, "Actions")
|
*rootDashboards = append(*rootDashboards, "Actions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,212 +7,212 @@ import (
|
||||||
// Action represents the core functionality of OliveTin - commands that show up
|
// Action represents the core functionality of OliveTin - commands that show up
|
||||||
// as buttons in the UI.
|
// as buttons in the UI.
|
||||||
type Action struct {
|
type Action struct {
|
||||||
ID string
|
ID string `koanf:"id"`
|
||||||
Title string
|
Title string `koanf:"title"`
|
||||||
Icon string
|
Icon string `koanf:"icon"`
|
||||||
Shell string
|
Shell string `koanf:"shell"`
|
||||||
Exec []string
|
Exec []string `koanf:"exec"`
|
||||||
ShellAfterCompleted string
|
ShellAfterCompleted string `koanf:"shellAfterCompleted"`
|
||||||
Timeout int
|
Timeout int `koanf:"timeout"`
|
||||||
Acls []string
|
Acls []string `koanf:"acls"`
|
||||||
Entity string
|
Entity string `koanf:"entity"`
|
||||||
Hidden bool
|
Hidden bool `koanf:"hidden"`
|
||||||
ExecOnStartup bool
|
ExecOnStartup bool `koanf:"execOnStartup"`
|
||||||
ExecOnCron []string
|
ExecOnCron []string `koanf:"execOnCron"`
|
||||||
ExecOnFileCreatedInDir []string
|
ExecOnFileCreatedInDir []string `koanf:"execOnFileCreatedInDir"`
|
||||||
ExecOnFileChangedInDir []string
|
ExecOnFileChangedInDir []string `koanf:"execOnFileChangedInDir"`
|
||||||
ExecOnCalendarFile string
|
ExecOnCalendarFile string `koanf:"execOnCalendarFile"`
|
||||||
Triggers []string
|
Triggers []string `koanf:"triggers"`
|
||||||
MaxConcurrent int
|
MaxConcurrent int `koanf:"maxConcurrent"`
|
||||||
MaxRate []RateSpec
|
MaxRate []RateSpec `koanf:"maxRate"`
|
||||||
Arguments []ActionArgument
|
Arguments []ActionArgument `koanf:"arguments"`
|
||||||
PopupOnStart string
|
PopupOnStart string `koanf:"popupOnStart"`
|
||||||
SaveLogs SaveLogsConfig
|
SaveLogs SaveLogsConfig `koanf:"saveLogs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionArgument objects appear on Actions.
|
// ActionArgument objects appear on Actions.
|
||||||
type ActionArgument struct {
|
type ActionArgument struct {
|
||||||
Name string
|
Name string `koanf:"name"`
|
||||||
Title string
|
Title string `koanf:"title"`
|
||||||
Description string
|
Description string `koanf:"description"`
|
||||||
Type string
|
Type string `koanf:"type"`
|
||||||
Default string
|
Default string `koanf:"default"`
|
||||||
Choices []ActionArgumentChoice
|
Choices []ActionArgumentChoice `koanf:"choices"`
|
||||||
Entity string
|
Entity string `koanf:"entity"`
|
||||||
RejectNull bool
|
RejectNull bool `koanf:"rejectNull"`
|
||||||
Suggestions map[string]string
|
Suggestions map[string]string `koanf:"suggestions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionArgumentChoice represents a predefined choice for an argument.
|
// ActionArgumentChoice represents a predefined choice for an argument.
|
||||||
type ActionArgumentChoice struct {
|
type ActionArgumentChoice struct {
|
||||||
Value string
|
Value string `koanf:"value"`
|
||||||
Title string
|
Title string `koanf:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RateSpec allows you to set a max frequency for an action.
|
// RateSpec allows you to set a max frequency for an action.
|
||||||
type RateSpec struct {
|
type RateSpec struct {
|
||||||
Limit int
|
Limit int `koanf:"limit"`
|
||||||
Duration string
|
Duration string `koanf:"duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entity represents a "thing" that can have multiple actions associated with it.
|
// Entity represents a "thing" that can have multiple actions associated with it.
|
||||||
// for example, a media player with a start and stop action.
|
// for example, a media player with a start and stop action.
|
||||||
type EntityFile struct {
|
type EntityFile struct {
|
||||||
File string
|
File string `koanf:"file"`
|
||||||
Name string
|
Name string `koanf:"name"`
|
||||||
Icon string
|
Icon string `koanf:"icon"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PermissionsList defines what users can do with an action.
|
// PermissionsList defines what users can do with an action.
|
||||||
type PermissionsList struct {
|
type PermissionsList struct {
|
||||||
View bool `mapstructure:"view"`
|
View bool `koanf:"view"`
|
||||||
Exec bool `mapstructure:"exec"`
|
Exec bool `koanf:"exec"`
|
||||||
Logs bool `mapstructure:"logs"`
|
Logs bool `koanf:"logs"`
|
||||||
Kill bool `mapstructure:"kill"`
|
Kill bool `koanf:"kill"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessControlList defines what permissions apply to a user or user group.
|
// AccessControlList defines what permissions apply to a user or user group.
|
||||||
type AccessControlList struct {
|
type AccessControlList struct {
|
||||||
Name string
|
Name string `koanf:"name"`
|
||||||
AddToEveryAction bool
|
AddToEveryAction bool `koanf:"addToEveryAction"`
|
||||||
MatchUsergroups []string
|
MatchUsergroups []string `koanf:"matchUsergroups"`
|
||||||
MatchUsernames []string
|
MatchUsernames []string `koanf:"matchUsernames"`
|
||||||
Permissions PermissionsList
|
Permissions PermissionsList `koanf:"permissions"`
|
||||||
Policy ConfigurationPolicy
|
Policy ConfigurationPolicy `koanf:"policy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigurationPolicy defines global settings which are overridden with an ACL.
|
// ConfigurationPolicy defines global settings which are overridden with an ACL.
|
||||||
type ConfigurationPolicy struct {
|
type ConfigurationPolicy struct {
|
||||||
ShowDiagnostics bool `mapstructure:"showDiagnostics"`
|
ShowDiagnostics bool `koanf:"showDiagnostics"`
|
||||||
ShowLogList bool `mapstructure:"showLogList"`
|
ShowLogList bool `koanf:"showLogList"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PrometheusConfig struct {
|
type PrometheusConfig struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `koanf:"enabled"`
|
||||||
DefaultGoMetrics bool `mapstructure:"defaultGoMetrics"`
|
DefaultGoMetrics bool `koanf:"defaultGoMetrics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config is the global config used through the whole app.
|
// Config is the global config used through the whole app.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
UseSingleHTTPFrontend bool `mapstructure:"useSingleHTTPFrontend"`
|
UseSingleHTTPFrontend bool `koanf:"useSingleHTTPFrontend"`
|
||||||
ThemeName string `mapstructure:"themeName"`
|
ThemeName string `koanf:"themeName"`
|
||||||
ThemeCacheDisabled bool `mapstructure:"themeCacheDisabled"`
|
ThemeCacheDisabled bool `koanf:"themeCacheDisabled"`
|
||||||
ListenAddressSingleHTTPFrontend string `mapstructure:"listenAddressSingleHTTPFrontend"`
|
ListenAddressSingleHTTPFrontend string `koanf:"listenAddressSingleHTTPFrontend"`
|
||||||
ListenAddressWebUI string `mapstructure:"listenAddressWebUI"`
|
ListenAddressWebUI string `koanf:"listenAddressWebUI"`
|
||||||
ListenAddressRestActions string `mapstructure:"listenAddressRestActions"`
|
ListenAddressRestActions string `koanf:"listenAddressRestActions"`
|
||||||
ListenAddressPrometheus string `mapstructure:"listenAddressPrometheus"`
|
ListenAddressPrometheus string `koanf:"listenAddressPrometheus"`
|
||||||
ExternalRestAddress string `mapstructure:"externalRestAddress"`
|
ExternalRestAddress string `koanf:"externalRestAddress"`
|
||||||
LogLevel string `mapstructure:"logLevel"`
|
LogLevel string `koanf:"logLevel"`
|
||||||
LogDebugOptions LogDebugOptions `mapstructure:"logDebugOptions"`
|
LogDebugOptions LogDebugOptions `koanf:"logDebugOptions"`
|
||||||
LogHistoryPageSize int64 `mapstructure:"logHistoryPageSize"`
|
LogHistoryPageSize int64 `koanf:"logHistoryPageSize"`
|
||||||
Actions []*Action `mapstructure:"actions"`
|
Actions []*Action `koanf:"actions"`
|
||||||
Entities []*EntityFile `mapstructure:"entities"`
|
Entities []*EntityFile `koanf:"entities"`
|
||||||
Dashboards []*DashboardComponent `mapstructure:"dashboards"`
|
Dashboards []*DashboardComponent `koanf:"dashboards"`
|
||||||
CheckForUpdates bool `mapstructure:"checkForUpdates"`
|
CheckForUpdates bool `koanf:"checkForUpdates"`
|
||||||
PageTitle string `mapstructure:"pageTitle"`
|
PageTitle string `koanf:"pageTitle"`
|
||||||
ShowFooter bool `mapstructure:"showFooter"`
|
ShowFooter bool `koanf:"showFooter"`
|
||||||
ShowNavigation bool `mapstructure:"showNavigation"`
|
ShowNavigation bool `koanf:"showNavigation"`
|
||||||
ShowNewVersions bool `mapstructure:"showNewVersions"`
|
ShowNewVersions bool `koanf:"showNewVersions"`
|
||||||
EnableCustomJs bool `mapstructure:"enableCustomJs"`
|
EnableCustomJs bool `koanf:"enableCustomJs"`
|
||||||
AuthJwtCookieName string `mapstructure:"authJwtCookieName"`
|
AuthJwtCookieName string `koanf:"authJwtCookieName"`
|
||||||
AuthJwtHeader string `mapstructure:"authJwtHeader"`
|
AuthJwtHeader string `koanf:"authJwtHeader"`
|
||||||
AuthJwtAud string `mapstructure:"authJwtAud"`
|
AuthJwtAud string `koanf:"authJwtAud"`
|
||||||
AuthJwtDomain string `mapstructure:"authJwtDomain"`
|
AuthJwtDomain string `koanf:"authJwtDomain"`
|
||||||
AuthJwtCertsURL string `mapstructure:"authJwtCertsUrl"`
|
AuthJwtCertsURL string `koanf:"authJwtCertsUrl"`
|
||||||
AuthJwtHmacSecret string `mapstructure:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
|
AuthJwtHmacSecret string `koanf:"authJwtHmacSecret"` // mutually exclusive with pub key config fields
|
||||||
AuthJwtClaimUsername string `mapstructure:"authJwtClaimUsername"`
|
AuthJwtClaimUsername string `koanf:"authJwtClaimUsername"`
|
||||||
AuthJwtClaimUserGroup string `mapstructure:"authJwtClaimUserGroup"`
|
AuthJwtClaimUserGroup string `koanf:"authJwtClaimUserGroup"`
|
||||||
AuthJwtPubKeyPath string `mapstructure:"authJwtPubKeyPath"` // will read pub key from file on disk
|
AuthJwtPubKeyPath string `koanf:"authJwtPubKeyPath"` // will read pub key from file on disk
|
||||||
AuthHttpHeaderUsername string `mapstructure:"authHttpHeaderUsername"`
|
AuthHttpHeaderUsername string `koanf:"authHttpHeaderUsername"`
|
||||||
AuthHttpHeaderUserGroup string `mapstructure:"authHttpHeaderUserGroup"`
|
AuthHttpHeaderUserGroup string `koanf:"authHttpHeaderUserGroup"`
|
||||||
AuthHttpHeaderUserGroupSep string `mapstructure:"authHttpHeaderUserGroupSep"`
|
AuthHttpHeaderUserGroupSep string `koanf:"authHttpHeaderUserGroupSep"`
|
||||||
AuthLocalUsers AuthLocalUsersConfig `mapstructure:"authLocalUsers"`
|
AuthLocalUsers AuthLocalUsersConfig `koanf:"authLocalUsers"`
|
||||||
AuthLoginUrl string `mapstructure:"authLoginUrl"`
|
AuthLoginUrl string `koanf:"authLoginUrl"`
|
||||||
AuthRequireGuestsToLogin bool `mapstructure:"authRequireGuestsToLogin"`
|
AuthRequireGuestsToLogin bool `koanf:"authRequireGuestsToLogin"`
|
||||||
AuthOAuth2RedirectURL string `mapstructure:"authOAuth2RedirectUrl"`
|
AuthOAuth2RedirectURL string `koanf:"authOAuth2RedirectUrl"`
|
||||||
AuthOAuth2Providers map[string]*OAuth2Provider `mapstructure:"authOAuth2Providers"`
|
AuthOAuth2Providers map[string]*OAuth2Provider `koanf:"authOAuth2Providers"`
|
||||||
DefaultPermissions PermissionsList `mapstructure:"defaultPermissions"`
|
DefaultPermissions PermissionsList `koanf:"defaultPermissions"`
|
||||||
DefaultPolicy ConfigurationPolicy `mapstructure:"defaultPolicy"`
|
DefaultPolicy ConfigurationPolicy `koanf:"defaultPolicy"`
|
||||||
AccessControlLists []*AccessControlList `mapstructure:"accessControlLists"`
|
AccessControlLists []*AccessControlList `koanf:"accessControlLists"`
|
||||||
WebUIDir string `mapstructure:"webUIDir"`
|
WebUIDir string `koanf:"webUIDir"`
|
||||||
CronSupportForSeconds bool `mapstructure:"cronSupportForSeconds"`
|
CronSupportForSeconds bool `koanf:"cronSupportForSeconds"`
|
||||||
SectionNavigationStyle string `mapstructure:"sectionNavigationStyle"`
|
SectionNavigationStyle string `koanf:"sectionNavigationStyle"`
|
||||||
DefaultPopupOnStart string `mapstructure:"defaultPopupOnStart"`
|
DefaultPopupOnStart string `koanf:"defaultPopupOnStart"`
|
||||||
InsecureAllowDumpOAuth2UserData bool `mapstructure:"insecureAllowDumpOAuth2UserData"`
|
InsecureAllowDumpOAuth2UserData bool `koanf:"insecureAllowDumpOAuth2UserData"`
|
||||||
InsecureAllowDumpVars bool `mapstructure:"insecureAllowDumpVars"`
|
InsecureAllowDumpVars bool `koanf:"insecureAllowDumpVars"`
|
||||||
InsecureAllowDumpSos bool `mapstructure:"insecureAllowDumpSos"`
|
InsecureAllowDumpSos bool `koanf:"insecureAllowDumpSos"`
|
||||||
InsecureAllowDumpActionMap bool `mapstructure:"insecureAllowDumpActionMap"`
|
InsecureAllowDumpActionMap bool `koanf:"insecureAllowDumpActionMap"`
|
||||||
InsecureAllowDumpJwtClaims bool `mapstructure:"insecureAllowDumpJwtClaims"`
|
InsecureAllowDumpJwtClaims bool `koanf:"insecureAllowDumpJwtClaims"`
|
||||||
Prometheus PrometheusConfig `mapstructure:"prometheus"`
|
Prometheus PrometheusConfig `koanf:"prometheus"`
|
||||||
SaveLogs SaveLogsConfig `mapstructure:"saveLogs"`
|
SaveLogs SaveLogsConfig `koanf:"saveLogs"`
|
||||||
DefaultIconForActions string `mapstructure:"defaultIconForActions"`
|
DefaultIconForActions string `koanf:"defaultIconForActions"`
|
||||||
DefaultIconForDirectories string `mapstructure:"defaultIconForDirectories"`
|
DefaultIconForDirectories string `koanf:"defaultIconForDirectories"`
|
||||||
DefaultIconForBack string `mapstructure:"defaultIconForBack"`
|
DefaultIconForBack string `koanf:"defaultIconForBack"`
|
||||||
AdditionalNavigationLinks []*NavigationLink `mapstructure:"additionalNavigationLinks"`
|
AdditionalNavigationLinks []*NavigationLink `koanf:"additionalNavigationLinks"`
|
||||||
ServiceHostMode string `mapstructure:"serviceHostMode"`
|
ServiceHostMode string `koanf:"serviceHostMode"`
|
||||||
StyleMods []string `mapstructure:"styleMods"`
|
StyleMods []string `koanf:"styleMods"`
|
||||||
BannerMessage string `mapstructure:"bannerMessage"`
|
BannerMessage string `koanf:"bannerMessage"`
|
||||||
BannerCSS string `mapstructure:"bannerCss"`
|
BannerCSS string `koanf:"bannerCss"`
|
||||||
Include string `mapstructure:"include"`
|
Include string `koanf:"include"`
|
||||||
|
|
||||||
sourceFiles []string
|
sourceFiles []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthLocalUsersConfig struct {
|
type AuthLocalUsersConfig struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `koanf:"enabled"`
|
||||||
Users []*LocalUser `mapstructure:"users"`
|
Users []*LocalUser `koanf:"users"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalUser struct {
|
type LocalUser struct {
|
||||||
Username string `mapstructure:"username"`
|
Username string `koanf:"username"`
|
||||||
Usergroup string `mapstructure:"usergroup"`
|
Usergroup string `koanf:"usergroup"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `koanf:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuth2Provider struct {
|
type OAuth2Provider struct {
|
||||||
Name string
|
Name string `koanf:"name"`
|
||||||
Title string
|
Title string `koanf:"title"`
|
||||||
ClientID string
|
ClientID string `koanf:"clientId"`
|
||||||
ClientSecret string
|
ClientSecret string `koanf:"clientSecret"`
|
||||||
Icon string
|
Icon string `koanf:"icon"`
|
||||||
Scopes []string
|
Scopes []string `koanf:"scopes"`
|
||||||
AuthUrl string
|
AuthUrl string `koanf:"authUrl"`
|
||||||
TokenUrl string
|
TokenUrl string `koanf:"tokenUrl"`
|
||||||
WhoamiUrl string
|
WhoamiUrl string `koanf:"whoamiUrl"`
|
||||||
UsernameField string
|
UsernameField string `koanf:"usernameField"`
|
||||||
UserGroupField string
|
UserGroupField string `koanf:"userGroupField"`
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool `koanf:"insecureSkipVerify"`
|
||||||
CallbackTimeout int
|
CallbackTimeout int `koanf:"callbackTimeout"`
|
||||||
CertBundlePath string
|
CertBundlePath string `koanf:"certBundlePath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NavigationLink struct {
|
type NavigationLink struct {
|
||||||
Title string `mapstructure:"title"`
|
Title string `koanf:"title"`
|
||||||
Url string `mapstructure:"url"`
|
Url string `koanf:"url"`
|
||||||
Target string `mapstructure:"target"`
|
Target string `koanf:"target"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SaveLogsConfig struct {
|
type SaveLogsConfig struct {
|
||||||
ResultsDirectory string `mapstructure:"resultsDirectory"`
|
ResultsDirectory string `koanf:"resultsDirectory"`
|
||||||
OutputDirectory string `mapstructure:"outputDirectory"`
|
OutputDirectory string `koanf:"outputDirectory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogDebugOptions struct {
|
type LogDebugOptions struct {
|
||||||
SingleFrontendRequests bool
|
SingleFrontendRequests bool `koanf:"singleFrontendRequests"`
|
||||||
SingleFrontendRequestHeaders bool
|
SingleFrontendRequestHeaders bool `koanf:"singleFrontendRequestHeaders"`
|
||||||
AclCheckStarted bool
|
AclCheckStarted bool `koanf:"aclCheckStarted"`
|
||||||
AclMatched bool
|
AclMatched bool `koanf:"aclMatched"`
|
||||||
AclNotMatched bool
|
AclNotMatched bool `koanf:"aclNotMatched"`
|
||||||
AclNoneMatched bool
|
AclNoneMatched bool `koanf:"aclNoneMatched"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardComponent struct {
|
type DashboardComponent struct {
|
||||||
Title string
|
Title string `koanf:"title"`
|
||||||
Type string
|
Type string `koanf:"type"`
|
||||||
Entity string
|
Entity string `koanf:"entity"`
|
||||||
Icon string
|
Icon string `koanf:"icon"`
|
||||||
CssClass string
|
CssClass string `koanf:"cssClass"`
|
||||||
Contents []*DashboardComponent
|
Contents []*DashboardComponent `koanf:"contents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
|
|
|
||||||
|
|
@ -34,90 +34,32 @@ func AddListener(l func()) {
|
||||||
listeners = append(listeners, l)
|
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) {
|
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) {
|
if !unmarshalRoot(k, cfg) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCollectionsFallbacks(k, cfg)
|
|
||||||
|
|
||||||
applyConfigOverrides(k, cfg)
|
|
||||||
|
|
||||||
afterLoadFinalize(cfg, configPath)
|
afterLoadFinalize(cfg, configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool {
|
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)
|
log.Errorf("Error unmarshalling config: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
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) {
|
func afterLoadFinalize(cfg *Config, configPath string) {
|
||||||
metricConfigReloadedCount.Inc()
|
metricConfigReloadedCount.Inc()
|
||||||
metricConfigActionCount.Set(float64(len(cfg.Actions)))
|
metricConfigActionCount.Set(float64(len(cfg.Actions)))
|
||||||
|
|
@ -130,39 +72,19 @@ func afterLoadFinalize(cfg *Config, configPath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyConfigOverrides(k *koanf.Koanf, cfg *Config) {
|
// loadIncludedConfigsFromDir loads configuration files from an include directory and merges them
|
||||||
// Override fields that should be read from config
|
func loadIncludedConfigsFromDir(k *koanf.Koanf, baseConfigPath string) {
|
||||||
// mapstructure tags should make most of this unnecessary, but keep for safety
|
relativeIncludePath := k.String("include")
|
||||||
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)
|
|
||||||
|
|
||||||
// Handle nested defaultPolicy struct
|
if relativeIncludePath == "" {
|
||||||
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 == "" {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
includePath := filepath.Join(filepath.Dir(baseConfigPath), cfg.Include)
|
includePath := filepath.Join(filepath.Dir(baseConfigPath), relativeIncludePath)
|
||||||
log.Infof("Loading included configs from: %s", includePath)
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"includePath": includePath,
|
||||||
|
}).Infof("Loading included configs from dir")
|
||||||
|
|
||||||
yamlFiles, ok := listYamlFiles(includePath)
|
yamlFiles, ok := listYamlFiles(includePath)
|
||||||
if !ok || len(yamlFiles) == 0 {
|
if !ok || len(yamlFiles) == 0 {
|
||||||
|
|
@ -171,11 +93,10 @@ func LoadIncludedConfigs(cfg *Config, k *koanf.Koanf, baseConfigPath string) {
|
||||||
|
|
||||||
sort.Strings(yamlFiles)
|
sort.Strings(yamlFiles)
|
||||||
for _, filename := range yamlFiles {
|
for _, filename := range yamlFiles {
|
||||||
loadAndMergeIncludedFile(cfg, includePath, filename)
|
loadAndMergeIncludedFile(k, includePath, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
|
log.Infof("Finished loading %d included config file(s)", len(yamlFiles))
|
||||||
cfg.Sanitize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func listYamlFiles(includePath string) ([]string, bool) {
|
func listYamlFiles(includePath string) ([]string, bool) {
|
||||||
|
|
@ -209,152 +130,45 @@ func listYamlFiles(includePath string) ([]string, bool) {
|
||||||
return yamlFiles, true
|
return yamlFiles, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAndMergeIncludedFile(cfg *Config, includePath, filename string) {
|
func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
|
||||||
filePath := filepath.Join(includePath, filename)
|
filePath := filepath.Join(includePath, filename)
|
||||||
log.Infof("Loading included config file: %s", filePath)
|
|
||||||
|
|
||||||
includeK := koanf.New(".")
|
if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
|
||||||
if err := includeK.Load(file.Provider(filePath), yaml.Parser()); err != nil {
|
|
||||||
log.Errorf("Error loading included config file %s: %v", filePath, err)
|
log.Errorf("Error loading included config file %s: %v", filePath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tempCfg := &Config{}
|
log.WithFields(log.Fields{
|
||||||
if err := includeK.Unmarshal(".", tempCfg); err != nil {
|
"filePath": filePath,
|
||||||
log.Errorf("Error unmarshalling included config file %s: %v", filePath, err)
|
}).Info("Successfully loaded included config file")
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCollectionsFallbacks(includeK, tempCfg)
|
|
||||||
|
|
||||||
mergeConfig(cfg, tempCfg)
|
|
||||||
log.Infof("Successfully loaded and merged %s", filename)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeConfig(base *Config, overlay *Config) {
|
func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
|
||||||
mergeSlices(base, overlay)
|
// Handle actions merging - koanf provides []interface{} not []*Action
|
||||||
overrideSimple(base, overlay)
|
// Merge src (new) into dest (existing) by appending src's actions to dest's actions
|
||||||
overrideNested(base, overlay)
|
if srcActions, ok := src["actions"]; ok {
|
||||||
overrideStrings(base, overlay)
|
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) {
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`)
|
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) {
|
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)
|
log.Debugf("envDecodeHookFunc called: from=%v, to=%v, data=%v", from, to, data)
|
||||||
if from.Kind() != reflect.String {
|
if from.Kind() != reflect.String {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ func (e *Executor) RebuildActionMap() {
|
||||||
|
|
||||||
findDashboardActionTitles(req)
|
findDashboardActionTitles(req)
|
||||||
|
|
||||||
log.Infof("dashboardActionTitles: %v", req.DashboardActionTitles)
|
log.WithFields(log.Fields{
|
||||||
|
"titles": req.DashboardActionTitles,
|
||||||
|
}).Trace("dashboardActionTitles")
|
||||||
|
|
||||||
for configOrder, action := range e.Cfg.Actions {
|
for configOrder, action := range e.Cfg.Actions {
|
||||||
if action.Entity != "" {
|
if action.Entity != "" {
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ func StartSingleHTTPFrontend(cfg *config.Config, ex *executor.Executor) {
|
||||||
|
|
||||||
r.URL.Path = apiPath + fn
|
r.URL.Path = apiPath + fn
|
||||||
|
|
||||||
log.Debugf("SingleFrontend HTTP API Req URL after rewrite: %v", r.URL.Path)
|
log.WithFields(log.Fields{
|
||||||
|
"path": r.URL.Path,
|
||||||
|
}).Tracef("SingleFrontend HTTP API Req URL after rewrite")
|
||||||
|
|
||||||
|
logDebugRequest(cfg, "api", r)
|
||||||
|
|
||||||
apiHandler.ServeHTTP(w, r)
|
apiHandler.ServeHTTP(w, r)
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
|
http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
|
log.Tracef("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
|
||||||
http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
|
http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
|
||||||
// http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
|
// http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
package websocket
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
|
||||||
"github.com/OliveTin/OliveTin/internal/executor"
|
|
||||||
ws "github.com/gorilla/websocket"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = ws.Upgrader{
|
|
||||||
CheckOrigin: checkOriginPermissive,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
sendmutex = sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type WebsocketClient struct {
|
|
||||||
conn *ws.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
var clients []*WebsocketClient
|
|
||||||
|
|
||||||
var marshalOptions = protojson.MarshalOptions{
|
|
||||||
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
|
|
||||||
EmitUnpopulated: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ExecutionListener WebsocketExecutionListener
|
|
||||||
|
|
||||||
type WebsocketExecutionListener struct{}
|
|
||||||
|
|
||||||
func (WebsocketExecutionListener) OnExecutionStarted(ile *executor.InternalLogEntry) {
|
|
||||||
broadcast(&apiv1.EventExecutionStarted{
|
|
||||||
LogEntry: internalLogEntryToPb(ile),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnEntityChanged() {
|
|
||||||
broadcast(&apiv1.EventEntityChanged{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (WebsocketExecutionListener) OnActionMapRebuilt() {
|
|
||||||
broadcast(&apiv1.EventConfigChanged{})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
The default checkOrigin function checks that the origin (browser) matches the
|
|
||||||
request origin. However in OliveTin we expect many users to deliberately proxy
|
|
||||||
the connection with reverse proxies.
|
|
||||||
|
|
||||||
So, we just permit any origin. After some searching I'm not sure if this exposes
|
|
||||||
OliveTin to security issues, but it seems probably not. It would be possible to
|
|
||||||
create a config option like PermitWebsocketConnectionsFrom or something, but
|
|
||||||
I'd prefer if OliveTin works as much as possible "out of the box".
|
|
||||||
|
|
||||||
If this does expose OliveTin to security issues, it will be changed in the
|
|
||||||
future obviously.
|
|
||||||
*/
|
|
||||||
func checkOriginPermissive(r *http.Request) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (WebsocketExecutionListener) OnOutputChunk(chunk []byte, executionTrackingId string) {
|
|
||||||
log.Tracef("outputchunk: %s", string(chunk))
|
|
||||||
|
|
||||||
oc := &apiv1.EventOutputChunk{
|
|
||||||
Output: string(chunk),
|
|
||||||
ExecutionTrackingId: executionTrackingId,
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast(oc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (WebsocketExecutionListener) OnExecutionFinished(logEntry *executor.InternalLogEntry) {
|
|
||||||
evt := &apiv1.EventExecutionFinished{
|
|
||||||
LogEntry: internalLogEntryToPb(logEntry),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("WS Execution finished: %v", evt.LogEntry)
|
|
||||||
|
|
||||||
broadcast(evt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func broadcast(pbmsg protoreflect.ProtoMessage) {
|
|
||||||
payload, err := marshalOptions.Marshal(pbmsg)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("websocket marshal error: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
messageType := pbmsg.ProtoReflect().Descriptor().FullName()
|
|
||||||
|
|
||||||
// <EVIL>
|
|
||||||
// So, the websocket wants to encode messages using the same protomarshaller
|
|
||||||
// as the REST API - this gives consistency instead of using encoding/json
|
|
||||||
// and allows us to set specific marshalOptions.
|
|
||||||
//
|
|
||||||
// However, the protomarshaller will marshal the type, but the JavaScript at
|
|
||||||
// the other end has no idea what type this object is - as we're just sending
|
|
||||||
// it as JSON over the websocket.
|
|
||||||
//
|
|
||||||
// Therefore, we wrap the nicely marsheled bytes in a hacky JSON string
|
|
||||||
// literal and encode that string just with a byte array cast.
|
|
||||||
hackyMessageEnvelope := "{\"type\": \"" + messageType + "\", \"payload\": "
|
|
||||||
|
|
||||||
hackyMessage := []byte{}
|
|
||||||
hackyMessage = append(hackyMessage, []byte(hackyMessageEnvelope)...)
|
|
||||||
hackyMessage = append(hackyMessage, payload...)
|
|
||||||
hackyMessage = append(hackyMessage, []byte("}")...)
|
|
||||||
// </EVIL>
|
|
||||||
|
|
||||||
sendmutex.Lock()
|
|
||||||
for _, client := range clients {
|
|
||||||
err := client.conn.WriteMessage(ws.TextMessage, hackyMessage)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("websocket send error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendmutex.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *WebsocketClient) messageLoop() {
|
|
||||||
for {
|
|
||||||
mt, message, err := c.conn.ReadMessage()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("err: %v", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("websocket recv: %s %d", message, mt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleWebsocket(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
c, err := upgrader.Upgrade(w, r, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Websocket issue: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// defer c.Close()
|
|
||||||
|
|
||||||
wsclient := &WebsocketClient{
|
|
||||||
conn: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
sendmutex.Lock()
|
|
||||||
|
|
||||||
clients = append(clients, wsclient)
|
|
||||||
|
|
||||||
sendmutex.Unlock()
|
|
||||||
|
|
||||||
go wsclient.messageLoop()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func internalLogEntryToPb(logEntry *executor.InternalLogEntry) *apiv1.LogEntry {
|
|
||||||
return &apiv1.LogEntry{
|
|
||||||
ActionTitle: logEntry.ActionTitle,
|
|
||||||
ActionIcon: logEntry.ActionIcon,
|
|
||||||
ActionId: logEntry.ActionId,
|
|
||||||
DatetimeStarted: logEntry.DatetimeStarted.Format("2006-01-02 15:04:05"),
|
|
||||||
DatetimeFinished: logEntry.DatetimeFinished.Format("2006-01-02 15:04:05"),
|
|
||||||
Output: logEntry.Output,
|
|
||||||
TimedOut: logEntry.TimedOut,
|
|
||||||
Blocked: logEntry.Blocked,
|
|
||||||
ExitCode: logEntry.ExitCode,
|
|
||||||
Tags: logEntry.Tags,
|
|
||||||
ExecutionTrackingId: logEntry.ExecutionTrackingID,
|
|
||||||
ExecutionStarted: logEntry.ExecutionStarted,
|
|
||||||
ExecutionFinished: logEntry.ExecutionFinished,
|
|
||||||
User: logEntry.Username,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"github.com/OliveTin/OliveTin/internal/onstartup"
|
"github.com/OliveTin/OliveTin/internal/onstartup"
|
||||||
"github.com/OliveTin/OliveTin/internal/servicehost"
|
"github.com/OliveTin/OliveTin/internal/servicehost"
|
||||||
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
|
updatecheck "github.com/OliveTin/OliveTin/internal/updatecheck"
|
||||||
"github.com/OliveTin/OliveTin/internal/websocket"
|
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -147,23 +146,33 @@ func initConfig(configDir string) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstConfigPath string
|
var baseConfigPath string
|
||||||
|
|
||||||
for _, directory := range directories {
|
for _, directory := range directories {
|
||||||
configPath := getConfigPath(directory)
|
configPath := getConfigPath(directory)
|
||||||
|
|
||||||
log.Debugf("Checking config path: %s", configPath)
|
found := true
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err != nil {
|
if _, err := os.Stat(configPath); err != nil {
|
||||||
log.Debugf("Config file not found at %s: %v", configPath, err)
|
found = false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"configPath": configPath,
|
||||||
|
"found": found,
|
||||||
|
}).Debug("Checking base config path")
|
||||||
|
|
||||||
|
if !found {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if firstConfigPath == "" {
|
if baseConfigPath == "" {
|
||||||
firstConfigPath = configPath
|
baseConfigPath = configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Loading config from %s", configPath)
|
log.WithFields(log.Fields{
|
||||||
|
"configPath": configPath,
|
||||||
|
}).Info("Loading config from path")
|
||||||
|
|
||||||
f := file.Provider(configPath)
|
f := file.Provider(configPath)
|
||||||
|
|
||||||
if err := k.Load(f, yaml.Parser()); err != nil {
|
if err := k.Load(f, yaml.Parser()); err != nil {
|
||||||
|
|
@ -177,16 +186,18 @@ func initConfig(configDir string) {
|
||||||
k.Load(f, yaml.Parser())
|
k.Load(f, yaml.Parser())
|
||||||
config.AppendSource(cfg, k, configPath)
|
config.AppendSource(cfg, k, configPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg = config.DefaultConfigWithBasePort(getBasePort())
|
cfg = config.DefaultConfigWithBasePort(getBasePort())
|
||||||
|
|
||||||
if firstConfigPath != "" {
|
if baseConfigPath == "" {
|
||||||
config.AppendSourceWithIncludes(cfg, k, firstConfigPath)
|
log.Fatalf("No base config file found")
|
||||||
} else {
|
os.Exit(1)
|
||||||
config.AppendSource(cfg, k, "base")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.AppendSource(cfg, k, baseConfigPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initInstallationInfo() {
|
func initInstallationInfo() {
|
||||||
|
|
@ -225,7 +236,6 @@ func main() {
|
||||||
|
|
||||||
executor := executor.DefaultExecutor(cfg)
|
executor := executor.DefaultExecutor(cfg)
|
||||||
executor.RebuildActionMap()
|
executor.RebuildActionMap()
|
||||||
executor.AddListener(websocket.ExecutionListener)
|
|
||||||
config.AddListener(executor.RebuildActionMap)
|
config.AddListener(executor.RebuildActionMap)
|
||||||
|
|
||||||
go onstartup.Execute(cfg, executor)
|
go onstartup.Execute(cfg, executor)
|
||||||
|
|
@ -233,7 +243,6 @@ func main() {
|
||||||
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
||||||
go oncalendarfile.Schedule(cfg, executor)
|
go oncalendarfile.Schedule(cfg, executor)
|
||||||
|
|
||||||
entities.AddListener(websocket.OnEntityChanged)
|
|
||||||
entities.AddListener(executor.RebuildActionMap)
|
entities.AddListener(executor.RebuildActionMap)
|
||||||
go entities.SetupEntityFileWatchers(cfg)
|
go entities.SetupEntityFileWatchers(cfg)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue