Plugins: Refactor plugin config into separate env var and request scoped services (#83261)

* seperate services for env + req

* merge with main

* fix tests

* undo changes to golden file

* fix linter

* remove unused fields

* split out new config struct

* provide config

* undo go mod changes

* more renaming

* fix tests

* undo bra.toml changes

* update go.work.sum

* undo changes

* trigger

* apply PR feedback
This commit is contained in:
Will Browne
2024-02-27 12:38:02 +01:00
committed by GitHub
parent d04c579fc2
commit 5edd96ae77
62 changed files with 1933 additions and 1207 deletions
@@ -0,0 +1,144 @@
package pluginconfig
import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
// ProvidePluginManagementConfig returns a new config.PluginManagementCfg.
// It is used to provide configuration to Grafana's implementation of the plugin management system.
func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Provider, features featuremgmt.FeatureToggles) (*config.PluginManagementCfg, error) {
plugins := settingProvider.Section("plugins")
allowedUnsigned := cfg.PluginsAllowUnsigned
if len(plugins.KeyValue("allow_loading_unsigned_plugins").Value()) > 0 {
allowedUnsigned = strings.Split(plugins.KeyValue("allow_loading_unsigned_plugins").Value(), ",")
}
tracingCfg, err := newTracingCfg(cfg)
if err != nil {
return nil, fmt.Errorf("new opentelemetry cfg: %w", err)
}
return config.NewPluginManagementCfg(
settingProvider.KeyValue("", "app_mode").MustBool(cfg.Env == setting.Dev),
cfg.PluginsPath,
extractPluginSettings(settingProvider),
allowedUnsigned,
cfg.PluginsCDNURLTemplate,
cfg.AppURL,
cfg.AppSubURL,
tracingCfg,
features,
cfg.AngularSupportEnabled,
cfg.GrafanaComURL,
cfg.DisablePlugins,
cfg.HideAngularDeprecation,
cfg.ForwardHostEnvVars,
), nil
}
// PluginInstanceCfg contains the configuration for a plugin instance.
// It is used to provide configuration to the plugin instance either via env vars or via each plugin request.
type PluginInstanceCfg struct {
GrafanaAppURL string
Features featuremgmt.FeatureToggles
Tracing config.Tracing
PluginSettings setting.PluginSettings
AWSAllowedAuthProviders []string
AWSAssumeRoleEnabled bool
AWSExternalId string
AWSSessionDuration string
AWSListMetricsPageLimit string
AWSForwardSettingsPlugins []string
Azure *azsettings.AzureSettings
AzureAuthEnabled bool
ProxySettings setting.SecureSocksDSProxySettings
GrafanaVersion string
ConcurrentQueryCount int
UserFacingDefaultError string
DataProxyRowLimit int64
SQLDatasourceMaxOpenConnsDefault int
SQLDatasourceMaxIdleConnsDefault int
SQLDatasourceMaxConnLifetimeDefault int
}
// ProvidePluginInstanceConfig returns a new PluginInstanceCfg.
func ProvidePluginInstanceConfig(cfg *setting.Cfg, settingProvider setting.Provider, features featuremgmt.FeatureToggles) (*PluginInstanceCfg, error) {
aws := settingProvider.Section("aws")
allowedAuth := cfg.AWSAllowedAuthProviders
if len(aws.KeyValue("allowed_auth_providers").Value()) > 0 {
allowedAuth = util.SplitString(aws.KeyValue("allowed_auth_providers").Value())
}
awsForwardSettingsPlugins := cfg.AWSForwardSettingsPlugins
if len(aws.KeyValue("forward_settings_to_plugins").Value()) > 0 {
awsForwardSettingsPlugins = util.SplitString(aws.KeyValue("forward_settings_to_plugins").Value())
}
tracingCfg, err := newTracingCfg(cfg)
if err != nil {
return nil, fmt.Errorf("new opentelemetry cfg: %w", err)
}
if cfg.Azure == nil {
cfg.Azure = &azsettings.AzureSettings{}
}
return &PluginInstanceCfg{
GrafanaAppURL: cfg.AppURL,
Features: features,
Tracing: tracingCfg,
PluginSettings: extractPluginSettings(settingProvider),
AWSAllowedAuthProviders: allowedAuth,
AWSAssumeRoleEnabled: aws.KeyValue("assume_role_enabled").MustBool(cfg.AWSAssumeRoleEnabled),
AWSExternalId: aws.KeyValue("external_id").Value(),
AWSSessionDuration: aws.KeyValue("session_duration").Value(),
AWSListMetricsPageLimit: aws.KeyValue("list_metrics_page_limit").Value(),
AWSForwardSettingsPlugins: awsForwardSettingsPlugins,
Azure: cfg.Azure,
AzureAuthEnabled: cfg.Azure.AzureAuthEnabled,
ProxySettings: cfg.SecureSocksDSProxy,
GrafanaVersion: cfg.BuildVersion,
ConcurrentQueryCount: cfg.ConcurrentQueryCount,
UserFacingDefaultError: cfg.UserFacingDefaultError,
DataProxyRowLimit: cfg.DataProxyRowLimit,
SQLDatasourceMaxOpenConnsDefault: cfg.SqlDatasourceMaxOpenConnsDefault,
SQLDatasourceMaxIdleConnsDefault: cfg.SqlDatasourceMaxIdleConnsDefault,
SQLDatasourceMaxConnLifetimeDefault: cfg.SqlDatasourceMaxConnLifetimeDefault,
}, nil
}
func extractPluginSettings(settingProvider setting.Provider) setting.PluginSettings {
ps := setting.PluginSettings{}
for sectionName, sectionCopy := range settingProvider.Current() {
if !strings.HasPrefix(sectionName, "plugin.") {
continue
}
// Calling Current() returns a redacted version of section. We need to replace the map values with the unredacted values.
section := settingProvider.Section(sectionName)
for k := range sectionCopy {
sectionCopy[k] = section.KeyValue(k).MustString("")
}
pluginID := strings.Replace(sectionName, "plugin.", "", 1)
ps[pluginID] = sectionCopy
}
return ps
}
@@ -0,0 +1,38 @@
package pluginconfig
import (
"testing"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestPluginSettings(t *testing.T) {
raw, err := ini.Load([]byte(`
[plugins]
test_key = 123
[plugin.test-datasource]
foo = 5m
bar = something
[plugin.secret-plugin]
secret_key = secret
normal_key = not a secret`))
require.NoError(t, err)
cfg := &setting.Cfg{Raw: raw}
settings := &setting.OSSImpl{Cfg: cfg}
ps := extractPluginSettings(settings)
require.Len(t, ps, 2)
require.Len(t, ps["test-datasource"], 2)
require.Equal(t, ps["test-datasource"]["foo"], "5m")
require.Equal(t, ps["test-datasource"]["bar"], "something")
require.Len(t, ps["secret-plugin"], 2)
require.Equal(t, ps["secret-plugin"]["secret_key"], "secret")
require.Equal(t, ps["secret-plugin"]["normal_key"], "not a secret")
}
@@ -0,0 +1,178 @@
package pluginconfig
import (
"context"
"fmt"
"os"
"slices"
"strconv"
"strings"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
var _ envvars.Provider = (*EnvVarsProvider)(nil)
type EnvVarsProvider struct {
cfg *PluginInstanceCfg
license plugins.Licensing
}
func NewEnvVarsProvider(cfg *PluginInstanceCfg, license plugins.Licensing) *EnvVarsProvider {
return &EnvVarsProvider{
cfg: cfg,
license: license,
}
}
func (p *EnvVarsProvider) PluginEnvVars(ctx context.Context, plugin *plugins.Plugin) []string {
hostEnv := []string{
envVar("GF_VERSION", p.cfg.GrafanaVersion),
}
if p.license != nil {
hostEnv = append(
hostEnv,
envVar("GF_EDITION", p.license.Edition()),
envVar("GF_ENTERPRISE_LICENSE_PATH", p.license.Path()),
envVar("GF_ENTERPRISE_APP_URL", p.license.AppURL()),
)
hostEnv = append(hostEnv, p.license.Environment()...)
}
if plugin.ExternalService != nil {
hostEnv = append(
hostEnv,
envVar("GF_APP_URL", p.cfg.GrafanaAppURL),
envVar("GF_PLUGIN_APP_CLIENT_ID", plugin.ExternalService.ClientID),
envVar("GF_PLUGIN_APP_CLIENT_SECRET", plugin.ExternalService.ClientSecret),
)
if plugin.ExternalService.PrivateKey != "" {
hostEnv = append(hostEnv, envVar("GF_PLUGIN_APP_PRIVATE_KEY", plugin.ExternalService.PrivateKey))
}
}
hostEnv = append(hostEnv, p.featureToggleEnableVars(ctx)...)
hostEnv = append(hostEnv, p.awsEnvVars(plugin.PluginID())...)
hostEnv = append(hostEnv, p.secureSocksProxyEnvVars()...)
hostEnv = append(hostEnv, azsettings.WriteToEnvStr(p.cfg.Azure)...)
hostEnv = append(hostEnv, p.tracingEnvVars(plugin)...)
hostEnv = append(hostEnv, p.pluginSettingsEnvVars(plugin.PluginID())...)
// If SkipHostEnvVars is enabled, get some allowed variables from the current process and pass
// them down to the plugin. If the flag is not set, do not add anything else because ALL env vars
// from the current process (os.Environ()) will be forwarded to the plugin's process by go-plugin
if plugin.SkipHostEnvVars {
hostEnv = append(hostEnv, envvars.PermittedHostEnvVars()...)
}
return hostEnv
}
func (p *EnvVarsProvider) featureToggleEnableVars(ctx context.Context) []string {
var variables []string // an array is used for consistency and keep the logic simpler for no features case
if p.cfg.Features == nil {
return variables
}
enabledFeatures := p.cfg.Features.GetEnabled(ctx)
if len(enabledFeatures) > 0 {
features := make([]string, 0, len(enabledFeatures))
for feat := range enabledFeatures {
features = append(features, feat)
}
variables = append(variables, envVar("GF_INSTANCE_FEATURE_TOGGLES_ENABLE", strings.Join(features, ",")))
}
return variables
}
func (p *EnvVarsProvider) awsEnvVars(pluginID string) []string {
if !slices.Contains[[]string, string](p.cfg.AWSForwardSettingsPlugins, pluginID) {
return []string{}
}
var variables []string
if !p.cfg.AWSAssumeRoleEnabled {
variables = append(variables, envVar(awsds.AssumeRoleEnabledEnvVarKeyName, "false"))
}
if len(p.cfg.AWSAllowedAuthProviders) > 0 {
variables = append(variables, envVar(awsds.AllowedAuthProvidersEnvVarKeyName, strings.Join(p.cfg.AWSAllowedAuthProviders, ",")))
}
if p.cfg.AWSExternalId != "" {
variables = append(variables, envVar(awsds.GrafanaAssumeRoleExternalIdKeyName, p.cfg.AWSExternalId))
}
if p.cfg.AWSSessionDuration != "" {
variables = append(variables, envVar(awsds.SessionDurationEnvVarKeyName, p.cfg.AWSSessionDuration))
}
if p.cfg.AWSListMetricsPageLimit != "" {
variables = append(variables, envVar(awsds.ListMetricsPageLimitKeyName, p.cfg.AWSListMetricsPageLimit))
}
return variables
}
func (p *EnvVarsProvider) secureSocksProxyEnvVars() []string {
if p.cfg.ProxySettings.Enabled {
return []string{
envVar(proxy.PluginSecureSocksProxyClientCert, p.cfg.ProxySettings.ClientCert),
envVar(proxy.PluginSecureSocksProxyClientKey, p.cfg.ProxySettings.ClientKey),
envVar(proxy.PluginSecureSocksProxyRootCACert, p.cfg.ProxySettings.RootCA),
envVar(proxy.PluginSecureSocksProxyProxyAddress, p.cfg.ProxySettings.ProxyAddress),
envVar(proxy.PluginSecureSocksProxyServerName, p.cfg.ProxySettings.ServerName),
envVar(proxy.PluginSecureSocksProxyEnabled, strconv.FormatBool(p.cfg.ProxySettings.Enabled)),
envVar(proxy.PluginSecureSocksProxyAllowInsecure, strconv.FormatBool(p.cfg.ProxySettings.AllowInsecure)),
}
}
return nil
}
func (p *EnvVarsProvider) tracingEnvVars(plugin *plugins.Plugin) []string {
pluginTracingEnabled := p.cfg.Features.IsEnabledGlobally(featuremgmt.FlagEnablePluginsTracingByDefault)
if v, exists := p.cfg.PluginSettings[plugin.ID]["tracing"]; exists && !pluginTracingEnabled {
pluginTracingEnabled = v == "true"
}
if !p.cfg.Tracing.IsEnabled() || !pluginTracingEnabled {
return nil
}
vars := []string{
envVar("GF_INSTANCE_OTLP_ADDRESS", p.cfg.Tracing.OpenTelemetry.Address),
envVar("GF_INSTANCE_OTLP_PROPAGATION", p.cfg.Tracing.OpenTelemetry.Propagation),
envVar("GF_INSTANCE_OTLP_SAMPLER_TYPE", p.cfg.Tracing.OpenTelemetry.Sampler),
fmt.Sprintf("GF_INSTANCE_OTLP_SAMPLER_PARAM=%.6f", p.cfg.Tracing.OpenTelemetry.SamplerParam),
envVar("GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL", p.cfg.Tracing.OpenTelemetry.SamplerRemoteURL),
}
if plugin.Info.Version != "" {
vars = append(vars, fmt.Sprintf("GF_PLUGIN_VERSION=%s", plugin.Info.Version))
}
return vars
}
func (p *EnvVarsProvider) pluginSettingsEnvVars(pluginID string) []string {
const customConfigPrefix = "GF_PLUGIN"
var env []string
for k, v := range p.cfg.PluginSettings[pluginID] {
if k == "path" || strings.ToLower(k) == "id" {
continue
}
key := fmt.Sprintf("%s_%s", customConfigPrefix, strings.ToUpper(k))
if value := os.Getenv(key); value != "" {
v = value
}
env = append(env, fmt.Sprintf("%s=%s", key, v))
}
return env
}
func envVar(key, value string) string {
return fmt.Sprintf("%s=%s", key, value)
}
@@ -0,0 +1,742 @@
package pluginconfig
import (
"context"
"strings"
"testing"
"gopkg.in/ini.v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/plugindef"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
func TestPluginEnvVarsProvider_PluginEnvVars(t *testing.T) {
t.Run("backend datasource with license", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
},
}
licensing := &fakes.FakeLicensingService{
LicenseEdition: "test",
TokenRaw: "token",
LicensePath: "/path/to/ent/license",
LicenseAppURL: "https://myorg.com/",
}
cfg := &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{
"test": {
"custom_env_var": "customVal",
},
},
AWSAssumeRoleEnabled: true,
Features: featuremgmt.WithFeatures(),
}
provider := NewEnvVarsProvider(cfg, licensing)
envVars := provider.PluginEnvVars(context.Background(), p)
assert.Len(t, envVars, 6)
assert.Equal(t, "GF_VERSION=", envVars[0])
assert.Equal(t, "GF_EDITION=test", envVars[1])
assert.Equal(t, "GF_ENTERPRISE_LICENSE_PATH=/path/to/ent/license", envVars[2])
assert.Equal(t, "GF_ENTERPRISE_APP_URL=https://myorg.com/", envVars[3])
assert.Equal(t, "GF_ENTERPRISE_LICENSE_TEXT=token", envVars[4])
assert.Equal(t, "GF_PLUGIN_CUSTOM_ENV_VAR=customVal", envVars[5])
})
}
func TestPluginEnvVarsProvider_skipHostEnvVars(t *testing.T) {
const (
envVarName = "HTTP_PROXY"
envVarValue = "lorem ipsum"
)
t.Setenv(envVarName, envVarValue)
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
},
}
t.Run("without FlagPluginsSkipHostEnvVars should not populate host env vars", func(t *testing.T) {
cfg := setting.NewCfg()
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
envVars := provider.PluginEnvVars(context.Background(), p)
// We want to test that the envvars.Provider does not add any of the host env vars.
// When starting the plugin via go-plugin, ALL host env vars will be added by go-plugin,
// but we are testing the envvars.Provider here, so that's outside the scope of this test.
_, ok := getEnvVarWithExists(envVars, envVarName)
require.False(t, ok, "host env var should not be present")
})
t.Run("with SkipHostEnvVars = true", func(t *testing.T) {
p.SkipHostEnvVars = true
cfg := setting.NewCfg()
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
t.Run("should populate allowed host env vars", func(t *testing.T) {
// Set all allowed variables
for _, ev := range envvars.PermittedHostEnvVarNames() {
t.Setenv(ev, envVarValue)
}
envVars := provider.PluginEnvVars(context.Background(), p)
// Test against each variable
for _, expEvName := range envvars.PermittedHostEnvVarNames() {
gotEvValue, ok := getEnvVarWithExists(envVars, expEvName)
require.True(t, ok, "host env var should be present")
require.Equal(t, envVarValue, gotEvValue)
}
})
t.Run("should not populate host env vars that aren't allowed", func(t *testing.T) {
// Set all allowed variables
for _, ev := range envvars.PermittedHostEnvVarNames() {
t.Setenv(ev, envVarValue)
}
// ...and an extra one, which should not leak
const superSecretEnvVariableName = "SUPER_SECRET_VALUE"
t.Setenv(superSecretEnvVariableName, "01189998819991197253")
envVars := provider.PluginEnvVars(context.Background(), p)
// Super secret should not leak
_, ok := getEnvVarWithExists(envVars, superSecretEnvVariableName)
require.False(t, ok, "super secret env var should not be leaked")
// Everything else should be present
for _, expEvName := range envvars.PermittedHostEnvVarNames() {
var gotEvValue string
gotEvValue, ok = getEnvVarWithExists(envVars, expEvName)
require.True(t, ok, "host env var should be present")
require.Equal(t, envVarValue, gotEvValue)
}
})
})
}
func TestPluginEnvVarsProvider_tracingEnvironmentVariables(t *testing.T) {
const pluginID = "plugin_id"
defaultPlugin := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
Info: plugins.Info{Version: "1.0.0"},
},
}
pluginWithoutVersion := &plugins.Plugin{
JSONData: plugins.JSONData{ID: pluginID},
}
defaultOTelCfg := config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "",
}
expDefaultOtlp := func(t *testing.T, envVars []string) {
found := map[string]bool{
"address": false,
"plugin_version": false,
"propagation": false,
}
setFound := func(v string) {
require.False(t, found[v], "duplicate env var found")
found[v] = true
}
for _, v := range envVars {
switch v {
case "GF_PLUGIN_VERSION=1.0.0":
setFound("plugin_version")
case "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317":
setFound("address")
case "GF_INSTANCE_OTLP_PROPAGATION=":
setFound("propagation")
}
}
for k, f := range found {
require.Truef(t, f, "%q env var not found: %+v", k, envVars)
}
}
expNoTracing := func(t *testing.T, envVars []string) {
for _, v := range envVars {
assert.False(t, strings.HasPrefix(v, "GF_TRACING"), "should not have tracing env var")
assert.False(
t,
strings.HasPrefix(v, "GF_PLUGIN_VERSION"),
"GF_PLUGIN_VERSION is tracing-only and should not be present when tracing is disabled",
)
}
}
expGfPluginVersionNotPresent := func(t *testing.T, envVars []string) {
for _, e := range envVars {
assert.False(t, strings.HasPrefix("GF_PLUGIN_VERSION=", e), "GF_PLUGIN_VERSION shouldn't be present")
}
}
expGfPluginVersionPresent := func(t *testing.T, envVars []string) {
var found bool
for _, e := range envVars {
if e != "GF_PLUGIN_VERSION=1.0.0" {
continue
}
assert.False(t, found, "GF_PLUGIN_VERSION is present multiple times")
found = true
}
assert.Truef(t, found, "GF_PLUGIN_VERSION is not present: %+v", envVars)
}
for _, tc := range []struct {
name string
cfg *PluginInstanceCfg
plugin *plugins.Plugin
exp func(t *testing.T, envVars []string)
}{
{
name: "otel not configured",
cfg: &PluginInstanceCfg{
AWSAssumeRoleEnabled: false,
Tracing: config.Tracing{},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expNoTracing,
},
{
name: "otel not configured but plugin-tracing enabled",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expNoTracing,
},
{
name: "otlp no propagation plugin enabled",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{
pluginID: {"tracing": "true"},
},
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expDefaultOtlp,
},
{
name: "otlp no propagation disabled by default",
cfg: &PluginInstanceCfg{
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expNoTracing,
},
{
name: "otlp propagation plugin enabled",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{
pluginID: {"tracing": "true"},
},
AWSAssumeRoleEnabled: true,
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "w3c",
// Sensible default values for the sampler set by pkg/infra/tracing while reading config.ini
Sampler: "",
SamplerParam: 1.0,
SamplerRemoteURL: "",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
assert.Len(t, envVars, 8)
assert.Equal(t, "GF_VERSION=", envVars[0])
assert.Equal(t, "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317", envVars[1])
assert.Equal(t, "GF_INSTANCE_OTLP_PROPAGATION=w3c", envVars[2])
assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_TYPE=", envVars[3])
assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_PARAM=1.000000", envVars[4])
assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=", envVars[5])
assert.Equal(t, "GF_PLUGIN_VERSION=1.0.0", envVars[6])
assert.Equal(t, "GF_PLUGIN_TRACING=true", envVars[7])
},
},
{
name: "otlp enabled composite propagation",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{
pluginID: {"tracing": "true"},
},
AWSAssumeRoleEnabled: true,
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "w3c,jaeger",
// Sensible default values for the sampler set by pkg/infra/tracing while reading config.ini
Sampler: "",
SamplerParam: 1.0,
SamplerRemoteURL: "",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
assert.Len(t, envVars, 8)
assert.Equal(t, "GF_VERSION=", envVars[0])
assert.Equal(t, "GF_INSTANCE_OTLP_ADDRESS=127.0.0.1:4317", envVars[1])
assert.Equal(t, "GF_INSTANCE_OTLP_PROPAGATION=w3c,jaeger", envVars[2])
assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_TYPE=", envVars[3])
assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_PARAM=1.000000", envVars[4])
assert.Equal(t, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL=", envVars[5])
assert.Equal(t, "GF_PLUGIN_VERSION=1.0.0", envVars[6])
assert.Equal(t, "GF_PLUGIN_TRACING=true", envVars[7])
},
},
{
name: "otlp no propagation disabled by default",
cfg: &PluginInstanceCfg{
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "w3c",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expNoTracing,
},
{
name: "disabled on plugin",
cfg: &PluginInstanceCfg{
PluginSettings: setting.PluginSettings{
pluginID: map[string]string{"tracing": "false"},
},
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expNoTracing,
},
{
name: "disabled on plugin with other plugin settings",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{
pluginID: {"some_other_option": "true"},
},
AWSAssumeRoleEnabled: true,
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expNoTracing,
},
{
name: "enabled on plugin with other plugin settings",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{
pluginID: {"some_other_option": "true", "tracing": "true"},
},
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expDefaultOtlp,
},
{
name: `enabled on plugin with no "tracing" plugin setting but with enablePluginsTracingByDefault feature flag`,
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {}},
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(featuremgmt.FlagEnablePluginsTracingByDefault),
},
plugin: defaultPlugin,
exp: expDefaultOtlp,
},
{
name: `enabled on plugin with plugin setting "tracing=false" but with enablePluginsTracingByDefault feature flag`,
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "false"}},
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(featuremgmt.FlagEnablePluginsTracingByDefault),
},
plugin: defaultPlugin,
exp: expDefaultOtlp,
},
{
name: "GF_PLUGIN_VERSION is not present if tracing is disabled",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expGfPluginVersionNotPresent,
},
{
name: "GF_PLUGIN_VERSION is present if tracing is enabled and plugin has version",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: defaultOTelCfg,
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: expGfPluginVersionPresent,
},
{
name: "GF_PLUGIN_VERSION is not present if tracing is enabled but plugin doesn't have a version",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{},
},
Features: featuremgmt.WithFeatures(),
},
plugin: pluginWithoutVersion,
exp: expGfPluginVersionNotPresent,
},
{
name: "no sampling (neversample)",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "jaeger",
Sampler: "",
SamplerParam: 0.0,
SamplerRemoteURL: "",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
require.Empty(t, getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_TYPE"))
require.Equal(t, "0.000000", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_PARAM"))
require.Empty(t, getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL"))
},
},
{
name: "empty sampler with param",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "jaeger",
Sampler: "",
SamplerParam: 0.5,
SamplerRemoteURL: "",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
require.Equal(t, "", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_TYPE"))
require.Equal(t, "0.500000", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_PARAM"))
require.Equal(t, "", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL"))
},
},
{
name: "const sampler with param",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "jaeger",
Sampler: "const",
SamplerParam: 0.5,
SamplerRemoteURL: "",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
require.Equal(t, "const", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_TYPE"))
require.Equal(t, "0.500000", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_PARAM"))
require.Equal(t, "", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL"))
},
},
{
name: "rateLimiting sampler",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "jaeger",
Sampler: "rateLimiting",
SamplerParam: 0.5,
SamplerRemoteURL: "",
},
},
Features: featuremgmt.WithFeatures(),
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
require.Equal(t, "rateLimiting", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_TYPE"))
require.Equal(t, "0.500000", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_PARAM"))
require.Equal(t, "", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL"))
},
},
{
name: "remote sampler",
cfg: &PluginInstanceCfg{
PluginSettings: map[string]map[string]string{pluginID: {"tracing": "true"}},
Features: featuremgmt.WithFeatures(),
Tracing: config.Tracing{
OpenTelemetry: config.OpenTelemetryCfg{
Address: "127.0.0.1:4317",
Propagation: "jaeger",
Sampler: "remote",
SamplerParam: 0.5,
SamplerRemoteURL: "127.0.0.1:10001",
},
},
},
plugin: defaultPlugin,
exp: func(t *testing.T, envVars []string) {
require.Equal(t, "remote", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_TYPE"))
require.Equal(t, "0.500000", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_PARAM"))
require.Equal(t, "127.0.0.1:10001", getEnvVar(envVars, "GF_INSTANCE_OTLP_SAMPLER_REMOTE_URL"))
},
},
} {
t.Run(tc.name, func(t *testing.T) {
p := NewEnvVarsProvider(tc.cfg, nil)
envVars := p.PluginEnvVars(context.Background(), tc.plugin)
tc.exp(t, envVars)
})
}
}
// getEnvVarWithExists takes a slice of strings in this format: "K=V" (env vars), and returns the "V" where K = wanted.
// If there's no such key, it returns false as the second argument.
func getEnvVarWithExists(vars []string, wanted string) (string, bool) {
for _, v := range vars {
parts := strings.SplitN(v, "=", 2)
if parts[0] != wanted {
continue
}
var r string
if len(parts) < 2 {
r = ""
} else {
r = parts[1]
}
return r, true
}
return "", false
}
// getEnvVar is like getEnvVarWithExists, but it returns just one string, without the boolean "ok" value.
// If the wanted environment variable does not exist, it returns an empty string.
func getEnvVar(vars []string, wanted string) string {
v, _ := getEnvVarWithExists(vars, wanted)
return v
}
func TestPluginEnvVarsProvider_authEnvVars(t *testing.T) {
t.Run("backend datasource with auth registration", func(t *testing.T) {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: "test",
IAM: &plugindef.IAM{},
},
ExternalService: &auth.ExternalService{
ClientID: "clientID",
ClientSecret: "clientSecret",
PrivateKey: "privatePem",
},
}
cfg := &setting.Cfg{
Raw: ini.Empty(),
AppURL: "https://myorg.com/",
}
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
envVars := provider.PluginEnvVars(context.Background(), p)
assert.Equal(t, "GF_VERSION=", envVars[0])
assert.Equal(t, "GF_APP_URL=https://myorg.com/", envVars[1])
assert.Equal(t, "GF_PLUGIN_APP_CLIENT_ID=clientID", envVars[2])
assert.Equal(t, "GF_PLUGIN_APP_CLIENT_SECRET=clientSecret", envVars[3])
assert.Equal(t, "GF_PLUGIN_APP_PRIVATE_KEY=privatePem", envVars[4])
})
}
func TestPluginEnvVarsProvider_awsEnvVars(t *testing.T) {
t.Run("backend datasource with aws settings", func(t *testing.T) {
tcs := []struct {
name string
pluginID string
forwardToPlugins []string
expected []string
}{
{
name: "Will generate AWS env vars for plugin as long as is in the forwardToPlugins list",
forwardToPlugins: []string{"foobar-datasource", "cloudwatch", "prometheus"},
pluginID: "cloudwatch",
expected: []string{"GF_VERSION=", "AWS_AUTH_AssumeRoleEnabled=false", "AWS_AUTH_AllowedAuthProviders=grafana_assume_role,keys", "AWS_AUTH_EXTERNAL_ID=mock_external_id", "AWS_AUTH_SESSION_DURATION=10m", "AWS_CW_LIST_METRICS_PAGE_LIMIT=100"},
},
{
name: "Will not generate AWS env vars for plugin as long as is in not the forwardToPlugins list",
forwardToPlugins: []string{"cloudwatch", "foobar-datasource"},
pluginID: "prometheus",
expected: []string{"GF_VERSION="},
},
}
for _, tc := range tcs {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: tc.pluginID,
},
}
cfg := &PluginInstanceCfg{
AWSAssumeRoleEnabled: false,
AWSAllowedAuthProviders: []string{"grafana_assume_role", "keys"},
AWSExternalId: "mock_external_id",
AWSSessionDuration: "10m",
AWSListMetricsPageLimit: "100",
AWSForwardSettingsPlugins: tc.forwardToPlugins,
Features: featuremgmt.WithFeatures(),
}
provider := NewEnvVarsProvider(cfg, nil)
envVars := provider.PluginEnvVars(context.Background(), p)
assert.ElementsMatch(t, tc.expected, envVars)
}
})
}
func TestPluginEnvVarsProvider_featureToggleEnvVar(t *testing.T) {
t.Run("backend datasource with feature toggle", func(t *testing.T) {
expectedFeatures := []string{"feat-1", "feat-2"}
featuresLookup := map[string]bool{
expectedFeatures[0]: true,
expectedFeatures[1]: true,
}
cfg := &PluginInstanceCfg{
Features: featuremgmt.WithFeatures(expectedFeatures[0], true, expectedFeatures[1], true),
}
p := NewEnvVarsProvider(cfg, nil)
envVars := p.PluginEnvVars(context.Background(), &plugins.Plugin{})
assert.Equal(t, 2, len(envVars))
toggleExpression := strings.Split(envVars[1], "=")
assert.Equal(t, 2, len(toggleExpression))
assert.Equal(t, "GF_INSTANCE_FEATURE_TOGGLES_ENABLE", toggleExpression[0])
toggleArgs := toggleExpression[1]
features := strings.Split(toggleArgs, ",")
assert.Equal(t, len(expectedFeatures), len(features))
// this is necessary because the features are not returned in the order they are provided
for _, f := range features {
_, ok := featuresLookup[f]
assert.True(t, ok)
}
})
}
func TestPluginEnvVarsProvider_azureEnvVars(t *testing.T) {
t.Run("backend datasource with azure settings", func(t *testing.T) {
cfg := &setting.Cfg{
Raw: ini.Empty(),
AWSAssumeRoleEnabled: true,
Azure: &azsettings.AzureSettings{
Cloud: azsettings.AzurePublic,
ManagedIdentityEnabled: true,
ManagedIdentityClientId: "mock_managed_identity_client_id",
WorkloadIdentityEnabled: true,
WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{
TenantId: "mock_workload_identity_tenant_id",
ClientId: "mock_workload_identity_client_id",
TokenFile: "mock_workload_identity_token_file",
},
UserIdentityEnabled: true,
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "mock_user_identity_token_url",
ClientId: "mock_user_identity_client_id",
ClientSecret: "mock_user_identity_client_secret",
UsernameAssertion: true,
},
},
}
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
envVars := provider.PluginEnvVars(context.Background(), &plugins.Plugin{})
assert.ElementsMatch(t, []string{"GF_VERSION=", "GFAZPL_AZURE_CLOUD=AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED=true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID=mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED=true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID=mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID=mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE=mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED=true",
"GFAZPL_USER_IDENTITY_TOKEN_URL=mock_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_ID=mock_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET=mock_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_ASSERTION=username",
}, envVars)
})
}
@@ -0,0 +1,16 @@
package pluginconfig
import "context"
var _ PluginRequestConfigProvider = (*FakePluginRequestConfigProvider)(nil)
type FakePluginRequestConfigProvider struct{}
func NewFakePluginRequestConfigProvider() *FakePluginRequestConfigProvider {
return &FakePluginRequestConfigProvider{}
}
// PluginRequestConfig returns a map of configuration that should be passed in a plugin request.
func (s *FakePluginRequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginID string) map[string]string {
return map[string]string{}
}
@@ -0,0 +1,151 @@
package pluginconfig
import (
"context"
"slices"
"sort"
"strconv"
"strings"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
)
var _ PluginRequestConfigProvider = (*RequestConfigProvider)(nil)
type PluginRequestConfigProvider interface {
PluginRequestConfig(ctx context.Context, pluginID string) map[string]string
}
type RequestConfigProvider struct {
cfg *PluginInstanceCfg
}
func NewRequestConfigProvider(cfg *PluginInstanceCfg) *RequestConfigProvider {
return &RequestConfigProvider{
cfg: cfg,
}
}
// PluginRequestConfig returns a map of configuration that should be passed in a plugin request.
// nolint:gocyclo
func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginID string) map[string]string {
m := make(map[string]string)
if s.cfg.GrafanaAppURL != "" {
m[backend.AppURL] = s.cfg.GrafanaAppURL
}
if s.cfg.ConcurrentQueryCount != 0 {
m[backend.ConcurrentQueryCount] = strconv.Itoa(s.cfg.ConcurrentQueryCount)
}
enabledFeatures := s.cfg.Features.GetEnabled(ctx)
if len(enabledFeatures) > 0 {
features := make([]string, 0, len(enabledFeatures))
for feat := range enabledFeatures {
features = append(features, feat)
}
sort.Strings(features)
m[featuretoggles.EnabledFeatures] = strings.Join(features, ",")
}
if slices.Contains[[]string, string](s.cfg.AWSForwardSettingsPlugins, pluginID) {
if !s.cfg.AWSAssumeRoleEnabled {
m[awsds.AssumeRoleEnabledEnvVarKeyName] = "false"
}
if len(s.cfg.AWSAllowedAuthProviders) > 0 {
m[awsds.AllowedAuthProvidersEnvVarKeyName] = strings.Join(s.cfg.AWSAllowedAuthProviders, ",")
}
if s.cfg.AWSExternalId != "" {
m[awsds.GrafanaAssumeRoleExternalIdKeyName] = s.cfg.AWSExternalId
}
if s.cfg.AWSSessionDuration != "" {
m[awsds.SessionDurationEnvVarKeyName] = s.cfg.AWSSessionDuration
}
if s.cfg.AWSListMetricsPageLimit != "" {
m[awsds.ListMetricsPageLimitKeyName] = s.cfg.AWSListMetricsPageLimit
}
}
if s.cfg.ProxySettings.Enabled {
m[proxy.PluginSecureSocksProxyEnabled] = "true"
m[proxy.PluginSecureSocksProxyClientCert] = s.cfg.ProxySettings.ClientCert
m[proxy.PluginSecureSocksProxyClientKey] = s.cfg.ProxySettings.ClientKey
m[proxy.PluginSecureSocksProxyRootCACert] = s.cfg.ProxySettings.RootCA
m[proxy.PluginSecureSocksProxyProxyAddress] = s.cfg.ProxySettings.ProxyAddress
m[proxy.PluginSecureSocksProxyServerName] = s.cfg.ProxySettings.ServerName
m[proxy.PluginSecureSocksProxyAllowInsecure] = strconv.FormatBool(s.cfg.ProxySettings.AllowInsecure)
}
// Settings here will be extracted by grafana-azure-sdk-go from the plugin context
if s.cfg.AzureAuthEnabled {
m[azsettings.AzureAuthEnabled] = strconv.FormatBool(s.cfg.AzureAuthEnabled)
}
azureSettings := s.cfg.Azure
if azureSettings != nil && slices.Contains[[]string, string](azureSettings.ForwardSettingsPlugins, pluginID) {
if azureSettings.Cloud != "" {
m[azsettings.AzureCloud] = azureSettings.Cloud
}
if azureSettings.ManagedIdentityEnabled {
m[azsettings.ManagedIdentityEnabled] = "true"
if azureSettings.ManagedIdentityClientId != "" {
m[azsettings.ManagedIdentityClientID] = azureSettings.ManagedIdentityClientId
}
}
if azureSettings.UserIdentityEnabled {
m[azsettings.UserIdentityEnabled] = "true"
if azureSettings.UserIdentityTokenEndpoint != nil {
if azureSettings.UserIdentityTokenEndpoint.TokenUrl != "" {
m[azsettings.UserIdentityTokenURL] = azureSettings.UserIdentityTokenEndpoint.TokenUrl
}
if azureSettings.UserIdentityTokenEndpoint.ClientId != "" {
m[azsettings.UserIdentityClientID] = azureSettings.UserIdentityTokenEndpoint.ClientId
}
if azureSettings.UserIdentityTokenEndpoint.ClientSecret != "" {
m[azsettings.UserIdentityClientSecret] = azureSettings.UserIdentityTokenEndpoint.ClientSecret
}
if azureSettings.UserIdentityTokenEndpoint.UsernameAssertion {
m[azsettings.UserIdentityAssertion] = "username"
}
}
}
if azureSettings.WorkloadIdentityEnabled {
m[azsettings.WorkloadIdentityEnabled] = "true"
if azureSettings.WorkloadIdentitySettings != nil {
if azureSettings.WorkloadIdentitySettings.ClientId != "" {
m[azsettings.WorkloadIdentityClientID] = azureSettings.WorkloadIdentitySettings.ClientId
}
if azureSettings.WorkloadIdentitySettings.TenantId != "" {
m[azsettings.WorkloadIdentityTenantID] = azureSettings.WorkloadIdentitySettings.TenantId
}
if azureSettings.WorkloadIdentitySettings.TokenFile != "" {
m[azsettings.WorkloadIdentityTokenFile] = azureSettings.WorkloadIdentitySettings.TokenFile
}
}
}
}
if s.cfg.UserFacingDefaultError != "" {
m[backend.UserFacingDefaultError] = s.cfg.UserFacingDefaultError
}
if s.cfg.DataProxyRowLimit != 0 {
m[backend.SQLRowLimit] = strconv.FormatInt(s.cfg.DataProxyRowLimit, 10)
}
m[backend.SQLMaxOpenConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxOpenConnsDefault)
m[backend.SQLMaxIdleConnsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxIdleConnsDefault)
m[backend.SQLMaxConnLifetimeSecondsDefault] = strconv.Itoa(s.cfg.SQLDatasourceMaxConnLifetimeDefault)
return m
}
@@ -0,0 +1,398 @@
package pluginconfig
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
func TestRequestConfigProvider_PluginRequestConfig_Defaults(t *testing.T) {
cfg := setting.NewCfg()
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Equal(t, map[string]string{
"GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0",
"GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0",
"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0",
}, p.PluginRequestConfig(context.Background(), ""))
}
func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) {
tcs := []struct {
name string
cfg *PluginInstanceCfg
expected map[string]string
}{
{
name: "Both features and proxy settings enabled",
cfg: &PluginInstanceCfg{
ProxySettings: setting.SecureSocksDSProxySettings{
Enabled: true,
ShowUI: true,
ClientCert: "c3rt",
ClientKey: "k3y",
RootCA: "ca",
ProxyAddress: "https://proxy.grafana.com",
ServerName: "secureProxy",
AllowInsecure: true,
},
Features: featuremgmt.WithFeatures("feat-2", "feat-500", "feat-1"),
},
expected: map[string]string{
"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "feat-1,feat-2,feat-500",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_ENABLED": "true",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_CERT": "c3rt",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_CLIENT_KEY": "k3y",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_ROOT_CA_CERT": "ca",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_PROXY_ADDRESS": "https://proxy.grafana.com",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_SERVER_NAME": "secureProxy",
"GF_SECURE_SOCKS_DATASOURCE_PROXY_ALLOW_INSECURE": "true",
},
},
{
name: "Features enabled but proxy settings disabled",
cfg: &PluginInstanceCfg{
ProxySettings: setting.SecureSocksDSProxySettings{
Enabled: false,
ShowUI: true,
ClientCert: "c3rt",
ClientKey: "k3y",
RootCA: "ca",
ProxyAddress: "https://proxy.grafana.com",
ServerName: "secureProxy",
},
Features: featuremgmt.WithFeatures("feat-2", "feat-500", "feat-1"),
},
expected: map[string]string{
"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "feat-1,feat-2,feat-500",
},
},
{
name: "Both features and proxy settings disabled",
cfg: &PluginInstanceCfg{
ProxySettings: setting.SecureSocksDSProxySettings{
Enabled: false,
ShowUI: true,
ClientCert: "c3rt",
ClientKey: "k3y",
RootCA: "ca",
ProxyAddress: "https://proxy.grafana.com",
ServerName: "secureProxy",
},
Features: featuremgmt.WithFeatures("feat-2", false),
},
expected: map[string]string{},
},
{
name: "Both features and proxy settings empty",
cfg: &PluginInstanceCfg{
ProxySettings: setting.SecureSocksDSProxySettings{},
Features: featuremgmt.WithFeatures(),
},
expected: map[string]string{},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
p := NewRequestConfigProvider(tc.cfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), ""), tc.expected)
})
}
}
func TestRequestConfigProvider_PluginRequestConfig_featureToggles(t *testing.T) {
t.Run("Feature toggles list is deterministic", func(t *testing.T) {
tcs := []struct {
features featuremgmt.FeatureToggles
expectedConfig map[string]string
}{
{
features: featuremgmt.WithFeatures(),
expectedConfig: map[string]string{},
},
{
features: featuremgmt.WithFeatures("A", "B", "C"),
expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"},
},
{
features: featuremgmt.WithFeatures("C", "B", "A"),
expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "A,B,C"},
},
{
features: featuremgmt.WithFeatures("b", "a", "c", "d"),
expectedConfig: map[string]string{"GF_INSTANCE_FEATURE_TOGGLES_ENABLE": "a,b,c,d"},
},
}
for _, tc := range tcs {
cfg := setting.NewCfg()
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), tc.features)
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), ""), tc.expectedConfig)
}
})
}
func TestRequestConfigProvider_PluginRequestConfig_appURL(t *testing.T) {
t.Run("Uses the configured app URL", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.AppURL = "https://myorg.com/"
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{"GF_APP_URL": "https://myorg.com/"})
})
}
func TestRequestConfigProvider_PluginRequestConfig_SQL(t *testing.T) {
t.Run("Uses the configured values", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.DataProxyRowLimit = 23
cfg.SqlDatasourceMaxOpenConnsDefault = 24
cfg.SqlDatasourceMaxIdleConnsDefault = 25
cfg.SqlDatasourceMaxConnLifetimeDefault = 26
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{
"GF_SQL_ROW_LIMIT": "23",
"GF_SQL_MAX_OPEN_CONNS_DEFAULT": "24",
"GF_SQL_MAX_IDLE_CONNS_DEFAULT": "25",
"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "26",
})
})
t.Run("Uses the configured max-default-values, even when they are zero", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.SqlDatasourceMaxOpenConnsDefault = 0
cfg.SqlDatasourceMaxIdleConnsDefault = 0
cfg.SqlDatasourceMaxConnLifetimeDefault = 0
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Equal(t, map[string]string{
"GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0",
"GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0",
"GF_SQL_MAX_CONN_LIFETIME_SECONDS_DEFAULT": "0",
}, p.PluginRequestConfig(context.Background(), ""))
})
}
func TestRequestConfigProvider_PluginRequestConfig_concurrentQueryCount(t *testing.T) {
t.Run("Uses the configured concurrent query count", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.ConcurrentQueryCount = 42
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"})
})
t.Run("Doesn't set the concurrent query count if it is not in the config", func(t *testing.T) {
cfg := setting.NewCfg()
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GF_CONCURRENT_QUERY_COUNT")
})
t.Run("Doesn't set the concurrent query count if it is zero", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.ConcurrentQueryCount = 0
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GF_CONCURRENT_QUERY_COUNT")
})
}
func TestRequestConfigProvider_PluginRequestConfig_azureAuthEnabled(t *testing.T) {
t.Run("Uses the configured azureAuthEnabled", func(t *testing.T) {
cfg := &PluginInstanceCfg{
AzureAuthEnabled: true,
Features: featuremgmt.WithFeatures(),
}
p := NewRequestConfigProvider(cfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), ""), map[string]string{"GFAZPL_AZURE_AUTH_ENABLED": "true"})
})
t.Run("Doesn't set the azureAuthEnabled if it is not in the config", func(t *testing.T) {
cfg := &PluginInstanceCfg{
Features: featuremgmt.WithFeatures(),
}
p := NewRequestConfigProvider(cfg)
require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GFAZPL_AZURE_AUTH_ENABLED")
})
t.Run("Doesn't set the azureAuthEnabled if it is false", func(t *testing.T) {
cfg := &PluginInstanceCfg{
AzureAuthEnabled: false,
Features: featuremgmt.WithFeatures(),
}
p := NewRequestConfigProvider(cfg)
require.NotContains(t, p.PluginRequestConfig(context.Background(), ""), "GFAZPL_AZURE_AUTH_ENABLED")
})
}
func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
azSettings := &azsettings.AzureSettings{
Cloud: azsettings.AzurePublic,
ManagedIdentityEnabled: true,
ManagedIdentityClientId: "mock_managed_identity_client_id",
WorkloadIdentityEnabled: true,
WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{
TenantId: "mock_workload_identity_tenant_id",
ClientId: "mock_workload_identity_client_id",
TokenFile: "mock_workload_identity_token_file",
},
UserIdentityEnabled: true,
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "mock_user_identity_token_url",
ClientId: "mock_user_identity_client_id",
ClientSecret: "mock_user_identity_client_secret",
UsernameAssertion: true,
},
ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"},
}
t.Run("uses the azure settings for an Azure plugin", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.Azure = azSettings
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource"), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED": "true",
"GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_ASSERTION": "username",
})
})
t.Run("does not use the azure settings for a non-Azure plugin", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.Azure = azSettings
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
m := p.PluginRequestConfig(context.Background(), "")
require.NotContains(t, m, "GFAZPL_AZURE_CLOUD")
require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_ENABLED")
require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_CLIENT_ID")
require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_ENABLED")
require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TENANT_ID")
require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID")
require.NotContains(t, m, "GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ENABLED")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_TOKEN_URL")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET")
require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ASSERTION")
})
t.Run("uses the azure settings for a non-Azure user-specified plugin", func(t *testing.T) {
azSettings.ForwardSettingsPlugins = append(azSettings.ForwardSettingsPlugins, "test-datasource")
cfg := setting.NewCfg()
cfg.Azure = azSettings
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource"), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED": "true",
"GFAZPL_USER_IDENTITY_TOKEN_URL": "mock_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_ASSERTION": "username",
})
})
}
func TestRequestConfigProvider_PluginRequestConfig_aws(t *testing.T) {
cfg := &PluginInstanceCfg{
Features: featuremgmt.WithFeatures(),
}
cfg.AWSAssumeRoleEnabled = false
cfg.AWSAllowedAuthProviders = []string{"grafana_assume_role", "keys"}
cfg.AWSExternalId = "mock_external_id"
cfg.AWSSessionDuration = "10m"
cfg.AWSListMetricsPageLimit = "100"
cfg.AWSForwardSettingsPlugins = []string{"cloudwatch", "prometheus", "elasticsearch"}
p := NewRequestConfigProvider(cfg)
t.Run("uses the aws settings for an AWS plugin", func(t *testing.T) {
require.Subset(t, p.PluginRequestConfig(context.Background(), "cloudwatch"), map[string]string{
"AWS_AUTH_AssumeRoleEnabled": "false",
"AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys",
"AWS_AUTH_EXTERNAL_ID": "mock_external_id",
"AWS_AUTH_SESSION_DURATION": "10m",
"AWS_CW_LIST_METRICS_PAGE_LIMIT": "100",
})
})
t.Run("does not use the aws settings for a non-aws plugin", func(t *testing.T) {
m := p.PluginRequestConfig(context.Background(), "")
require.NotContains(t, m, "AWS_AUTH_AssumeRoleEnabled")
require.NotContains(t, m, "AWS_AUTH_AllowedAuthProviders")
require.NotContains(t, m, "AWS_AUTH_EXTERNAL_ID")
require.NotContains(t, m, "AWS_AUTH_SESSION_DURATION")
require.NotContains(t, m, "AWS_CW_LIST_METRICS_PAGE_LIMIT")
})
t.Run("uses the aws settings for a non-aws user-specified plugin", func(t *testing.T) {
cfg.AWSForwardSettingsPlugins = append(cfg.AWSForwardSettingsPlugins, "test-datasource")
p = NewRequestConfigProvider(cfg)
require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource"), map[string]string{
"AWS_AUTH_AssumeRoleEnabled": "false",
"AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys",
"AWS_AUTH_EXTERNAL_ID": "mock_external_id",
"AWS_AUTH_SESSION_DURATION": "10m",
"AWS_CW_LIST_METRICS_PAGE_LIMIT": "100",
})
})
}
@@ -0,0 +1,30 @@
package pluginconfig
import (
"fmt"
"github.com/grafana/grafana/pkg/infra/tracing"
pCfg "github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/setting"
)
// newTracingCfg creates a plugins tracing configuration based on the provided Grafana tracing config.
// If OpenTelemetry (OTLP) is disabled, a zero-value OpenTelemetryCfg is returned.
func newTracingCfg(grafanaCfg *setting.Cfg) (pCfg.Tracing, error) {
ots, err := tracing.ParseSettings(grafanaCfg)
if err != nil {
return pCfg.Tracing{}, fmt.Errorf("parse settings: %w", err)
}
if !ots.OTelExporterEnabled() {
return pCfg.Tracing{}, nil
}
return pCfg.Tracing{
OpenTelemetry: pCfg.OpenTelemetryCfg{
Address: ots.Address,
Propagation: ots.Propagation,
Sampler: ots.Sampler,
SamplerParam: ots.SamplerParam,
SamplerRemoteURL: ots.SamplerRemoteURL,
},
}, nil
}
@@ -0,0 +1,50 @@
package pluginconfig
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewTracingCfg(t *testing.T) {
t.Run("empty", func(t *testing.T) {
cfg := setting.NewCfg()
tracingCfg, err := newTracingCfg(cfg)
require.NoError(t, err)
assert.False(t, tracingCfg.IsEnabled(), "tracing should be disabled")
assert.Empty(t, tracingCfg.OpenTelemetry.Address)
assert.Empty(t, tracingCfg.OpenTelemetry.Propagation)
})
t.Run("enabled", func(t *testing.T) {
for _, tc := range []struct {
name string
propagation string
}{
{"empty", ""},
{"jaeger", "jaeger"},
{"w3c", "w3c"},
{"multiple", "jaeger,w3c"},
} {
t.Run(tc.name, func(t *testing.T) {
const address = "127.0.0.1:4317"
cfg := setting.NewCfg()
otlpSect := cfg.Raw.Section("tracing.opentelemetry.otlp")
otlpSect.Key("address").SetValue(address)
if tc.propagation != "" {
otlpSect.Key("propagation").SetValue(tc.propagation)
}
tracingCfg, err := newTracingCfg(cfg)
require.NoError(t, err)
assert.True(t, tracingCfg.IsEnabled(), "tracing should be enabled")
assert.Equal(t, address, tracingCfg.OpenTelemetry.Address)
assert.Equal(t, tc.propagation, tracingCfg.OpenTelemetry.Propagation)
})
}
})
}