Chore: Migrate pluginsAutoUpdate flag to OpenFeature (#114404)

* Chore: Migrate pluginsAutoUpdate flag to OpenFeature

* Update workspace

* fixup! Chore: Migrate pluginsAutoUpdate flag to OpenFeature

* Add a test

* Refactor

* Apply suggestion from @hairyhenderson

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>

* Apply suggestions

* Update pkg/services/updatemanager/plugins_test.go

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>

* Reorder code blocks

---------

Co-authored-by: Dave Henderson <dave.henderson@grafana.com>
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
Tania
2025-12-10 17:40:30 +01:00
committed by GitHub
co-authored by Dave Henderson Will Browne
parent 094b6a36dc
commit 3ec1c27ad4
2 changed files with 101 additions and 12 deletions
+12 -12
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/open-feature/go-sdk/openfeature"
"go.opentelemetry.io/otel/codes"
"github.com/grafana/grafana/pkg/infra/httpclient/httpclientprovider"
@@ -96,11 +97,7 @@ func (s *PluginsService) IsDisabled() bool {
}
func (s *PluginsService) Run(ctx context.Context) error {
s.instrumentedCheckForUpdates(ctx)
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) {
s.updateAll(ctx)
}
s.checkAndUpdate(ctx)
ticker := time.NewTicker(time.Minute * 10)
run := true
@@ -108,11 +105,7 @@ func (s *PluginsService) Run(ctx context.Context) error {
for run {
select {
case <-ticker.C:
s.instrumentedCheckForUpdates(ctx)
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) {
s.updateAll(ctx)
}
s.checkAndUpdate(ctx)
case <-ctx.Done():
run = false
}
@@ -140,6 +133,14 @@ func (s *PluginsService) HasUpdate(ctx context.Context, pluginID string) (string
return "", false
}
// checkAndUpdate checks for updates and applies them if auto-update is enabled.
func (s *PluginsService) checkAndUpdate(ctx context.Context) {
s.instrumentedCheckForUpdates(ctx)
if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) {
s.updateAll(ctx)
}
}
func (s *PluginsService) instrumentedCheckForUpdates(ctx context.Context) {
start := time.Now()
ctx, span := s.tracer.Start(ctx, "updatechecker.PluginsService.checkForUpdates")
@@ -226,8 +227,7 @@ func (s *PluginsService) canUpdate(ctx context.Context, plugin pluginstore.Plugi
return false
}
//nolint:staticcheck // not yet migrated to OpenFeature
if s.features.IsEnabledGlobally(featuremgmt.FlagPluginsAutoUpdate) {
if openfeature.NewDefaultClient().Boolean(ctx, featuremgmt.FlagPluginsAutoUpdate, false, openfeature.TransactionContext(ctx)) {
return s.updateChecker.CanUpdate(plugin.ID, plugin.Info.Version, gcomVersion, s.updateStrategy == setting.PluginUpdateStrategyMinor)
}
@@ -6,8 +6,10 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"testing"
"github.com/open-feature/go-sdk/openfeature"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
@@ -19,6 +21,7 @@ import (
"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 {
@@ -240,6 +243,7 @@ func TestPluginUpdateChecker_checkForUpdates(t *testing.T) {
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{}
@@ -300,3 +304,88 @@ func (c *fakeHTTPClient) Do(req *http.Request) (*http.Response, error) {
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]bool{
featuremgmt.FlagPluginsAutoUpdate: flagValue,
},
})
require.NoError(t, err)
t.Cleanup(func() {
_ = openfeature.SetProviderAndWait(openfeature.NoopProvider{})
openfeatureTestMutex.Unlock()
})
}