Plugins: Support MT app registration (#113348)

This commit is contained in:
Todd Treece
2025-12-02 09:59:46 -05:00
committed by GitHub
parent 77e13f7ef8
commit bdf529c545
22 changed files with 385 additions and 240 deletions
@@ -4,21 +4,29 @@ import (
"context"
"github.com/grafana/grafana/apps/plugins/pkg/app/install"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
var _ installsync.Syncer = &FakeSyncer{}
type FakeSyncer struct {
SyncFunc func(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error
SyncFunc func(ctx context.Context, source install.Source, installedPlugins []pluginstore.Plugin) error
}
func NewFakeSyncer() *FakeSyncer {
return &FakeSyncer{}
}
func (f *FakeSyncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
func (f *FakeSyncer) IsDisabled() bool {
return false
}
func (f *FakeSyncer) Run(ctx context.Context) error {
return nil
}
func (f *FakeSyncer) Sync(ctx context.Context, source install.Source, installedPlugins []pluginstore.Plugin) error {
if f.SyncFunc != nil {
return f.SyncFunc(ctx, source, installedPlugins)
}
@@ -4,18 +4,25 @@ import (
"context"
"time"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/resource"
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
"github.com/grafana/grafana/apps/plugins/pkg/app/install"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/configprovider"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
const (
ServiceName = "plugins.installsync"
syncerLockActionName = "plugin-install-api-sync"
)
@@ -25,7 +32,9 @@ var (
// Syncer is the interface for syncing plugin installations to the Kubernetes-style API.
type Syncer interface {
Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error
registry.BackgroundService
registry.CanBeDisabled
Sync(ctx context.Context, source install.Source, installedPlugins []pluginstore.Plugin) error
}
// ServerLock is the interface for acquiring distributed locks.
@@ -34,14 +43,22 @@ type ServerLock interface {
}
type syncer struct {
featureToggles featuremgmt.FeatureToggles
clientGenerator resource.ClientGenerator
installRegistrar *install.InstallRegistrar
orgService org.Service
namespaceMapper request.NamespaceMapper
serverLock ServerLock
services.NamedService
featureToggles featuremgmt.FeatureToggles
clientGenerator resource.ClientGenerator
installRegistrar *install.InstallRegistrar
orgService org.Service
namespaceMapper request.NamespaceMapper
serverLock ServerLock
restConfigProvider apiserver.RestConfigProvider
pluginsStoreService pluginstore.Store
}
var _ Syncer = (*syncer)(nil)
var _ registry.BackgroundService = (*syncer)(nil)
var _ registry.CanBeDisabled = (*syncer)(nil)
var _ services.NamedService = (*syncer)(nil)
// newSyncer creates a new syncer with the provided dependencies.
func newSyncer(
featureToggles featuremgmt.FeatureToggles,
@@ -50,15 +67,21 @@ func newSyncer(
orgService org.Service,
namespaceMapper request.NamespaceMapper,
serverLock ServerLock,
restConfigProvider apiserver.RestConfigProvider,
pluginsStoreService pluginstore.Store,
) *syncer {
return &syncer{
clientGenerator: clientGenerator,
featureToggles: featureToggles,
installRegistrar: installRegistrar,
orgService: orgService,
namespaceMapper: namespaceMapper,
serverLock: serverLock,
s := syncer{
clientGenerator: clientGenerator,
featureToggles: featureToggles,
installRegistrar: installRegistrar,
orgService: orgService,
namespaceMapper: namespaceMapper,
serverLock: serverLock,
restConfigProvider: restConfigProvider,
pluginsStoreService: pluginsStoreService,
}
s.NamedService = services.NewBasicService(nil, s.running, nil).WithName(ServiceName)
return &s
}
// ProvideSyncer creates a new Syncer for syncing plugin installations to the API.
@@ -68,6 +91,8 @@ func ProvideSyncer(
orgService org.Service,
cfgProvider configprovider.ConfigProvider,
serverLock ServerLock,
restConfigProvider apiserver.RestConfigProvider,
pluginsStoreService pluginstore.Store,
) (Syncer, error) {
cfg, err := cfgProvider.Get(context.Background())
if err != nil {
@@ -83,18 +108,49 @@ func ProvideSyncer(
orgService,
namespaceMapper,
serverLock,
restConfigProvider,
pluginsStoreService,
), nil
}
func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
func (s *syncer) IsDisabled() bool {
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync) {
return nil
syncEnabled := s.featureToggles.IsEnabled(context.Background(), featuremgmt.FlagPluginInstallAPISync)
//nolint:staticcheck // not yet migrated to OpenFeature
serviceLoadingEnabled := s.featureToggles.IsEnabled(context.Background(), featuremgmt.FlagPluginStoreServiceLoading)
return !syncEnabled || !serviceLoadingEnabled
}
func (s *syncer) Run(ctx context.Context) error {
if err := s.StartAsync(ctx); err != nil {
return err
}
return s.AwaitTerminated(context.Background())
}
func (s *syncer) running(ctx context.Context) error {
ctxLog := logging.FromContext(ctx)
restConfig, err := s.restConfigProvider.GetRestConfig(ctx)
if err != nil {
return err
}
discoveryClient, err := client.NewDiscoveryClient(restConfig)
if err != nil {
ctxLog.Warn("Failed to create discovery client, skipping plugin sync", "error", err)
}
if err := discoveryClient.WaitForAvailability(ctx, pluginsv0alpha1.PluginKind().GroupVersionKind().GroupVersion()); err != nil {
ctxLog.Warn("Failed to wait for plugin API availability, skipping plugin sync", "error", err)
}
//nolint:staticcheck // not yet migrated to OpenFeature
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginStoreServiceLoading) {
logging.DefaultLogger.Warn("pluginInstallAPISync is enabled, but pluginStoreServiceLoading is disabled. skipping plugin sync.")
if err := s.Sync(ctx, install.SourcePluginStore, s.pluginsStoreService.Plugins(ctx)); err != nil {
ctxLog.Warn("Failed to sync plugins", "error", err)
}
<-ctx.Done()
return nil
}
func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugins []pluginstore.Plugin) error {
if s.IsDisabled() {
return nil
}
@@ -113,13 +169,14 @@ func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugi
return syncErr
}
func (s *syncer) syncAllNamespaces(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
func (s *syncer) syncAllNamespaces(ctx context.Context, source install.Source, installedPlugins []pluginstore.Plugin) error {
orgs, err := s.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return err
}
for _, org := range orgs {
ctx = identity.WithServiceIdentityForSingleNamespaceContext(ctx, s.namespaceMapper(org.ID))
err := s.syncNamespace(ctx, s.namespaceMapper(org.ID), source, installedPlugins)
if err != nil {
return err
@@ -129,7 +186,7 @@ func (s *syncer) syncAllNamespaces(ctx context.Context, source install.Source, i
return nil
}
func (s *syncer) syncNamespace(ctx context.Context, namespace string, source install.Source, installedPlugins []*plugins.Plugin) error {
func (s *syncer) syncNamespace(ctx context.Context, namespace string, source install.Source, installedPlugins []pluginstore.Plugin) error {
client, err := s.installRegistrar.GetClient()
if err != nil {
return err
@@ -140,9 +197,9 @@ func (s *syncer) syncNamespace(ctx context.Context, namespace string, source ins
return err
}
installedMap := make(map[string]*plugins.Plugin)
installedMap := make(map[string]struct{})
for _, p := range installedPlugins {
installedMap[p.ID] = p
installedMap[p.ID] = struct{}{}
}
// unregister plugins that are not installed
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
func TestSyncer_Sync(t *testing.T) {
@@ -25,7 +26,7 @@ func TestSyncer_Sync(t *testing.T) {
name string
pluginInstallAPISyncEnabled bool
pluginStoreServiceEnabled bool
installedPlugins []*plugins.Plugin
installedPlugins []pluginstore.Plugin
orgs []*org.OrgDTO
orgServiceError error
serverLockError error
@@ -36,7 +37,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "plugin install API sync feature toggle disabled",
pluginInstallAPISyncEnabled: false,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
expectedError: nil,
expectSyncCalls: 0,
@@ -45,7 +46,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "plugin store service feature toggle disabled",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: false,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
expectedError: nil,
expectSyncCalls: 0,
@@ -54,7 +55,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "both feature toggles enabled, no orgs",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: []*org.OrgDTO{},
expectedError: nil,
expectSyncCalls: 0,
@@ -63,7 +64,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "both feature toggles enabled, empty installed plugins",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{},
installedPlugins: []pluginstore.Plugin{},
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
expectedError: nil,
expectSyncCalls: 0,
@@ -72,7 +73,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "both feature toggles enabled, single org",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
expectedError: nil,
expectSyncCalls: 1,
@@ -81,7 +82,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "both feature toggles enabled, multiple orgs",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: []*org.OrgDTO{
{ID: 1, Name: "Org 1"},
{ID: 2, Name: "Org 2"},
@@ -94,7 +95,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "org service error",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: nil,
orgServiceError: errors.New("org service error"),
expectedError: errors.New("org service error"),
@@ -104,7 +105,7 @@ func TestSyncer_Sync(t *testing.T) {
name: "server lock error",
pluginInstallAPISyncEnabled: true,
pluginStoreServiceEnabled: true,
installedPlugins: []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore}},
installedPlugins: []pluginstore.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}, Class: plugins.ClassCore}},
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
serverLockError: errors.New("lock error"),
expectedError: errors.New("lock error"),
@@ -156,6 +157,8 @@ func TestSyncer_Sync(t *testing.T) {
orgService,
func(orgID int64) string { return "org-1" },
serverLock,
nil,
nil,
)
// Execute
@@ -177,7 +180,7 @@ func TestSyncer_Sync(t *testing.T) {
func TestSyncer_syncNamespace(t *testing.T) {
tests := []struct {
name string
installedPlugins []*plugins.Plugin
installedPlugins []pluginstore.Plugin
apiPlugins []pluginsv0alpha1.Plugin
clientListError error
expectedError error
@@ -188,7 +191,7 @@ func TestSyncer_syncNamespace(t *testing.T) {
}{
{
name: "no installed plugins, no API plugins",
installedPlugins: []*plugins.Plugin{},
installedPlugins: []pluginstore.Plugin{},
apiPlugins: []pluginsv0alpha1.Plugin{},
expectedError: nil,
expectedRegCalls: 0,
@@ -196,7 +199,7 @@ func TestSyncer_syncNamespace(t *testing.T) {
},
{
name: "installed plugins only",
installedPlugins: []*plugins.Plugin{
installedPlugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin-1", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore},
{JSONData: plugins.JSONData{ID: "plugin-2", Info: plugins.Info{Version: "2.0.0"}}, Class: plugins.ClassExternal},
},
@@ -208,7 +211,7 @@ func TestSyncer_syncNamespace(t *testing.T) {
},
{
name: "API plugins only",
installedPlugins: []*plugins.Plugin{},
installedPlugins: []pluginstore.Plugin{},
apiPlugins: []pluginsv0alpha1.Plugin{
{
ObjectMeta: metav1.ObjectMeta{
@@ -236,7 +239,7 @@ func TestSyncer_syncNamespace(t *testing.T) {
},
{
name: "mixed - some match",
installedPlugins: []*plugins.Plugin{
installedPlugins: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: "plugin-1", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore},
{JSONData: plugins.JSONData{ID: "plugin-2", Info: plugins.Info{Version: "2.0.0"}}, Class: plugins.ClassExternal},
{JSONData: plugins.JSONData{ID: "plugin-3", Info: plugins.Info{Version: "3.0.0"}}, Class: plugins.ClassExternal},
@@ -269,7 +272,7 @@ func TestSyncer_syncNamespace(t *testing.T) {
},
{
name: "list error",
installedPlugins: []*plugins.Plugin{},
installedPlugins: []pluginstore.Plugin{},
apiPlugins: []pluginsv0alpha1.Plugin{},
clientListError: errors.New("list error"),
expectedError: errors.New("list error"),
@@ -327,6 +330,8 @@ func TestSyncer_syncNamespace(t *testing.T) {
orgtest.NewOrgServiceFake(),
func(orgID int64) string { return "org-1" },
&fakeServerLock{},
nil,
nil,
)
// Execute
@@ -378,6 +383,8 @@ func TestInstallRegistrar_GetClient(t *testing.T) {
orgtest.NewOrgServiceFake(),
func(orgID int64) string { return "org-1" },
&fakeServerLock{},
nil,
nil,
)
// First call