Skip to content

Commit

Permalink
Add support for grafana config to PluginContext (#726)
Browse files Browse the repository at this point in the history
* first pass

* fix linter

* update factory func

* simplify field

* fix linter

* tidy

* fix linter

* apply PR feedback

* update field name

* update field

* refactor instance stale check

* fix linter

* tidy

* Revert "refactor instance stale check"

This reverts commit 0273225.

* update proto field name

* update func names and remove unused func

* rename func and fix imports

* add pdc to cfg

* fix linter

* remove newline

* update field names
  • Loading branch information
wbrowne authored Sep 19, 2023
1 parent 05a3321 commit be36c43
Show file tree
Hide file tree
Showing 21 changed files with 799 additions and 329 deletions.
22 changes: 14 additions & 8 deletions backend/app/instance_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"context"
"errors"
"fmt"

"github.com/grafana/grafana-plugin-sdk-go/backend"
Expand All @@ -10,7 +11,7 @@ import (
)

// InstanceFactoryFunc factory method for creating app instances.
type InstanceFactoryFunc func(settings backend.AppInstanceSettings) (instancemgmt.Instance, error)
type InstanceFactoryFunc func(ctx context.Context, settings backend.AppInstanceSettings) (instancemgmt.Instance, error)

// NewInstanceManager creates a new app instance manager,
//
Expand Down Expand Up @@ -44,8 +45,7 @@ type instanceProvider struct {

func (ip *instanceProvider) GetKey(ctx context.Context, pluginContext backend.PluginContext) (interface{}, error) {
if pluginContext.AppInstanceSettings == nil {
// fail fast if there is no app settings
return nil, fmt.Errorf("app instance settings cannot be nil")
return nil, errors.New("app instance settings cannot be nil")
}

// The instance key generated for app plugins should include both plugin ID, and the OrgID, since for a single
Expand All @@ -59,11 +59,17 @@ func (ip *instanceProvider) GetKey(ctx context.Context, pluginContext backend.Pl
}

func (ip *instanceProvider) NeedsUpdate(_ context.Context, pluginContext backend.PluginContext, cachedInstance instancemgmt.CachedInstance) bool {
curSettings := pluginContext.AppInstanceSettings
cachedSettings := cachedInstance.PluginContext.AppInstanceSettings
return !curSettings.Updated.Equal(cachedSettings.Updated)
curConfig := pluginContext.GrafanaConfig
cachedConfig := cachedInstance.PluginContext.GrafanaConfig
configUpdated := !cachedConfig.Equal(curConfig)

cachedAppSettings := cachedInstance.PluginContext.AppInstanceSettings
curAppSettings := pluginContext.AppInstanceSettings
appUpdated := !curAppSettings.Updated.Equal(cachedAppSettings.Updated)

return appUpdated || configUpdated
}

func (ip *instanceProvider) NewInstance(_ context.Context, pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
return ip.factory(*pluginContext.AppInstanceSettings)
func (ip *instanceProvider) NewInstance(ctx context.Context, pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
return ip.factory(ctx, *pluginContext.AppInstanceSettings)
}
168 changes: 154 additions & 14 deletions backend/app/instance_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ func TestInstanceProvider(t *testing.T) {
type testInstance struct {
value string
}
ip := NewInstanceProvider(func(settings backend.AppInstanceSettings) (instancemgmt.Instance, error) {
ip := NewInstanceProvider(func(ctx context.Context, settings backend.AppInstanceSettings) (instancemgmt.Instance, error) {
return testInstance{value: "what an app"}, nil
})

t.Run("When data source instance settings not provided should return error", func(t *testing.T) {
t.Run("When app instance settings not provided should return error", func(t *testing.T) {
_, err := ip.GetKey(context.Background(), backend.PluginContext{})
require.Error(t, err)
})
Expand All @@ -38,38 +38,66 @@ func TestInstanceProvider(t *testing.T) {
require.Equal(t, "super-app-plugin#42", key)
})

t.Run("When current app instance settings compared to cached instance haven't been updated should return false", func(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{
"foo": "bar",
}

curSettings := backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: time.Now(),
},
GrafanaConfig: backend.NewGrafanaCfg(config),
}
cachedInstance := instancemgmt.CachedInstance{
PluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: curSettings.AppInstanceSettings.Updated,
},

cachedSettings := backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: curSettings.AppInstanceSettings.Updated,
},
GrafanaConfig: backend.NewGrafanaCfg(config),
}

cachedInstance := instancemgmt.CachedInstance{
PluginContext: cachedSettings,
}
needsUpdate := ip.NeedsUpdate(context.Background(), curSettings, cachedInstance)
require.False(t, needsUpdate)
})

t.Run("When current app instance settings compared to cached instance have been updated should return true", func(t *testing.T) {
t.Run("When either the config or updated field of current app instance settings are not equal to the cache, should return true", func(t *testing.T) {
curSettings := backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: time.Now(),
},
}
cachedInstance := instancemgmt.CachedInstance{
PluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: curSettings.AppInstanceSettings.Updated.Add(time.Second),
},

cachedSettings := backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: curSettings.AppInstanceSettings.Updated.Add(time.Second),
},
}

cachedInstance := instancemgmt.CachedInstance{
PluginContext: cachedSettings,
}
needsUpdate := ip.NeedsUpdate(context.Background(), curSettings, cachedInstance)
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{
"foo": "bar",
})

cachedSettings.GrafanaConfig = backend.NewGrafanaCfg(map[string]string{
"baz": "qux",
})

cachedInstance = instancemgmt.CachedInstance{
PluginContext: cachedSettings,
}
needsUpdate = ip.NeedsUpdate(context.Background(), curSettings, cachedInstance)
require.True(t, needsUpdate)
})
})

