diff --git a/backend/common.go b/backend/common.go index 885d00486..183c071e8 100644 --- a/backend/common.go +++ b/backend/common.go @@ -7,7 +7,6 @@ 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/internal/tenant" ) @@ -161,52 +160,6 @@ func (s *DataSourceInstanceSettings) GVK() GroupVersionKind { } } -// PluginContext holds contextual information about a plugin request, such as -// Grafana organization, user and plugin instance settings. -type PluginContext struct { - // OrgID is The Grafana organization identifier the request originating from. - OrgID int64 - - // PluginID is the unique identifier of the plugin that the request is for. - PluginID string - - // PluginVersion is the version of the plugin that the request is for. - PluginVersion string - - // User is the Grafana user making the request. - // - // Will not be provided if Grafana backend initiated the request, - // for example when request is coming from Grafana Alerting. - User *User - - // AppInstanceSettings is the configured app instance settings. - // - // In Grafana an app instance is an app plugin of certain - // type that have been configured and enabled in a Grafana organization. - // - // Will only be set if request targeting an app instance. - AppInstanceSettings *AppInstanceSettings - - // DataSourceConfig is the configured data source instance - // settings. - // - // In Grafana a data source instance is a data source plugin of certain - // type that have been configured and created in a Grafana organization. - // - // Will only be set if request targeting a data source instance. - DataSourceInstanceSettings *DataSourceInstanceSettings - - // GrafanaConfig is the configuration settings provided by Grafana. - GrafanaConfig *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. - UserAgent *useragent.UserAgent - - // The requested API version - APIVersion string -} - func setCustomOptionsFromHTTPSettings(opts *httpclient.Options, httpSettings *HTTPSettings) { opts.CustomOptions = map[string]interface{}{} diff --git a/backend/plugin_context.go b/backend/plugin_context.go new file mode 100644 index 000000000..ac0419973 --- /dev/null +++ b/backend/plugin_context.go @@ -0,0 +1,112 @@ +package backend + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" + + "github.com/grafana/grafana-plugin-sdk-go/backend/useragent" +) + +// PluginContext holds contextual information about a plugin request, such as +// Grafana organization, user and plugin instance settings. +type PluginContext struct { + // OrgID is The Grafana organization identifier the request originating from. + OrgID int64 + + // PluginID is the unique identifier of the plugin that the request is for. + PluginID string + + // PluginVersion is the version of the plugin that the request is for. + PluginVersion string + + // User is the Grafana user making the request. + // + // Will not be provided if Grafana backend initiated the request, + // for example when request is coming from Grafana Alerting. + User *User + + // AppInstanceSettings is the configured app instance settings. + // + // In Grafana an app instance is an app plugin of certain + // type that have been configured and enabled in a Grafana organization. + // + // Will only be set if request targeting an app instance. + AppInstanceSettings *AppInstanceSettings + + // DataSourceConfig is the configured data source instance + // settings. + // + // In Grafana a data source instance is a data source plugin of certain + // type that have been configured and created in a Grafana organization. + // + // Will only be set if request targeting a data source instance. + DataSourceInstanceSettings *DataSourceInstanceSettings + + // GrafanaConfig is the configuration settings provided by Grafana. + GrafanaConfig *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. + UserAgent *useragent.UserAgent + + // The requested API version + APIVersion string +} + +// GetSettingFromEnv retrieves the environment variable value based on the provided key. +// It first checks for a general plugin setting, then a plugin-specific setting, and finally +// a data source-specific setting. +// The search is case-insensitive for the key but case-sensitive for the data source UID. +// NOTE: This method can't be used in multi-tenant environment such as grafana cloud +func (pCtx *PluginContext) GetSettingFromEnv(key string) (output string) { + key = strings.TrimSpace(strings.ToUpper(key)) + + if v := strings.TrimSpace(os.Getenv(fmt.Sprintf("GF_PLUGIN_%s", key))); v != "" { + output = v + } + + if pCtx == nil { + return output + } + + pluginID := strings.TrimSpace(strings.ToUpper(pCtx.PluginID)) + if v := strings.TrimSpace(os.Getenv(fmt.Sprintf("GF_PLUGIN_%s_%s", pluginID, key))); v != "" && pluginID != "" { + output = v + } + + if pCtx.DataSourceInstanceSettings == nil { + return output + } + + dsUID := strings.TrimSpace(strings.ToUpper(pCtx.DataSourceInstanceSettings.UID)) + if v := strings.TrimSpace(os.Getenv(fmt.Sprintf("GF_DS_%s_%s", dsUID, key))); v != "" && dsUID != "" { + output = v + } + + caseSensitiveDsUID := strings.TrimSpace(pCtx.DataSourceInstanceSettings.UID) + if v := strings.TrimSpace(os.Getenv(fmt.Sprintf("GF_DS_%s_%s", caseSensitiveDsUID, key))); v != "" && caseSensitiveDsUID != "" { + output = v + } + + return output +} + +// GetSettingAsBoolFromEnv retrieves the environment variable value as boolean based on the provided key. +// NOTE: This method can't be used in multi-tenant environment such as grafana cloud +func (pCtx *PluginContext) GetSettingAsBoolFromEnv(key string, defaultValue bool) (output bool, err error) { + if pCtx == nil { + return defaultValue, errors.New("invalid plugin context") + } + strValue := pCtx.GetSettingFromEnv(key) + if strValue == "" { + return defaultValue, nil + } + value, err := strconv.ParseBool(strValue) + if err != nil { + return defaultValue, fmt.Errorf("environment variable '%s' is invalid bool value '%s'", key, strValue) + } + return value, nil +} diff --git a/backend/plugin_context_test.go b/backend/plugin_context_test.go new file mode 100644 index 000000000..5d0804698 --- /dev/null +++ b/backend/plugin_context_test.go @@ -0,0 +1,74 @@ +package backend_test + +import ( + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/stretchr/testify/require" +) + +func TestPluginContext_GetSettingsFromEnv(t *testing.T) { + t.Run("should return blank value if no env variables present", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + output := pCtx.GetSettingFromEnv("MY_KEY") + require.Empty(t, output) + }) + t.Run("should respect GF_PLUGIN_KEY value", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + t.Setenv("GF_PLUGIN_MY_KEY", "foo") + output := pCtx.GetSettingFromEnv("MY_KEY") + require.Equal(t, "foo", output) + }) + t.Run("should respect GF_PLUGIN_PLUGIN_ID_KEY value", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + t.Setenv("GF_PLUGIN_MY_KEY", "foo") + t.Setenv("GF_PLUGIN_MY-PLUGIN-ID_MY_KEY", "bar") + output := pCtx.GetSettingFromEnv("MY_KEY") + require.Equal(t, "bar", output) + }) + t.Run("should respect GF_DS_DS_ID_KEY value", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + t.Setenv("GF_PLUGIN_MY_KEY", "foo") + t.Setenv("GF_PLUGIN_MY-PLUGIN-ID_MY_KEY", "bar") + t.Setenv("GF_DS_MYDSUID_MY_KEY", "baz") + output := pCtx.GetSettingFromEnv("MY_KEY") + require.Equal(t, "baz", output) + }) + t.Run("should respect case sensitive ds uid", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + t.Setenv("GF_PLUGIN_MY_KEY", "foo") + t.Setenv("GF_PLUGIN_MY-PLUGIN-ID_MY_KEY", "bar") + t.Setenv("GF_DS_myDsUid_MY_KEY", "BaZ") + t.Setenv("GF_DS_MYDSUID_MY_KEY", "baz") + output := pCtx.GetSettingFromEnv("MY_KEY") + require.Equal(t, "BaZ", output) + }) +} + +func TestPluginContext_GetSettingsAsBoolFromEnv(t *testing.T) { + t.Run("should return default value when no env variables present", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + output, err := pCtx.GetSettingAsBoolFromEnv("MY_KEY", false) + require.Nil(t, err) + require.Equal(t, false, output) + }) + t.Run("should return default value when no env variables present but default value", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + output, err := pCtx.GetSettingAsBoolFromEnv("MY_KEY", true) + require.Nil(t, err) + require.Equal(t, true, output) + }) + t.Run("should fail with incorrect bool value", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + t.Setenv("GF_PLUGIN_MY_KEY", "foo") + _, err := pCtx.GetSettingAsBoolFromEnv("MY_KEY", false) + require.NotNil(t, err) + }) + t.Run("should parse with correct bool value from environment", func(t *testing.T) { + pCtx := backend.PluginContext{PluginID: "my-plugin-id", DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{UID: "myDsUid"}} + t.Setenv("GF_PLUGIN_MY_KEY", "True") + output, err := pCtx.GetSettingAsBoolFromEnv("MY_KEY", false) + require.Nil(t, err) + require.Equal(t, true, output) + }) +}