diff --git a/pkg/services/updatemanager/plugins.go b/pkg/services/updatemanager/plugins.go index 79270f8a1ee..7ee11b261f9 100644 --- a/pkg/services/updatemanager/plugins.go +++ b/pkg/services/updatemanager/plugins.go @@ -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) } diff --git a/pkg/services/updatemanager/plugins_test.go b/pkg/services/updatemanager/plugins_test.go index 0455963f168..75834c44c7d 100644 --- a/pkg/services/updatemanager/plugins_test.go +++ b/pkg/services/updatemanager/plugins_test.go @@ -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() + }) +}