From 28d37ec4e3d41a8c7c01cd8b15e6fbe51f887b55 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Wed, 26 Mar 2025 17:14:20 +0000 Subject: [PATCH 1/7] introduce new package --- backend/config.go | 20 +-- .../httpclient/response_limit_middleware.go | 7 + .../response_limit_middleware_test.go | 151 ++++++++++++------ config/grafana_cfg.go | 58 +++++++ 4 files changed, 176 insertions(+), 60 deletions(-) create mode 100644 config/grafana_cfg.go diff --git a/backend/config.go b/backend/config.go index 80c1b2bb3..d2f660179 100644 --- a/backend/config.go +++ b/backend/config.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" ) @@ -30,24 +31,13 @@ const ( type configKey struct{} // GrafanaConfigFromContext returns Grafana config from context. -func GrafanaConfigFromContext(ctx context.Context) *GrafanaCfg { - v := ctx.Value(configKey{}) - if v == nil { - return NewGrafanaCfg(nil) - } - - cfg := v.(*GrafanaCfg) - if cfg == nil { - return NewGrafanaCfg(nil) - } - - return cfg +func GrafanaConfigFromContext(ctx context.Context) *config.GrafanaCfg { + return config.GrafanaConfigFromContext(ctx) } // WithGrafanaConfig injects supplied Grafana config into context. -func WithGrafanaConfig(ctx context.Context, cfg *GrafanaCfg) context.Context { - ctx = context.WithValue(ctx, configKey{}, cfg) - return ctx +func WithGrafanaConfig(ctx context.Context, cfg *config.GrafanaCfg) context.Context { + return config.WithGrafanaConfig(ctx, cfg) } type GrafanaCfg struct { diff --git a/backend/httpclient/response_limit_middleware.go b/backend/httpclient/response_limit_middleware.go index d36c01551..d91b88507 100644 --- a/backend/httpclient/response_limit_middleware.go +++ b/backend/httpclient/response_limit_middleware.go @@ -2,6 +2,8 @@ package httpclient import ( "net/http" + + "github.com/grafana/grafana-plugin-sdk-go/config" ) // ResponseLimitMiddlewareName is the middleware name used by ResponseLimitMiddleware. @@ -15,6 +17,11 @@ func ResponseLimitMiddleware(limit int64) Middleware { return nil, err } + // Try to get limit from context first, fall back to static limit + if cfgLimit := config.GrafanaConfigFromContext(req.Context()).ResponseLimit(); cfgLimit > 0 { + limit = cfgLimit + } + if limit <= 0 { return res, nil } diff --git a/backend/httpclient/response_limit_middleware_test.go b/backend/httpclient/response_limit_middleware_test.go index ec5b3b810..56a0dd60a 100644 --- a/backend/httpclient/response_limit_middleware_test.go +++ b/backend/httpclient/response_limit_middleware_test.go @@ -1,58 +1,119 @@ package httpclient import ( - "context" - "errors" - "fmt" "io" "net/http" "strings" "testing" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/stretchr/testify/require" ) func TestResponseLimitMiddleware(t *testing.T) { - tcs := []struct { - limit int64 - expectedBodyLength int - expectedBody string - err error - }{ - {limit: 1, expectedBodyLength: 1, expectedBody: "d", err: errors.New("error: http: response body too large, response limit is set to: 1")}, - {limit: 1000000, expectedBodyLength: 5, expectedBody: "dummy", err: nil}, - {limit: 0, expectedBodyLength: 5, expectedBody: "dummy", err: nil}, - } - for _, tc := range tcs { - t.Run(fmt.Sprintf("Test ResponseLimitMiddleware with limit: %d", tc.limit), func(t *testing.T) { - finalRoundTripper := RoundTripperFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: http.StatusOK, Request: req, Body: io.NopCloser(strings.NewReader("dummy"))}, nil - }) - - mw := ResponseLimitMiddleware(tc.limit) - rt := mw.CreateMiddleware(Options{}, finalRoundTripper) - require.NotNil(t, rt) - middlewareName, ok := mw.(MiddlewareName) - require.True(t, ok) - require.Equal(t, ResponseLimitMiddlewareName, middlewareName.MiddlewareName()) - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://test.com/query", nil) - require.NoError(t, err) - res, err := rt.RoundTrip(req) - require.NoError(t, err) - require.NotNil(t, res) - require.NotNil(t, res.Body) - - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - require.EqualError(t, tc.err, err.Error()) - } else { - require.NoError(t, tc.err) - } - require.NoError(t, res.Body.Close()) - - require.Len(t, bodyBytes, tc.expectedBodyLength) - require.Equal(t, string(bodyBytes), tc.expectedBody) - }) - } + t.Run("should use static limit when no context limit is set", func(t *testing.T) { + next := &mockRoundTripper{ + response: &http.Response{ + Body: io.NopCloser(strings.NewReader("dummy")), + }, + } + + middleware := ResponseLimitMiddleware(1) + rt := middleware.CreateMiddleware(Options{}, next) + + req, err := http.NewRequest(http.MethodGet, "http://", nil) + require.NoError(t, err) + + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + + body, err := io.ReadAll(res.Body) + require.Error(t, err) + require.Equal(t, "error: http: response body too large, response limit is set to: 1", err.Error()) + require.Equal(t, "d", string(body)) + }) + + t.Run("should use context limit when set", func(t *testing.T) { + next := &mockRoundTripper{ + response: &http.Response{ + Body: io.NopCloser(strings.NewReader("dummy")), + }, + } + + middleware := ResponseLimitMiddleware(1000) // High static limit + rt := middleware.CreateMiddleware(Options{}, next) + + req, err := http.NewRequest(http.MethodGet, "http://", nil) + require.NoError(t, err) + + // Set a lower limit in the context + ctx := config.WithGrafanaConfig(req.Context(), config.NewGrafanaCfg(map[string]string{ + config.ResponseLimit: "1", + })) + req = req.WithContext(ctx) + + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + + body, err := io.ReadAll(res.Body) + require.Error(t, err) + require.Equal(t, "error: http: response body too large, response limit is set to: 1", err.Error()) + require.Equal(t, "d", string(body)) + }) + + t.Run("should not limit response when limit is 0", func(t *testing.T) { + next := &mockRoundTripper{ + response: &http.Response{ + Body: io.NopCloser(strings.NewReader("dummy")), + }, + } + + middleware := ResponseLimitMiddleware(0) + rt := middleware.CreateMiddleware(Options{}, next) + + req, err := http.NewRequest(http.MethodGet, "http://", nil) + require.NoError(t, err) + + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, "dummy", string(body)) + }) + + t.Run("should not limit response when status is switching protocols", func(t *testing.T) { + next := &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusSwitchingProtocols, + Body: io.NopCloser(strings.NewReader("dummy")), + }, + } + + middleware := ResponseLimitMiddleware(1) + rt := middleware.CreateMiddleware(Options{}, next) + + req, err := http.NewRequest(http.MethodGet, "http://", nil) + require.NoError(t, err) + + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, "dummy", string(body)) + }) +} + +type mockRoundTripper struct { + response *http.Response + err error +} + +func (m *mockRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { + return m.response, m.err } diff --git a/config/grafana_cfg.go b/config/grafana_cfg.go new file mode 100644 index 000000000..26acd6c75 --- /dev/null +++ b/config/grafana_cfg.go @@ -0,0 +1,58 @@ +package config + +import ( + "context" + "strconv" +) + +const ( + ResponseLimit = "GF_RESPONSE_LIMIT" +) + +// GrafanaCfg represents Grafana configuration +type GrafanaCfg struct { + config map[string]string +} + +// NewGrafanaCfg creates a new GrafanaCfg instance +func NewGrafanaCfg(cfg map[string]string) *GrafanaCfg { + return &GrafanaCfg{config: cfg} +} + +// Get returns a value from the config map +func (c *GrafanaCfg) Get(key string) string { + return c.config[key] +} + +// ResponseLimit returns the response limit value +func (c *GrafanaCfg) ResponseLimit() int64 { + if v, exists := c.config[ResponseLimit]; exists { + if i, err := strconv.ParseInt(v, 10, 64); err == nil { + return i + } + } + return 0 +} + +type configKey struct{} + +// GrafanaConfigFromContext returns Grafana config from context +func GrafanaConfigFromContext(ctx context.Context) *GrafanaCfg { + v := ctx.Value(configKey{}) + if v == nil { + return NewGrafanaCfg(nil) + } + + cfg := v.(*GrafanaCfg) + if cfg == nil { + return NewGrafanaCfg(nil) + } + + return cfg +} + +// WithGrafanaConfig injects supplied Grafana config into context +func WithGrafanaConfig(ctx context.Context, cfg *GrafanaCfg) context.Context { + ctx = context.WithValue(ctx, configKey{}, cfg) + return ctx +} From 03f4ceb2e78358a038324fb89d3c859f2cfe4b85 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Wed, 26 Mar 2025 18:06:45 +0000 Subject: [PATCH 2/7] update package refs --- backend/app/instance_provider_test.go | 17 +- backend/common.go | 15 +- backend/common_test.go | 15 +- backend/config.go | 270 ---------------- backend/config_test.go | 203 ++++++------ backend/convert_from_protobuf.go | 5 +- backend/convert_from_protobuf_test.go | 2 +- backend/convert_to_protobuf.go | 5 +- backend/convert_to_protobuf_test.go | 3 +- backend/datasource/instance_provider_test.go | 23 +- .../response_limit_middleware_test.go | 4 + config/grafana_cfg.go | 290 ++++++++++++++++-- 12 files changed, 422 insertions(+), 430 deletions(-) diff --git a/backend/app/instance_provider_test.go b/backend/app/instance_provider_test.go index a5735af34..ae8eef012 100644 --- a/backend/app/instance_provider_test.go +++ b/backend/app/instance_provider_test.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/stretchr/testify/require" ) @@ -39,7 +40,7 @@ func TestInstanceProvider(t *testing.T) { }) t.Run("When both the configuration and updated field of current app instance settings are equal to the cache, should return false", func(t *testing.T) { - config := map[string]string{ + cfg := map[string]string{ "foo": "bar", } @@ -47,14 +48,14 @@ func TestInstanceProvider(t *testing.T) { AppInstanceSettings: &backend.AppInstanceSettings{ Updated: time.Now(), }, - GrafanaConfig: backend.NewGrafanaCfg(config), + GrafanaConfig: config.NewGrafanaCfg(cfg), } cachedSettings := backend.PluginContext{ AppInstanceSettings: &backend.AppInstanceSettings{ Updated: curSettings.AppInstanceSettings.Updated, }, - GrafanaConfig: backend.NewGrafanaCfg(config), + GrafanaConfig: config.NewGrafanaCfg(cfg), } cachedInstance := instancemgmt.CachedInstance{ @@ -84,11 +85,11 @@ func TestInstanceProvider(t *testing.T) { require.True(t, needsUpdate) t.Run("Should return true when cached config is changed", func(t *testing.T) { - curSettings.GrafanaConfig = backend.NewGrafanaCfg(map[string]string{ + curSettings.GrafanaConfig = config.NewGrafanaCfg(map[string]string{ "foo": "bar", }) - cachedSettings.GrafanaConfig = backend.NewGrafanaCfg(map[string]string{ + cachedSettings.GrafanaConfig = config.NewGrafanaCfg(map[string]string{ "baz": "qux", }) @@ -163,14 +164,14 @@ func Test_instanceProvider_NeedsUpdate(t *testing.T) { AppInstanceSettings: &backend.AppInstanceSettings{ Updated: ts, }, - GrafanaConfig: backend.NewGrafanaCfg(map[string]string{"foo": "bar", "baz": "qux"}), + GrafanaConfig: config.NewGrafanaCfg(map[string]string{"foo": "bar", "baz": "qux"}), }, cachedInstance: instancemgmt.CachedInstance{ PluginContext: backend.PluginContext{ AppInstanceSettings: &backend.AppInstanceSettings{ Updated: ts, }, - GrafanaConfig: backend.NewGrafanaCfg(map[string]string{"foo": "bar", "baz": "qux"}), + GrafanaConfig: config.NewGrafanaCfg(map[string]string{"foo": "bar", "baz": "qux"}), }, }, }, @@ -201,7 +202,7 @@ func Test_instanceProvider_NeedsUpdate(t *testing.T) { AppInstanceSettings: &backend.AppInstanceSettings{ Updated: ts, }, - GrafanaConfig: backend.NewGrafanaCfg(map[string]string{"foo": "bar"}), + GrafanaConfig: config.NewGrafanaCfg(map[string]string{"foo": "bar"}), }, cachedInstance: instancemgmt.CachedInstance{ PluginContext: backend.PluginContext{ diff --git a/backend/common.go b/backend/common.go index 885d00486..acea5d147 100644 --- a/backend/common.go +++ b/backend/common.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/grafana/grafana-plugin-sdk-go/internal/tenant" ) @@ -141,11 +142,11 @@ func (s *DataSourceInstanceSettings) HTTPClientOptions(ctx context.Context) (htt setCustomOptionsFromHTTPSettings(&opts, httpSettings) cfg := GrafanaConfigFromContext(ctx) - proxy, err := cfg.proxy() + proxy, err := cfg.Proxy() if err != nil { return opts, err } - opts.ProxyOptions, err = s.ProxyOptions(proxy.clientCfg) + opts.ProxyOptions, err = s.ProxyOptions(proxy.ClientCfg) if err != nil { return opts, err } @@ -197,7 +198,7 @@ type PluginContext struct { DataSourceInstanceSettings *DataSourceInstanceSettings // GrafanaConfig is the configuration settings provided by Grafana. - GrafanaConfig *GrafanaCfg + GrafanaConfig *config.GrafanaCfg // UserAgent is the user agent of the Grafana server that initiated the gRPC request. // Will only be set if request is made from Grafana v10.2.0 or later. @@ -299,11 +300,11 @@ func SecureJSONDataFromHTTPClientOptions(opts httpclient.Options) (res map[strin func (s *DataSourceInstanceSettings) ProxyOptionsFromContext(ctx context.Context) (*proxy.Options, error) { cfg := GrafanaConfigFromContext(ctx) - p, err := cfg.proxy() + p, err := cfg.Proxy() if err != nil { return nil, err } - return s.ProxyOptions(p.clientCfg) + return s.ProxyOptions(p.ClientCfg) } func (s *DataSourceInstanceSettings) ProxyOptions(clientCfg *proxy.ClientCfg) (*proxy.Options, error) { @@ -360,11 +361,11 @@ func (s *DataSourceInstanceSettings) ProxyOptions(clientCfg *proxy.ClientCfg) (* func (s *DataSourceInstanceSettings) ProxyClient(ctx context.Context) (proxy.Client, error) { cfg := GrafanaConfigFromContext(ctx) - p, err := cfg.proxy() + p, err := cfg.Proxy() if err != nil { return nil, err } - proxyOpts, err := s.ProxyOptions(p.clientCfg) + proxyOpts, err := s.ProxyOptions(p.ClientCfg) if err != nil { return nil, err } diff --git a/backend/common_test.go b/backend/common_test.go index e624899e3..e15d6b4a7 100644 --- a/backend/common_test.go +++ b/backend/common_test.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -403,7 +404,7 @@ func TestProxyOptionsFromContext(t *testing.T) { tcs := []struct { name string instanceSettings *DataSourceInstanceSettings - grafanaCfg *GrafanaCfg + grafanaCfg *config.GrafanaCfg expectedClientOptions *proxy.Options err error }{ @@ -415,7 +416,7 @@ func TestProxyOptionsFromContext(t *testing.T) { JSONData: []byte("{ \"enableSecureSocksProxy\": true, \"timeout\": 10, \"keepAlive\": 15, \"secureSocksProxyUsername\": \"user\" }"), DecryptedSecureJSONData: map[string]string{"secureSocksProxyPassword": "pass"}, }, - grafanaCfg: NewGrafanaCfg( + grafanaCfg: config.NewGrafanaCfg( map[string]string{ proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyClientCert: "/path/to/client-cert", @@ -455,7 +456,7 @@ func TestProxyOptionsFromContext(t *testing.T) { }, }, { - name: "Datasource UID becomes user name when secureSocksProxyUsername is not set", + name: "Proxy options are configured when enableSecureSocksProxy is true and no username/password set", instanceSettings: &DataSourceInstanceSettings{ Name: "ds-name", UID: "ds-uid", @@ -463,7 +464,7 @@ func TestProxyOptionsFromContext(t *testing.T) { JSONData: []byte("{ \"enableSecureSocksProxy\": true, \"timeout\": 10, \"keepAlive\": 15 }"), DecryptedSecureJSONData: map[string]string{"secureSocksProxyPassword": "pass"}, }, - grafanaCfg: NewGrafanaCfg( + grafanaCfg: config.NewGrafanaCfg( map[string]string{ proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyClientCert: "/path/to/client-cert", @@ -510,7 +511,7 @@ func TestProxyOptionsFromContext(t *testing.T) { Type: "example-datasource", JSONData: []byte("{ \"enableSecureSocksProxy\": false }"), }, - grafanaCfg: NewGrafanaCfg( + grafanaCfg: config.NewGrafanaCfg( map[string]string{ proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyClientCert: "/path/to/client-cert", @@ -534,7 +535,7 @@ func TestProxyOptionsFromContext(t *testing.T) { Type: "example-datasource", JSONData: []byte("{ \"enableSecureSocksProxy\": true }"), }, - grafanaCfg: NewGrafanaCfg( + grafanaCfg: config.NewGrafanaCfg( map[string]string{ proxy.PluginSecureSocksProxyEnabled: "false", }, @@ -556,7 +557,7 @@ func TestProxyOptionsFromContext(t *testing.T) { } for _, tc := range tcs { - ctx := WithGrafanaConfig(context.Background(), tc.grafanaCfg) + ctx := config.WithGrafanaConfig(context.Background(), tc.grafanaCfg) opts, err := tc.instanceSettings.ProxyOptionsFromContext(ctx) if tc.err != nil { require.ErrorIs(t, err, tc.err) diff --git a/backend/config.go b/backend/config.go index d2f660179..a4001f30c 100644 --- a/backend/config.go +++ b/backend/config.go @@ -2,34 +2,11 @@ package backend import ( "context" - "encoding/base64" - "encoding/pem" - "errors" - "fmt" - "os" - "strconv" - "strings" - "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" "github.com/grafana/grafana-plugin-sdk-go/config" - "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" ) -const ( - AppURL = "GF_APP_URL" - ConcurrentQueryCount = "GF_CONCURRENT_QUERY_COUNT" - UserFacingDefaultError = "GF_USER_FACING_DEFAULT_ERROR" - SQLRowLimit = "GF_SQL_ROW_LIMIT" - SQLMaxOpenConnsDefault = "GF_SQL_MAX_OPEN_CONNS_DEFAULT" - SQLMaxIdleConnsDefault = "GF_SQL_MAX_IDLE_CONNS_DEFAULT" - SQLMaxConnLifetimeSecondsDefault = "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT" - ResponseLimit = "GF_RESPONSE_LIMIT" - AppClientSecret = "GF_PLUGIN_APP_CLIENT_SECRET" // nolint:gosec -) - -type configKey struct{} - // GrafanaConfigFromContext returns Grafana config from context. func GrafanaConfigFromContext(ctx context.Context) *config.GrafanaCfg { return config.GrafanaConfigFromContext(ctx) @@ -40,253 +17,6 @@ func WithGrafanaConfig(ctx context.Context, cfg *config.GrafanaCfg) context.Cont return config.WithGrafanaConfig(ctx, cfg) } -type GrafanaCfg struct { - config map[string]string -} - -func NewGrafanaCfg(cfg map[string]string) *GrafanaCfg { - return &GrafanaCfg{config: cfg} -} - -func (c *GrafanaCfg) Get(key string) string { - return c.config[key] -} - -func (c *GrafanaCfg) FeatureToggles() FeatureToggles { - features, exists := c.config[featuretoggles.EnabledFeatures] - if !exists || features == "" { - return FeatureToggles{} - } - - fs := strings.Split(features, ",") - enabledFeatures := make(map[string]struct{}, len(fs)) - for _, f := range fs { - enabledFeatures[f] = struct{}{} - } - - return FeatureToggles{ - enabled: enabledFeatures, - } -} - -func (c *GrafanaCfg) Equal(c2 *GrafanaCfg) bool { - if c == nil && c2 == nil { - return true - } - if c == nil || c2 == nil { - return false - } - - if len(c.config) != len(c2.config) { - return false - } - for k, v1 := range c.config { - if v2, ok := c2.config[k]; !ok || v1 != v2 { - return false - } - } - return true -} - -// ProxyHash returns the last four characters of the base64-encoded -// PDC client key contents, if present, for use in datasource instance -// caching. The contents should be PEM-encoded, so we try to PEM-decode -// them, and, if successful, return the base-64 encoding of the final three bytes, -// giving a four character hash. -func (c *GrafanaCfg) ProxyHash() string { - if c == nil { - return "" - } - contents := c.config[proxy.PluginSecureSocksProxyClientKeyContents] - if contents == "" { - return "" - } - block, _ := pem.Decode([]byte(contents)) - if block == nil { - Logger.Warn("ProxyHash(): key contents are not PEM-encoded") - return "" - } - if block.Type != "PRIVATE KEY" { - Logger.Warn("ProxyHash(): key contents are not PEM-encoded private key") - return "" - } - bl := len(block.Bytes) - if bl < 3 { - Logger.Warn("ProxyHash(): key contents too short") - return "" - } - return base64.StdEncoding.EncodeToString(block.Bytes[bl-3:]) -} - -type FeatureToggles struct { - // enabled is a set-like map of feature flags that are enabled. - enabled map[string]struct{} -} - -// IsEnabled returns true if feature f is contained in ft.enabled. -func (ft FeatureToggles) IsEnabled(f string) bool { - _, exists := ft.enabled[f] - return exists -} - -type Proxy struct { - clientCfg *proxy.ClientCfg -} - -func (c *GrafanaCfg) proxy() (Proxy, error) { - if v, exists := c.config[proxy.PluginSecureSocksProxyEnabled]; exists && v == strconv.FormatBool(true) { - var ( - allowInsecure = false - err error - ) - - if v := c.Get(proxy.PluginSecureSocksProxyAllowInsecure); v != "" { - allowInsecure, err = strconv.ParseBool(c.Get(proxy.PluginSecureSocksProxyAllowInsecure)) - if err != nil { - return Proxy{}, fmt.Errorf("parsing %s, value must be a boolean: %w", proxy.PluginSecureSocksProxyAllowInsecure, err) - } - } - - var rootCaVals []string - if v = c.Get(proxy.PluginSecureSocksProxyRootCAsContents); v != "" { - rootCaVals = strings.Split(c.Get(proxy.PluginSecureSocksProxyRootCAsContents), ",") - } - - return Proxy{ - clientCfg: &proxy.ClientCfg{ - ClientCert: c.Get(proxy.PluginSecureSocksProxyClientCert), - ClientCertVal: c.Get(proxy.PluginSecureSocksProxyClientCertContents), - ClientKey: c.Get(proxy.PluginSecureSocksProxyClientKey), - ClientKeyVal: c.Get(proxy.PluginSecureSocksProxyClientKeyContents), - RootCAs: strings.Split(c.Get(proxy.PluginSecureSocksProxyRootCAs), " "), - RootCAsVals: rootCaVals, - ProxyAddress: c.Get(proxy.PluginSecureSocksProxyProxyAddress), - ServerName: c.Get(proxy.PluginSecureSocksProxyServerName), - AllowInsecure: allowInsecure, - }, - }, nil - } - - return Proxy{}, nil -} - -func (c *GrafanaCfg) AppURL() (string, error) { - url, ok := c.config[AppURL] - if !ok { - // Fallback to environment variable for backwards compatibility - url = os.Getenv(AppURL) - if url == "" { - return "", errors.New("app URL not set in config. A more recent version of Grafana may be required") - } - } - return url, nil -} - -func (c *GrafanaCfg) ConcurrentQueryCount() (int, error) { - count, ok := c.config[ConcurrentQueryCount] - if !ok { - return 0, fmt.Errorf("ConcurrentQueryCount not set in config") - } - i, err := strconv.Atoi(count) - if err != nil { - return 0, fmt.Errorf("ConcurrentQueryCount cannot be converted to integer") - } - return i, nil -} - -type SQLConfig struct { - RowLimit int64 - DefaultMaxOpenConns int - DefaultMaxIdleConns int - DefaultMaxConnLifetimeSeconds int -} - -func (c *GrafanaCfg) SQL() (SQLConfig, error) { - // max open connections - maxOpenString, ok := c.config[SQLMaxOpenConnsDefault] - if !ok { - return SQLConfig{}, errors.New("SQLDatasourceMaxOpenConnsDefault not set in config") - } - - maxOpen, err := strconv.Atoi(maxOpenString) - if err != nil { - return SQLConfig{}, errors.New("SQLDatasourceMaxOpenConnsDefault config value is not a valid integer") - } - - // max idle connections - maxIdleString, ok := c.config[SQLMaxIdleConnsDefault] - if !ok { - return SQLConfig{}, errors.New("SQLDatasourceMaxIdleConnsDefault not set in config") - } - - maxIdle, err := strconv.Atoi(maxIdleString) - if err != nil { - return SQLConfig{}, errors.New("SQLDatasourceMaxIdleConnsDefault config value is not a valid integer") - } - - // max connection lifetime - maxLifeString, ok := c.config[SQLMaxConnLifetimeSecondsDefault] - if !ok { - return SQLConfig{}, errors.New("SQLDatasourceMaxConnLifetimeDefault not set in config") - } - - maxLife, err := strconv.Atoi(maxLifeString) - if err != nil { - return SQLConfig{}, errors.New("SQLDatasourceMaxConnLifetimeDefault config value is not a valid integer") - } - - rowLimitString, ok := c.config[SQLRowLimit] - if !ok { - return SQLConfig{}, errors.New("RowLimit not set in config") - } - - rowLimit, err := strconv.ParseInt(rowLimitString, 10, 64) - if err != nil { - return SQLConfig{}, errors.New("RowLimit in config is not a valid integer") - } - - return SQLConfig{ - RowLimit: rowLimit, - DefaultMaxOpenConns: maxOpen, - DefaultMaxIdleConns: maxIdle, - DefaultMaxConnLifetimeSeconds: maxLife, - }, nil -} - -func (c *GrafanaCfg) UserFacingDefaultError() (string, error) { - value, ok := c.config[UserFacingDefaultError] - if !ok { - return "", errors.New("UserFacingDefaultError not set in config") - } - - return value, nil -} - -func (c *GrafanaCfg) ResponseLimit() int64 { - count, ok := c.config[ResponseLimit] - if !ok { - return 0 - } - i, err := strconv.ParseInt(count, 10, 64) - if err != nil { - return 0 - } - return i -} - -func (c *GrafanaCfg) PluginAppClientSecret() (string, error) { - value, ok := c.config[AppClientSecret] - if !ok { - // Fallback to environment variable for backwards compatibility - value = os.Getenv(AppClientSecret) - if value == "" { - return "", errors.New("PluginAppClientSecret not set in config") - } - } - - return value, nil -} - type userAgentKey struct{} // UserAgentFromContext returns user agent from context. diff --git a/backend/config_test.go b/backend/config_test.go index f3cc7d101..c581fdbf7 100644 --- a/backend/config_test.go +++ b/backend/config_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" ) @@ -21,37 +22,37 @@ func TestConfig(t *testing.T) { t.Run("GrafanaConfigFromContext", func(t *testing.T) { tcs := []struct { name string - cfg *GrafanaCfg - expectedFeatureToggles FeatureToggles - expectedProxy Proxy + cfg *config.GrafanaCfg + expectedFeatureToggles config.FeatureToggles + expectedProxy config.Proxy }{ { name: "nil config", cfg: nil, - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{}, + expectedFeatureToggles: config.NewGrafanaCfg(nil).FeatureToggles(), + expectedProxy: config.Proxy{}, }, { name: "empty config", - cfg: &GrafanaCfg{}, - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{}, + cfg: &config.GrafanaCfg{}, + expectedFeatureToggles: config.NewGrafanaCfg(nil).FeatureToggles(), + expectedProxy: config.Proxy{}, }, { name: "nil config map", - cfg: NewGrafanaCfg(nil), - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{}, + cfg: config.NewGrafanaCfg(nil), + expectedFeatureToggles: config.NewGrafanaCfg(nil).FeatureToggles(), + expectedProxy: config.Proxy{}, }, { name: "empty config map", - cfg: NewGrafanaCfg(make(map[string]string)), - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{}, + cfg: config.NewGrafanaCfg(make(map[string]string)), + expectedFeatureToggles: config.NewGrafanaCfg(nil).FeatureToggles(), + expectedProxy: config.Proxy{}, }, { name: "feature toggles and proxy enabled", - cfg: NewGrafanaCfg(map[string]string{ + cfg: config.NewGrafanaCfg(map[string]string{ featuretoggles.EnabledFeatures: "TestFeature", proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyProxyAddress: "localhost:1234", @@ -60,13 +61,11 @@ func TestConfig(t *testing.T) { proxy.PluginSecureSocksProxyClientCert: "clientCert", proxy.PluginSecureSocksProxyRootCAs: "rootCACert", }), - expectedFeatureToggles: FeatureToggles{ - enabled: map[string]struct{}{ - "TestFeature": {}, - }, - }, - expectedProxy: Proxy{ - clientCfg: &proxy.ClientCfg{ + expectedFeatureToggles: config.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "TestFeature", + }).FeatureToggles(), + expectedProxy: config.Proxy{ + ClientCfg: &proxy.ClientCfg{ ClientCert: "clientCert", ClientKey: "clientKey", RootCAs: []string{"rootCACert"}, @@ -77,7 +76,7 @@ func TestConfig(t *testing.T) { }, { name: "feature toggles enabled and proxy disabled", - cfg: NewGrafanaCfg(map[string]string{ + cfg: config.NewGrafanaCfg(map[string]string{ featuretoggles.EnabledFeatures: "TestFeature", proxy.PluginSecureSocksProxyEnabled: "false", proxy.PluginSecureSocksProxyProxyAddress: "localhost:1234", @@ -86,16 +85,14 @@ func TestConfig(t *testing.T) { proxy.PluginSecureSocksProxyClientCert: "clientCert", proxy.PluginSecureSocksProxyRootCAs: "rootCACert", }), - expectedFeatureToggles: FeatureToggles{ - enabled: map[string]struct{}{ - "TestFeature": {}, - }, - }, - expectedProxy: Proxy{}, + expectedFeatureToggles: config.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "TestFeature", + }).FeatureToggles(), + expectedProxy: config.Proxy{}, }, { name: "feature toggles disabled and proxy enabled", - cfg: NewGrafanaCfg(map[string]string{ + cfg: config.NewGrafanaCfg(map[string]string{ featuretoggles.EnabledFeatures: "", proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyProxyAddress: "localhost:1234", @@ -104,9 +101,11 @@ func TestConfig(t *testing.T) { proxy.PluginSecureSocksProxyClientCert: "clientCert", proxy.PluginSecureSocksProxyRootCAs: "rootCACert", }), - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{ - clientCfg: &proxy.ClientCfg{ + expectedFeatureToggles: config.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "", + }).FeatureToggles(), + expectedProxy: config.Proxy{ + ClientCfg: &proxy.ClientCfg{ ClientCert: "clientCert", ClientKey: "clientKey", RootCAs: []string{"rootCACert"}, @@ -117,7 +116,7 @@ func TestConfig(t *testing.T) { }, { name: "feature toggles disabled and insecure proxy enabled", - cfg: NewGrafanaCfg(map[string]string{ + cfg: config.NewGrafanaCfg(map[string]string{ featuretoggles.EnabledFeatures: "", proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyProxyAddress: "localhost:1234", @@ -127,9 +126,11 @@ func TestConfig(t *testing.T) { proxy.PluginSecureSocksProxyRootCAs: "rootCACert", proxy.PluginSecureSocksProxyAllowInsecure: "true", }), - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{ - clientCfg: &proxy.ClientCfg{ + expectedFeatureToggles: config.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "", + }).FeatureToggles(), + expectedProxy: config.Proxy{ + ClientCfg: &proxy.ClientCfg{ ClientCert: "clientCert", ClientKey: "clientKey", RootCAs: []string{"rootCACert"}, @@ -141,7 +142,7 @@ func TestConfig(t *testing.T) { }, { name: "feature toggles disabled and secure proxy enabled with file contents", - cfg: NewGrafanaCfg(map[string]string{ + cfg: config.NewGrafanaCfg(map[string]string{ featuretoggles.EnabledFeatures: "", proxy.PluginSecureSocksProxyEnabled: "true", proxy.PluginSecureSocksProxyProxyAddress: "localhost:1234", @@ -154,9 +155,11 @@ func TestConfig(t *testing.T) { proxy.PluginSecureSocksProxyRootCAsContents: "rootCACert,rootCACert2", proxy.PluginSecureSocksProxyAllowInsecure: "true", }), - expectedFeatureToggles: FeatureToggles{}, - expectedProxy: Proxy{ - clientCfg: &proxy.ClientCfg{ + expectedFeatureToggles: config.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "", + }).FeatureToggles(), + expectedProxy: config.Proxy{ + ClientCfg: &proxy.ClientCfg{ ClientCert: "./clientCert", ClientCertVal: "clientCert", ClientKey: "./clientKey", @@ -172,11 +175,11 @@ func TestConfig(t *testing.T) { } for _, tc := range tcs { - ctx := WithGrafanaConfig(context.Background(), tc.cfg) - cfg := GrafanaConfigFromContext(ctx) + ctx := config.WithGrafanaConfig(context.Background(), tc.cfg) + cfg := config.GrafanaConfigFromContext(ctx) require.Equal(t, tc.expectedFeatureToggles, cfg.FeatureToggles()) - proxy, err := cfg.proxy() + proxy, err := cfg.Proxy() assert.NoError(t, err) require.Equal(t, tc.expectedProxy, proxy) @@ -186,8 +189,8 @@ func TestConfig(t *testing.T) { func TestAppURL(t *testing.T) { t.Run("it should return the configured app URL", func(t *testing.T) { - cfg := NewGrafanaCfg(map[string]string{ - AppURL: "http://localhost:3000", + cfg := config.NewGrafanaCfg(map[string]string{ + config.AppURL: "http://localhost:3000", }) url, err := cfg.AppURL() require.NoError(t, err) @@ -195,15 +198,15 @@ func TestAppURL(t *testing.T) { }) t.Run("it should return an error if the app URL is missing", func(t *testing.T) { - cfg := NewGrafanaCfg(map[string]string{}) + cfg := config.NewGrafanaCfg(map[string]string{}) _, err := cfg.AppURL() require.Error(t, err) }) t.Run("it should return the configured app URL from env", func(t *testing.T) { - os.Setenv(AppURL, "http://localhost-env:3000") - defer os.Unsetenv(AppURL) - cfg := NewGrafanaCfg(map[string]string{}) + os.Setenv(config.AppURL, "http://localhost-env:3000") + defer os.Unsetenv(config.AppURL) + cfg := config.NewGrafanaCfg(map[string]string{}) v, err := cfg.AppURL() require.NoError(t, err) require.Equal(t, "http://localhost-env:3000", v) @@ -231,8 +234,8 @@ func TestUserAgentFromContext_NoUserAgent(t *testing.T) { func TestUserFacingDefaultError(t *testing.T) { t.Run("it should return the configured default error message", func(t *testing.T) { - cfg := NewGrafanaCfg(map[string]string{ - UserFacingDefaultError: "something failed", + cfg := config.NewGrafanaCfg(map[string]string{ + config.UserFacingDefaultError: "something failed", }) v, err := cfg.UserFacingDefaultError() require.NoError(t, err) @@ -240,7 +243,7 @@ func TestUserFacingDefaultError(t *testing.T) { }) t.Run("it should return an error if the default error message is missing", func(t *testing.T) { - cfg := NewGrafanaCfg(map[string]string{}) + cfg := config.NewGrafanaCfg(map[string]string{}) _, err := cfg.UserFacingDefaultError() require.Error(t, err) }) @@ -248,11 +251,11 @@ func TestUserFacingDefaultError(t *testing.T) { func TestSql(t *testing.T) { t.Run("it should return the configured sql default values", func(t *testing.T) { - cfg := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "22", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "22", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) v, err := cfg.SQL() require.NoError(t, err) @@ -263,28 +266,28 @@ func TestSql(t *testing.T) { }) t.Run("it should return an error if any of the defaults is missing", func(t *testing.T) { - cfg1 := NewGrafanaCfg(map[string]string{ - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "22", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg1 := config.NewGrafanaCfg(map[string]string{ + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "22", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) - cfg2 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxIdleConnsDefault: "22", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg2 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxIdleConnsDefault: "22", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) - cfg3 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxOpenConnsDefault: "11", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg3 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) - cfg4 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "22", + cfg4 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "22", }) _, err := cfg1.SQL() @@ -301,32 +304,32 @@ func TestSql(t *testing.T) { }) t.Run("it should return an error if any of the defaults is not an integer", func(t *testing.T) { - cfg1 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "not-an-integer", - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "22", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg1 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "not-an-integer", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "22", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) - cfg2 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "not-an-integer", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg2 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "not-an-integer", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) - cfg3 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "not-an-integer", - SQLMaxConnLifetimeSecondsDefault: "33", + cfg3 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "not-an-integer", + config.SQLMaxConnLifetimeSecondsDefault: "33", }) - cfg4 := NewGrafanaCfg(map[string]string{ - SQLRowLimit: "5", - SQLMaxOpenConnsDefault: "11", - SQLMaxIdleConnsDefault: "22", - SQLMaxConnLifetimeSecondsDefault: "not-an-integer", + cfg4 := config.NewGrafanaCfg(map[string]string{ + config.SQLRowLimit: "5", + config.SQLMaxOpenConnsDefault: "11", + config.SQLMaxIdleConnsDefault: "22", + config.SQLMaxConnLifetimeSecondsDefault: "not-an-integer", }) _, err := cfg1.SQL() @@ -345,8 +348,8 @@ func TestSql(t *testing.T) { func TestPluginAppClientSecret(t *testing.T) { t.Run("it should return the configured PluginAppClientSecret", func(t *testing.T) { - cfg := NewGrafanaCfg(map[string]string{ - AppClientSecret: "client-secret", + cfg := config.NewGrafanaCfg(map[string]string{ + config.AppClientSecret: "client-secret", }) v, err := cfg.PluginAppClientSecret() require.NoError(t, err) @@ -354,9 +357,9 @@ func TestPluginAppClientSecret(t *testing.T) { }) t.Run("it should return the configured PluginAppClientSecret from env", func(t *testing.T) { - os.Setenv(AppClientSecret, "client-secret") - defer os.Unsetenv(AppClientSecret) - cfg := NewGrafanaCfg(map[string]string{}) + os.Setenv(config.AppClientSecret, "client-secret") + defer os.Unsetenv(config.AppClientSecret) + cfg := config.NewGrafanaCfg(map[string]string{}) v, err := cfg.PluginAppClientSecret() require.NoError(t, err) require.Equal(t, "client-secret", v) @@ -384,7 +387,7 @@ func BenchmarkProxyHash(b *testing.B) { for i := 0; i < b.N; i++ { kBytes[88] = b64chars[mathrand.Intn(64)] //nolint:gosec cm[proxy.PluginSecureSocksProxyClientKeyContents] = string(kBytes) - cfg := NewGrafanaCfg(cm) + cfg := config.NewGrafanaCfg(cm) hash := cfg.ProxyHash() if hash[0] == 'a' { count++ diff --git a/backend/convert_from_protobuf.go b/backend/convert_from_protobuf.go index d18c9e38e..8ca3369d9 100644 --- a/backend/convert_from_protobuf.go +++ b/backend/convert_from_protobuf.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" ) @@ -406,6 +407,6 @@ func (f ConvertFromProtobuf) ConversionResponse(rsp *pluginv2.ConversionResponse } } -func (f ConvertFromProtobuf) GrafanaConfig(cfg map[string]string) *GrafanaCfg { - return NewGrafanaCfg(cfg) +func (f ConvertFromProtobuf) GrafanaConfig(cfg map[string]string) *config.GrafanaCfg { + return config.NewGrafanaCfg(cfg) } diff --git a/backend/convert_from_protobuf_test.go b/backend/convert_from_protobuf_test.go index e28432770..42d0c977b 100644 --- a/backend/convert_from_protobuf_test.go +++ b/backend/convert_from_protobuf_test.go @@ -275,7 +275,7 @@ func TestConvertFromProtobufPluginContext(t *testing.T) { requireCounter.Equal(t, time.Unix(0, 86400*2*1e9), sdkCtx.DataSourceInstanceSettings.Updated) requireCounter.Equal(t, protoCtx.UserAgent, sdkCtx.UserAgent.String()) - requireCounter.Equal(t, protoCtx.GrafanaConfig, sdkCtx.GrafanaConfig.config) + requireCounter.Equal(t, protoCtx.GrafanaConfig, sdkCtx.GrafanaConfig.Config()) require.Equal(t, requireCounter.Count, sdkWalker.FieldCount-3, "untested fields in conversion") // -3 Struct Fields } diff --git a/backend/convert_to_protobuf.go b/backend/convert_to_protobuf.go index e0d315b3c..d76d8aa62 100644 --- a/backend/convert_to_protobuf.go +++ b/backend/convert_to_protobuf.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2" "google.golang.org/protobuf/proto" ) @@ -437,9 +438,9 @@ func (t ConvertToProtobuf) ConversionResponse(rsp *ConversionResponse) *pluginv2 } // GrafanaConfig converts the SDK version of a GrafanaCfg to the protobuf version. -func (t ConvertToProtobuf) GrafanaConfig(cfg *GrafanaCfg) map[string]string { +func (t ConvertToProtobuf) GrafanaConfig(cfg *config.GrafanaCfg) map[string]string { if cfg == nil { return map[string]string{} } - return cfg.config + return cfg.Config() } diff --git a/backend/convert_to_protobuf_test.go b/backend/convert_to_protobuf_test.go index c72a3167b..03fc0637e 100644 --- a/backend/convert_to_protobuf_test.go +++ b/backend/convert_to_protobuf_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/mitchellh/reflectwalk" "github.com/stretchr/testify/require" @@ -180,7 +181,7 @@ var testPluginContext = PluginContext{ Updated: time.Unix(2, 0), APIVersion: "v1", }, - GrafanaConfig: &GrafanaCfg{config: map[string]string{"key": "value"}}, + GrafanaConfig: config.NewGrafanaCfg(map[string]string{"key": "value"}), UserAgent: testUserAgent, APIVersion: "v1", } diff --git a/backend/datasource/instance_provider_test.go b/backend/datasource/instance_provider_test.go index 744f0679f..e39399491 100644 --- a/backend/datasource/instance_provider_test.go +++ b/backend/datasource/instance_provider_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" + "github.com/grafana/grafana-plugin-sdk-go/config" "github.com/stretchr/testify/require" ) @@ -49,7 +50,7 @@ func TestInstanceProvider(t *testing.T) { want := base64.StdEncoding.EncodeToString([]byte("this will work.")) want = strings.TrimRight(want, "=") want = want[len(want)-4:] - cfg := backend.NewGrafanaCfg(map[string]string{ + cfg := config.NewGrafanaCfg(map[string]string{ proxy.PluginSecureSocksProxyClientKeyContents: string(contents), }) key, err := ip.GetKey(context.Background(), backend.PluginContext{ @@ -61,7 +62,7 @@ func TestInstanceProvider(t *testing.T) { }) t.Run("When PDC is configured but the key is empty, no problem", func(t *testing.T) { - cfg := backend.NewGrafanaCfg(map[string]string{ + cfg := config.NewGrafanaCfg(map[string]string{ proxy.PluginSecureSocksProxyClientKeyContents: "", }) key, err := ip.GetKey(context.Background(), backend.PluginContext{ @@ -73,7 +74,7 @@ func TestInstanceProvider(t *testing.T) { }) t.Run("When PDC is configured but the key is not PEM-encoded, no problem", func(t *testing.T) { - cfg := backend.NewGrafanaCfg(map[string]string{ + cfg := config.NewGrafanaCfg(map[string]string{ proxy.PluginSecureSocksProxyClientKeyContents: "this is not\na valid string\n", }) key, err := ip.GetKey(context.Background(), backend.PluginContext{ @@ -85,7 +86,7 @@ func TestInstanceProvider(t *testing.T) { }) t.Run("When both the configuration and updated field of current data source instance settings are equal to the cache, should return false", func(t *testing.T) { - config := map[string]string{ + cfg := map[string]string{ "foo": "bar", "baz": "qux", } @@ -94,14 +95,14 @@ func TestInstanceProvider(t *testing.T) { DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ Updated: time.Now(), }, - GrafanaConfig: backend.NewGrafanaCfg(config), + GrafanaConfig: config.NewGrafanaCfg(cfg), } cachedSettings := backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ Updated: curSettings.DataSourceInstanceSettings.Updated, }, - GrafanaConfig: backend.NewGrafanaCfg(config), + GrafanaConfig: config.NewGrafanaCfg(cfg), } cachedInstance := instancemgmt.CachedInstance{ @@ -130,11 +131,11 @@ func TestInstanceProvider(t *testing.T) { require.True(t, needsUpdate) t.Run("Should return true when cached config is changed", func(t *testing.T) { - curSettings.GrafanaConfig = backend.NewGrafanaCfg(map[string]string{ + curSettings.GrafanaConfig = config.NewGrafanaCfg(map[string]string{ "foo": "true", }) - cachedSettings.GrafanaConfig = backend.NewGrafanaCfg(map[string]string{ + cachedSettings.GrafanaConfig = config.NewGrafanaCfg(map[string]string{ "foo": "false", }) @@ -207,7 +208,7 @@ func Test_instanceProvider_NeedsUpdate(t *testing.T) { DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ Updated: ts, }, - GrafanaConfig: backend.NewGrafanaCfg(map[string]string{ + GrafanaConfig: config.NewGrafanaCfg(map[string]string{ "foo": "bar", "baz": "qux", }), @@ -217,7 +218,7 @@ func Test_instanceProvider_NeedsUpdate(t *testing.T) { DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ Updated: ts, }, - GrafanaConfig: backend.NewGrafanaCfg(map[string]string{ + GrafanaConfig: config.NewGrafanaCfg(map[string]string{ "baz": "qux", "foo": "bar", }), @@ -251,7 +252,7 @@ func Test_instanceProvider_NeedsUpdate(t *testing.T) { DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ Updated: ts, }, - GrafanaConfig: backend.NewGrafanaCfg(map[string]string{ + GrafanaConfig: config.NewGrafanaCfg(map[string]string{ "foo": "bar", }), }, diff --git a/backend/httpclient/response_limit_middleware_test.go b/backend/httpclient/response_limit_middleware_test.go index 56a0dd60a..68f434780 100644 --- a/backend/httpclient/response_limit_middleware_test.go +++ b/backend/httpclient/response_limit_middleware_test.go @@ -32,6 +32,7 @@ func TestResponseLimitMiddleware(t *testing.T) { require.Error(t, err) require.Equal(t, "error: http: response body too large, response limit is set to: 1", err.Error()) require.Equal(t, "d", string(body)) + require.NoError(t, res.Body.Close()) }) t.Run("should use context limit when set", func(t *testing.T) { @@ -61,6 +62,7 @@ func TestResponseLimitMiddleware(t *testing.T) { require.Error(t, err) require.Equal(t, "error: http: response body too large, response limit is set to: 1", err.Error()) require.Equal(t, "d", string(body)) + require.NoError(t, res.Body.Close()) }) t.Run("should not limit response when limit is 0", func(t *testing.T) { @@ -83,6 +85,7 @@ func TestResponseLimitMiddleware(t *testing.T) { body, err := io.ReadAll(res.Body) require.NoError(t, err) require.Equal(t, "dummy", string(body)) + require.NoError(t, res.Body.Close()) }) t.Run("should not limit response when status is switching protocols", func(t *testing.T) { @@ -106,6 +109,7 @@ func TestResponseLimitMiddleware(t *testing.T) { body, err := io.ReadAll(res.Body) require.NoError(t, err) require.Equal(t, "dummy", string(body)) + require.NoError(t, res.Body.Close()) }) } diff --git a/config/grafana_cfg.go b/config/grafana_cfg.go index 26acd6c75..38b330970 100644 --- a/config/grafana_cfg.go +++ b/config/grafana_cfg.go @@ -2,11 +2,29 @@ package config import ( "context" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "os" "strconv" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/backend/proxy" + "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" ) const ( - ResponseLimit = "GF_RESPONSE_LIMIT" + AppURL = "GF_APP_URL" + ConcurrentQueryCount = "GF_CONCURRENT_QUERY_COUNT" + UserFacingDefaultError = "GF_USER_FACING_DEFAULT_ERROR" + SQLRowLimit = "GF_SQL_ROW_LIMIT" + SQLMaxOpenConnsDefault = "GF_SQL_MAX_OPEN_CONNS_DEFAULT" + SQLMaxIdleConnsDefault = "GF_SQL_MAX_IDLE_CONNS_DEFAULT" + SQLMaxConnLifetimeSecondsDefault = "GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT" + ResponseLimit = "GF_RESPONSE_LIMIT" + AppClientSecret = "GF_PLUGIN_APP_CLIENT_SECRET" // nolint:gosec ) // GrafanaCfg represents Grafana configuration @@ -14,45 +32,275 @@ type GrafanaCfg struct { config map[string]string } +type configKey struct{} + +// GrafanaConfigFromContext returns Grafana config from context. +func GrafanaConfigFromContext(ctx context.Context) *GrafanaCfg { + v := ctx.Value(configKey{}) + if v == nil { + return NewGrafanaCfg(nil) + } + + cfg := v.(*GrafanaCfg) + if cfg == nil { + return NewGrafanaCfg(nil) + } + + return cfg +} + +// WithGrafanaConfig injects supplied Grafana config into context. +func WithGrafanaConfig(ctx context.Context, cfg *GrafanaCfg) context.Context { + ctx = context.WithValue(ctx, configKey{}, cfg) + return ctx +} + // NewGrafanaCfg creates a new GrafanaCfg instance func NewGrafanaCfg(cfg map[string]string) *GrafanaCfg { return &GrafanaCfg{config: cfg} } +// Config returns the config map +func (c *GrafanaCfg) Config() map[string]string { + return c.config +} + // Get returns a value from the config map func (c *GrafanaCfg) Get(key string) string { return c.config[key] } -// ResponseLimit returns the response limit value -func (c *GrafanaCfg) ResponseLimit() int64 { - if v, exists := c.config[ResponseLimit]; exists { - if i, err := strconv.ParseInt(v, 10, 64); err == nil { - return i +func (c *GrafanaCfg) FeatureToggles() FeatureToggles { + features, exists := c.config[featuretoggles.EnabledFeatures] + if !exists || features == "" { + return FeatureToggles{} + } + + fs := strings.Split(features, ",") + enabledFeatures := make(map[string]struct{}, len(fs)) + for _, f := range fs { + enabledFeatures[f] = struct{}{} + } + + return FeatureToggles{ + enabled: enabledFeatures, + } +} + +func (c *GrafanaCfg) Equal(c2 *GrafanaCfg) bool { + if c == nil && c2 == nil { + return true + } + if c == nil || c2 == nil { + return false + } + + if len(c.config) != len(c2.config) { + return false + } + for k, v1 := range c.config { + if v2, ok := c2.config[k]; !ok || v1 != v2 { + return false } } - return 0 + return true } -type configKey struct{} +// ProxyHash returns the last four characters of the base64-encoded +// PDC client key contents, if present, for use in datasource instance +// caching. The contents should be PEM-encoded, so we try to PEM-decode +// them, and, if successful, return the base-64 encoding of the final three bytes, +// giving a four character hash. +func (c *GrafanaCfg) ProxyHash() string { + if c == nil { + return "" + } + contents := c.config[proxy.PluginSecureSocksProxyClientKeyContents] + if contents == "" { + return "" + } + block, _ := pem.Decode([]byte(contents)) + if block == nil { + log.DefaultLogger.Warn("ProxyHash(): key contents are not PEM-encoded") + return "" + } + if block.Type != "PRIVATE KEY" { + log.DefaultLogger.Warn("ProxyHash(): key contents are not PEM-encoded private key") + return "" + } + bl := len(block.Bytes) + if bl < 3 { + log.DefaultLogger.Warn("ProxyHash(): key contents too short") + return "" + } + return base64.StdEncoding.EncodeToString(block.Bytes[bl-3:]) +} -// GrafanaConfigFromContext returns Grafana config from context -func GrafanaConfigFromContext(ctx context.Context) *GrafanaCfg { - v := ctx.Value(configKey{}) - if v == nil { - return NewGrafanaCfg(nil) +type FeatureToggles struct { + // enabled is a set-like map of feature flags that are enabled. + enabled map[string]struct{} +} + +// IsEnabled returns true if feature f is contained in ft.enabled. +func (ft FeatureToggles) IsEnabled(f string) bool { + _, exists := ft.enabled[f] + return exists +} + +type Proxy struct { + ClientCfg *proxy.ClientCfg +} + +func (c *GrafanaCfg) Proxy() (Proxy, error) { + if v, exists := c.config[proxy.PluginSecureSocksProxyEnabled]; exists && v == strconv.FormatBool(true) { + var ( + allowInsecure = false + err error + ) + + if v := c.Get(proxy.PluginSecureSocksProxyAllowInsecure); v != "" { + allowInsecure, err = strconv.ParseBool(c.Get(proxy.PluginSecureSocksProxyAllowInsecure)) + if err != nil { + return Proxy{}, fmt.Errorf("parsing %s, value must be a boolean: %w", proxy.PluginSecureSocksProxyAllowInsecure, err) + } + } + + var rootCaVals []string + if v = c.Get(proxy.PluginSecureSocksProxyRootCAsContents); v != "" { + rootCaVals = strings.Split(c.Get(proxy.PluginSecureSocksProxyRootCAsContents), ",") + } + + return Proxy{ + ClientCfg: &proxy.ClientCfg{ + ClientCert: c.Get(proxy.PluginSecureSocksProxyClientCert), + ClientCertVal: c.Get(proxy.PluginSecureSocksProxyClientCertContents), + ClientKey: c.Get(proxy.PluginSecureSocksProxyClientKey), + ClientKeyVal: c.Get(proxy.PluginSecureSocksProxyClientKeyContents), + RootCAs: strings.Split(c.Get(proxy.PluginSecureSocksProxyRootCAs), " "), + RootCAsVals: rootCaVals, + ProxyAddress: c.Get(proxy.PluginSecureSocksProxyProxyAddress), + ServerName: c.Get(proxy.PluginSecureSocksProxyServerName), + AllowInsecure: allowInsecure, + }, + }, nil } - cfg := v.(*GrafanaCfg) - if cfg == nil { - return NewGrafanaCfg(nil) + return Proxy{}, nil +} + +func (c *GrafanaCfg) AppURL() (string, error) { + url, ok := c.config[AppURL] + if !ok { + // Fallback to environment variable for backwards compatibility + url = os.Getenv(AppURL) + if url == "" { + return "", errors.New("app URL not set in config. A more recent version of Grafana may be required") + } + } + return url, nil +} + +func (c *GrafanaCfg) ConcurrentQueryCount() (int, error) { + count, ok := c.config[ConcurrentQueryCount] + if !ok { + return 0, fmt.Errorf("ConcurrentQueryCount not set in config") } + i, err := strconv.Atoi(count) + if err != nil { + return 0, fmt.Errorf("ConcurrentQueryCount cannot be converted to integer") + } + return i, nil +} - return cfg +type SQLConfig struct { + RowLimit int64 + DefaultMaxOpenConns int + DefaultMaxIdleConns int + DefaultMaxConnLifetimeSeconds int } -// WithGrafanaConfig injects supplied Grafana config into context -func WithGrafanaConfig(ctx context.Context, cfg *GrafanaCfg) context.Context { - ctx = context.WithValue(ctx, configKey{}, cfg) - return ctx +func (c *GrafanaCfg) SQL() (SQLConfig, error) { + // max open connections + maxOpenString, ok := c.config[SQLMaxOpenConnsDefault] + if !ok { + return SQLConfig{}, errors.New("SQLDatasourceMaxOpenConnsDefault not set in config") + } + + maxOpen, err := strconv.Atoi(maxOpenString) + if err != nil { + return SQLConfig{}, errors.New("SQLDatasourceMaxOpenConnsDefault config value is not a valid integer") + } + + // max idle connections + maxIdleString, ok := c.config[SQLMaxIdleConnsDefault] + if !ok { + return SQLConfig{}, errors.New("SQLDatasourceMaxIdleConnsDefault not set in config") + } + + maxIdle, err := strconv.Atoi(maxIdleString) + if err != nil { + return SQLConfig{}, errors.New("SQLDatasourceMaxIdleConnsDefault config value is not a valid integer") + } + + // max connection lifetime + maxLifeString, ok := c.config[SQLMaxConnLifetimeSecondsDefault] + if !ok { + return SQLConfig{}, errors.New("SQLDatasourceMaxConnLifetimeDefault not set in config") + } + + maxLife, err := strconv.Atoi(maxLifeString) + if err != nil { + return SQLConfig{}, errors.New("SQLDatasourceMaxConnLifetimeDefault config value is not a valid integer") + } + + rowLimitString, ok := c.config[SQLRowLimit] + if !ok { + return SQLConfig{}, errors.New("RowLimit not set in config") + } + + rowLimit, err := strconv.ParseInt(rowLimitString, 10, 64) + if err != nil { + return SQLConfig{}, errors.New("RowLimit in config is not a valid integer") + } + + return SQLConfig{ + RowLimit: rowLimit, + DefaultMaxOpenConns: maxOpen, + DefaultMaxIdleConns: maxIdle, + DefaultMaxConnLifetimeSeconds: maxLife, + }, nil +} + +func (c *GrafanaCfg) UserFacingDefaultError() (string, error) { + value, ok := c.config[UserFacingDefaultError] + if !ok { + return "", errors.New("UserFacingDefaultError not set in config") + } + + return value, nil +} + +func (c *GrafanaCfg) ResponseLimit() int64 { + count, ok := c.config[ResponseLimit] + if !ok { + return 0 + } + i, err := strconv.ParseInt(count, 10, 64) + if err != nil { + return 0 + } + return i +} + +func (c *GrafanaCfg) PluginAppClientSecret() (string, error) { + value, ok := c.config[AppClientSecret] + if !ok { + // Fallback to environment variable for backwards compatibility + value = os.Getenv(AppClientSecret) + if value == "" { + return "", errors.New("PluginAppClientSecret not set in config") + } + } + + return value, nil } From a7f613991660903368ac5036cebd17ff4f3b53e6 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Mar 2025 09:37:25 +0000 Subject: [PATCH 3/7] lessen breaking changes --- backend/config.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/config.go b/backend/config.go index a4001f30c..fb07b711c 100644 --- a/backend/config.go +++ b/backend/config.go @@ -7,6 +7,22 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/config" ) +const ( + AppURL = config.AppURL + ConcurrentQueryCount = config.ConcurrentQueryCount + UserFacingDefaultError = config.UserFacingDefaultError + SQLRowLimit = config.SQLRowLimit + SQLMaxOpenConnsDefault = config.SQLMaxOpenConnsDefault + SQLMaxIdleConnsDefault = config.SQLMaxIdleConnsDefault + SQLMaxConnLifetimeSecondsDefault = config.SQLMaxConnLifetimeSecondsDefault + ResponseLimit = config.ResponseLimit + AppClientSecret = config.AppClientSecret +) + +func NewGrafanaConfig(m map[string]string) *config.GrafanaCfg { + return config.NewGrafanaCfg(m) +} + // GrafanaConfigFromContext returns Grafana config from context. func GrafanaConfigFromContext(ctx context.Context) *config.GrafanaCfg { return config.GrafanaConfigFromContext(ctx) From a3a821f2fce7954b657cddf4f1f4d67e3873399a Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Mar 2025 09:46:05 +0000 Subject: [PATCH 4/7] fix func name --- backend/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config.go b/backend/config.go index fb07b711c..4da2338db 100644 --- a/backend/config.go +++ b/backend/config.go @@ -19,7 +19,7 @@ const ( AppClientSecret = config.AppClientSecret ) -func NewGrafanaConfig(m map[string]string) *config.GrafanaCfg { +func NewGrafanaCfg(m map[string]string) *config.GrafanaCfg { return config.NewGrafanaCfg(m) } From a38a211a3bde5694763c91d86d07d9777542c3dd Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Mar 2025 09:59:34 +0000 Subject: [PATCH 5/7] add type aliases --- backend/config.go | 11 +++++++++-- config/grafana_cfg.go | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/config.go b/backend/config.go index 4da2338db..e2cd4b128 100644 --- a/backend/config.go +++ b/backend/config.go @@ -19,16 +19,23 @@ const ( AppClientSecret = config.AppClientSecret ) +// Deprecated: Use the config package instead. +type GrafanaCfg = config.GrafanaCfg + +// Deprecated: Use the config package instead. +type FeatureToggles = config.FeatureToggles + +// Deprecated: Use the config package instead. func NewGrafanaCfg(m map[string]string) *config.GrafanaCfg { return config.NewGrafanaCfg(m) } -// GrafanaConfigFromContext returns Grafana config from context. +// Deprecated: Use the config package instead. func GrafanaConfigFromContext(ctx context.Context) *config.GrafanaCfg { return config.GrafanaConfigFromContext(ctx) } -// WithGrafanaConfig injects supplied Grafana config into context. +// Deprecated: Use the config package instead. func WithGrafanaConfig(ctx context.Context, cfg *config.GrafanaCfg) context.Context { return config.WithGrafanaConfig(ctx, cfg) } diff --git a/config/grafana_cfg.go b/config/grafana_cfg.go index 38b330970..eb368ae40 100644 --- a/config/grafana_cfg.go +++ b/config/grafana_cfg.go @@ -136,6 +136,7 @@ func (c *GrafanaCfg) ProxyHash() string { return base64.StdEncoding.EncodeToString(block.Bytes[bl-3:]) } +// FeatureToggles represents Grafana feature toggles. type FeatureToggles struct { // enabled is a set-like map of feature flags that are enabled. enabled map[string]struct{} @@ -147,6 +148,7 @@ func (ft FeatureToggles) IsEnabled(f string) bool { return exists } +// Proxy represents Grafana PDC proxy configuration. type Proxy struct { ClientCfg *proxy.ClientCfg } From 23a170d05d0d8ddc6fd939c1e830ec5ec3c5f98c Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Mar 2025 10:15:09 +0000 Subject: [PATCH 6/7] make context limit check the fallback --- backend/common_test.go | 2 +- backend/httpclient/response_limit_middleware.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/common_test.go b/backend/common_test.go index e15d6b4a7..f72f8b155 100644 --- a/backend/common_test.go +++ b/backend/common_test.go @@ -456,7 +456,7 @@ func TestProxyOptionsFromContext(t *testing.T) { }, }, { - name: "Proxy options are configured when enableSecureSocksProxy is true and no username/password set", + name: "Datasource UID becomes user name when secureSocksProxyUsername is not set", instanceSettings: &DataSourceInstanceSettings{ Name: "ds-name", UID: "ds-uid", diff --git a/backend/httpclient/response_limit_middleware.go b/backend/httpclient/response_limit_middleware.go index d91b88507..d339a55be 100644 --- a/backend/httpclient/response_limit_middleware.go +++ b/backend/httpclient/response_limit_middleware.go @@ -17,13 +17,13 @@ func ResponseLimitMiddleware(limit int64) Middleware { return nil, err } - // Try to get limit from context first, fall back to static limit - if cfgLimit := config.GrafanaConfigFromContext(req.Context()).ResponseLimit(); cfgLimit > 0 { - limit = cfgLimit - } - if limit <= 0 { - return res, nil + // Check if limit is set in Grafana config + if cfgLimit := config.GrafanaConfigFromContext(req.Context()).ResponseLimit(); cfgLimit > 0 { + limit = cfgLimit + } else { + return res, nil + } } if res != nil && res.StatusCode != http.StatusSwitchingProtocols { From 41c1f76c53044ff7c4655afc5aca515f0cc916ef Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 31 Mar 2025 09:20:31 +0100 Subject: [PATCH 7/7] fix test --- backend/httpclient/response_limit_middleware_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/httpclient/response_limit_middleware_test.go b/backend/httpclient/response_limit_middleware_test.go index 68f434780..78adf6e5b 100644 --- a/backend/httpclient/response_limit_middleware_test.go +++ b/backend/httpclient/response_limit_middleware_test.go @@ -35,7 +35,7 @@ func TestResponseLimitMiddleware(t *testing.T) { require.NoError(t, res.Body.Close()) }) - t.Run("should use context limit when set", func(t *testing.T) { + t.Run("should prefer static even when context limit is set", func(t *testing.T) { next := &mockRoundTripper{ response: &http.Response{ Body: io.NopCloser(strings.NewReader("dummy")), @@ -59,9 +59,8 @@ func TestResponseLimitMiddleware(t *testing.T) { require.NotNil(t, res) body, err := io.ReadAll(res.Body) - require.Error(t, err) - require.Equal(t, "error: http: response body too large, response limit is set to: 1", err.Error()) - require.Equal(t, "d", string(body)) + require.NoError(t, err) + require.Equal(t, "dummy", string(body)) require.NoError(t, res.Body.Close()) })