ce9ab6a89a
* initial commit * add support of integerts * finialise the static provider * minor refactoring * the rest * revert: the rest * add new thiongs * more tests added * add ff parsing tests to check if types are handled correctly * update tests according to recent changes * address golint issues * Update pkg/setting/setting_feature_toggles.go Co-authored-by: Dave Henderson <dave.henderson@grafana.com> * fix rebase issues * addressing review comments * add test cases for enterprise * handle enterprise cases * minor refactoring to make api a bit easier to debug * make test names a bit more precise * fix linter * add openfeature sdk to goleak ignore in testutil * Remove only boolean check in ff gen tests * add non-boolean types top the doc in default.ini and doc string in FeatureFlag type * apply remarks, add docs to sample.ini * reflect changes in feature flags in the public grafana configuration doc * fix doc formatting * apply suggestions to the doc file --------- Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
395 lines
11 KiB
Go
395 lines
11 KiB
Go
package updatemanager
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/open-feature/go-sdk/openfeature"
|
|
"github.com/open-feature/go-sdk/openfeature/memprovider"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
type mockPluginPreinstall struct {
|
|
pluginchecker.Preinstall
|
|
}
|
|
|
|
func (m *mockPluginPreinstall) IsPinned(pluginID string) bool {
|
|
return false
|
|
}
|
|
|
|
func TestPluginUpdateChecker_HasUpdate(t *testing.T) {
|
|
t.Run("update is available", func(t *testing.T) {
|
|
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
|
|
|
|
svc := PluginsService{
|
|
availableUpdates: map[string]availableUpdate{
|
|
"test-ds": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "1.0.0",
|
|
},
|
|
},
|
|
pluginStore: &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-ds",
|
|
Info: plugins.Info{Version: "0.9.0"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
updateCheckURL: updateCheckURL,
|
|
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
|
|
features: &featuremgmt.FeatureManager{},
|
|
}
|
|
|
|
update, exists := svc.HasUpdate(context.Background(), "test-ds")
|
|
require.True(t, exists)
|
|
require.Equal(t, "1.0.0", update)
|
|
})
|
|
|
|
t.Run("update is not available", func(t *testing.T) {
|
|
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
|
|
|
|
svc := PluginsService{
|
|
availableUpdates: map[string]availableUpdate{
|
|
"test-panel": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "0.9.0",
|
|
},
|
|
"test-app": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "0.9.0",
|
|
},
|
|
},
|
|
pluginStore: &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-ds",
|
|
Info: plugins.Info{Version: "0.9.0"},
|
|
},
|
|
},
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-panel",
|
|
Info: plugins.Info{Version: "0.9.0"},
|
|
},
|
|
},
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-app",
|
|
Info: plugins.Info{Version: "0.9.0"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
updateCheckURL: updateCheckURL,
|
|
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
|
|
}
|
|
|
|
update, exists := svc.HasUpdate(context.Background(), "test-ds")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
|
|
update, exists = svc.HasUpdate(context.Background(), "test-panel")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
|
|
update, exists = svc.HasUpdate(context.Background(), "test-app")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
})
|
|
|
|
t.Run("update is available but plugin is not in store", func(t *testing.T) {
|
|
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
|
|
|
|
svc := PluginsService{
|
|
availableUpdates: map[string]availableUpdate{
|
|
"test-panel": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "0.9.0",
|
|
},
|
|
},
|
|
pluginStore: &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-ds",
|
|
Info: plugins.Info{Version: "1.0.0"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
updateCheckURL: updateCheckURL,
|
|
}
|
|
|
|
update, exists := svc.HasUpdate(context.Background(), "test-panel")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
|
|
update, exists = svc.HasUpdate(context.Background(), "test-ds")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
})
|
|
}
|
|
|
|
func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
|
|
t.Run("update is available", func(t *testing.T) {
|
|
jsonResp := `[
|
|
{
|
|
"slug": "test-ds",
|
|
"version": "1.0.12"
|
|
},
|
|
{
|
|
"slug": "test-panel",
|
|
"version": "2.5.7"
|
|
},
|
|
{
|
|
"slug": "test-core-panel",
|
|
"version": "1.0.0"
|
|
}
|
|
]`
|
|
|
|
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
|
|
|
|
svc := PluginsService{
|
|
availableUpdates: map[string]availableUpdate{
|
|
"test-app": {
|
|
localVersion: "0.5.0",
|
|
availableVersion: "1.0.0",
|
|
},
|
|
},
|
|
pluginStore: &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-ds",
|
|
Info: plugins.Info{Version: "0.9.0"},
|
|
Type: plugins.TypeDataSource,
|
|
},
|
|
Class: plugins.ClassExternal,
|
|
},
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-app",
|
|
Info: plugins.Info{Version: "0.5.0"},
|
|
Type: plugins.TypeApp,
|
|
},
|
|
Class: plugins.ClassExternal,
|
|
},
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-panel",
|
|
Info: plugins.Info{Version: "2.5.7"},
|
|
Type: plugins.TypePanel,
|
|
},
|
|
Class: plugins.ClassExternal,
|
|
},
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "test-core-panel",
|
|
Info: plugins.Info{Version: "0.0.1"},
|
|
Type: plugins.TypePanel,
|
|
},
|
|
Class: plugins.ClassCore,
|
|
},
|
|
},
|
|
},
|
|
httpClient: &fakeHTTPClient{
|
|
fakeResp: jsonResp,
|
|
},
|
|
log: log.NewNopLogger(),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
updateCheckURL: updateCheckURL,
|
|
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
|
|
features: &featuremgmt.FeatureManager{},
|
|
}
|
|
|
|
svc.instrumentedCheckForUpdates(context.Background())
|
|
|
|
require.Equal(t, 1, len(svc.availableUpdates))
|
|
|
|
require.Equal(t, "1.0.12", svc.availableUpdates["test-ds"].availableVersion)
|
|
update, exists := svc.HasUpdate(context.Background(), "test-ds")
|
|
require.True(t, exists)
|
|
require.Equal(t, "1.0.12", update)
|
|
|
|
require.Empty(t, svc.availableUpdates["test-app"])
|
|
update, exists = svc.HasUpdate(context.Background(), "test-app")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
|
|
require.Empty(t, svc.availableUpdates["test-panel"])
|
|
update, exists = svc.HasUpdate(context.Background(), "test-panel")
|
|
require.False(t, exists)
|
|
require.Empty(t, update)
|
|
|
|
require.Empty(t, svc.availableUpdates["test-core-panel"])
|
|
})
|
|
}
|
|
|
|
func TestPluginUpdateChecker_updateAll(t *testing.T) {
|
|
t.Run("update is available", func(t *testing.T) {
|
|
pluginsFakeStore := map[string]string{}
|
|
availableUpdates := map[string]availableUpdate{
|
|
"test-app-0": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "1.0.0",
|
|
},
|
|
"test-app-1": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "1.0.0",
|
|
},
|
|
"test-app-2": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "1.0.0",
|
|
},
|
|
}
|
|
|
|
svc := PluginsService{
|
|
availableUpdates: availableUpdates,
|
|
log: log.NewNopLogger(),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
pluginInstaller: &pluginfakes.FakePluginInstaller{
|
|
AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
|
|
pluginsFakeStore[pluginID] = version
|
|
return nil
|
|
},
|
|
RemoveFunc: func(ctx context.Context, pluginID, version string) error {
|
|
delete(pluginsFakeStore, pluginID)
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
svc.updateAll(context.Background())
|
|
|
|
require.Equal(t, 0, len(svc.availableUpdates))
|
|
require.Equal(t, len(availableUpdates), len(pluginsFakeStore))
|
|
|
|
for pluginID, availableUpdate := range availableUpdates {
|
|
require.Equal(t, availableUpdate.availableVersion, pluginsFakeStore[pluginID])
|
|
}
|
|
})
|
|
}
|
|
|
|
type fakeHTTPClient struct {
|
|
fakeResp string
|
|
|
|
requestURL string
|
|
}
|
|
|
|
func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|
c.requestURL = req.URL.String()
|
|
|
|
resp := &http.Response{
|
|
Body: io.NopCloser(strings.NewReader(c.fakeResp)),
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func TestPluginsService_PluginsAutoUpdateFlag(t *testing.T) {
|
|
updateCheckURL, _ := url.Parse("https://grafana.com/api/plugins/versioncheck")
|
|
|
|
tests := []struct {
|
|
name string
|
|
flagEnabled bool
|
|
expectUpdate bool
|
|
}{
|
|
{
|
|
name: "pluginsAutoUpdate enabled calls updateAll",
|
|
flagEnabled: true,
|
|
expectUpdate: true,
|
|
},
|
|
{
|
|
name: "pluginsAutoUpdate disabled does not call updateAll",
|
|
flagEnabled: false,
|
|
expectUpdate: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
setupOpenFeatureProvider(t, tt.flagEnabled)
|
|
|
|
updateCallCount := 0
|
|
|
|
availableUpdates := map[string]availableUpdate{
|
|
"test-plugin": {
|
|
localVersion: "0.9.0",
|
|
availableVersion: "1.0.0",
|
|
},
|
|
}
|
|
|
|
svc := &PluginsService{
|
|
availableUpdates: availableUpdates,
|
|
httpClient: &fakeHTTPClient{
|
|
fakeResp: `[]`,
|
|
},
|
|
log: log.NewNopLogger(),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
updateCheckURL: updateCheckURL,
|
|
updateChecker: pluginchecker.ProvideService(managedplugins.NewNoop(), provisionedplugins.NewNoop(), &mockPluginPreinstall{}),
|
|
pluginStore: &pluginstore.FakePluginStore{PluginList: []pluginstore.Plugin{}},
|
|
pluginInstaller: &pluginfakes.FakePluginInstaller{
|
|
AddFunc: func(ctx context.Context, pluginID, version string, opts plugins.AddOpts) error {
|
|
updateCallCount++
|
|
return nil
|
|
},
|
|
},
|
|
grafanaVersion: "10.0.0",
|
|
}
|
|
|
|
ctx := context.Background()
|
|
// Test the synchronous initialization work directly, without the long-running ticker loop
|
|
svc.checkAndUpdate(ctx)
|
|
|
|
if tt.expectUpdate {
|
|
require.Equal(t, updateCallCount, 1, "updateAll should be called when flag is enabled")
|
|
} else {
|
|
require.Equal(t, 0, updateCallCount, "updateAll should not be called when flag is disabled")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var openfeatureTestMutex sync.Mutex
|
|
|
|
func setupOpenFeatureProvider(t *testing.T, flagValue bool) {
|
|
t.Helper()
|
|
openfeatureTestMutex.Lock()
|
|
|
|
err := featuremgmt.InitOpenFeature(featuremgmt.OpenFeatureConfig{
|
|
ProviderType: setting.StaticProviderType,
|
|
StaticFlags: map[string]memprovider.InMemoryFlag{
|
|
featuremgmt.FlagPluginsAutoUpdate: {
|
|
Key: featuremgmt.FlagPluginsAutoUpdate, Variants: map[string]any{"": flagValue},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
_ = openfeature.SetProviderAndWait(openfeature.NoopProvider{})
|
|
openfeatureTestMutex.Unlock()
|
|
})
|
|
}
|