/
githubmirror
/
grafana
Обзор
Документация
Войти
/
githubmirror
/
grafana
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
pkg/setting/setting.go
2 608 строк
99 KB
Denis Vodopianov
chore: replace `grafanaAPIServerEnsureKubectlAccess` with config param (#127478)
8 часов назад
Не верифицирован
8 часов назад
af5e98b
Код
Авторство
О чём код?
// Copyright 2014 Unknwon // Copyright 2014 Torkel Ödegaard package setting import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "path" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "github.com/gobwas/glob" "github.com/open-feature/go-sdk/openfeature" "github.com/prometheus/common/model" "gopkg.in/ini.v1" "github.com/grafana/grafana-aws-sdk/pkg/awsds" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/osutil" ) type Scheme string const ( HTTPScheme Scheme = "http" HTTPSScheme Scheme = "https" HTTP2Scheme Scheme = "h2" SocketScheme Scheme = "socket" SocketHTTP2Scheme Scheme = "socket_h2" DefaultSQLExpressionCellLimit = 100000 DefaultSQLExpressionOutputCellLimit = 100000 DefaultSQLExpressionTimeout = time.Second * 10 DefaultSQLExpressionQueryLengthLimit = 10000 ) const ( RedactedPassword = "*********" DefaultHTTPAddr = "0.0.0.0" Dev = "development" Prod = "production" ApplicationName = "Grafana" ) // zoneInfo names environment variable for setting the path to look for the timezone database in go const zoneInfo = "ZONEINFO" // Default renderer auth token from [rendering]renderer_token. const DefaultRendererAuthToken = "-" // ProvisioningMaxFileSizeDefault is the default value for the // [provisioning] max_file_size key (5 MiB). It bounds files read from or // written to a provisioning repository through the files API. const ProvisioningMaxFileSizeDefault int64 = 5 * 1024 * 1024 // ProvisioningSyncResourceTimeoutDefault is the default value for the // [provisioning] sync_resource_timeout key. It bounds how long applying a // single resource (or folder) may take during a sync job before that // operation is cancelled. Large resources (e.g. multi-MB dashboards) can // exceed a short timeout, which is why it is configurable. 30s matches the // timeout used by the provisioning files write API. const ProvisioningSyncResourceTimeoutDefault = 30 * time.Second // ProvisioningControllerResyncIntervalDefault is the default value for the // [provisioning] resync_interval key. It sets how often the provisioning // controllers' informers re-list (repository, connection) as a fallback to the // live watch/NATS notifications. A shorter value reconciles stale state sooner // at the cost of more full LISTs. The jobs informer resyncs on // job_poll_interval instead. const ProvisioningControllerResyncIntervalDefault = 60 * time.Second // ProvisioningHistoryExpirationDefault is the default value for the // [provisioning] history_expiration key. It is both how long a HistoricJob is // retained before the resync-driven cleanup removes it and the resync interval // of the historic-job informer that feeds that cleanup. const ProvisioningHistoryExpirationDefault = 10 * time.Minute // ProvisioningJobPollIntervalDefault is the default value for the [provisioning] // job_poll_interval key: the jobs informer's resync/re-list interval, which is // how often unclaimed jobs missed by the live watch/NATS notifications are // picked up. The key predates the informer-driven job queue (it used to be a // poll loop) and keeps its name and default so existing config keeps its meaning. const ProvisioningJobPollIntervalDefault = 30 * time.Second var ( customInitPath = "conf/custom.ini" // App settings. Env = Dev AppUrl string AppSubUrl string // build BuildVersion string BuildCommit string EnterpriseBuildCommit string BuildBranch string BuildStamp int64 IsEnterprise bool // packaging Packaging = "unknown" CookieSecure bool CookieSameSiteDisabled bool CookieSameSiteMode http.SameSite ) // TODO move all global vars to this struct // Deprecated: Direct access to Cfg will be disallowed in a future release. // Use the ConfigProvider interface (see pkg/configprovider/configprovider.go) instead. type Cfg struct { Target []string Raw *ini.File Logger log.Logger // for logging purposes configFiles []string appliedCommandLineProperties []string appliedEnvOverrides []string commandLineProps map[string]string // HTTP Server Settings CertFile string KeyFile string CertPassword string CertWatchInterval time.Duration HTTPAddr string HTTPPort string Env string AppURL string AppSubURL string InstanceName string ServeFromSubPath bool StaticRootPath string Protocol Scheme ServeOnSocket bool SocketGid int SocketMode int SocketPath string RouterLogging bool Domain string CDNRootURL *url.URL ReadTimeout time.Duration EnableGzip bool EnforceDomain bool MinTLSVersion string // Security settings SecretKey string EmailCodeValidMinutes int // build BuildVersion string BuildCommit string EnterpriseBuildCommit string BuildBranch string BuildStamp int64 IsEnterprise bool // packaging Packaging string // Paths HomePath string ProvisioningPath string PermittedProvisioningPaths []string // Grafana API Server DisableControllers bool // Provisioning config // ProvisioningEnabled: enable or disable Git Sync / as-code provisioning // for Grafana resources. See [provisioning] enabled in defaults.ini. ProvisioningEnabled bool ProvisioningAllowedTargets []string // ProvisioningResources is the configured set of provisionable resources, each as a // "<group>/<Kind>[:cap...]" token (parsed by resources.ParseSupportedResources at startup). ProvisioningResources []string ProvisioningAllowImageRendering bool ProvisioningAllowInsecure bool // allow http:// repository URLs together with a token (cleartext credentials); local/dev only ProvisioningMinSyncInterval time.Duration ProvisioningRepositoryTypes []string ProvisioningLokiURL string ProvisioningLokiUser string ProvisioningLokiPassword string ProvisioningLokiTenantID string ProvisioningMaxResourcesPerRepository int64 // 0 = unlimited ProvisioningMaxRepositories int64 // default 10, 0 in config = unlimited (converted to -1 internally) ProvisioningMaxIncrementalChanges int // default 100, 0 in config = unlimited ProvisioningMaxFileSize int64 // bytes; default 5 MiB (5242880); <=0 = unlimited ProvisioningSyncResourceTimeout time.Duration // per-resource apply timeout during sync; default 30s; <=0 = default ProvisioningWebhookSecretRotationInterval time.Duration // default 30 days ProvisioningControllerResyncInterval time.Duration // informer re-list interval for the repo/connection controllers (jobs use ProvisioningJobPollInterval); default 60s; <=0 = default ProvisioningHistoryExpiration time.Duration // HistoricJob retention and historic-job informer resync; default 10m; <=0 = default ProvisioningJobPollInterval time.Duration // jobs informer resync/re-list interval (recovery for jobs missed by live notifications); default 30s; <=0 = default ProvisioningPublicRootURL string // public-facing root URL of this Grafana instance for provisioning consumers (webhooks, screenshots); falls back to AppURL when empty ProvisioningWebhookTrustedIPHeader string // name of the proxy-set header carrying the real client IP for webhook rate-limiting; empty falls back to the real TCP peer ProvisioningWebhookRateLimitRPS int // sustained requests per second allowed per client by the webhook rate limiter; <= 0 disables rate limiting DataPath string LogsPath string EnterpriseLicensePath string MarketplaceLicenseDirectory string // PluginsPaths: list of paths where Grafana will look for plugins. // Order is important, if multiple paths contain the same plugin, only the first one will be used. PluginsPaths []string // Classic Provisioning settings ClassicProvisioningDashboardsServerLockMaxIntervalSeconds int64 ClassicProvisioningDashboardsServerLockMinWaitMs int64 ClassicProvisioningDashboardsServerLockMaxWaitMs int64 // SMTP email settings Smtp SmtpSettings // Rendering ImagesDir string CSVsDir string PDFsDir string RendererServerUrl string RendererCallbackUrl string RendererAuthToken string RendererConcurrentRequestLimit int RendererRenderKeyLifeTime time.Duration RendererDefaultImageWidth int RendererDefaultImageHeight int RendererDefaultImageScale float64 RendererCACert string // Security DisableInitAdminCreation bool DisableBruteForceLoginProtection bool BruteForceLoginProtectionMaxAttempts int64 DisableUsernameLoginProtection bool DisableIPAddressLoginProtection bool CookieSecure bool CookieSameSiteDisabled bool CookieSameSiteMode http.SameSite AllowEmbedding bool XSSProtectionHeader bool ContentTypeProtectionHeader bool StrictTransportSecurity bool StrictTransportSecurityMaxAge int StrictTransportSecurityPreload bool StrictTransportSecuritySubDomains bool // CSPEnabled toggles Content Security Policy support. CSPEnabled bool // CSPTemplate contains the Content Security Policy template. CSPTemplate string // CSPReportEnabled toggles Content Security Policy Report Only support. CSPReportOnlyEnabled bool // CSPReportOnlyTemplate contains the Content Security Policy Report Only template. CSPReportOnlyTemplate string // FormActionAdditionalHosts is the list of additional hostnames for the CSP form-action directive. // These are appended to the template; 'self' should be in the template itself. FormActionAdditionalHosts []string EnableFrontendSandboxForPlugins []string DisableGravatar bool GravatarURL string DataProxyWhiteList map[string]bool ActionsAllowPostURL string // K8s Dashboard Cleanup K8sDashboardCleanup K8sDashboardCleanupSettings TempDataLifetime time.Duration // Plugins PluginsEnableAlpha bool PluginsAppsSkipVerifyTLS bool PluginSettings config.PluginSettings PluginsAllowUnsigned []string PluginCatalogURL string PluginCatalogHiddenPlugins []string PluginAdminEnabled bool PluginAdminExternalManageEnabled bool PluginForcePublicKeyDownload bool PluginSkipPublicKeyDownload bool DisablePlugins []string ForwardHostEnvVars []string PreinstallPluginsAsync []InstallPlugin PreinstallPluginsSync []InstallPlugin PreinstallAutoUpdate bool PluginsCDNURLTemplate string PluginLogBackendRequests bool PluginUpdateStrategy string // Plugin API restrictions - maps API name to list of plugin IDs/patterns PluginRestrictedAPIsAllowList map[string][]string PluginRestrictedAPIsBlockList map[string][]string // Panels DisableSanitizeHtml bool // Metrics MetricsEndpointEnabled bool MetricsEndpointBasicAuthUsername string MetricsEndpointBasicAuthPassword string MetricsEndpointDisableTotalStats bool // MetricsIncludeTeamLabel configures grafana to set a label for // the team responsible for the code at Grafana labs. We don't expect anyone else to // use this setting. MetricsIncludeTeamLabel bool MetricsTotalStatsIntervalSeconds int ClassicHTTPHistogramEnabled bool MetricsGrafanaEnvironmentInfo map[string]string // Dashboards DashboardVersionsToKeep int MinRefreshInterval string DefaultHomeDashboardPath string DashboardPerformanceMetrics []string PanelSeriesLimit int DashboardDefaultPreload bool DashboardSchemaMigrationCacheTTL time.Duration // Auth LoginCookieName string LoginMaxInactiveLifetime time.Duration LoginMaxLifetime time.Duration TokenRotationIntervalMinutes int SigV4AuthEnabled bool SigV4VerboseLogging bool AzureAuthEnabled bool AzureSkipOrgRoleSync bool BasicAuthEnabled bool BasicAuthStrongPasswordPolicy bool AdminUser string AdminPassword string DisableLogin bool AdminEmail string DisableLoginForm bool SignoutRedirectUrl string IDResponseHeaderEnabled bool IDResponseHeaderPrefix string IDResponseHeaderNamespaces map[string]struct{} ManagedServiceAccountsEnabled bool IDUseExternalGroupsForGroupsClaim bool // AWS Plugin Auth AWSAllowedAuthProviders []string AWSAssumeRoleEnabled bool AWSPerDatasourceHTTPProxyEnabled bool AWSSessionDuration string AWSExternalId string AWSListMetricsPageLimit int AWSForwardSettingsPlugins []string // Azure Cloud settings Azure *azsettings.AzureSettings // Auth proxy settings AuthProxy AuthProxySettings // OAuth OAuthAutoLogin bool OAuthLoginErrorMessage string OAuthCookieMaxAge int OAuthAllowInsecureEmailLookup bool OAuthRefreshTokenServerLockMinWaitMs int64 JWTAuth AuthJWTSettings ExtJWTAuth ExtJWTSettings // SSO Settings Auth SSOSettingsReloadInterval time.Duration SSOSettingsConfigurableProviders map[string]bool // Dataproxy SendUserHeader bool DataProxyLogging bool DataProxyTimeout int DataProxyDialTimeout int DataProxyTLSHandshakeTimeout int DataProxyExpectContinueTimeout int DataProxyMaxConnsPerHost int DataProxyMaxIdleConns int DataProxyKeepAlive int DataProxyIdleConnTimeout int ResponseLimit int64 DataProxyRowLimit int64 DataProxyUserAgent string DataProxyForwardUserAgent bool // DistributedCache RemoteCacheOptions *RemoteCacheSettings // Deprecated: no longer used ViewersCanEdit bool ApiKeyMaxSecondsToLive int64 // Check if a feature toggle is enabled // Deprecated: use featuremgmt.FeatureFlags IsFeatureToggleEnabled func(key string) bool // filled in dynamically Anonymous AnonymousSettings DateFormats DateFormats QuickRanges QuickRanges // User UserInviteMaxLifetime time.Duration HiddenUsers map[string]struct{} CaseInsensitiveLogin bool // Login and Email will be considered case insensitive UserLastSeenUpdateInterval time.Duration VerificationEmailMaxLifetime time.Duration // Service Accounts SATokenExpirationDayLimit int // Annotations AnnotationCleanupJobBatchSize int64 AnnotationMaximumTagsLength int64 AlertingAnnotationCleanupSetting AnnotationCleanupSettings DashboardAnnotationCleanupSettings AnnotationCleanupSettings APIAnnotationCleanupSettings AnnotationCleanupSettings AnnotationAppPlatform AnnotationAppPlatformSettings // GrafanaJavascriptAgent config GrafanaJavascriptAgent GrafanaJavascriptAgent // Data sources DataSourceLimit int // Number of queries to be executed concurrently. Only for the datasource supports concurrency. ConcurrentQueryCount int // Default behavior for the "Manage alerts via Alerting UI" toggle when configuring a data source. // It only works if the data source's `jsonData.manageAlerts` prop does not contain a previously configured value. DefaultDatasourceManageAlertsUIToggle bool // Default behavior for the "Allow as recording rules target" toggle when configuring a data source. // It only works if the data source's `jsonData.allowAsRecordingRulesTarget` prop does not contain a previously configured value. DefaultAllowRecordingRulesTargetAlertsUIToggle bool // IP range access control IPRangeACEnabled bool IPRangeACAllowedURLs []*url.URL IPRangeACSecretKey string // SQL Data sources SqlDatasourceMaxOpenConnsDefault int SqlDatasourceMaxIdleConnsDefault int SqlDatasourceMaxConnLifetimeDefault int // Snapshots SnapshotEnabled bool ExternalSnapshotUrl string ExternalSnapshotName string ExternalEnabled bool ExternalSnapshotToken string // Only used in https://snapshots.raintank.io/ SnapshotPublicMode bool ErrTemplateName string StackID string Slug string LocalFileSystemAvailable bool // Analytics CheckForGrafanaUpdates bool CheckForPluginUpdates bool ReportingDistributor string ReportingEnabled bool ApplicationInsightsConnectionString string ApplicationInsightsEndpointUrl string ApplicationInsightsAutoRouteTracking bool FeedbackLinksEnabled bool ReportingStaticContext map[string]string // Frontend analytics GoogleAnalyticsID string GoogleAnalytics4ID string GoogleAnalytics4SendManualPageViews bool GoogleTagManagerID string RudderstackDataPlaneURL string RudderstackWriteKey string RudderstackSDKURL string RudderstackV3SDKURL string RudderstackConfigURL string RudderstackIntegrationsURL string IntercomSecret string PostHogToken string PostHogHost string FrontendAnalyticsConsoleReporting bool MeticulousAIRecordingToken string // LDAP LDAPAuthEnabled bool LDAPSkipOrgRoleSync bool LDAPConfigFilePath string LDAPAllowSignup bool LDAPActiveSyncEnabled bool LDAPSyncCron string DefaultTheme string DefaultLanguage string HomePage string Quota QuotaSettings // User settings AllowUserSignUp bool AllowUserOrgCreate bool VerifyEmailEnabled bool LoginHint string PasswordHint string DisableSignoutMenu bool ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string ExternalUserMngAnalytics bool ExternalUserMngAnalyticsParams string ExternalUserUpgradeLinkUrl string AutoAssignOrg bool AutoAssignOrgId int AutoAssignOrgRole string LoginDefaultOrgId int64 OAuthSkipOrgRoleUpdateSync bool // ExpressionsEnabled specifies whether expressions are enabled. ExpressionsEnabled bool // SQLExpressionCellLimit is the maximum number of cells (rows × columns, across all frames) that can be accepted by a SQL expression. SQLExpressionCellLimit int64 // SQLExpressionOutputCellLimit is the maximum number of cells (rows × columns) that can be outputted by a SQL expression. SQLExpressionOutputCellLimit int64 // SQLExpressionQueryLengthLimit is the maximum length of a SQL query that can be used in a SQL expression. SQLExpressionQueryLengthLimit int64 // SQLExpressionTimeoutSeconds is the duration a SQL expression will run before timing out SQLExpressionTimeout time.Duration // MathExpressionMemoryLimit is the maximum estimated memory (in bytes) for a // single math expression binary operation. Memory usage is estimated before // the expression runs. When the estimate exceeds this limit, evaluation fails // with a descriptive error. A value of 0 disables the limit. Default: 1 GiB. MathExpressionMemoryLimit int64 ImageUploadProvider string // LiveMaxConnections is a maximum number of WebSocket connections to // Grafana Live ws endpoint (per Grafana server instance). 0 disables // Live, -1 means unlimited connections. LiveMaxConnections int // LiveHAEngine is a type of engine to use to achieve HA with Grafana Live. // Zero value means in-memory single node setup. LiveHAEngine string // LiveHAPRefix is a prefix for HA engine keys. LiveHAPrefix string // LiveHAEngineAddress is a connection address for Live HA engine. LiveHAEngineAddress string LiveHAEnginePassword string // LiveAllowedOrigins is a set of origins accepted by Live. If not provided // then Live uses AppURL as the only allowed origin. LiveAllowedOrigins []string // LiveMessageSizeLimit is the maximum size in bytes of Websocket messages // from clients. Defaults to 64KB. LiveMessageSizeLimit int // LiveClientQueueMaxSize is the maximum size in bytes of the client queue // for Live connections. Defaults to 4MB. LiveClientQueueMaxSize int // Grafana.com URL, used for OAuth redirect. GrafanaComURL string // Grafana.com API URL. Can be set separately to GrafanaComURL // in case API is not publicly accessible. // Defaults to GrafanaComURL setting + "/api" if unset. GrafanaComAPIURL string // Grafana.com SSO API token used for Unified SSO between instances and Grafana.com. GrafanaComSSOAPIToken string // GrafanaComProxyAPIToken is the dedicated auth token for Grafana.com proxy requests and plugin installs. GrafanaComProxyAPIToken string // Geomap base layer config GeomapDefaultBaseLayerConfig map[string]any GeomapEnableCustomBaseLayers bool // Unified Alerting UnifiedAlerting UnifiedAlertingSettings // Query history QueryHistoryEnabled bool // Open feature settings OpenFeature OpenFeatureSettings Storage StorageSettings Search SearchSettings // MaxNestedFolderDepth is the hard ceiling for folder nesting depth. // The SQL query builders use this value to generate JOIN chains. MaxNestedFolderDepth int SecureSocksDSProxy SecureSocksDSProxySettings // SAML Auth SAMLAuthEnabled bool SAMLSkipOrgRoleSync bool SAMLRoleValuesGrafanaAdmin string // OAuth2 Server OAuth2ServerEnabled bool // OAuth2Server supports the two recommended key types from the RFC https://www.rfc-editor.org/rfc/rfc7518#section-3.1: RS256 and ES256 OAuth2ServerGeneratedKeyTypeForClient string OAuth2ServerAccessTokenLifespan time.Duration RBAC RBACSettings ZanzanaClient ZanzanaClientSettings ZanzanaServer ZanzanaServerSettings ZanzanaReconciler ZanzanaReconcilerSettings ZanzanaGRPCStore ZanzanaGRPCStoreSettings ZanzanaRollout ZanzanaRolloutSettings // GRPC Server. GRPCServer GRPCServerSettings // NATS message bus. NATS NATSSettings CustomResponseHeaders map[string]string // This is used to override the general error message shown to users when we want to obfuscate a sensitive backend error UserFacingDefaultError string // DatabaseInstrumentQueries is used to decide if database queries // should be instrumented with metrics, logs and traces. // This needs to be on the global object since its used in the // sqlstore package and HTTP middlewares. DatabaseInstrumentQueries bool // DatabaseRegisterDeprecatedMetrics decides whether to register the deprecated `grafana_database_conn_*` and `go_sql_stats_*` metrics. DatabaseRegisterDeprecatedMetrics bool // DatabaseForceDashboardTitleIndex (MySQL only): when true, dashboard search uses FORCE INDEX (IDX_dashboard_title). // Set to false to let the optimizer choose the index (can be faster for selective filters). DatabaseForceDashboardTitleIndex bool // Public dashboards PublicDashboardsEnabled bool // Cloud Migration CloudMigration CloudMigrationSettings // Alerting AlertingEvaluationTimeout time.Duration AlertingNotificationTimeout time.Duration AlertingMaxAttempts int AlertingMinInterval int64 // Explore UI ExploreEnabled bool ExploreDefaultTimeOffset string ExploreHideLogsDownload bool // Help UI HelpEnabled bool // Profile UI ProfileEnabled bool // News Feed NewsFeedEnabled bool // Experimental scope settings ScopesApiEnabled bool ScopesListScopesURL string ScopesListDashboardsURL string // Short Links ShortLinkExpiration int // Unified Storage UnifiedStorage map[string]UnifiedStorageConfig UnifiedStorageAuthzExemptionEnabled bool UnifiedStorageAuthzExemptResources []string // DisableLegacyTableRename will skip renaming legacy tables (e.g., playlist → playlist_legacy) after migration DisableLegacyTableRename bool // MigrationCacheSizeKB sets SQLite PRAGMA cache_size during data migrations (in KB). // Larger values reduce lock contention. Default: 1000000 (~1GB). MigrationCacheSizeKB int // MigrationParquetBuffer enables bulk migration data through a temporary Parquet file. // This separates the read phase (legacy DB) from the write phase (unified storage) // Default: false. MigrationParquetBuffer bool // MigrationChunkedWrites writes each migration chunk in its own transaction, // bounded by migration_chunk_max_bytes. Requires migration_locking=true for HA. Default: false. MigrationChunkedWrites bool // MigrationChunkMaxBytes is the soft maximum byte budget per migration chunk // when MigrationChunkedWrites is enabled. Default: 256 MiB. MigrationChunkMaxBytes int64 // RenameWaitDeadline is the maximum time to wait for MySQL RENAME TABLE // statements to appear in the processlist. Default: 1 minute. RenameWaitDeadline time.Duration MaxPageSizeBytes int // IndexPath the directory where index files are stored. // Note: Bleve locks index files, so mounts cannot be shared between multiple instances. IndexPath string IndexWorkers int IndexRebuildWorkers int IndexFileThreshold int IndexMinCount int IndexRebuildInterval time.Duration IndexCacheTTL time.Duration IndexMinUpdateInterval time.Duration // Don't update index if it was updated less than this interval ago. IndexModificationCacheTTL time.Duration // TTL for dedup cache used in ListModifiedSince. 0 disables the cache. IndexDeletedDocuments bool // Keep deleted objects in the search index, so trash searches can find them. MaxFileIndexAge time.Duration // Max age of file-based indexes. Index older than this will be rebuilt asynchronously. MinFileIndexBuildVersion string // Minimum version of Grafana that built the file-based index. If index was built with older Grafana, it will be rebuilt asynchronously. IndexSnapshotEnabled bool // Enable remote index snapshots IndexSnapshotBucketURL string // Go CDK bucket URL for snapshot storage (s3://, gs://, azblob://, mem://, file:///) IndexSnapshotStorageKV bool // Store snapshots in the same KV used by the storage backend instead of an object-storage bucket. Mutually exclusive with index_snapshot_bucket_url; requires enable_kv_leases. IndexSnapshotKVChunkConcurrency int // Per-file chunk I/O fan-out for KV-backed snapshots. 0 / 1 = serial. Used only when index_snapshot_storage_kv is true. IndexSnapshotKVChunkSizeMiB int // Size in MiB of a single KV value used to store snapshot file data. Files larger than this are split into chunks. 0 = use built-in default. Valid range: 1..1024 MiB. Used only when index_snapshot_storage_kv is true. IndexSnapshotThreshold int // Min doc count to use remote snapshots (must be >= IndexFileThreshold, default: 5000) IndexSnapshotMaxAge time.Duration // Max snapshot age before deletion (must be >= MaxFileIndexAge, default: 7d) IndexSnapshotCleanupGracePeriod time.Duration // Time a new snapshot must exist before its predecessor in the same Grafana-version group is eligible for cleanup (default: 30m) DiskIndexCleanupInterval time.Duration // How often to scan the bleve index root for folders to reclaim. Zero disables the loop (default: 0). DiskIndexCleanupGracePeriod time.Duration // Minimum age a candidate folder must have before disk cleanup is allowed to delete it (default: 1h). DiskIndexCleanupUnopenedGracePeriod time.Duration // Longer grace applied to the newest on-disk index of a resource this pod owns but has not opened. Lets cold-start BuildIndex reuse it instead of rebuilding from scratch (default: 24h). EnableSharding bool QOSEnabled bool QOSNumberWorker int QOSMaxSizePerTenant int MemberlistBindAddr string MemberlistAdvertiseAddr string MemberlistAdvertisePort int MemberlistJoinMember string MemberlistClusterLabel string MemberlistClusterLabelVerificationDisabled bool SearchRingReplicationFactor int SearchRingExtendReplicaSet bool InstanceID string SprinklesApiServer string SprinklesApiServerPageLimit int CACertPath string HttpsSkipVerify bool ResourceServerJoinRingTimeout time.Duration SearchInjectFailuresPercent int EnableSearch bool EnableSearchClient bool // SearchPostRankAuthz enables the post-filter authorization search path: // bleve ranks without the in-searcher authz wrapper and authorization runs // app-side in rank order with early exit once the page is filled. SearchPostRankAuthz bool // SearchPostRankAuthz tunables. Zero falls back to the defaults in // search.PostRankAuthzConfig.effective(). SearchPostRankAuthzOverFetchFactor int SearchPostRankAuthzMaxWindow int SearchPostRankAuthzMaxCandidates int // SearchPostRankAuthzFacetSampleSize bounds the candidate budget when // aggregating facets on the post-filter path. Zero falls back to the // default in search.PostRankAuthzConfig.effective(). SearchPostRankAuthzFacetSampleSize int // Vector storage EnableVectorBackend bool // Vector API collection allowlists: "group/resource" entries. Internal // defaults to dashboards; external defaults to none. VectorAllowedInternalCollections []string VectorAllowedExternalCollections []string // Registers the VectorStore write RPCs on the storage server. EnableVectorStore bool // Service identities allowed to call the VectorStore write RPCs. // Empty = no identity restriction. VectorAllowedWriteServices []string VectorDBHost string VectorDBPort string VectorDBName string VectorDBUser string VectorDBPassword string VectorDBSSLMode string VectorIndexingEnabled bool // run the embedding backfiller and reconciler VectorReconcilerInterval time.Duration // reconciler tick interval; default 60s VectorEmbeddingCountInterval time.Duration // stored-embedding gauge sample interval; 0 disables VectorPromotionThreshold int // row count per tenant to trigger promotion VectorPromoterInterval time.Duration // promoter tick interval; 0 disables // VectorSearch per-tenant query-embedding cache (DB-backed, FIFO). VectorQueryCacheEnabled bool VectorQueryCacheMaxPerTenant int // VectorSearch per-tenant rate limit (DB-backed, sliding window). VectorRateLimitEnabled bool VectorRateLimitPerTenant int VectorRateLimitWindow time.Duration // Embedding provider used by the VectorSearch RPC. "" = disabled. EmbeddingProvider string // "vertex" | "bedrock" | "azure" | "" VertexProjectID string VertexLocation string // default "us-central1" VertexModel string // default "gemini-embedding-001" VertexDimensions int // default 768 VertexBatchSize int // texts per Vertex predict call; default 50 BedrockRegion string // default "us-east-1" BedrockModel string // default "cohere.embed-v4:0" BedrockDimensions int // default 1024 BedrockBatchSize int // texts per Bedrock invoke call; default 50 BedrockMaxAttempts int // max InvokeModel attempts per call under throttling; default 5 AzureEndpoint string // Azure OpenAI resource endpoint, e.g. https://<resource>.openai.azure.com AzureDeployment string // Azure OpenAI embeddings deployment name; default "text-embedding-3-small" AzureAPIVersion string // Azure OpenAI REST API version; default "2024-02-01" AzureDimensions int // requested output dimensionality; default 1024 (text-embedding-3-small reduced from native 1536) AzureBatchSize int // texts per Azure embeddings call; default 50 // Rerank provider for the HybridSearch RPC ([vector_reranker] section). // Empty = disabled (RRF ordering is returned as-is, min_relevance is a // no-op). RerankProvider string RerankVertexProjectID string RerankVertexLocation string RerankVertexModel string RerankBedrockRegion string RerankBedrockModel string // Overrides/Quotas OverridesFilePath string OverridesReloadInterval time.Duration EnforcedQuotaResources []string QuotasErrorMessageSupportInfo string EnableSQLKVBackend bool EnableSQLKVCompatibilityMode bool // LogSQLBackendCalls, when true, logs every call that reaches an exported // method of the legacy SQL backend. Temporary smoke-test instrumentation // used to confirm no production traffic still reaches sql/backend before // removing the sqlkv backwards-compatibility layer. // TODO: remove this when sql/backend backwards compatibility is no longer needed. LogSQLBackendCalls bool EnableKVLeases bool KVLeaseTTL time.Duration KVLeaseAutoRenew bool EnableGarbageCollection bool GarbageCollectionDryRun bool GarbageCollectionInterval time.Duration GarbageCollectionBatchSize int GarbageCollectionBatchWait time.Duration GarbageCollectionMaxAge time.Duration DashboardsGarbageCollectionMaxAge time.Duration // StorageModeCacheTTL is the TTL for caching statusReader results in the dynamic dualwrite service. // Default: 5 seconds, 0 or negative means no expiration. StorageModeCacheTTL time.Duration EventRetentionPeriod time.Duration EventPruningInterval time.Duration SearchLookback time.Duration NotifierSettleDelay time.Duration // ResourceVersionBatchTransactionTimeout bounds one batched WithTx in the // resource version manager (all WriteEventFunc calls + RV stamp updates). ResourceVersionBatchTransactionTimeout time.Duration // SimulatedNetworkLatency is used for testing only SimulatedNetworkLatency time.Duration DisablePruner bool TenantApiServerAddress string TenantWatcherAllowInsecureTLS bool TenantWatcherCAFile string TenantWatcherUsePolling bool TenantWatcherPollInterval time.Duration EnableTenantDeleter bool TenantDeleterDryRun bool TenantDeleterInterval time.Duration ManifestApiServerAddress string ManifestWatcherAllowInsecureTLS bool ManifestWatcherCAFile string ManifestWatcherPollInterval time.Duration // Secrets Management SecretsManagement SecretsManagerSettings // EnableKubernetesAggregator routes API traffic through a kube-aggregator layer // (required for Kubernetes-style API aggregation, e.g. the service API builder). EnableKubernetesAggregator bool // Enable CAP token based authentication in grafana's embedded kube-aggregator EnableKubernetesAggregatorCapTokenAuth bool // EnableEmbeddedAPIExtensions runs the apiextensions apiserver and // the AppManifest installer in-process inside single-tenant Grafana. Enterprise // only; OSS keeps the no-op path. Default off. EnableEmbeddedAPIExtensions bool // EnableVersionPolicy turns on the global API version policy (preferred and // max-allowed API version per group). Off leaves today's behavior unchanged. // Temporary opt-in kill-switch while the feature is experimental; remove it and // enforce unconditionally once the version policy is enabled by default. EnableVersionPolicy bool // Enable playlist reconciler EnablePlaylistsReconciler bool } type UnifiedStorageConfig struct { DualWriterMode rest.DualWriterMode // EnableMigration indicates whether migration is enabled for the resource. // If not set, will use the default from MigratedUnifiedResources. EnableMigration bool // AutoMigrationThreshold is the threshold below which a resource is automatically migrated. AutoMigrationThreshold int } type InstallPlugin struct { ID string `json:"id"` Version string `json:"version"` URL string `json:"url,omitempty"` } // ResolveGrafanaComProxyAPIToken must be called after OpenFeature is initialized. // It sets GrafanaComProxyAPIToken to the dedicated token when the grafana.dedicatedGrafanaComProxyAPIToken // flag is enabled and proxy_token is configured; otherwise falls back to sso_api_token. func (cfg *Cfg) ResolveGrafanaComProxyAPIToken() { if openfeature.NewDefaultClient().Boolean(context.Background(), "grafana.dedicatedGrafanaComProxyAPIToken", false, openfeature.EvaluationContext{}) && cfg.GrafanaComProxyAPIToken != "" { return } cfg.GrafanaComProxyAPIToken = cfg.GrafanaComSSOAPIToken } // AddChangePasswordLink returns if login form is disabled or not since // the same intention can be used to hide both features. func (cfg *Cfg) AddChangePasswordLink() bool { return !cfg.DisableLoginForm && !cfg.DisableLogin } // IsDevEnv reports whether Grafana is running in a non-production environment. // Some experimental startup params should only honoured when this condition is true. func (cfg *Cfg) IsDevEnv() bool { return cfg.Env != Prod } type CommandLineArgs struct { Config string HomePath string Args []string } func copyServerURLSettingsToGlobals(cfg *Cfg) { AppUrl = cfg.AppURL AppSubUrl = cfg.AppSubURL } func ToAbsUrl(relativeUrl string) string { return AppUrl + relativeUrl } func RedactedValue(key, value string) string { if value == "" { return "" } uppercased := strings.ToUpper(key) // Sensitive information: password, secrets etc for _, pattern := range []string{ "PASSWORD", "SECRET", "PROVIDER_CONFIG", "PRIVATE_KEY", "SECRET_KEY", "CERTIFICATE", "ACCOUNT_KEY", "ENCRYPTION_KEY", "VAULT_TOKEN", "CLIENT_SECRET", "ENTERPRISE_LICENSE", "API_DB_PASS", "^TOKEN$", "ID_FORWARDING_TOKEN$", "AUTHENTICATION_TOKEN$", "AUTH_TOKEN$", "RENDERER_TOKEN$", "API_TOKEN$", "WEBHOOK_TOKEN$", "INSTALL_TOKEN$", "PROXY_TOKEN$", } { if match, err := regexp.MatchString(pattern, uppercased); match && err == nil { return RedactedPassword } } for _, exception := range []string{ "RUDDERSTACK", "APPLICATION_INSIGHTS", "SENTRY", } { if strings.Contains(uppercased, exception) { return value } } if u, err := RedactedURL(value); err == nil { return u } return value } func RedactedURL(value string) (string, error) { // Value could be a list of URLs chunks := util.SplitString(value) for i, chunk := range chunks { var hasTmpPrefix bool const tmpPrefix = "http://" if !strings.Contains(chunk, "://") { chunk = tmpPrefix + chunk hasTmpPrefix = true } u, err := url.Parse(chunk) if err != nil { return "", err } redacted := u.Redacted() if hasTmpPrefix { redacted = strings.Replace(redacted, tmpPrefix, "", 1) } chunks[i] = redacted } if strings.Contains(value, ",") { return strings.Join(chunks, ","), nil } return strings.Join(chunks, " "), nil } func (cfg *Cfg) applyEnvVariableOverrides(file *ini.File) error { cfg.appliedEnvOverrides = make([]string, 0) // First pass: apply overrides for keys that already exist in the ini file. appliedKeys := make(map[string]bool) for _, section := range file.Sections() { for _, key := range section.Keys() { envKey := EnvKey(section.Name(), key.Name()) envValue := os.Getenv(envKey) if len(envValue) > 0 { key.SetValue(envValue) cfg.appliedEnvOverrides = append(cfg.appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue))) } appliedKeys[envKey] = true } } // Second pass: scan all GF_ environment variables and create new keys // for any that match a known section but don't have a corresponding key yet. // This allows env vars to add new config keys without requiring them to be // pre-defined in defaults.ini or custom.ini. // Build a mapping from env-style section prefix to ini section. type sectionMapping struct { prefix string section *ini.Section } sectionMappings := make([]sectionMapping, 0, len(file.Sections())) for _, section := range file.Sections() { prefix := EnvSectionPrefix(section.Name()) sectionMappings = append(sectionMappings, sectionMapping{prefix: prefix, section: section}) } // Sort by prefix length descending so more specific sections match first. // e.g., GF_AUTH_GOOGLE_ matches before GF_AUTH_. sort.Slice(sectionMappings, func(i, j int) bool { return len(sectionMappings[i].prefix) > len(sectionMappings[j].prefix) }) for _, env := range os.Environ() { if !strings.HasPrefix(env, "GF_") { continue } parts := strings.SplitN(env, "=", 2) if len(parts) != 2 || len(parts[1]) == 0 { continue } envKey := parts[0] envValue := parts[1] if appliedKeys[envKey] { continue } // Skip unified_storage env vars — they use camelCase key names // and are handled by applyUnifiedStorageEnvOverrides which // preserves the correct casing. if strings.HasPrefix(envKey, EnvSectionPrefix("unified_storage")) { continue } for _, m := range sectionMappings { if strings.HasPrefix(envKey, m.prefix) { // Skip root feature_toggles section — handled by // applyFeatureToggleEnvOverrides which preserves casing. // Subsections like feature_toggles.openfeature are still // handled here because they match a longer prefix first. if m.section.Name() == "feature_toggles" { break } keyName := strings.ToLower(envKey[len(m.prefix):]) if keyName == "" { continue } m.section.Key(keyName).SetValue(envValue) cfg.appliedEnvOverrides = append(cfg.appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue))) appliedKeys[envKey] = true break } } } return nil } func (cfg *Cfg) readGrafanaEnvironmentMetrics() error { environmentMetricsSection := cfg.Raw.Section("metrics.environment_info") keys := environmentMetricsSection.Keys() cfg.MetricsGrafanaEnvironmentInfo = make(map[string]string, len(keys)) cfg.MetricsGrafanaEnvironmentInfo["version"] = cfg.BuildVersion cfg.MetricsGrafanaEnvironmentInfo["commit"] = cfg.BuildCommit if cfg.EnterpriseBuildCommit != "NA" && cfg.EnterpriseBuildCommit != "" { cfg.MetricsGrafanaEnvironmentInfo["enterprise_commit"] = cfg.EnterpriseBuildCommit } for _, key := range keys { labelName := model.LabelName(key.Name()) labelValue := model.LabelValue(key.Value()) if !labelName.IsValid() { // nolint:staticcheck return fmt.Errorf("invalid label name in [metrics.environment_info] configuration. name %q", labelName) } if !labelValue.IsValid() { return fmt.Errorf("invalid label value in [metrics.environment_info] configuration. name %q value %q", labelName, labelValue) } cfg.MetricsGrafanaEnvironmentInfo[string(labelName)] = string(labelValue) } return nil } func (cfg *Cfg) readAnnotationSettings() error { section := cfg.Raw.Section("annotations") cfg.AnnotationCleanupJobBatchSize = section.Key("cleanupjob_batchsize").MustInt64(100) cfg.AnnotationMaximumTagsLength = section.Key("tags_length").MustInt64(500) annotationAppPlatformSettings, err := loadAnnotationAppPlatformSettings(cfg) if err != nil { return err } cfg.AnnotationAppPlatform = annotationAppPlatformSettings switch { case cfg.AnnotationMaximumTagsLength > 4096: // ensure that the configuration does not exceed the respective column size return fmt.Errorf("[annotations.tags_length] configuration exceeds the maximum allowed (4096)") case cfg.AnnotationMaximumTagsLength > 500: cfg.Logger.Info("[annotations.tags_length] has been increased from its default value; this may affect the performance", "tagLength", cfg.AnnotationMaximumTagsLength) case cfg.AnnotationMaximumTagsLength < 500: cfg.Logger.Warn("[annotations.tags_length] is too low; the minimum allowed (500) is enforced") cfg.AnnotationMaximumTagsLength = 500 } dashboardAnnotation := cfg.Raw.Section("annotations.dashboard") apiIAnnotation := cfg.Raw.Section("annotations.api") newAnnotationCleanupSettings := func(section *ini.Section, maxAgeField string) AnnotationCleanupSettings { maxAge, err := gtime.ParseDuration(section.Key(maxAgeField).MustString("")) if err != nil { maxAge = 0 } return AnnotationCleanupSettings{ MaxAge: maxAge, MaxCount: section.Key("max_annotations_to_keep").MustInt64(0), } } alertingAnnotations := cfg.Raw.Section("unified_alerting.state_history.annotations") if alertingAnnotations.Key("max_age").Value() == "" && section.Key("max_annotations_to_keep").Value() == "" { // Although this section is not documented anymore, we decided to keep it to avoid potential data-loss when user upgrades Grafana and does not change the setting. // TODO delete some time after Grafana 11. alertingSection := cfg.Raw.Section("alerting") cleanup := newAnnotationCleanupSettings(alertingSection, "max_annotation_age") if cleanup.MaxCount > 0 || cleanup.MaxAge > 0 { cfg.Logger.Warn("settings 'max_annotations_to_keep' and 'max_annotation_age' in section [alerting] are deprecated. Please use settings 'max_annotations_to_keep' and 'max_age' in section [unified_alerting.state_history.annotations]") } cfg.AlertingAnnotationCleanupSetting = cleanup } else { cfg.AlertingAnnotationCleanupSetting = newAnnotationCleanupSettings(alertingAnnotations, "max_age") } cfg.DashboardAnnotationCleanupSettings = newAnnotationCleanupSettings(dashboardAnnotation, "max_age") cfg.APIAnnotationCleanupSettings = newAnnotationCleanupSettings(apiIAnnotation, "max_age") return nil } func (cfg *Cfg) readExpressionsSettings() { expressions := cfg.Raw.Section("expressions") cfg.ExpressionsEnabled = expressions.Key("enabled").MustBool(true) cfg.SQLExpressionCellLimit = expressions.Key("sql_expression_cell_limit").MustInt64(DefaultSQLExpressionCellLimit) cfg.SQLExpressionOutputCellLimit = expressions.Key("sql_expression_output_cell_limit").MustInt64(DefaultSQLExpressionOutputCellLimit) cfg.SQLExpressionTimeout = expressions.Key("sql_expression_timeout").MustDuration(DefaultSQLExpressionTimeout) cfg.SQLExpressionQueryLengthLimit = expressions.Key("sql_expression_query_length_limit").MustInt64(DefaultSQLExpressionQueryLengthLimit) cfg.MathExpressionMemoryLimit = expressions.Key("math_expression_memory_limit").MustInt64(1 << 30) // 1 GiB } type AnnotationCleanupSettings struct { MaxAge time.Duration MaxCount int64 } // envNameFromIniName converts an ini-style name (section or key) to the // uppercased, underscore-separated form used in GF_ environment variables. // Dots and dashes become underscores; everything is uppercased. func envNameFromIniName(name string) string { s := strings.ToUpper(strings.ReplaceAll(name, ".", "_")) return strings.ReplaceAll(s, "-", "_") } // EnvSectionPrefix returns the GF_ environment variable prefix for a given // ini section name, e.g. "auth.google" → "GF_AUTH_GOOGLE_". func EnvSectionPrefix(sectionName string) string { return "GF_" + envNameFromIniName(sectionName) + "_" } func EnvKey(sectionName string, keyName string) string { return "GF_" + envNameFromIniName(sectionName) + "_" + envNameFromIniName(keyName) } func (cfg *Cfg) applyCommandLineDefaultProperties(file *ini.File) { cfg.appliedCommandLineProperties = make([]string, 0) for _, section := range file.Sections() { for _, key := range section.Keys() { keyString := fmt.Sprintf("default.%s.%s", section.Name(), key.Name()) value, exists := cfg.commandLineProps[keyString] if exists { key.SetValue(value) cfg.appliedCommandLineProperties = append(cfg.appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, RedactedValue(keyString, value))) } } } } func (cfg *Cfg) applyCommandLineProperties(file *ini.File) { for _, section := range file.Sections() { sectionName := section.Name() + "." if section.Name() == ini.DefaultSection { sectionName = "" } for _, key := range section.Keys() { keyString := sectionName + key.Name() value, exists := cfg.commandLineProps[keyString] if exists { cfg.appliedCommandLineProperties = append(cfg.appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value)) key.SetValue(value) } } } } func (cfg *Cfg) getCommandLineProperties(args []string) map[string]string { props := make(map[string]string) for _, arg := range args { if !strings.HasPrefix(arg, "cfg:") { continue } trimmed := strings.TrimPrefix(arg, "cfg:") parts := strings.Split(trimmed, "=") if len(parts) != 2 { cfg.Logger.Error("Invalid command line argument.", "argument", arg) os.Exit(1) } props[parts[0]] = parts[1] } return props } func makeAbsolute(path string, root string) string { if filepath.IsAbs(path) { return path } return filepath.Join(root, path) } func (cfg *Cfg) loadSpecifiedConfigFile(configFile string, masterFile *ini.File) error { if configFile == "" { configFile = filepath.Join(cfg.HomePath, customInitPath) // return without error if custom file does not exist if !pathExists(configFile) { return nil } } userConfig, err := ini.Load(configFile) if err != nil { return fmt.Errorf("failed to parse %q: %w", configFile, err) } // micro-optimization since we don't need to share this ini file. In // general, prefer to leave this flag as true as it is by default to prevent // data races userConfig.BlockMode = false for _, section := range userConfig.Sections() { for _, key := range section.Keys() { if key.Value() == "" { continue } defaultSec, err := masterFile.GetSection(section.Name()) if err != nil { defaultSec, _ = masterFile.NewSection(section.Name()) } defaultKey, err := defaultSec.GetKey(key.Name()) if err != nil { defaultKey, _ = defaultSec.NewKey(key.Name(), key.Value()) } defaultKey.SetValue(key.Value()) } } cfg.configFiles = append(cfg.configFiles, configFile) return nil } func (cfg *Cfg) loadConfiguration(args CommandLineArgs) (*ini.File, error) { // load config defaults defaultConfigFile := path.Join(cfg.HomePath, "conf/defaults.ini") cfg.configFiles = append(cfg.configFiles, defaultConfigFile) // check if config file exists if _, err := os.Stat(defaultConfigFile); os.IsNotExist(err) { fmt.Println("Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepath") os.Exit(1) } // load defaults parsedFile, err := ini.Load(defaultConfigFile) if err != nil { fmt.Printf("Failed to parse defaults.ini, %v\n", err) os.Exit(1) return nil, err } // command line props cfg.commandLineProps = cfg.getCommandLineProperties(args.Args) // load default overrides cfg.applyCommandLineDefaultProperties(parsedFile) // load specified config file err = cfg.loadSpecifiedConfigFile(args.Config, parsedFile) if err != nil { err2 := cfg.initLogging(parsedFile) if err2 != nil { return nil, err2 } cfg.Logger.Error(err.Error()) os.Exit(1) } // apply environment overrides err = cfg.applyEnvVariableOverrides(parsedFile) if err != nil { return nil, err } // apply command line overrides cfg.applyCommandLineProperties(parsedFile) // evaluate config values containing environment variables err = expandConfig(parsedFile) if err != nil { return nil, err } // update data path and logging config dataPath := valueAsString(parsedFile.Section("paths"), "data", "") cfg.DataPath = makeAbsolute(dataPath, cfg.HomePath) err = cfg.initLogging(parsedFile) if err != nil { return nil, err } cfg.Logger.Info(fmt.Sprintf("Starting %s", ApplicationName), "version", BuildVersion, "commit", BuildCommit, "branch", BuildBranch, "compiled", time.Unix(BuildStamp, 0)) return parsedFile, err } func pathExists(path string) bool { _, err := os.Stat(path) if err == nil { return true } if os.IsNotExist(err) { return false } return false } func (cfg *Cfg) setHomePath(args CommandLineArgs) { if args.HomePath != "" { cfg.HomePath = args.HomePath return } var err error cfg.HomePath, err = filepath.Abs(".") if err != nil { panic(err) } // check if homepath is correct if pathExists(filepath.Join(cfg.HomePath, "conf/defaults.ini")) { return } // try down one path if pathExists(filepath.Join(cfg.HomePath, "../conf/defaults.ini")) { cfg.HomePath = filepath.Join(cfg.HomePath, "../") } } var skipStaticRootValidation = false func NewCfg() *Cfg { return &Cfg{ Env: Dev, Target: []string{"all"}, Logger: log.New("settings"), Raw: ini.Empty(), Azure: &azsettings.AzureSettings{}, // Initialize plugin API restriction maps PluginRestrictedAPIsAllowList: make(map[string][]string), PluginRestrictedAPIsBlockList: make(map[string][]string), // Avoid nil pointer IsFeatureToggleEnabled: func(_ string) bool { return false }, MaxNestedFolderDepth: maxDeptFolderSettings(nil), } } // Deprecated: Avoid using IsFeatureToggleEnabled from settings. If you need to access // feature flags, read them from the FeatureToggle (or FeatureManager) interface func NewCfgWithFeatures(features func(string) bool) *Cfg { cfg := NewCfg() cfg.IsFeatureToggleEnabled = features return cfg } func NewCfgFromArgs(args CommandLineArgs) (*Cfg, error) { cfg := NewCfg() if err := cfg.Load(args); err != nil { return nil, err } return cfg, nil } // NewCfgFromBytes specialized function to create a new Cfg from bytes (INI file). func NewCfgFromBytes(bytes []byte) (*Cfg, error) { parsedFile, err := ini.Load(bytes) if err != nil { return nil, fmt.Errorf("failed to parse bytes as INI file: %w", err) } return NewCfgFromINIFile(parsedFile) } // prevents a log line from being printed when the static root path is not found, useful for apiservers that have no frontend func NewCfgFromBytesWithoutJSValidation(bytes []byte) (*Cfg, error) { skipStaticRootValidation = true return NewCfgFromBytes(bytes) } // NewCfgFromINIFile specialized function to create a new Cfg from an ini.File. func NewCfgFromINIFile(iniFile *ini.File) (*Cfg, error) { cfg := NewCfg() if err := cfg.parseINIFile(iniFile); err != nil { return nil, fmt.Errorf("failed to parse setting from INI file: %w", err) } return cfg, nil } func (cfg *Cfg) validateStaticRootPath() error { if skipStaticRootValidation { return nil } if _, err := os.Stat(path.Join(cfg.StaticRootPath, "build")); err != nil { cfg.Logger.Error("Failed to detect generated javascript files in public/build") } return nil } func (cfg *Cfg) Load(args CommandLineArgs) error { cfg.setHomePath(args) // Fix for missing IANA db on Windows or Alpine _, zoneInfoSet := os.LookupEnv(zoneInfo) if !zoneInfoSet { if err := os.Setenv(zoneInfo, filepath.Join(cfg.HomePath, "tools", "zoneinfo.zip")); err != nil { cfg.Logger.Error("Can't set ZONEINFO environment variable", "err", err) } } iniFile, err := cfg.loadConfiguration(args) if err != nil { return err } err = cfg.parseINIFile(iniFile) if err != nil { return err } cfg.LogConfigSources() return nil } // nolint:gocyclo func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.Raw = iniFile cfg.BuildVersion = BuildVersion cfg.BuildCommit = BuildCommit cfg.EnterpriseBuildCommit = EnterpriseBuildCommit cfg.BuildStamp = BuildStamp cfg.BuildBranch = BuildBranch cfg.IsEnterprise = IsEnterprise cfg.Packaging = Packaging cfg.ErrTemplateName = "error" Target := valueAsString(iniFile.Section(""), "target", "all") if Target != "" { cfg.Target = util.SplitString(Target) } cfg.Env = valueAsString(iniFile.Section(""), "app_mode", "development") cfg.StackID = valueAsString(iniFile.Section("environment"), "stack_id", "") cfg.Slug = valueAsString(iniFile.Section("environment"), "stack_slug", "") cfg.LocalFileSystemAvailable = iniFile.Section("environment").Key("local_file_system_available").MustBool(true) cfg.InstanceName = valueAsString(iniFile.Section(""), "instance_name", "unknown_instance_name") plugins := valueAsString(iniFile.Section("paths"), "plugins", "") bundledPlugins := valueAsString(iniFile.Section("paths"), "bundled_plugins", "data/plugins-bundled") cfg.PluginsPaths = []string{makeAbsolute(plugins, cfg.HomePath), makeAbsolute(bundledPlugins, cfg.HomePath)} provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath) if err := cfg.readServerSettings(iniFile); err != nil { return err } if err := readDataProxySettings(iniFile, cfg); err != nil { return err } if err := readSecuritySettings(iniFile, cfg); err != nil { return err } if err := readSnapshotsSettings(cfg, iniFile); err != nil { return err } if err := readGRPCServerSettings(cfg, iniFile); err != nil { return err } if err := readNATSSettings(cfg); err != nil { return err } if err := cfg.readProvisioningSettings(iniFile); err != nil { return err } cfg.readClassicProvisioningSettings(iniFile) // read dashboard settings dashboards := iniFile.Section("dashboards") cfg.DashboardVersionsToKeep = dashboards.Key("versions_to_keep").MustInt(20) cfg.MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s") cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) cfg.PanelSeriesLimit = dashboards.Key("panel_series_limit").MustInt(0) cfg.DashboardDefaultPreload = dashboards.Key("default_preload").MustBool(false) cfg.DashboardSchemaMigrationCacheTTL = dashboards.Key("schema_migration_cache_ttl").MustDuration(time.Minute) if err := readUserSettings(iniFile, cfg); err != nil { return err } if err := readServiceAccountSettings(iniFile, cfg); err != nil { return err } if err := readAuthSettings(iniFile, cfg); err != nil { return err } readOAuth2ServerSettings(cfg) cfg.readRBACSettings() cfg.readZanzanaSettings() cfg.readRenderingSettings(iniFile) cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) cfg.MetricsEndpointBasicAuthUsername = valueAsString(iniFile.Section("metrics"), "basic_auth_username", "") cfg.MetricsEndpointBasicAuthPassword = valueAsString(iniFile.Section("metrics"), "basic_auth_password", "") cfg.MetricsEndpointDisableTotalStats = iniFile.Section("metrics").Key("disable_total_stats").MustBool(false) cfg.MetricsIncludeTeamLabel = iniFile.Section("metrics").Key("include_team_label").MustBool(false) cfg.MetricsTotalStatsIntervalSeconds = iniFile.Section("metrics").Key("total_stats_collector_interval_seconds").MustInt(1800) cfg.ClassicHTTPHistogramEnabled = iniFile.Section("metrics").Key("classic_http_histogram_enabled").MustBool(true) analytics := iniFile.Section("analytics") cfg.CheckForGrafanaUpdates = analytics.Key("check_for_updates").MustBool(true) cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true) cfg.GoogleAnalyticsID = analytics.Key("google_analytics_ua_id").String() cfg.GoogleAnalytics4ID = analytics.Key("google_analytics_4_id").String() cfg.GoogleAnalytics4SendManualPageViews = analytics.Key("google_analytics_4_send_manual_page_views").MustBool(false) cfg.GoogleTagManagerID = analytics.Key("google_tag_manager_id").String() cfg.RudderstackWriteKey = analytics.Key("rudderstack_write_key").String() cfg.RudderstackDataPlaneURL = analytics.Key("rudderstack_data_plane_url").String() cfg.RudderstackSDKURL = analytics.Key("rudderstack_sdk_url").String() cfg.RudderstackV3SDKURL = analytics.Key("rudderstack_v3_sdk_url").String() cfg.RudderstackConfigURL = analytics.Key("rudderstack_config_url").String() cfg.RudderstackIntegrationsURL = analytics.Key("rudderstack_integrations_url").String() cfg.IntercomSecret = analytics.Key("intercom_secret").String() cfg.PostHogToken = analytics.Key("posthog_token").String() cfg.PostHogHost = analytics.Key("posthog_host").String() cfg.FrontendAnalyticsConsoleReporting = analytics.Key("browser_console_reporter").MustBool(false) cfg.MeticulousAIRecordingToken = analytics.Key("meticulous_ai_recording_token").String() cfg.ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) cfg.ReportingDistributor = analytics.Key("reporting_distributor").MustString("grafana-labs") if len(cfg.ReportingDistributor) >= 100 { cfg.ReportingDistributor = cfg.ReportingDistributor[:100] } cfg.ApplicationInsightsConnectionString = analytics.Key("application_insights_connection_string").String() cfg.ApplicationInsightsEndpointUrl = analytics.Key("application_insights_endpoint_url").String() cfg.ApplicationInsightsAutoRouteTracking = analytics.Key("application_insights_connection_string").MustBool(true) cfg.FeedbackLinksEnabled = analytics.Key("feedback_links_enabled").MustBool(true) // parse reporting static context string of key=value, key=value pairs into an object cfg.ReportingStaticContext = make(map[string]string) for _, pair := range strings.Split(analytics.Key("reporting_static_context").String(), ",") { kv := strings.Split(pair, "=") if len(kv) == 2 { cfg.ReportingStaticContext[strings.TrimSpace("_static_context_"+kv[0])] = strings.TrimSpace(kv[1]) } } if err := cfg.readAlertingSettings(iniFile); err != nil { return err } explore := iniFile.Section("explore") cfg.ExploreEnabled = explore.Key("enabled").MustBool(true) exploreDefaultTimeOffset := valueAsString(explore, "defaultTimeOffset", "1h") // we want to ensure the value parses as a duration, but we send it forward as a string to the frontend if _, err := gtime.ParseDuration(exploreDefaultTimeOffset); err != nil { return err } else { cfg.ExploreDefaultTimeOffset = exploreDefaultTimeOffset } cfg.ExploreHideLogsDownload = explore.Key("hide_logs_download").MustBool(false) help := iniFile.Section("help") cfg.HelpEnabled = help.Key("enabled").MustBool(true) profile := iniFile.Section("profile") cfg.ProfileEnabled = profile.Key("enabled").MustBool(true) news := iniFile.Section("news") cfg.NewsFeedEnabled = news.Key("news_feed_enabled").MustBool(true) queryHistory := iniFile.Section("query_history") cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true) shortLinks := iniFile.Section("short_links") cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(-1) if cfg.ShortLinkExpiration > 365 { cfg.Logger.Warn("short_links expire_time must be less than 366 days. Setting to 365 days") cfg.ShortLinkExpiration = 365 } panelsSection := iniFile.Section("panels") cfg.DisableSanitizeHtml = panelsSection.Key("disable_sanitize_html").MustBool(false) // nolint:staticcheck if err := cfg.readFeatureToggles(iniFile); err != nil { return err } if err := cfg.readPluginSettings(iniFile); err != nil { return err } if err := cfg.ReadUnifiedAlertingSettings(iniFile); err != nil { return err } // check old location for this option if panelsSection.Key("enable_alpha").MustBool(false) { cfg.PluginsEnableAlpha = true } cfg.readSAMLConfig() cfg.readLDAPConfig() cfg.handleAWSConfig() cfg.readAzureSettings() cfg.readAuthJWTSettings() cfg.readAuthExtJWTSettings() cfg.readAuthProxySettings() cfg.readSessionConfig() if err := cfg.readSmtpSettings(); err != nil { return err } if err := cfg.readAnnotationSettings(); err != nil { return err } cfg.readQuotaSettings() cfg.readExpressionsSettings() if err := cfg.readGrafanaEnvironmentMetrics(); err != nil { return err } if err := cfg.readOpenFeatureSettings(); err != nil { cfg.Logger.Error("Failed to read open feature settings", "error", err) return err } cfg.readDataSourcesSettings() cfg.readDataSourceSecuritySettings() cfg.readK8sDashboardCleanupSettings() cfg.readSqlDataSourceSettings() cfg.Storage = readStorageSettings(iniFile) cfg.Search = readSearchSettings(iniFile) cfg.MaxNestedFolderDepth = maxDeptFolderSettings(iniFile) var err error cfg.SecureSocksDSProxy, err = readSecureSocksDSProxySettings(iniFile) if err != nil { // if the proxy is misconfigured, disable it rather than crashing cfg.SecureSocksDSProxy.Enabled = false cfg.Logger.Error("secure_socks_datasource_proxy unable to start up", "err", err.Error()) } if cfg.VerifyEmailEnabled && !cfg.Smtp.Enabled { cfg.Logger.Warn("require_email_validation is enabled but smtp is disabled") } readGrafanaComSettings(iniFile, cfg) cfg.GrafanaComSSOAPIToken = valueAsString(iniFile.Section("grafana_com"), "sso_api_token", "") cfg.GrafanaComProxyAPIToken = valueAsString(iniFile.Section("grafana_com"), "proxy_token", "") imageUploadingSection := iniFile.Section("external_image_storage") cfg.ImageUploadProvider = valueAsString(imageUploadingSection, "provider", "") enterprise := iniFile.Section("enterprise") cfg.EnterpriseLicensePath = valueAsString(enterprise, "license_path", filepath.Join(cfg.DataPath, "license.jwt")) marketplace := iniFile.Section("marketplace") cfg.MarketplaceLicenseDirectory = valueAsString(marketplace, "license_directory", cfg.DataPath) geomapSection := iniFile.Section("geomap") basemapJSON := valueAsString(geomapSection, "default_baselayer_config", "") if basemapJSON != "" { layer := make(map[string]any) err := json.Unmarshal([]byte(basemapJSON), &layer) if err != nil { cfg.Logger.Error("Error reading json from default_baselayer_config", "error", err) } else { cfg.GeomapDefaultBaseLayerConfig = layer } } cfg.GeomapEnableCustomBaseLayers = geomapSection.Key("enable_custom_baselayers").MustBool(true) cfg.readRemoteCacheSettings() cfg.readDateFormats() cfg.readGrafanaJavascriptAgentConfig() if err := cfg.readLiveSettings(iniFile); err != nil { return err } databaseSection := iniFile.Section("database") cfg.DatabaseInstrumentQueries = databaseSection.Key("instrument_queries").MustBool(false) cfg.DatabaseRegisterDeprecatedMetrics = databaseSection.Key("register_deprecated_metrics").MustBool(true) cfg.DatabaseForceDashboardTitleIndex = databaseSection.Key("force_dashboard_title_index").MustBool(true) logSection := iniFile.Section("log") cfg.UserFacingDefaultError = logSection.Key("user_facing_default_error").MustString("please inspect Grafana server log for details") cfg.readPublicDashboardsSettings() cfg.readCloudMigrationSettings() cfg.readSecretsManagerSettings() // read experimental scopes settings. scopesSection := iniFile.Section("scopes") cfg.ScopesApiEnabled = scopesSection.Key("api_enabled").MustBool(false) cfg.ScopesListScopesURL = scopesSection.Key("list_scopes_endpoint").MustString("") cfg.ScopesListDashboardsURL = scopesSection.Key("list_dashboards_endpoint").MustString("") // Time picker settings if err := cfg.readTimePicker(); err != nil { return err } // unified storage config cfg.setUnifiedStorageConfig() cfg.readStartupParams(iniFile) return nil } func valueAsString(section *ini.Section, keyName string, defaultValue string) string { return section.Key(keyName).MustString(defaultValue) } func (cfg *Cfg) readSAMLConfig() { samlSec := cfg.Raw.Section("auth.saml") cfg.SAMLAuthEnabled = samlSec.Key("enabled").MustBool(false) cfg.SAMLSkipOrgRoleSync = samlSec.Key("skip_org_role_sync").MustBool(false) cfg.SAMLRoleValuesGrafanaAdmin = samlSec.Key("role_values_grafana_admin").MustString("") } func (cfg *Cfg) readLDAPConfig() { ldapSec := cfg.Raw.Section("auth.ldap") cfg.LDAPConfigFilePath = ldapSec.Key("config_file").String() cfg.LDAPSyncCron = ldapSec.Key("sync_cron").String() cfg.LDAPAuthEnabled = ldapSec.Key("enabled").MustBool(false) cfg.LDAPSkipOrgRoleSync = ldapSec.Key("skip_org_role_sync").MustBool(false) cfg.LDAPActiveSyncEnabled = ldapSec.Key("active_sync_enabled").MustBool(false) cfg.LDAPAllowSignup = ldapSec.Key("allow_sign_up").MustBool(true) } func (cfg *Cfg) handleAWSConfig() { awsPluginSec := cfg.Raw.Section("aws") cfg.AWSAssumeRoleEnabled = awsPluginSec.Key("assume_role_enabled").MustBool(true) cfg.AWSPerDatasourceHTTPProxyEnabled = awsPluginSec.Key("per_datasource_http_proxy_enabled").MustBool(false) allowedAuthProviders := awsPluginSec.Key("allowed_auth_providers").MustString("default,keys,credentials") for _, authProvider := range strings.Split(allowedAuthProviders, ",") { authProvider = strings.TrimSpace(authProvider) if authProvider != "" { cfg.AWSAllowedAuthProviders = append(cfg.AWSAllowedAuthProviders, authProvider) } } cfg.AWSListMetricsPageLimit = awsPluginSec.Key("list_metrics_page_limit").MustInt(500) cfg.AWSExternalId = awsPluginSec.Key("external_id").Value() cfg.AWSSessionDuration = awsPluginSec.Key("session_duration").Value() cfg.AWSForwardSettingsPlugins = util.SplitString(awsPluginSec.Key("forward_settings_to_plugins").String()) // Also set environment variables that can be used by core plugins err := os.Setenv(awsds.AssumeRoleEnabledEnvVarKeyName, strconv.FormatBool(cfg.AWSAssumeRoleEnabled)) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.AssumeRoleEnabledEnvVarKeyName), err) } err = os.Setenv(awsds.AllowedAuthProvidersEnvVarKeyName, allowedAuthProviders) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.AllowedAuthProvidersEnvVarKeyName), err) } err = os.Setenv(awsds.ListMetricsPageLimitKeyName, strconv.Itoa(cfg.AWSListMetricsPageLimit)) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.ListMetricsPageLimitKeyName), err) } err = os.Setenv(awsds.GrafanaAssumeRoleExternalIdKeyName, cfg.AWSExternalId) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.GrafanaAssumeRoleExternalIdKeyName), err) } err = os.Setenv(awsds.SessionDurationEnvVarKeyName, cfg.AWSSessionDuration) if err != nil { cfg.Logger.Error(fmt.Sprintf("could not set environment variable '%s'", awsds.SessionDurationEnvVarKeyName), err) } } func (cfg *Cfg) readSessionConfig() { sec, _ := cfg.Raw.GetSection("session") if sec != nil { cfg.Logger.Warn( "[Removed] Session setting was removed in v6.2, use remote_cache option instead", ) } } func (cfg *Cfg) initLogging(file *ini.File) error { logModeStr := valueAsString(file.Section("log"), "mode", "console") // split on comma logModes := strings.Split(logModeStr, ",") // also try space if len(logModes) == 1 { logModes = strings.Split(logModeStr, " ") } logsPath := valueAsString(file.Section("paths"), "logs", "") cfg.LogsPath = makeAbsolute(logsPath, cfg.HomePath) return log.ReadLoggingConfig(logModes, cfg.LogsPath, file) } func (cfg *Cfg) readStartupParams(iniFile *ini.File) { cfg.EnablePlaylistsReconciler = iniFile.Section("playlists").Key("enable_playlists_reconciler").MustBool(false) cfg.EnableKubernetesAggregator = iniFile.Section("grafana-apiserver").Key("kubernetes_aggregator_enabled").MustBool(false) cfg.EnableKubernetesAggregatorCapTokenAuth = iniFile.Section("grafana-apiserver").Key("kubernetes_aggregator_cap_token_auth_enabled").MustBool(false) cfg.EnableEmbeddedAPIExtensions = iniFile.Section("grafana-apiserver").Key("apiextensions_enabled").MustBool(false) cfg.EnableVersionPolicy = iniFile.Section("grafana-apiserver").Key("version_policy_enabled").MustBool(false) } func (cfg *Cfg) LogConfigSources() { var text bytes.Buffer for _, file := range cfg.configFiles { cfg.Logger.Info("Config loaded from", "file", file) } if len(cfg.appliedCommandLineProperties) > 0 { for _, prop := range cfg.appliedCommandLineProperties { cfg.Logger.Info("Config overridden from command line", "arg", prop) } } if len(cfg.appliedEnvOverrides) > 0 { text.WriteString("\tEnvironment variables used:\n") for _, prop := range cfg.appliedEnvOverrides { cfg.Logger.Info("Config overridden from Environment variable", "var", prop) } } cfg.Logger.Info("Target", "target", cfg.Target) cfg.Logger.Info("Path Home", "path", cfg.HomePath) cfg.Logger.Info("Path Data", "path", cfg.DataPath) cfg.Logger.Info("Path Logs", "path", cfg.LogsPath) cfg.Logger.Info("Path Plugins", "path", cfg.PluginsPaths) cfg.Logger.Info("Path Provisioning", "path", cfg.ProvisioningPath) cfg.Logger.Info("App mode " + cfg.Env) } type DynamicSection struct { section *ini.Section Logger log.Logger env osutil.Env } // Key dynamically overrides keys with environment variables. // As a side effect, the value of the setting key will be updated if an environment variable is present. func (s *DynamicSection) Key(k string) *ini.Key { envKey := EnvKey(s.section.Name(), k) envValue := s.env.Getenv(envKey) key := s.section.Key(k) if len(envValue) == 0 { return key } key.SetValue(envValue) s.Logger.Info("Config overridden from Environment variable", "var", fmt.Sprintf("%s=%s", envKey, RedactedValue(envKey, envValue))) return key } func (s *DynamicSection) KeysHash() map[string]string { hash := s.section.KeysHash() for k := range hash { envKey := EnvKey(s.section.Name(), k) envValue := s.env.Getenv(envKey) if len(envValue) > 0 { hash[k] = envValue } } return hash } // SectionWithEnvOverrides dynamically overrides keys with environment variables. // As a side effect, the value of the setting key will be updated if an environment variable is present. func (cfg *Cfg) SectionWithEnvOverrides(s string) *DynamicSection { return &DynamicSection{ section: cfg.Raw.Section(s), Logger: cfg.Logger, env: osutil.RealEnv{}, } } func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error { security := iniFile.Section("security") readSecretKey(iniFile, cfg) cfg.DisableGravatar = security.Key("disable_gravatar").MustBool(true) cfg.GravatarURL = security.Key("gravatar_url").MustString("https://secure.gravatar.com/avatar") cfg.DisableBruteForceLoginProtection = security.Key("disable_brute_force_login_protection").MustBool(false) cfg.BruteForceLoginProtectionMaxAttempts = security.Key("brute_force_login_protection_max_attempts").MustInt64(5) cfg.DisableUsernameLoginProtection = security.Key("disable_username_login_protection").MustBool(false) cfg.DisableIPAddressLoginProtection = security.Key("disable_ip_address_login_protection").MustBool(true) // Ensure at least one login attempt can be performed. if cfg.BruteForceLoginProtectionMaxAttempts <= 0 { cfg.BruteForceLoginProtectionMaxAttempts = 1 } readCookieSecuritySettings(iniFile, cfg) copyCookieSecuritySettingsToGlobals(cfg) cfg.AllowEmbedding = security.Key("allow_embedding").MustBool(false) cfg.ContentTypeProtectionHeader = security.Key("x_content_type_options").MustBool(true) cfg.XSSProtectionHeader = security.Key("x_xss_protection").MustBool(true) cfg.ActionsAllowPostURL = security.Key("actions_allow_post_url").MustString("") cfg.StrictTransportSecurity = security.Key("strict_transport_security").MustBool(false) cfg.StrictTransportSecurityMaxAge = security.Key("strict_transport_security_max_age_seconds").MustInt(86400) cfg.StrictTransportSecurityPreload = security.Key("strict_transport_security_preload").MustBool(false) cfg.StrictTransportSecuritySubDomains = security.Key("strict_transport_security_subdomains").MustBool(false) cfg.CSPEnabled = security.Key("content_security_policy").MustBool(false) cfg.CSPTemplate = security.Key("content_security_policy_template").MustString("") cfg.CSPReportOnlyEnabled = security.Key("content_security_policy_report_only").MustBool(false) cfg.CSPReportOnlyTemplate = security.Key("content_security_policy_report_only_template").MustString("") cfg.FormActionAdditionalHosts = security.Key("form_action_additional_hosts").Strings(" ") enableFrontendSandboxForPlugins := security.Key("enable_frontend_sandbox_for_plugins").MustString("") for _, plug := range strings.Split(enableFrontendSandboxForPlugins, ",") { plug = strings.TrimSpace(plug) cfg.EnableFrontendSandboxForPlugins = append(cfg.EnableFrontendSandboxForPlugins, plug) } if cfg.CSPEnabled && cfg.CSPTemplate == "" { return fmt.Errorf("enabling content_security_policy requires a content_security_policy_template configuration") } if cfg.CSPReportOnlyEnabled && cfg.CSPReportOnlyTemplate == "" { return fmt.Errorf("enabling content_security_policy_report_only requires a content_security_policy_report_only_template configuration") } // read data source proxy whitelist cfg.DataProxyWhiteList = make(map[string]bool) securityStr := valueAsString(security, "data_source_proxy_whitelist", "") for _, hostAndIP := range util.SplitString(securityStr) { cfg.DataProxyWhiteList[hostAndIP] = true } // admin cfg.DisableInitAdminCreation = security.Key("disable_initial_admin_creation").MustBool(false) cfg.AdminUser = valueAsString(security, "admin_user", "") cfg.AdminPassword = valueAsString(security, "admin_password", "") cfg.AdminEmail = valueAsString(security, "admin_email", fmt.Sprintf("%s@localhost", cfg.AdminUser)) return nil } func copyCookieSecuritySettingsToGlobals(cfg *Cfg) { CookieSecure = cfg.CookieSecure if cfg.CookieSameSiteDisabled { CookieSameSiteDisabled = cfg.CookieSameSiteDisabled } else { CookieSameSiteMode = cfg.CookieSameSiteMode } } func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { if err := readSessionAuthSettings(iniFile, cfg); err != nil { return err } auth := iniFile.Section("auth") cfg.OAuthAllowInsecureEmailLookup = auth.Key("oauth_allow_insecure_email_lookup").MustBool(false) cfg.ApiKeyMaxSecondsToLive = auth.Key("api_key_max_seconds_to_live").MustInt64(-1) cfg.DisableLoginForm = auth.Key("disable_login_form").MustBool(false) cfg.DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) // Deprecated cfg.OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) if cfg.OAuthAutoLogin { cfg.Logger.Warn("[Deprecated] The oauth_auto_login configuration setting is deprecated. Please use auto_login inside auth provider section instead.") } // Default to the translation key used in the frontend cfg.OAuthLoginErrorMessage = valueAsString(auth, "oauth_login_error_message", "oauth.login.error") readOAuthCookieMaxAge(iniFile, cfg) cfg.OAuthRefreshTokenServerLockMinWaitMs = auth.Key("oauth_refresh_token_server_lock_min_wait_ms").MustInt64(1000) cfg.SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "") // Deprecated cfg.OAuthSkipOrgRoleUpdateSync = false cfg.DisableLogin = auth.Key("disable_login").MustBool(false) // SigV4 cfg.SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false) cfg.SigV4VerboseLogging = auth.Key("sigv4_verbose_logging").MustBool(false) // Azure Auth cfg.AzureAuthEnabled = auth.Key("azure_auth_enabled").MustBool(false) // ID response header cfg.IDResponseHeaderEnabled = auth.Key("id_response_header_enabled").MustBool(false) cfg.IDResponseHeaderPrefix = auth.Key("id_response_header_prefix").MustString("X-Grafana") idHeaderNamespaces := util.SplitString(auth.Key("id_response_header_namespaces").MustString("")) cfg.IDResponseHeaderNamespaces = make(map[string]struct{}, len(idHeaderNamespaces)) for _, namespace := range idHeaderNamespaces { cfg.IDResponseHeaderNamespaces[namespace] = struct{}{} } cfg.IDUseExternalGroupsForGroupsClaim = auth.Key("id_use_external_groups_for_groups_claim").MustBool(false) // anonymous access cfg.readAnonymousSettings() // basic auth authBasic := iniFile.Section("auth.basic") cfg.BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) cfg.BasicAuthStrongPasswordPolicy = authBasic.Key("password_policy").MustBool(false) // SSO Settings ssoSettings := iniFile.Section("sso_settings") cfg.SSOSettingsReloadInterval = ssoSettings.Key("reload_interval").MustDuration(1 * time.Minute) providers := ssoSettings.Key("configurable_providers").String() cfg.SSOSettingsConfigurableProviders = make(map[string]bool) for _, provider := range util.SplitString(providers) { cfg.SSOSettingsConfigurableProviders[provider] = true } // Managed Service Accounts cfg.ManagedServiceAccountsEnabled = auth.Key("managed_service_accounts_enabled").MustBool(false) return nil } func readOAuth2ServerSettings(cfg *Cfg) { oauth2Srv := cfg.SectionWithEnvOverrides("oauth2_server") cfg.OAuth2ServerEnabled = oauth2Srv.Key("enabled").MustBool(false) cfg.OAuth2ServerGeneratedKeyTypeForClient = strings.ToUpper(oauth2Srv.Key("generated_key_type_for_client").In("ECDSA", []string{"RSA", "ECDSA"})) cfg.OAuth2ServerAccessTokenLifespan = oauth2Srv.Key("access_token_lifespan").MustDuration(time.Minute * 3) } func readUserSettings(iniFile *ini.File, cfg *Cfg) error { users := iniFile.Section("users") cfg.AllowUserSignUp = users.Key("allow_sign_up").MustBool(true) cfg.AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) cfg.AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) cfg.AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1) cfg.LoginDefaultOrgId = users.Key("login_default_org_id").MustInt64(-1) cfg.AutoAssignOrgRole = users.Key("auto_assign_org_role").In( string(identity.RoleViewer), []string{ string(identity.RoleNone), string(identity.RoleViewer), string(identity.RoleEditor), string(identity.RoleAdmin), }) cfg.VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) // Deprecated // cfg.CaseInsensitiveLogin = users.Key("case_insensitive_login").MustBool(true) cfg.CaseInsensitiveLogin = true cfg.LoginHint = valueAsString(users, "login_hint", "") cfg.PasswordHint = valueAsString(users, "password_hint", "") cfg.DefaultTheme = valueAsString(users, "default_theme", "") cfg.DefaultLanguage = valueAsString(users, "default_language", "") cfg.HomePage = valueAsString(users, "home_page", "") cfg.ExternalUserMngLinkUrl = valueAsString(users, "external_manage_link_url", "") cfg.ExternalUserMngLinkName = valueAsString(users, "external_manage_link_name", "") cfg.ExternalUserMngInfo = valueAsString(users, "external_manage_info", "") cfg.ExternalUserMngAnalytics = users.Key("external_manage_analytics").MustBool(false) cfg.ExternalUserMngAnalyticsParams = valueAsString(users, "external_manage_analytics_params", "") cfg.ExternalUserUpgradeLinkUrl = valueAsString(users, "external_upgrade_link_url", "") //nolint:staticcheck cfg.ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false) //nolint:staticcheck if cfg.ViewersCanEdit { cfg.Logger.Warn("[Deprecated] The viewers_can_edit configuration setting is deprecated. Please upgrade viewers to editors.") } userInviteMaxLifetimeVal := valueAsString(users, "user_invite_max_lifetime_duration", "24h") userInviteMaxLifetimeDuration, err := gtime.ParseDuration(userInviteMaxLifetimeVal) if err != nil { return err } cfg.UserInviteMaxLifetime = userInviteMaxLifetimeDuration if cfg.UserInviteMaxLifetime < time.Minute*15 { return errors.New("the minimum supported value for the `user_invite_max_lifetime_duration` configuration is 15m (15 minutes)") } if err := readUserLastSeenUpdateInterval(iniFile, cfg); err != nil { return err } cfg.HiddenUsers = make(map[string]struct{}) hiddenUsers := users.Key("hidden_users").MustString("") for _, user := range strings.Split(hiddenUsers, ",") { user = strings.TrimSpace(user) if user != "" { cfg.HiddenUsers[user] = struct{}{} } } verificationEmailMaxLifetimeVal := valueAsString(users, "verification_email_max_lifetime_duration", "1h") verificationEmailMaxLifetimeDuration, err := gtime.ParseDuration(verificationEmailMaxLifetimeVal) if err != nil { return err } cfg.VerificationEmailMaxLifetime = verificationEmailMaxLifetimeDuration return nil } func readServiceAccountSettings(iniFile *ini.File, cfg *Cfg) error { serviceAccount := iniFile.Section("service_accounts") cfg.SATokenExpirationDayLimit = serviceAccount.Key("token_expiration_day_limit").MustInt(-1) return nil } func (cfg *Cfg) readRenderingSettings(iniFile *ini.File) { renderSec := iniFile.Section("rendering") cfg.RendererServerUrl = valueAsString(renderSec, "server_url", "") cfg.RendererCallbackUrl = valueAsString(renderSec, "callback_url", "") cfg.RendererAuthToken = valueAsString(renderSec, "renderer_token", DefaultRendererAuthToken) cfg.RendererConcurrentRequestLimit = renderSec.Key("concurrent_render_request_limit").MustInt(30) cfg.RendererRenderKeyLifeTime = renderSec.Key("render_key_lifetime").MustDuration(5 * time.Minute) cfg.RendererDefaultImageWidth = renderSec.Key("default_image_width").MustInt(1000) cfg.RendererDefaultImageHeight = renderSec.Key("default_image_height").MustInt(500) cfg.RendererDefaultImageScale = renderSec.Key("default_image_scale").MustFloat64(1) cfg.RendererCACert = valueAsString(renderSec, "ca_cert_file_path", "") cfg.ImagesDir = filepath.Join(cfg.DataPath, "png") cfg.CSVsDir = filepath.Join(cfg.DataPath, "csv") cfg.PDFsDir = filepath.Join(cfg.DataPath, "pdf") } func (cfg *Cfg) readAlertingSettings(iniFile *ini.File) error { // This check is kept to prevent users that upgrade to Grafana 11 with the legacy alerting enabled. This should prevent them from accidentally upgrading without migration to Unified Alerting. alerting := iniFile.Section("alerting") enabled, err := alerting.Key("enabled").Bool() if err == nil && enabled { cfg.Logger.Error("Option '[alerting].enabled' cannot be true. Legacy Alerting is removed. It is no longer deployed, enhanced, or supported. Delete '[alerting].enabled' and use '[unified_alerting].enabled' to enable Grafana Alerting. For more information, refer to the documentation on upgrading to Grafana Alerting (https://grafana.com/docs/grafana/v10.4/alerting/set-up/migrating-alerts)") return fmt.Errorf("invalid setting [alerting].enabled") } return nil } func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error { snapshots := iniFile.Section("snapshots") cfg.SnapshotEnabled = snapshots.Key("enabled").MustBool(true) cfg.ExternalSnapshotUrl = valueAsString(snapshots, "external_snapshot_url", "") cfg.ExternalSnapshotName = valueAsString(snapshots, "external_snapshot_name", "") cfg.ExternalEnabled = snapshots.Key("external_enabled").MustBool(true) cfg.ExternalSnapshotToken = valueAsString(snapshots, "external_snapshot_token", "") cfg.SnapshotPublicMode = snapshots.Key("public_mode").MustBool(false) return nil } func (cfg *Cfg) readServerSettings(iniFile *ini.File) error { server := iniFile.Section("server") if err := readServerURLSettings(iniFile, cfg); err != nil { cfg.Logger.Error("Invalid root_url.", "url", cfg.AppURL, "error", err) os.Exit(1) } copyServerURLSettingsToGlobals(cfg) cfg.Protocol = HTTPScheme cfg.ServeFromSubPath = server.Key("serve_from_sub_path").MustBool(false) cfg.CertWatchInterval = server.Key("certs_watch_interval").MustDuration(0) protocolStr := valueAsString(server, "protocol", "http") cfg.ServeOnSocket = server.Key("serve_on_socket").MustBool(false) if cfg.ServeOnSocket && (protocolStr == "http" || protocolStr == "https" || protocolStr == "h2") { cfg.SocketGid = server.Key("socket_gid").MustInt(-1) cfg.SocketMode = server.Key("socket_mode").MustInt(0660) cfg.SocketPath = server.Key("socket").String() } switch protocolStr { case "https": cfg.Protocol = HTTPSScheme cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() cfg.CertPassword = server.Key("cert_pass").String() case "h2": cfg.Protocol = HTTP2Scheme cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() cfg.CertPassword = server.Key("cert_pass").String() case "socket": cfg.Protocol = SocketScheme cfg.SocketGid = server.Key("socket_gid").MustInt(-1) cfg.SocketMode = server.Key("socket_mode").MustInt(0660) cfg.SocketPath = server.Key("socket").String() case "socket_h2": cfg.Protocol = SocketHTTP2Scheme cfg.SocketGid = server.Key("socket_gid").MustInt(-1) cfg.SocketMode = server.Key("socket_mode").MustInt(0660) cfg.SocketPath = server.Key("socket").String() cfg.CertFile = server.Key("cert_file").String() cfg.KeyFile = server.Key("cert_key").String() cfg.CertPassword = server.Key("cert_pass").String() default: } cfg.MinTLSVersion = valueAsString(server, "min_tls_version", "TLS1.2") if cfg.MinTLSVersion == "TLS1.0" || cfg.MinTLSVersion == "TLS1.1" { return fmt.Errorf("TLS version not configured correctly:%v, allowed values are TLS1.2 and TLS1.3", cfg.MinTLSVersion) } cfg.Domain = valueAsString(server, "domain", "localhost") cfg.HTTPAddr = valueAsString(server, "http_addr", DefaultHTTPAddr) cfg.HTTPPort = valueAsString(server, "http_port", "3000") cfg.RouterLogging = server.Key("router_logging").MustBool(false) cfg.EnableGzip = server.Key("enable_gzip").MustBool(true) cfg.EnforceDomain = server.Key("enforce_domain").MustBool(false) staticRoot := valueAsString(server, "static_root_path", "") cfg.StaticRootPath = makeAbsolute(staticRoot, cfg.HomePath) if err := cfg.validateStaticRootPath(); err != nil { return err } cdnURL := valueAsString(server, "cdn_url", "") if cdnURL != "" { parsedCDNURL, err := url.Parse(cdnURL) if err != nil { return err } cfg.CDNRootURL = parsedCDNURL } cfg.ReadTimeout = server.Key("read_timeout").MustDuration(0) headersSection := cfg.Raw.Section("server.custom_response_headers") keys := headersSection.Keys() cfg.CustomResponseHeaders = make(map[string]string, len(keys)) for _, key := range keys { cfg.CustomResponseHeaders[key.Name()] = key.Value() } return nil } // GetContentDeliveryURL returns full content delivery URL with /<edition>/<version> added to URL func (cfg *Cfg) GetContentDeliveryURL(prefix string) (string, error) { if cfg.CDNRootURL == nil { return "", nil } if cfg.BuildVersion == "" { return "", errors.New("BuildVersion is not set") } url := *cfg.CDNRootURL url.Path = path.Join(url.Path, prefix, cfg.BuildVersion) return url.String() + "/", nil } func (cfg *Cfg) readDataSourcesSettings() { datasources := cfg.Raw.Section("datasources") cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000) cfg.ConcurrentQueryCount = datasources.Key("concurrent_query_count").MustInt(10) cfg.DefaultDatasourceManageAlertsUIToggle = datasources.Key("default_manage_alerts_ui_toggle").MustBool(true) cfg.DefaultAllowRecordingRulesTargetAlertsUIToggle = datasources.Key("default_allow_recording_rules_target_alerts_ui_toggle").MustBool(true) } func (cfg *Cfg) readDataSourceSecuritySettings() { datasources := cfg.Raw.Section("datasources.ip_range_security") cfg.IPRangeACEnabled = datasources.Key("enabled").MustBool(false) cfg.IPRangeACSecretKey = datasources.Key("secret_key").MustString("") if cfg.IPRangeACEnabled && cfg.IPRangeACSecretKey == "" { cfg.Logger.Error("IP range access control is enabled but no secret key is set") } allowedURLString := datasources.Key("allow_list").MustString("") for _, urlString := range util.SplitString(allowedURLString) { allowedURL, err := url.Parse(urlString) if err != nil { cfg.Logger.Error("Error parsing allowed URL for IP range access control", "error", err) continue } else { cfg.IPRangeACAllowedURLs = append(cfg.IPRangeACAllowedURLs, allowedURL) } } } func (cfg *Cfg) readSqlDataSourceSettings() { sqlDatasources := cfg.Raw.Section("sql_datasources") cfg.SqlDatasourceMaxOpenConnsDefault = sqlDatasources.Key("max_open_conns_default").MustInt(100) cfg.SqlDatasourceMaxIdleConnsDefault = sqlDatasources.Key("max_idle_conns_default").MustInt(100) cfg.SqlDatasourceMaxConnLifetimeDefault = sqlDatasources.Key("max_conn_lifetime_default").MustInt(14400) } func GetAllowedOriginGlobs(originPatterns []string) ([]glob.Glob, error) { allowedOrigins := originPatterns originGlobs := make([]glob.Glob, 0, len(allowedOrigins)) for _, originPattern := range allowedOrigins { g, err := glob.Compile(originPattern) if err != nil { return nil, fmt.Errorf("error parsing origin pattern: %v", err) } originGlobs = append(originGlobs, g) } return originGlobs, nil } func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { section := iniFile.Section("live") cfg.LiveMaxConnections = section.Key("max_connections").MustInt(100) if cfg.LiveMaxConnections < -1 { return fmt.Errorf("unexpected value %d for [live] max_connections", cfg.LiveMaxConnections) } cfg.LiveMessageSizeLimit = section.Key("message_size_limit").MustInt(65536) if cfg.LiveMessageSizeLimit < -1 { return fmt.Errorf("unexpected value %d for [live] message_size_limit", cfg.LiveMaxConnections) } cfg.LiveClientQueueMaxSize = section.Key("client_queue_max_size").MustInt(4194304) if cfg.LiveClientQueueMaxSize <= 0 { return fmt.Errorf("unexpected value %d for [live] client_queue_max_size", cfg.LiveMaxConnections) } cfg.LiveHAEngine = section.Key("ha_engine").MustString("") switch cfg.LiveHAEngine { case "", "redis": default: return fmt.Errorf("unsupported live HA engine type: %s", cfg.LiveHAEngine) } cfg.LiveHAPrefix = section.Key("ha_prefix").MustString("") cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379") cfg.LiveHAEnginePassword = section.Key("ha_engine_password").MustString("") allowedOrigins := section.Key("allowed_origins").MustString("") origins := strings.Split(allowedOrigins, ",") originPatterns := make([]string, 0, len(origins)) for _, originPattern := range origins { originPattern = strings.TrimSpace(originPattern) if originPattern == "" { continue } originPatterns = append(originPatterns, originPattern) } _, err := GetAllowedOriginGlobs(originPatterns) if err != nil { return err } cfg.LiveAllowedOrigins = originPatterns return nil } func (cfg *Cfg) readClassicProvisioningSettings(iniFile *ini.File) { cfg.ClassicProvisioningDashboardsServerLockMinWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_min_wait_ms").MustInt64(100) cfg.ClassicProvisioningDashboardsServerLockMaxWaitMs = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_wait_ms").MustInt64(1000) cfg.ClassicProvisioningDashboardsServerLockMaxIntervalSeconds = iniFile.Section("classic_provisioning").Key("dashboards_server_lock_max_interval_seconds").MustInt64(15) } func (cfg *Cfg) readProvisioningSettings(iniFile *ini.File) error { provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") cfg.ProvisioningPath = makeAbsolute(provisioning, cfg.HomePath) provisioningPaths := strings.TrimSpace(valueAsString(iniFile.Section("paths"), "permitted_provisioning_paths", "")) if provisioningPaths != "|" && provisioningPaths != "" { cfg.PermittedProvisioningPaths = strings.Split(provisioningPaths, "|") for i, s := range cfg.PermittedProvisioningPaths { s = strings.TrimSpace(s) if s == "" { return fmt.Errorf("a provisioning path is empty in '%s' (at index %d)", provisioningPaths, i) } cfg.PermittedProvisioningPaths[i] = makeAbsolute(s, cfg.HomePath) } } repositoryTypes := strings.TrimSpace(valueAsString(iniFile.Section("provisioning"), "repository_types", "")) if repositoryTypes != "|" && repositoryTypes != "" { cfg.ProvisioningRepositoryTypes = strings.Split(repositoryTypes, "|") for i, s := range cfg.ProvisioningRepositoryTypes { s = strings.TrimSpace(s) if s == "" { return fmt.Errorf("a provisioning repository type is empty in '%s' (at index %d)", repositoryTypes, i) } cfg.ProvisioningRepositoryTypes[i] = s } } cfg.DisableControllers = iniFile.Section("provisioning").Key("disable_controllers").MustBool(false) // if disable_controllers is not set, check if grafana-apiserver.disable_controllers is set // TODO: consolidate to just [grafana-apiserver]disable_controllers if !cfg.DisableControllers { cfg.DisableControllers = iniFile.Section("grafana-apiserver").Key("disable_controllers").MustBool(false) } cfg.ProvisioningEnabled = iniFile.Section("provisioning").Key("enabled").MustBool(true) cfg.ProvisioningAllowedTargets = iniFile.Section("provisioning").Key("allowed_targets").Strings("|") if len(cfg.ProvisioningAllowedTargets) == 0 { cfg.ProvisioningAllowedTargets = []string{"folder", "folderless"} } cfg.ProvisioningResources = iniFile.Section("provisioning").Key("resources").Strings(",") if len(cfg.ProvisioningResources) == 0 { cfg.ProvisioningResources = defaultProvisioningResources() } cfg.ProvisioningAllowImageRendering = iniFile.Section("provisioning").Key("allow_image_rendering").MustBool(true) cfg.ProvisioningAllowInsecure = iniFile.Section("provisioning").Key("allow_insecure").MustBool(false) cfg.ProvisioningMinSyncInterval = iniFile.Section("provisioning").Key("min_sync_interval").MustDuration(10 * time.Second) cfg.ProvisioningMaxResourcesPerRepository = iniFile.Section("provisioning").Key("max_resources_per_repository").MustInt64(0) cfg.ProvisioningMaxRepositories = iniFile.Section("provisioning").Key("max_repositories").MustInt64(10) cfg.ProvisioningMaxIncrementalChanges = iniFile.Section("provisioning").Key("max_incremental_changes").MustInt(100) cfg.ProvisioningMaxFileSize = iniFile.Section("provisioning").Key("max_file_size").MustInt64(ProvisioningMaxFileSizeDefault) cfg.ProvisioningSyncResourceTimeout = iniFile.Section("provisioning").Key("sync_resource_timeout").MustDuration(ProvisioningSyncResourceTimeoutDefault) cfg.ProvisioningWebhookSecretRotationInterval = iniFile.Section("provisioning").Key("webhook_secret_rotation_interval").MustDuration(30 * 24 * time.Hour) cfg.ProvisioningControllerResyncInterval = iniFile.Section("provisioning").Key("resync_interval").MustDuration(ProvisioningControllerResyncIntervalDefault) cfg.ProvisioningHistoryExpiration = iniFile.Section("provisioning").Key("history_expiration").MustDuration(ProvisioningHistoryExpirationDefault) cfg.ProvisioningJobPollInterval = iniFile.Section("provisioning").Key("job_poll_interval").MustDuration(ProvisioningJobPollIntervalDefault) cfg.ProvisioningPublicRootURL = strings.TrimRight(valueAsString(iniFile.Section("provisioning"), "public_root_url", ""), "/") cfg.ProvisioningWebhookTrustedIPHeader = iniFile.Section("provisioning").Key("webhook_trusted_ip_header").MustString("") cfg.ProvisioningWebhookRateLimitRPS = iniFile.Section("provisioning").Key("webhook_rate_limit_rps").MustInt(0) // Read job history configuration cfg.ProvisioningLokiURL = valueAsString(iniFile.Section("provisioning"), "loki_url", "") cfg.ProvisioningLokiUser = valueAsString(iniFile.Section("provisioning"), "loki_user", "") cfg.ProvisioningLokiPassword = valueAsString(iniFile.Section("provisioning"), "loki_password", "") cfg.ProvisioningLokiTenantID = valueAsString(iniFile.Section("provisioning"), "loki_tenant_id", "") return nil } // defaultProvisioningResources is the built-in set used when [provisioning] resources is // unset. Tokens use the shared "<group>/<Kind>[:cap...]" grammar (see // resources.ParseSupportedResources). Library panels and playlists are declared but // disabled by default. func defaultProvisioningResources() []string { return []string{ "folder.grafana.app/Folder:folder", "dashboard.grafana.app/Dashboard:folder", "dashboard.grafana.app/LibraryPanel:folder:disabled", "playlist.grafana.app/Playlist:disabled", } } func (cfg *Cfg) readPublicDashboardsSettings() { publicDashboards := cfg.Raw.Section("public_dashboards") cfg.PublicDashboardsEnabled = publicDashboards.Key("enabled").MustBool(true) } func (cfg *Cfg) DefaultOrgID() int64 { if cfg.AutoAssignOrg && cfg.AutoAssignOrgId > 0 { return int64(cfg.AutoAssignOrgId) } return int64(1) }