diff --git a/pkg/setting/setting_data_proxy.go b/pkg/setting/setting_data_proxy.go index 5dd31676e74..257430e8d0c 100644 --- a/pkg/setting/setting_data_proxy.go +++ b/pkg/setting/setting_data_proxy.go @@ -8,29 +8,68 @@ import ( const defaultDataProxyRowLimit = int64(1000000) +type ProxySettings struct { + SendUserHeader bool + Logging bool + Timeout int + DialTimeout int + KeepAlive int + TLSHandshakeTimeout int + ExpectContinueTimeout int + MaxConnsPerHost int + MaxIdleConns int + IdleConnTimeout int + ResponseLimit int64 + RowLimit int64 + UserAgent string +} + func readDataProxySettings(iniFile *ini.File, cfg *Cfg) error { - dataproxy := iniFile.Section("dataproxy") - cfg.SendUserHeader = dataproxy.Key("send_user_header").MustBool(false) - cfg.DataProxyLogging = dataproxy.Key("logging").MustBool(false) - cfg.DataProxyTimeout = dataproxy.Key("timeout").MustInt(30) - cfg.DataProxyDialTimeout = dataproxy.Key("dialTimeout").MustInt(10) - cfg.DataProxyKeepAlive = dataproxy.Key("keep_alive_seconds").MustInt(30) - cfg.DataProxyTLSHandshakeTimeout = dataproxy.Key("tls_handshake_timeout_seconds").MustInt(10) - cfg.DataProxyExpectContinueTimeout = dataproxy.Key("expect_continue_timeout_seconds").MustInt(1) - cfg.DataProxyMaxConnsPerHost = dataproxy.Key("max_conns_per_host").MustInt(0) - cfg.DataProxyMaxIdleConns = dataproxy.Key("max_idle_connections").MustInt() - cfg.DataProxyIdleConnTimeout = dataproxy.Key("idle_conn_timeout_seconds").MustInt(90) - cfg.ResponseLimit = dataproxy.Key("response_limit").MustInt64(0) - cfg.DataProxyRowLimit = dataproxy.Key("row_limit").MustInt64(defaultDataProxyRowLimit) - cfg.DataProxyUserAgent = dataproxy.Key("user_agent").String() + proxy := ReadDataProxySettings(iniFile) - if cfg.DataProxyUserAgent == "" { - cfg.DataProxyUserAgent = fmt.Sprintf("Grafana/%s", BuildVersion) - } - - if cfg.DataProxyRowLimit <= 0 { - cfg.DataProxyRowLimit = defaultDataProxyRowLimit - } + cfg.SendUserHeader = proxy.SendUserHeader + cfg.DataProxyLogging = proxy.Logging + cfg.DataProxyTimeout = proxy.Timeout + cfg.DataProxyDialTimeout = proxy.DialTimeout + cfg.DataProxyKeepAlive = proxy.KeepAlive + cfg.DataProxyTLSHandshakeTimeout = proxy.TLSHandshakeTimeout + cfg.DataProxyExpectContinueTimeout = proxy.ExpectContinueTimeout + cfg.DataProxyMaxConnsPerHost = proxy.MaxConnsPerHost + cfg.DataProxyMaxIdleConns = proxy.MaxIdleConns + cfg.DataProxyIdleConnTimeout = proxy.IdleConnTimeout + cfg.ResponseLimit = proxy.ResponseLimit + cfg.DataProxyRowLimit = proxy.RowLimit + cfg.DataProxyUserAgent = proxy.UserAgent return nil } + +func ReadDataProxySettings(iniFile *ini.File) ProxySettings { + section := iniFile.Section("dataproxy") + + proxy := ProxySettings{ + SendUserHeader: section.Key("send_user_header").MustBool(false), + Logging: section.Key("logging").MustBool(false), + Timeout: section.Key("timeout").MustInt(30), + DialTimeout: section.Key("dialTimeout").MustInt(10), + KeepAlive: section.Key("keep_alive_seconds").MustInt(30), + TLSHandshakeTimeout: section.Key("tls_handshake_timeout_seconds").MustInt(10), + ExpectContinueTimeout: section.Key("expect_continue_timeout_seconds").MustInt(1), + MaxConnsPerHost: section.Key("max_conns_per_host").MustInt(0), + MaxIdleConns: section.Key("max_idle_connections").MustInt(), + IdleConnTimeout: section.Key("idle_conn_timeout_seconds").MustInt(90), + ResponseLimit: section.Key("response_limit").MustInt64(0), + RowLimit: section.Key("row_limit").MustInt64(defaultDataProxyRowLimit), + UserAgent: section.Key("user_agent").String(), + } + + if proxy.UserAgent == "" { + proxy.UserAgent = fmt.Sprintf("Grafana/%s", BuildVersion) + } + + if proxy.RowLimit <= 0 { + proxy.RowLimit = defaultDataProxyRowLimit + } + + return proxy +} diff --git a/pkg/setting/setting_data_proxy_test.go b/pkg/setting/setting_data_proxy_test.go new file mode 100644 index 00000000000..6b833087762 --- /dev/null +++ b/pkg/setting/setting_data_proxy_test.go @@ -0,0 +1,112 @@ +package setting + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" +) + +func TestReadDataProxySettings(t *testing.T) { + t.Run("should use default values when ini is empty", func(t *testing.T) { + f := ini.Empty() + proxy := ReadDataProxySettings(f) + + assertDataProxyDefaults(t, proxy) + }) + + t.Run("should use default values when section exists with no values", func(t *testing.T) { + f := ini.Empty() + _, err := f.NewSection("dataproxy") + require.NoError(t, err) + + proxy := ReadDataProxySettings(f) + + assertDataProxyDefaults(t, proxy) + }) + + t.Run("should use custom values when section has overrides", func(t *testing.T) { + iniFile := ` +[dataproxy] +send_user_header = true +logging = true +timeout = 60 +dialTimeout = 20 +keep_alive_seconds = 60 +tls_handshake_timeout_seconds = 20 +expect_continue_timeout_seconds = 2 +max_conns_per_host = 100 +max_idle_connections = 50 +idle_conn_timeout_seconds = 120 +response_limit = 5242880 +row_limit = 500000 +user_agent = CustomAgent/1.0 +` + f, err := ini.Load([]byte(iniFile)) + require.NoError(t, err) + + proxy := ReadDataProxySettings(f) + + assert.True(t, proxy.SendUserHeader) + assert.True(t, proxy.Logging) + assert.Equal(t, 60, proxy.Timeout) + assert.Equal(t, 20, proxy.DialTimeout) + assert.Equal(t, 60, proxy.KeepAlive) + assert.Equal(t, 20, proxy.TLSHandshakeTimeout) + assert.Equal(t, 2, proxy.ExpectContinueTimeout) + assert.Equal(t, 100, proxy.MaxConnsPerHost) + assert.Equal(t, 50, proxy.MaxIdleConns) + assert.Equal(t, 120, proxy.IdleConnTimeout) + assert.Equal(t, int64(5242880), proxy.ResponseLimit) + assert.Equal(t, int64(500000), proxy.RowLimit) + assert.Equal(t, "CustomAgent/1.0", proxy.UserAgent) + }) + + t.Run("should set default row limit when row_limit is zero", func(t *testing.T) { + iniFile := ` +[dataproxy] +row_limit = 0 +` + f, err := ini.Load([]byte(iniFile)) + require.NoError(t, err) + + proxy := ReadDataProxySettings(f) + + assert.Equal(t, defaultDataProxyRowLimit, proxy.RowLimit) + }) + + t.Run("should set default row limit when row_limit is negative", func(t *testing.T) { + iniFile := ` +[dataproxy] +row_limit = -100 +` + f, err := ini.Load([]byte(iniFile)) + require.NoError(t, err) + + proxy := ReadDataProxySettings(f) + + assert.Equal(t, defaultDataProxyRowLimit, proxy.RowLimit) + }) +} + +func assertDataProxyDefaults(t *testing.T, proxy ProxySettings) { + t.Helper() + + assert.False(t, proxy.SendUserHeader) + assert.False(t, proxy.Logging) + assert.Equal(t, 30, proxy.Timeout) + assert.Equal(t, 10, proxy.DialTimeout) + assert.Equal(t, 30, proxy.KeepAlive) + assert.Equal(t, 10, proxy.TLSHandshakeTimeout) + assert.Equal(t, 1, proxy.ExpectContinueTimeout) + assert.Equal(t, 0, proxy.MaxConnsPerHost) + assert.Equal(t, 0, proxy.MaxIdleConns) + assert.Equal(t, 90, proxy.IdleConnTimeout) + assert.Equal(t, int64(0), proxy.ResponseLimit) + assert.Equal(t, defaultDataProxyRowLimit, proxy.RowLimit) + + // UserAgent should match "Grafana/{version}" pattern + assert.Regexp(t, regexp.MustCompile(`^Grafana/.*`), proxy.UserAgent) +} diff --git a/pkg/setting/setting_quota.go b/pkg/setting/setting_quota.go index f35cb29411c..05ecb8a9bc2 100644 --- a/pkg/setting/setting_quota.go +++ b/pkg/setting/setting_quota.go @@ -1,5 +1,7 @@ package setting +import "gopkg.in/ini.v1" + type OrgQuota struct { User int64 `target:"org_user"` DataSource int64 `target:"data_source"` @@ -32,34 +34,42 @@ type QuotaSettings struct { } func (cfg *Cfg) readQuotaSettings() { + iniFile := cfg.Raw + quota := ReadQuotaSettings(iniFile) // set global defaults. - quota := cfg.Raw.Section("quota") - cfg.Quota.Enabled = quota.Key("enabled").MustBool(false) + cfg.Quota = quota +} + +func ReadQuotaSettings(iniFile *ini.File) QuotaSettings { + section := iniFile.Section("quota") + var quota QuotaSettings + quota.Enabled = section.Key("enabled").MustBool(false) // per ORG Limits - cfg.Quota.Org = OrgQuota{ - User: quota.Key("org_user").MustInt64(10), - DataSource: quota.Key("org_data_source").MustInt64(10), - Dashboard: quota.Key("org_dashboard").MustInt64(10), - ApiKey: quota.Key("org_api_key").MustInt64(10), - AlertRule: quota.Key("org_alert_rule").MustInt64(100), + quota.Org = OrgQuota{ + User: section.Key("org_user").MustInt64(10), + DataSource: section.Key("org_data_source").MustInt64(10), + Dashboard: section.Key("org_dashboard").MustInt64(10), + ApiKey: section.Key("org_api_key").MustInt64(10), + AlertRule: section.Key("org_alert_rule").MustInt64(100), } // per User limits - cfg.Quota.User = UserQuota{ - Org: quota.Key("user_org").MustInt64(10), + quota.User = UserQuota{ + Org: section.Key("user_org").MustInt64(10), } // Global Limits - cfg.Quota.Global = GlobalQuota{ - User: quota.Key("global_user").MustInt64(-1), - Org: quota.Key("global_org").MustInt64(-1), - DataSource: quota.Key("global_data_source").MustInt64(-1), - Dashboard: quota.Key("global_dashboard").MustInt64(-1), - ApiKey: quota.Key("global_api_key").MustInt64(-1), - Session: quota.Key("global_session").MustInt64(-1), - File: quota.Key("global_file").MustInt64(-1), - AlertRule: quota.Key("global_alert_rule").MustInt64(-1), - Correlations: quota.Key("global_correlations").MustInt64(-1), + quota.Global = GlobalQuota{ + User: section.Key("global_user").MustInt64(-1), + Org: section.Key("global_org").MustInt64(-1), + DataSource: section.Key("global_data_source").MustInt64(-1), + Dashboard: section.Key("global_dashboard").MustInt64(-1), + ApiKey: section.Key("global_api_key").MustInt64(-1), + Session: section.Key("global_session").MustInt64(-1), + File: section.Key("global_file").MustInt64(-1), + AlertRule: section.Key("global_alert_rule").MustInt64(-1), + Correlations: section.Key("global_correlations").MustInt64(-1), } + return quota } diff --git a/pkg/setting/setting_quota_test.go b/pkg/setting/setting_quota_test.go new file mode 100644 index 00000000000..c0db45f61c8 --- /dev/null +++ b/pkg/setting/setting_quota_test.go @@ -0,0 +1,112 @@ +package setting + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/ini.v1" +) + +func TestReadQuotaSettings(t *testing.T) { + t.Run("should use custom values when section has overrides", func(t *testing.T) { + iniFile := ` +[quota] +enabled = true + +# Org quotas +org_user = 20 +org_data_source = 30 +org_dashboard = 40 +org_api_key = 50 +org_alert_rule = 200 + +# User quotas +user_org = 5 + +# Global quotas +global_user = 1000 +global_org = 500 +global_data_source = 2000 +global_dashboard = 3000 +global_api_key = 100 +global_session = 10000 +global_file = 500 +global_alert_rule = 5000 +global_correlations = 250 +` + f, err := ini.Load([]byte(iniFile)) + require.NoError(t, err) + + quota := ReadQuotaSettings(f) + + // Enabled should be true + assert.True(t, quota.Enabled) + + // Org quotas should have custom values + assert.Equal(t, int64(20), quota.Org.User) + assert.Equal(t, int64(30), quota.Org.DataSource) + assert.Equal(t, int64(40), quota.Org.Dashboard) + assert.Equal(t, int64(50), quota.Org.ApiKey) + assert.Equal(t, int64(200), quota.Org.AlertRule) + + // User quotas should have custom values + assert.Equal(t, int64(5), quota.User.Org) + + // Global quotas should have custom values + assert.Equal(t, int64(1000), quota.Global.User) + assert.Equal(t, int64(500), quota.Global.Org) + assert.Equal(t, int64(2000), quota.Global.DataSource) + assert.Equal(t, int64(3000), quota.Global.Dashboard) + assert.Equal(t, int64(100), quota.Global.ApiKey) + assert.Equal(t, int64(10000), quota.Global.Session) + assert.Equal(t, int64(500), quota.Global.File) + assert.Equal(t, int64(5000), quota.Global.AlertRule) + assert.Equal(t, int64(250), quota.Global.Correlations) + }) + + t.Run("should use default values when ini is empty", func(t *testing.T) { + f := ini.Empty() + quota := ReadQuotaSettings(f) + + assertDefaults(t, quota) + }) + + t.Run("should use default values when section exists with no values", func(t *testing.T) { + f := ini.Empty() + _, err := f.NewSection("quota") + require.NoError(t, err) + + quota := ReadQuotaSettings(f) + + assertDefaults(t, quota) + }) +} + +func assertDefaults(t *testing.T, quota QuotaSettings) { + t.Helper() + + // Enabled should be false by default + assert.False(t, quota.Enabled) + + // Org quotas should have default values + assert.Equal(t, int64(10), quota.Org.User) + assert.Equal(t, int64(10), quota.Org.DataSource) + assert.Equal(t, int64(10), quota.Org.Dashboard) + assert.Equal(t, int64(10), quota.Org.ApiKey) + assert.Equal(t, int64(100), quota.Org.AlertRule) + + // User quotas should have default values + assert.Equal(t, int64(10), quota.User.Org) + + // Global quotas should have default values (-1 means unlimited) + assert.Equal(t, int64(-1), quota.Global.User) + assert.Equal(t, int64(-1), quota.Global.Org) + assert.Equal(t, int64(-1), quota.Global.DataSource) + assert.Equal(t, int64(-1), quota.Global.Dashboard) + assert.Equal(t, int64(-1), quota.Global.ApiKey) + assert.Equal(t, int64(-1), quota.Global.Session) + assert.Equal(t, int64(-1), quota.Global.File) + assert.Equal(t, int64(-1), quota.Global.AlertRule) + assert.Equal(t, int64(-1), quota.Global.Correlations) +}