t.Run("When creating a new instance should return expected instance", func(t *testing.T) {
Expand All @@ -83,3 +111,115 @@ func TestInstanceProvider(t *testing.T) {
require.Equal(t, "what an app", i.(testInstance).value)
})
}

func Test_instanceProvider_NeedsUpdate(t *testing.T) {
ts := time.Now()

type args struct {
pluginContext backend.PluginContext
cachedInstance instancemgmt.CachedInstance
}
tests := []struct {
name string
args args
expected bool
}{
{
name: "Empty instance settings should return false",
args: args{
pluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{},
},
cachedInstance: instancemgmt.CachedInstance{
PluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{},
},
},
},
expected: false,
},
{
name: "Instance settings with identical updated field should return false",
args: args{
pluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts,
},
},
cachedInstance: instancemgmt.CachedInstance{
PluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts,
},
},
},
},
expected: false,
},
{
name: "Instance settings with identical updated field and config should return false",
args: args{
pluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts,
},
GrafanaConfig: backend.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"}),
},
},
},
expected: false,
},
{
name: "Instance settings with different updated field should return true",
args: args{
pluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts,
},
},
cachedInstance: instancemgmt.CachedInstance{
PluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts.Add(time.Millisecond),
},
},
},
},
expected: true,
},
{
name: "Instance settings with identical updated field and different config should return true",
args: args{
pluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts,
},
GrafanaConfig: backend.NewGrafanaCfg(map[string]string{"foo": "bar"}),
},
cachedInstance: instancemgmt.CachedInstance{
PluginContext: backend.PluginContext{
AppInstanceSettings: &backend.AppInstanceSettings{
Updated: ts,
},
},
},
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ip := &instanceProvider{}
if got := ip.NeedsUpdate(context.Background(), tt.args.pluginContext, tt.args.cachedInstance); got != tt.expected {
t.Errorf("NeedsUpdate() = %v, expected %v", got, tt.expected)
}
})
}
}
3 changes: 3 additions & 0 deletions backend/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ type PluginContext struct {
//
// Will only be set if request targeting a data source instance.
DataSourceInstanceSettings *DataSourceInstanceSettings

// GrafanaConfig is the configuration settings provided by Grafana.
GrafanaConfig *GrafanaCfg
}

func setCustomOptionsFromHTTPSettings(opts *httpclient.Options, httpSettings *HTTPSettings) {
Expand Down
111 changes: 111 additions & 0 deletions backend/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package backend

import (
"context"
"strconv"
"strings"

"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
)

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)
}

return v.(*GrafanaCfg)
}

// withGrafanaConfig injects supplied Grafana config into context.
func withGrafanaConfig(ctx context.Context, cfg *GrafanaCfg) context.Context {
ctx = context.WithValue(ctx, configKey{}, cfg)
return ctx
}

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 {
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
}

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 (pc Proxy) ClientConfig() proxy.ClientCfg {
return pc.clientCfg
}

func (c *GrafanaCfg) Proxy() Proxy {
if v, exists := c.config[proxy.PluginSecureSocksProxyEnabled]; exists && v == strconv.FormatBool(true) {
return Proxy{
clientCfg: proxy.ClientCfg{
Enabled: true,
ClientCert: c.Get(proxy.PluginSecureSocksProxyClientCert),
ClientKey: c.Get(proxy.PluginSecureSocksProxyClientKey),
RootCA: c.Get(proxy.PluginSecureSocksProxyRootCACert),
ProxyAddress: c.Get(proxy.PluginSecureSocksProxyProxyAddress),
ServerName: c.Get(proxy.PluginSecureSocksProxyServerName),
},
}
}
return Proxy{clientCfg: proxy.ClientCfg{}}
}
5 changes: 5 additions & 0 deletions backend/convert_from_protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func (f ConvertFromProtobuf) PluginContext(proto *pluginv2.PluginContext) Plugin
User: f.User(proto.User),
AppInstanceSettings: f.AppInstanceSettings(proto.AppInstanceSettings),
DataSourceInstanceSettings: f.DataSourceInstanceSettings(proto.DataSourceInstanceSettings, proto.PluginId),
GrafanaConfig: f.GrafanaConfig(proto.GrafanaConfig),
}
}

Expand Down Expand Up @@ -276,3 +277,7 @@ func (f ConvertFromProtobuf) StreamPacket(protoReq *pluginv2.StreamPacket) *Stre
Data: protoReq.GetData(),
}
}

func (f ConvertFromProtobuf) GrafanaConfig(cfg map[string]string) *GrafanaCfg {
return NewGrafanaCfg(cfg)
}
Loading

0 comments on commit be36c43

Please sign in to comment.