Plugins: API sync (#112452)
This commit is contained in:
@@ -240,6 +240,7 @@ require (
|
|||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.3.1 // indirect
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.281.0 // indirect
|
github.com/grafana/grafana-plugin-sdk-go v0.281.0 // indirect
|
||||||
github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect
|
github.com/grafana/grafana/apps/dashboard v0.0.0 // indirect
|
||||||
|
github.com/grafana/grafana/apps/plugins v0.0.0 // indirect
|
||||||
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
github.com/grafana/grafana/apps/provisioning v0.0.0 // indirect
|
||||||
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
|
github.com/grafana/grafana/apps/secret v0.0.0 // indirect
|
||||||
github.com/grafana/grafana/pkg/aggregator v0.0.0 // indirect
|
github.com/grafana/grafana/pkg/aggregator v0.0.0 // indirect
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ pluginInstallV0Alpha1: {
|
|||||||
plural: "plugininstalls"
|
plural: "plugininstalls"
|
||||||
scope: "Namespaced"
|
scope: "Namespaced"
|
||||||
schema: {
|
schema: {
|
||||||
spec: {
|
spec: {
|
||||||
id: string
|
id: string
|
||||||
version: string
|
version: string
|
||||||
}
|
url?: string
|
||||||
|
class: "core" | "external" | "cdn"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,22 @@ package v0alpha1
|
|||||||
|
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type PluginInstallSpec struct {
|
type PluginInstallSpec struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
Url *string `json:"url,omitempty"`
|
||||||
|
Class PluginInstallSpecClass `json:"class"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPluginInstallSpec creates a new PluginInstallSpec object.
|
// NewPluginInstallSpec creates a new PluginInstallSpec object.
|
||||||
func NewPluginInstallSpec() *PluginInstallSpec {
|
func NewPluginInstallSpec() *PluginInstallSpec {
|
||||||
return &PluginInstallSpec{}
|
return &PluginInstallSpec{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
type PluginInstallSpecClass string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PluginInstallSpecClassCore PluginInstallSpecClass = "core"
|
||||||
|
PluginInstallSpecClassExternal PluginInstallSpecClass = "external"
|
||||||
|
PluginInstallSpecClassCdn PluginInstallSpecClass = "cdn"
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,163 @@
|
|||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/resource"
|
||||||
|
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PluginInstallSourceAnnotation = "plugins.grafana.app/install-source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Class represents the plugin class type in an unversioned internal format.
|
||||||
|
// This intentionally duplicates the versioned API type (PluginInstallSpecClass) to decouple
|
||||||
|
// internal code from API version changes, making it easier to support multiple API versions.
|
||||||
|
type Class = string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClassCore Class = "core"
|
||||||
|
ClassExternal Class = "external"
|
||||||
|
ClassCDN Class = "cdn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Source = string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceUnknown Source = "unknown"
|
||||||
|
SourcePluginStore Source = "plugin-store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PluginInstall struct {
|
||||||
|
ID string
|
||||||
|
Version string
|
||||||
|
URL string
|
||||||
|
Class Class
|
||||||
|
Source Source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PluginInstall) ToPluginInstallV0Alpha1(namespace string) *pluginsv0alpha1.PluginInstall {
|
||||||
|
var url *string = nil
|
||||||
|
if p.URL != "" {
|
||||||
|
url = &p.URL
|
||||||
|
}
|
||||||
|
return &pluginsv0alpha1.PluginInstall{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: p.ID,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
PluginInstallSourceAnnotation: p.Source,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: pluginsv0alpha1.PluginInstallSpec{
|
||||||
|
Id: p.ID,
|
||||||
|
Version: p.Version,
|
||||||
|
Url: url,
|
||||||
|
Class: pluginsv0alpha1.PluginInstallSpecClass(p.Class),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PluginInstall) ShouldUpdate(existing *pluginsv0alpha1.PluginInstall) bool {
|
||||||
|
update := p.ToPluginInstallV0Alpha1(existing.Namespace)
|
||||||
|
if source, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && source != p.Source {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if existing.Spec.Version != update.Spec.Version {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if existing.Spec.Class != update.Spec.Class {
|
||||||
|
return true // this should never really happen
|
||||||
|
}
|
||||||
|
if !equalStringPointers(existing.Spec.Url, update.Spec.Url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func equalStringPointers(a, b *string) bool {
|
||||||
|
if a == nil && b == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if a == nil || b == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *a == *b
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstallRegistrar struct {
|
||||||
|
clientGenerator resource.ClientGenerator
|
||||||
|
client *pluginsv0alpha1.PluginInstallClient
|
||||||
|
clientOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstallRegistrar(clientGenerator resource.ClientGenerator) *InstallRegistrar {
|
||||||
|
return &InstallRegistrar{
|
||||||
|
clientGenerator: clientGenerator,
|
||||||
|
clientOnce: sync.Once{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *InstallRegistrar) GetClient() (*pluginsv0alpha1.PluginInstallClient, error) {
|
||||||
|
r.clientOnce.Do(func() {
|
||||||
|
client, err := pluginsv0alpha1.NewPluginInstallClientFromGenerator(r.clientGenerator)
|
||||||
|
if err != nil {
|
||||||
|
r.client = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.client = client
|
||||||
|
})
|
||||||
|
|
||||||
|
return r.client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register creates or updates a plugin install in the registry.
|
||||||
|
func (r *InstallRegistrar) Register(ctx context.Context, namespace string, install *PluginInstall) error {
|
||||||
|
client, err := r.GetClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
identifier := resource.Identifier{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: install.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
existing, err := client.Get(ctx, identifier)
|
||||||
|
if err != nil && !errorsK8s.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing != nil && install.ShouldUpdate(existing) {
|
||||||
|
_, err = client.Update(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.UpdateOptions{ResourceVersion: existing.ResourceVersion})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Create(ctx, install.ToPluginInstallV0Alpha1(namespace), resource.CreateOptions{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister removes a plugin install from the registry.
|
||||||
|
func (r *InstallRegistrar) Unregister(ctx context.Context, namespace string, name string, source Source) error {
|
||||||
|
client, err := r.GetClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
identifier := resource.Identifier{
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
existing, err := client.Get(ctx, identifier)
|
||||||
|
if err != nil && !errorsK8s.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if the source is different, do not unregister
|
||||||
|
if existingSource, ok := existing.Annotations[PluginInstallSourceAnnotation]; ok && existingSource != source {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client.Delete(ctx, identifier, resource.DeleteOptions{})
|
||||||
|
}
|
||||||
@@ -1208,6 +1208,11 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
cdnPluginsUrls?: boolean;
|
cdnPluginsUrls?: boolean;
|
||||||
/**
|
/**
|
||||||
|
* Enable syncing plugin installations to the installs API
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
pluginInstallAPISync?: boolean;
|
||||||
|
/**
|
||||||
* Enable new gauge visualization
|
* Enable new gauge visualization
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
|
||||||
@@ -527,7 +528,7 @@ func callGetPluginAsset(sc *scenarioContext) {
|
|||||||
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string,
|
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string,
|
||||||
cfg *setting.Cfg, pluginRegistry registry.Service, fn scenarioFunc) {
|
cfg *setting.Cfg, pluginRegistry registry.Service, fn scenarioFunc) {
|
||||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
hs := HTTPServer{
|
hs := HTTPServer{
|
||||||
@@ -642,7 +643,7 @@ func Test_PluginsList_AccessControl(t *testing.T) {
|
|||||||
for _, tc := range tcs {
|
for _, tc := range tcs {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
hs.Cfg = setting.NewCfg()
|
hs.Cfg = setting.NewCfg()
|
||||||
@@ -832,7 +833,7 @@ func Test_PluginsSettings(t *testing.T) {
|
|||||||
for _, tc := range tcs {
|
for _, tc := range tcs {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
hs.Cfg = setting.NewCfg()
|
hs.Cfg = setting.NewCfg()
|
||||||
@@ -902,7 +903,7 @@ func Test_UpdatePluginSetting(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
|
t.Run("should return an error when trying to disable an auto-enabled plugin", func(t *testing.T) {
|
||||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||||
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
store, err := pluginstore.NewPluginStoreForTest(pluginRegistry, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
hs.Cfg = setting.NewCfg()
|
hs.Cfg = setting.NewCfg()
|
||||||
|
|||||||
@@ -10,14 +10,13 @@ import (
|
|||||||
"github.com/grafana/grafana-app-sdk/app"
|
"github.com/grafana/grafana-app-sdk/app"
|
||||||
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
||||||
"github.com/grafana/grafana-app-sdk/simple"
|
"github.com/grafana/grafana-app-sdk/simple"
|
||||||
"github.com/grafana/grafana/apps/plugins/pkg/apis"
|
pluginsappapis "github.com/grafana/grafana/apps/plugins/pkg/apis"
|
||||||
|
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
||||||
|
pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
pluginsv0alpha1 "github.com/grafana/grafana/apps/plugins/pkg/apis/plugins/v0alpha1"
|
|
||||||
pluginsapp "github.com/grafana/grafana/apps/plugins/pkg/app"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -38,13 +37,13 @@ func RegisterAppInstaller(
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
specificConfig := any(nil)
|
specificConfig := any(nil)
|
||||||
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, pluginsapp.New)
|
provider := simple.NewAppProvider(pluginsappapis.LocalManifest(), specificConfig, pluginsapp.New)
|
||||||
appConfig := app.Config{
|
appConfig := app.Config{
|
||||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||||
ManifestData: *apis.LocalManifest().ManifestData,
|
ManifestData: *pluginsappapis.LocalManifest().ManifestData,
|
||||||
SpecificConfig: specificConfig,
|
SpecificConfig: specificConfig,
|
||||||
}
|
}
|
||||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, &apis.GoTypeAssociator{})
|
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, pluginsappapis.NewGoTypeAssociator())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ import (
|
|||||||
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration"
|
||||||
pluginDashboards "github.com/grafana/grafana/pkg/services/pluginsintegration/dashboards"
|
pluginDashboards "github.com/grafana/grafana/pkg/services/pluginsintegration/dashboards"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/preference/prefimpl"
|
"github.com/grafana/grafana/pkg/services/preference/prefimpl"
|
||||||
promTypeMigration "github.com/grafana/grafana/pkg/services/promtypemigration"
|
promTypeMigration "github.com/grafana/grafana/pkg/services/promtypemigration"
|
||||||
@@ -250,6 +251,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
httpclientprovider.New,
|
httpclientprovider.New,
|
||||||
wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)),
|
wire.Bind(new(httpclient.Provider), new(*sdkhttpclient.Provider)),
|
||||||
serverlock.ProvideService,
|
serverlock.ProvideService,
|
||||||
|
wire.Bind(new(installsync.ServerLock), new(*serverlock.ServerLockService)),
|
||||||
annotationsimpl.ProvideCleanupService,
|
annotationsimpl.ProvideCleanupService,
|
||||||
wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)),
|
wire.Bind(new(annotations.Cleaner), new(*annotationsimpl.CleanupServiceImpl)),
|
||||||
cleanup.ProvideService,
|
cleanup.ProvideService,
|
||||||
|
|||||||
+14
-3
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
|||||||
|
package apiserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/k8s"
|
||||||
|
"github.com/grafana/grafana-app-sdk/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProvideClientGenerator creates a lazy-initialized ClientGenerator.
|
||||||
|
func ProvideClientGenerator(restConfigProvider RestConfigProvider) resource.ClientGenerator {
|
||||||
|
return &lazyClientGenerator{
|
||||||
|
restConfigProvider: restConfigProvider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type lazyClientGenerator struct {
|
||||||
|
restConfigProvider RestConfigProvider
|
||||||
|
clientGenerator resource.ClientGenerator
|
||||||
|
initOnce sync.Once
|
||||||
|
initError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *lazyClientGenerator) ClientFor(kind resource.Kind) (resource.Client, error) {
|
||||||
|
g.initOnce.Do(func() {
|
||||||
|
restConfig, err := g.restConfigProvider.GetRestConfig(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
g.initError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
restConfig.APIPath = "apis"
|
||||||
|
g.clientGenerator = k8s.NewClientRegistry(*restConfig, k8s.DefaultClientConfig())
|
||||||
|
})
|
||||||
|
|
||||||
|
if g.initError != nil {
|
||||||
|
return nil, g.initError
|
||||||
|
}
|
||||||
|
|
||||||
|
return g.clientGenerator.ClientFor(kind)
|
||||||
|
}
|
||||||
@@ -15,4 +15,5 @@ var WireSet = wire.NewSet(
|
|||||||
ProvideService,
|
ProvideService,
|
||||||
wire.Bind(new(Service), new(*service)),
|
wire.Bind(new(Service), new(*service)),
|
||||||
wire.Bind(new(builder.APIRegistrar), new(*service)),
|
wire.Bind(new(builder.APIRegistrar), new(*service)),
|
||||||
|
ProvideClientGenerator,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2091,6 +2091,14 @@ var (
|
|||||||
Owner: grafanaPluginsPlatformSquad,
|
Owner: grafanaPluginsPlatformSquad,
|
||||||
Expression: "false",
|
Expression: "false",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "pluginInstallAPISync",
|
||||||
|
Description: "Enable syncing plugin installations to the installs API",
|
||||||
|
FrontendOnly: false,
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaPluginsPlatformSquad,
|
||||||
|
Expression: "false",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "newGauge",
|
Name: "newGauge",
|
||||||
Description: "Enable new gauge visualization",
|
Description: "Enable new gauge visualization",
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ pluginContainers,privatePreview,@grafana/plugins-platform-backend,false,true,fal
|
|||||||
tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false
|
tempoSearchBackendMigration,GA,@grafana/oss-big-tent,false,true,false
|
||||||
cdnPluginsLoadFirst,experimental,@grafana/plugins-platform-backend,false,false,false
|
cdnPluginsLoadFirst,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false
|
cdnPluginsUrls,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
|
pluginInstallAPISync,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
newGauge,experimental,@grafana/dataviz-squad,false,false,true
|
newGauge,experimental,@grafana/dataviz-squad,false,false,true
|
||||||
preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
|
preventPanelChromeOverflow,preview,@grafana/grafana-frontend-platform,false,false,true
|
||||||
pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
|
pluginStoreServiceLoading,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
|
|||||||
|
@@ -1086,6 +1086,10 @@ const (
|
|||||||
// Enable loading plugins via declarative URLs
|
// Enable loading plugins via declarative URLs
|
||||||
FlagCdnPluginsUrls = "cdnPluginsUrls"
|
FlagCdnPluginsUrls = "cdnPluginsUrls"
|
||||||
|
|
||||||
|
// FlagPluginInstallAPISync
|
||||||
|
// Enable syncing plugin installations to the installs API
|
||||||
|
FlagPluginInstallAPISync = "pluginInstallAPISync"
|
||||||
|
|
||||||
// FlagNewGauge
|
// FlagNewGauge
|
||||||
// Enable new gauge visualization
|
// Enable new gauge visualization
|
||||||
FlagNewGauge = "newGauge"
|
FlagNewGauge = "newGauge"
|
||||||
|
|||||||
@@ -2982,6 +2982,19 @@
|
|||||||
"expression": "false"
|
"expression": "false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "pluginInstallAPISync",
|
||||||
|
"resourceVersion": "1760543624249",
|
||||||
|
"creationTimestamp": "2025-10-15T15:53:44Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Enable syncing plugin installations to the installs API",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/plugins-platform-backend",
|
||||||
|
"expression": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "pluginProxyPreserveTrailingSlash",
|
"name": "pluginProxyPreserveTrailingSlash",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package installsyncfakes
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ installsync.Syncer = &FakeSyncer{}
|
||||||
|
|
||||||
|
type FakeSyncer struct {
|
||||||
|
SyncFunc func(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFakeSyncer() *FakeSyncer {
|
||||||
|
return &FakeSyncer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeSyncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
|
||||||
|
if f.SyncFunc != nil {
|
||||||
|
return f.SyncFunc(ctx, source, installedPlugins)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package installsync
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/resource"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/apps/plugins/pkg/app/install"
|
||||||
|
"github.com/grafana/grafana/pkg/configprovider"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
syncerLockActionName = "plugin-install-api-sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lockTimeout = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerLock is the interface for acquiring distributed locks.
|
||||||
|
type ServerLock interface {
|
||||||
|
LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type syncer struct {
|
||||||
|
featureToggles featuremgmt.FeatureToggles
|
||||||
|
clientGenerator resource.ClientGenerator
|
||||||
|
installRegistrar *install.InstallRegistrar
|
||||||
|
orgService org.Service
|
||||||
|
namespaceMapper request.NamespaceMapper
|
||||||
|
serverLock ServerLock
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSyncer creates a new syncer with the provided dependencies.
|
||||||
|
func newSyncer(
|
||||||
|
featureToggles featuremgmt.FeatureToggles,
|
||||||
|
clientGenerator resource.ClientGenerator,
|
||||||
|
installRegistrar *install.InstallRegistrar,
|
||||||
|
orgService org.Service,
|
||||||
|
namespaceMapper request.NamespaceMapper,
|
||||||
|
serverLock ServerLock,
|
||||||
|
) *syncer {
|
||||||
|
return &syncer{
|
||||||
|
clientGenerator: clientGenerator,
|
||||||
|
featureToggles: featureToggles,
|
||||||
|
installRegistrar: installRegistrar,
|
||||||
|
orgService: orgService,
|
||||||
|
namespaceMapper: namespaceMapper,
|
||||||
|
serverLock: serverLock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideSyncer creates a new Syncer for syncing plugin installations to the API.
|
||||||
|
func ProvideSyncer(
|
||||||
|
featureToggles featuremgmt.FeatureToggles,
|
||||||
|
clientGenerator resource.ClientGenerator,
|
||||||
|
orgService org.Service,
|
||||||
|
cfgProvider configprovider.ConfigProvider,
|
||||||
|
serverLock ServerLock,
|
||||||
|
) (Syncer, error) {
|
||||||
|
cfg, err := cfgProvider.Get(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
installRegistrar := install.NewInstallRegistrar(clientGenerator)
|
||||||
|
namespaceMapper := request.GetNamespaceMapper(cfg)
|
||||||
|
|
||||||
|
return newSyncer(
|
||||||
|
featureToggles,
|
||||||
|
clientGenerator,
|
||||||
|
installRegistrar,
|
||||||
|
orgService,
|
||||||
|
namespaceMapper,
|
||||||
|
serverLock,
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *syncer) Sync(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
|
||||||
|
if !s.featureToggles.IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(installedPlugins) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var syncErr error
|
||||||
|
lockErr := s.serverLock.LockExecuteAndRelease(ctx, syncerLockActionName, lockTimeout, func(ctx context.Context) {
|
||||||
|
syncErr = s.syncAllNamespaces(ctx, source, installedPlugins)
|
||||||
|
})
|
||||||
|
|
||||||
|
if lockErr != nil {
|
||||||
|
return lockErr
|
||||||
|
}
|
||||||
|
return syncErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *syncer) syncAllNamespaces(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
|
||||||
|
orgs, err := s.orgService.Search(ctx, &org.SearchOrgsQuery{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, org := range orgs {
|
||||||
|
err := s.syncNamespace(ctx, s.namespaceMapper(org.ID), source, installedPlugins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *syncer) syncNamespace(ctx context.Context, namespace string, source install.Source, installedPlugins []*plugins.Plugin) error {
|
||||||
|
client, err := s.installRegistrar.GetClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiPlugins, err := client.ListAll(ctx, namespace, resource.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
installedMap := make(map[string]*plugins.Plugin)
|
||||||
|
for _, p := range installedPlugins {
|
||||||
|
installedMap[p.ID] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// unregister plugins that are not installed
|
||||||
|
for _, apiPlugin := range apiPlugins.Items {
|
||||||
|
if _, exists := installedMap[apiPlugin.Spec.Id]; !exists {
|
||||||
|
err := s.installRegistrar.Unregister(ctx, namespace, apiPlugin.Spec.Id, source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register plugins that are installed
|
||||||
|
for _, p := range installedPlugins {
|
||||||
|
err := s.installRegistrar.Register(ctx, namespace, &install.PluginInstall{
|
||||||
|
ID: p.ID,
|
||||||
|
Version: p.Info.Version,
|
||||||
|
Class: install.Class(p.Class),
|
||||||
|
Source: source,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,650 @@
|
|||||||
|
package installsync
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/resource"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
||||||
|
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/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test helpers to avoid import cycles
|
||||||
|
type fakeServerLock struct {
|
||||||
|
lockFunc func(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeServerLock) LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error {
|
||||||
|
if f.lockFunc != nil {
|
||||||
|
return f.lockFunc(ctx, actionName, maxInterval, fn)
|
||||||
|
}
|
||||||
|
fn(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePluginInstallClient struct {
|
||||||
|
listAllFunc func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error)
|
||||||
|
getFunc func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.PluginInstall, error)
|
||||||
|
createFunc func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error)
|
||||||
|
updateFunc func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.UpdateOptions) (*pluginsv0alpha1.PluginInstall, error)
|
||||||
|
deleteFunc func(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) Get(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
if f.getFunc != nil {
|
||||||
|
return f.getFunc(ctx, identifier)
|
||||||
|
}
|
||||||
|
// Return a proper k8s NotFound error
|
||||||
|
return nil, errorsK8s.NewNotFound(schema.GroupResource{
|
||||||
|
Group: pluginsv0alpha1.APIGroup,
|
||||||
|
Resource: "plugininstalls",
|
||||||
|
}, identifier.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) {
|
||||||
|
if f.listAllFunc != nil {
|
||||||
|
return f.listAllFunc(ctx, namespace, opts)
|
||||||
|
}
|
||||||
|
return &pluginsv0alpha1.PluginInstallList{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) {
|
||||||
|
return f.ListAll(ctx, namespace, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) Create(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
if f.createFunc != nil {
|
||||||
|
return f.createFunc(ctx, obj, opts)
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) Update(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.UpdateOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
if f.updateFunc != nil {
|
||||||
|
return f.updateFunc(ctx, obj, opts)
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) UpdateStatus(ctx context.Context, identifier resource.Identifier, newStatus pluginsv0alpha1.PluginInstallStatus, opts resource.UpdateOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginInstallClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||||
|
if f.deleteFunc != nil {
|
||||||
|
return f.deleteFunc(ctx, identifier, opts)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeClientGenerator struct {
|
||||||
|
client *fakePluginInstallClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeClientGenerator) ClientFor(kind resource.Kind) (resource.Client, error) {
|
||||||
|
return &fakeResourceClient{client: f.client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeResourceClient struct {
|
||||||
|
client *fakePluginInstallClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) Get(ctx context.Context, identifier resource.Identifier) (resource.Object, error) {
|
||||||
|
return f.client.Get(ctx, identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) GetInto(ctx context.Context, identifier resource.Identifier, into resource.Object) error {
|
||||||
|
obj, err := f.client.Get(ctx, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Copy the object data into the provided 'into' object
|
||||||
|
if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
*target = *obj
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) List(ctx context.Context, namespace string, options resource.ListOptions) (resource.ListObject, error) {
|
||||||
|
return f.client.ListAll(ctx, namespace, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) ListInto(ctx context.Context, namespace string, options resource.ListOptions, into resource.ListObject) error {
|
||||||
|
list, err := f.client.ListAll(ctx, namespace, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Copy the list data into the provided 'into' object
|
||||||
|
if target, ok := into.(*pluginsv0alpha1.PluginInstallList); ok {
|
||||||
|
*target = *list
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) Create(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions) (resource.Object, error) {
|
||||||
|
plugin := obj.(*pluginsv0alpha1.PluginInstall)
|
||||||
|
return f.client.Create(ctx, plugin, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) CreateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.CreateOptions, into resource.Object) error {
|
||||||
|
created, err := f.Create(ctx, identifier, obj, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Copy the created object data into the provided 'into' object
|
||||||
|
if plugin, ok := created.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
*target = *plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) Update(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions) (resource.Object, error) {
|
||||||
|
plugin := obj.(*pluginsv0alpha1.PluginInstall)
|
||||||
|
return f.client.Update(ctx, plugin, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) UpdateInto(ctx context.Context, identifier resource.Identifier, obj resource.Object, options resource.UpdateOptions, into resource.Object) error {
|
||||||
|
updated, err := f.Update(ctx, identifier, obj, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Copy the updated object data into the provided 'into' object
|
||||||
|
if plugin, ok := updated.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
*target = *plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) Patch(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions) (resource.Object, error) {
|
||||||
|
return f.client.Patch(ctx, identifier, patch, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) PatchInto(ctx context.Context, identifier resource.Identifier, patch resource.PatchRequest, options resource.PatchOptions, into resource.Object) error {
|
||||||
|
patched, err := f.Patch(ctx, identifier, patch, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Copy the patched object data into the provided 'into' object
|
||||||
|
if plugin, ok := patched.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
if target, ok := into.(*pluginsv0alpha1.PluginInstall); ok {
|
||||||
|
*target = *plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) Delete(ctx context.Context, identifier resource.Identifier, options resource.DeleteOptions) error {
|
||||||
|
return f.client.Delete(ctx, identifier, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) SubresourceRequest(ctx context.Context, identifier resource.Identifier, req resource.CustomRouteRequestOptions) ([]byte, error) {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceClient) Watch(ctx context.Context, namespace string, options resource.WatchOptions) (resource.WatchResponse, error) {
|
||||||
|
return &fakeWatchResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeWatchResponse struct{}
|
||||||
|
|
||||||
|
func (f *fakeWatchResponse) Stop() {}
|
||||||
|
|
||||||
|
func (f *fakeWatchResponse) WatchEvents() <-chan resource.WatchEvent {
|
||||||
|
ch := make(chan resource.WatchEvent)
|
||||||
|
close(ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncer_Sync(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
featureToggleEnabled bool
|
||||||
|
orgs []*org.OrgDTO
|
||||||
|
orgServiceError error
|
||||||
|
serverLockError error
|
||||||
|
expectedError error
|
||||||
|
expectSyncCalls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "feature toggle disabled",
|
||||||
|
featureToggleEnabled: false,
|
||||||
|
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
|
||||||
|
expectedError: nil,
|
||||||
|
expectSyncCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature toggle enabled, no orgs",
|
||||||
|
featureToggleEnabled: true,
|
||||||
|
orgs: []*org.OrgDTO{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectSyncCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature toggle enabled, single org",
|
||||||
|
featureToggleEnabled: true,
|
||||||
|
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
|
||||||
|
expectedError: nil,
|
||||||
|
expectSyncCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "feature toggle enabled, multiple orgs",
|
||||||
|
featureToggleEnabled: true,
|
||||||
|
orgs: []*org.OrgDTO{
|
||||||
|
{ID: 1, Name: "Org 1"},
|
||||||
|
{ID: 2, Name: "Org 2"},
|
||||||
|
{ID: 3, Name: "Org 3"},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
expectSyncCalls: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org service error",
|
||||||
|
featureToggleEnabled: true,
|
||||||
|
orgs: nil,
|
||||||
|
orgServiceError: errors.New("org service error"),
|
||||||
|
expectedError: errors.New("org service error"),
|
||||||
|
expectSyncCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "server lock error",
|
||||||
|
featureToggleEnabled: true,
|
||||||
|
orgs: []*org.OrgDTO{{ID: 1, Name: "Org 1"}},
|
||||||
|
serverLockError: errors.New("lock error"),
|
||||||
|
expectedError: errors.New("lock error"),
|
||||||
|
expectSyncCalls: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Setup feature toggles
|
||||||
|
ft := featuremgmt.NewMockFeatureToggles(t)
|
||||||
|
ft.EXPECT().IsEnabled(ctx, featuremgmt.FlagPluginInstallAPISync).Return(tt.featureToggleEnabled).Maybe()
|
||||||
|
|
||||||
|
// Setup org service
|
||||||
|
orgService := orgtest.NewOrgServiceFake()
|
||||||
|
orgService.ExpectedOrgs = tt.orgs
|
||||||
|
orgService.ExpectedError = tt.orgServiceError
|
||||||
|
|
||||||
|
// Setup server lock
|
||||||
|
serverLock := &fakeServerLock{}
|
||||||
|
if tt.serverLockError != nil {
|
||||||
|
serverLock.lockFunc = func(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error {
|
||||||
|
return tt.serverLockError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup fake client and registrar
|
||||||
|
syncCalls := 0
|
||||||
|
fakeClient := &fakePluginInstallClient{
|
||||||
|
createFunc: func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
syncCalls++
|
||||||
|
return obj, nil
|
||||||
|
},
|
||||||
|
listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) {
|
||||||
|
return &pluginsv0alpha1.PluginInstallList{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
clientGen := &fakeClientGenerator{client: fakeClient}
|
||||||
|
registrar := install.NewInstallRegistrar(clientGen)
|
||||||
|
|
||||||
|
// Create syncer
|
||||||
|
s := newSyncer(
|
||||||
|
ft,
|
||||||
|
clientGen,
|
||||||
|
registrar,
|
||||||
|
orgService,
|
||||||
|
func(orgID int64) string { return "org-1" },
|
||||||
|
serverLock,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
installedPlugins := []*plugins.Plugin{
|
||||||
|
{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}},
|
||||||
|
}
|
||||||
|
err := s.Sync(ctx, install.SourcePluginStore, installedPlugins)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
if tt.expectedError != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.expectedError.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.expectSyncCalls, syncCalls)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncer_syncNamespace(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
installedPlugins []*plugins.Plugin
|
||||||
|
apiPlugins []pluginsv0alpha1.PluginInstall
|
||||||
|
clientListError error
|
||||||
|
expectedError error
|
||||||
|
expectedRegCalls int
|
||||||
|
expectedUnregCalls int
|
||||||
|
registeredIDs []string
|
||||||
|
unregisteredIDs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no installed plugins, no API plugins",
|
||||||
|
installedPlugins: []*plugins.Plugin{},
|
||||||
|
apiPlugins: []pluginsv0alpha1.PluginInstall{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedRegCalls: 0,
|
||||||
|
expectedUnregCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "installed plugins only",
|
||||||
|
installedPlugins: []*plugins.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},
|
||||||
|
},
|
||||||
|
apiPlugins: []pluginsv0alpha1.PluginInstall{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedRegCalls: 2,
|
||||||
|
expectedUnregCalls: 0,
|
||||||
|
registeredIDs: []string{"plugin-1", "plugin-2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "API plugins only",
|
||||||
|
installedPlugins: []*plugins.Plugin{},
|
||||||
|
apiPlugins: []pluginsv0alpha1.PluginInstall{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plugin-1",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
install.PluginInstallSourceAnnotation: install.SourcePluginStore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plugin-2",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
install.PluginInstallSourceAnnotation: install.SourcePluginStore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedRegCalls: 0,
|
||||||
|
expectedUnregCalls: 2,
|
||||||
|
unregisteredIDs: []string{"plugin-1", "plugin-2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed - some match",
|
||||||
|
installedPlugins: []*plugins.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},
|
||||||
|
},
|
||||||
|
apiPlugins: []pluginsv0alpha1.PluginInstall{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plugin-2",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
install.PluginInstallSourceAnnotation: install.SourcePluginStore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-2", Version: "2.0.0"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "plugin-4",
|
||||||
|
Annotations: map[string]string{
|
||||||
|
install.PluginInstallSourceAnnotation: install.SourcePluginStore,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: pluginsv0alpha1.PluginInstallSpec{Id: "plugin-4"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedRegCalls: 2, // plugin-1 and plugin-3 are new, plugin-2 already exists
|
||||||
|
expectedUnregCalls: 1, // plugin-4 removed
|
||||||
|
registeredIDs: []string{"plugin-1", "plugin-3"},
|
||||||
|
unregisteredIDs: []string{"plugin-4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list error",
|
||||||
|
installedPlugins: []*plugins.Plugin{},
|
||||||
|
apiPlugins: []pluginsv0alpha1.PluginInstall{},
|
||||||
|
clientListError: errors.New("list error"),
|
||||||
|
expectedError: errors.New("list error"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Track calls
|
||||||
|
var registeredIDs []string
|
||||||
|
var unregisteredIDs []string
|
||||||
|
|
||||||
|
// Setup fake client
|
||||||
|
fakeClient := &fakePluginInstallClient{
|
||||||
|
listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) {
|
||||||
|
if tt.clientListError != nil {
|
||||||
|
return nil, tt.clientListError
|
||||||
|
}
|
||||||
|
return &pluginsv0alpha1.PluginInstallList{
|
||||||
|
Items: tt.apiPlugins,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
createFunc: func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
registeredIDs = append(registeredIDs, obj.Spec.Id)
|
||||||
|
return obj, nil
|
||||||
|
},
|
||||||
|
deleteFunc: func(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||||
|
unregisteredIDs = append(unregisteredIDs, identifier.Name)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
getFunc: func(ctx context.Context, identifier resource.Identifier) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
// Check if plugin exists in apiPlugins
|
||||||
|
for i := range tt.apiPlugins {
|
||||||
|
if tt.apiPlugins[i].Name == identifier.Name {
|
||||||
|
return &tt.apiPlugins[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errorsK8s.NewNotFound(schema.GroupResource{
|
||||||
|
Group: pluginsv0alpha1.APIGroup,
|
||||||
|
Resource: "plugininstalls",
|
||||||
|
}, identifier.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
clientGen := &fakeClientGenerator{client: fakeClient}
|
||||||
|
registrar := install.NewInstallRegistrar(clientGen)
|
||||||
|
|
||||||
|
// Create syncer
|
||||||
|
s := newSyncer(
|
||||||
|
featuremgmt.NewMockFeatureToggles(t),
|
||||||
|
clientGen,
|
||||||
|
registrar,
|
||||||
|
orgtest.NewOrgServiceFake(),
|
||||||
|
func(orgID int64) string { return "org-1" },
|
||||||
|
&fakeServerLock{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
err := s.syncNamespace(ctx, "org-1", install.SourcePluginStore, tt.installedPlugins)
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
if tt.expectedError != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.expectedError.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectedRegCalls > 0 {
|
||||||
|
require.Len(t, registeredIDs, tt.expectedRegCalls)
|
||||||
|
if tt.registeredIDs != nil {
|
||||||
|
require.ElementsMatch(t, tt.registeredIDs, registeredIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectedUnregCalls > 0 {
|
||||||
|
require.Len(t, unregisteredIDs, tt.expectedUnregCalls)
|
||||||
|
if tt.unregisteredIDs != nil {
|
||||||
|
require.ElementsMatch(t, tt.unregisteredIDs, unregisteredIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncer_getClient(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "first call success and subsequent calls return cached client",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
fakeClient := &fakePluginInstallClient{}
|
||||||
|
clientGen := &fakeClientGenerator{client: fakeClient}
|
||||||
|
|
||||||
|
s := newSyncer(
|
||||||
|
featuremgmt.NewMockFeatureToggles(t),
|
||||||
|
clientGen,
|
||||||
|
install.NewInstallRegistrar(clientGen),
|
||||||
|
orgtest.NewOrgServiceFake(),
|
||||||
|
func(orgID int64) string { return "org-1" },
|
||||||
|
&fakeServerLock{},
|
||||||
|
)
|
||||||
|
|
||||||
|
// First call
|
||||||
|
client1, err1 := s.installRegistrar.GetClient()
|
||||||
|
require.NoError(t, err1)
|
||||||
|
require.NotNil(t, client1)
|
||||||
|
|
||||||
|
// Second call should return cached client
|
||||||
|
client2, err2 := s.installRegistrar.GetClient()
|
||||||
|
require.NoError(t, err2)
|
||||||
|
require.NotNil(t, client2)
|
||||||
|
// Both calls should return the same client instance
|
||||||
|
require.Equal(t, client1, client2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncer_syncAllNamespaces(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
orgs []*org.OrgDTO
|
||||||
|
orgServiceError error
|
||||||
|
expectedError error
|
||||||
|
expectedCalls int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no orgs",
|
||||||
|
orgs: []*org.OrgDTO{},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedCalls: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single org",
|
||||||
|
orgs: []*org.OrgDTO{
|
||||||
|
{ID: 1, Name: "Org 1"},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple orgs",
|
||||||
|
orgs: []*org.OrgDTO{
|
||||||
|
{ID: 1, Name: "Org 1"},
|
||||||
|
{ID: 2, Name: "Org 2"},
|
||||||
|
{ID: 3, Name: "Org 3"},
|
||||||
|
},
|
||||||
|
expectedError: nil,
|
||||||
|
expectedCalls: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "org service error",
|
||||||
|
orgs: nil,
|
||||||
|
orgServiceError: errors.New("org service error"),
|
||||||
|
expectedError: errors.New("org service error"),
|
||||||
|
expectedCalls: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
orgService := orgtest.NewOrgServiceFake()
|
||||||
|
orgService.ExpectedOrgs = tt.orgs
|
||||||
|
orgService.ExpectedError = tt.orgServiceError
|
||||||
|
|
||||||
|
// Track namespace sync calls
|
||||||
|
syncCalls := 0
|
||||||
|
fakeClient := &fakePluginInstallClient{
|
||||||
|
createFunc: func(ctx context.Context, obj *pluginsv0alpha1.PluginInstall, opts resource.CreateOptions) (*pluginsv0alpha1.PluginInstall, error) {
|
||||||
|
syncCalls++
|
||||||
|
return obj, nil
|
||||||
|
},
|
||||||
|
listAllFunc: func(ctx context.Context, namespace string, opts resource.ListOptions) (*pluginsv0alpha1.PluginInstallList, error) {
|
||||||
|
return &pluginsv0alpha1.PluginInstallList{}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
clientGen := &fakeClientGenerator{client: fakeClient}
|
||||||
|
|
||||||
|
s := newSyncer(
|
||||||
|
featuremgmt.NewMockFeatureToggles(t),
|
||||||
|
clientGen,
|
||||||
|
install.NewInstallRegistrar(clientGen),
|
||||||
|
orgService,
|
||||||
|
func(orgID int64) string { return "org-1" },
|
||||||
|
&fakeServerLock{},
|
||||||
|
)
|
||||||
|
|
||||||
|
installedPlugins := []*plugins.Plugin{
|
||||||
|
{JSONData: plugins.JSONData{ID: "test-plugin", Info: plugins.Info{Version: "1.0.0"}}, Class: plugins.ClassCore},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.syncAllNamespaces(ctx, install.SourcePluginStore, installedPlugins)
|
||||||
|
|
||||||
|
if tt.expectedError != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, tt.expectedError.Error(), err.Error())
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.expectedCalls, syncCalls)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
@@ -41,7 +42,7 @@ func TestGet(t *testing.T) {
|
|||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
ds := &fakeDatasources.FakeDataSourceService{}
|
ds := &fakeDatasources.FakeDataSourceService{}
|
||||||
db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound}
|
db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound}
|
||||||
store, err := pluginstore.NewPluginStoreForTest(preg, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
store, err := pluginstore.NewPluginStoreForTest(preg, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(),
|
pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(),
|
||||||
store, &fakeDatasources.FakeCacheService{},
|
store, &fakeDatasources.FakeCacheService{},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||||
@@ -26,7 +27,7 @@ func TestService_IsDisabled(t *testing.T) {
|
|||||||
&setting.Cfg{
|
&setting.Cfg{
|
||||||
PreinstallPluginsAsync: []setting.InstallPlugin{{ID: "myplugin"}},
|
PreinstallPluginsAsync: []setting.InstallPlugin{{ID: "myplugin"}},
|
||||||
},
|
},
|
||||||
pluginstore.New(registry.NewInMemory(), &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}),
|
pluginstore.New(registry.NewInMemory(), &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer()),
|
||||||
&pluginfakes.FakePluginInstaller{},
|
&pluginfakes.FakePluginInstaller{},
|
||||||
prometheus.NewRegistry(),
|
prometheus.NewRegistry(),
|
||||||
&pluginfakes.FakePluginRepo{},
|
&pluginfakes.FakePluginRepo{},
|
||||||
@@ -160,7 +161,7 @@ func TestService_Run(t *testing.T) {
|
|||||||
}
|
}
|
||||||
installed := 0
|
installed := 0
|
||||||
installedFromURL := 0
|
installedFromURL := 0
|
||||||
store, err := pluginstore.NewPluginStoreForTest(preg, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
store, err := pluginstore.NewPluginStoreForTest(preg, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
s, err := ProvideService(
|
s, err := ProvideService(
|
||||||
&setting.Cfg{
|
&setting.Cfg{
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/clientmiddleware"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/keystore"
|
||||||
@@ -72,6 +73,7 @@ var WireSet = wire.NewSet(
|
|||||||
pluginconfig.NewRequestConfigProvider,
|
pluginconfig.NewRequestConfigProvider,
|
||||||
wire.Bind(new(pluginconfig.PluginRequestConfigProvider), new(*pluginconfig.RequestConfigProvider)),
|
wire.Bind(new(pluginconfig.PluginRequestConfigProvider), new(*pluginconfig.RequestConfigProvider)),
|
||||||
pluginstore.ProvideService,
|
pluginstore.ProvideService,
|
||||||
|
installsync.ProvideSyncer,
|
||||||
wire.Bind(new(pluginstore.Store), new(*pluginstore.Service)),
|
wire.Bind(new(pluginstore.Store), new(*pluginstore.Service)),
|
||||||
wire.Bind(new(plugins.StaticRouteResolver), new(*pluginstore.Service)),
|
wire.Bind(new(plugins.StaticRouteResolver), new(*pluginstore.Service)),
|
||||||
process.ProvideService,
|
process.ProvideService,
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/dskit/services"
|
"github.com/grafana/dskit/services"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/apps/plugins/pkg/app/install"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
"github.com/grafana/grafana/pkg/plugins/manager/loader"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"golang.org/x/sync/errgroup"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Store = (*Service)(nil)
|
var _ Store = (*Service)(nil)
|
||||||
@@ -31,16 +34,17 @@ type Store interface {
|
|||||||
type Service struct {
|
type Service struct {
|
||||||
services.NamedService
|
services.NamedService
|
||||||
|
|
||||||
pluginRegistry registry.Service
|
pluginRegistry registry.Service
|
||||||
pluginLoader loader.Service
|
pluginLoader loader.Service
|
||||||
pluginSources sources.Registry
|
pluginSources sources.Registry
|
||||||
loadOnStartup bool
|
installsRegistrar installsync.Syncer
|
||||||
|
loadOnStartup bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry,
|
func ProvideService(pluginRegistry registry.Service, pluginSources sources.Registry,
|
||||||
pluginLoader loader.Service, features featuremgmt.FeatureToggles) (*Service, error) {
|
pluginLoader loader.Service, installsRegistrar installsync.Syncer, features featuremgmt.FeatureToggles) (*Service, error) {
|
||||||
if features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
|
if features.IsEnabledGlobally(featuremgmt.FlagPluginStoreServiceLoading) {
|
||||||
s := New(pluginRegistry, pluginLoader, pluginSources)
|
s := New(pluginRegistry, pluginLoader, pluginSources, installsRegistrar)
|
||||||
s.loadOnStartup = true
|
s.loadOnStartup = true
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
@@ -51,19 +55,24 @@ func ProvideService(pluginRegistry registry.Service, pluginSources sources.Regis
|
|||||||
logger := log.New("plugin.store")
|
logger := log.New("plugin.store")
|
||||||
logger.Info("Loading plugins...")
|
logger.Info("Loading plugins...")
|
||||||
|
|
||||||
|
loadedPluginsToSync := make([]*plugins.Plugin, 0)
|
||||||
for _, ps := range pluginSources.List(ctx) {
|
for _, ps := range pluginSources.List(ctx) {
|
||||||
loadedPlugins, err := pluginLoader.Load(ctx, ps)
|
loadedPlugins, err := pluginLoader.Load(ctx, ps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err)
|
logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
loadedPluginsToSync = append(loadedPluginsToSync, loadedPlugins...)
|
||||||
totalPlugins += len(loadedPlugins)
|
totalPlugins += len(loadedPlugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := installsRegistrar.Sync(ctx, install.SourcePluginStore, loadedPluginsToSync); err != nil {
|
||||||
|
logger.Error("Syncing plugin installations failed", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start))
|
logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start))
|
||||||
|
|
||||||
return New(pluginRegistry, pluginLoader, pluginSources), nil
|
return New(pluginRegistry, pluginLoader, pluginSources, installsRegistrar), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Run(ctx context.Context) error {
|
func (s *Service) Run(ctx context.Context) error {
|
||||||
@@ -74,8 +83,8 @@ func (s *Service) Run(ctx context.Context) error {
|
|||||||
return s.AwaitTerminated(stopCtx)
|
return s.AwaitTerminated(stopCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPluginStoreForTest(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) (*Service, error) {
|
func NewPluginStoreForTest(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry, installsRegistrar installsync.Syncer) (*Service, error) {
|
||||||
s := New(pluginRegistry, pluginLoader, pluginSources)
|
s := New(pluginRegistry, pluginLoader, pluginSources, installsRegistrar)
|
||||||
s.loadOnStartup = true
|
s.loadOnStartup = true
|
||||||
if err := s.StartAsync(context.Background()); err != nil {
|
if err := s.StartAsync(context.Background()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -86,11 +95,12 @@ func NewPluginStoreForTest(pluginRegistry registry.Service, pluginLoader loader.
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry) *Service {
|
func New(pluginRegistry registry.Service, pluginLoader loader.Service, pluginSources sources.Registry, installsRegistrar installsync.Syncer) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
pluginRegistry: pluginRegistry,
|
pluginRegistry: pluginRegistry,
|
||||||
pluginLoader: pluginLoader,
|
pluginLoader: pluginLoader,
|
||||||
pluginSources: pluginSources,
|
pluginSources: pluginSources,
|
||||||
|
installsRegistrar: installsRegistrar,
|
||||||
}
|
}
|
||||||
s.NamedService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(ServiceName)
|
s.NamedService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(ServiceName)
|
||||||
return s
|
return s
|
||||||
@@ -105,15 +115,21 @@ func (s *Service) starting(ctx context.Context) error {
|
|||||||
logger := log.New(ServiceName)
|
logger := log.New(ServiceName)
|
||||||
logger.Info("Loading plugins...")
|
logger.Info("Loading plugins...")
|
||||||
|
|
||||||
|
loadedPluginsToSync := make([]*plugins.Plugin, 0)
|
||||||
for _, ps := range s.pluginSources.List(ctx) {
|
for _, ps := range s.pluginSources.List(ctx) {
|
||||||
loadedPlugins, err := s.pluginLoader.Load(ctx, ps)
|
loadedPlugins, err := s.pluginLoader.Load(ctx, ps)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err)
|
logger.Error("Loading plugin source failed", "source", ps.PluginClass(ctx), "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
loadedPluginsToSync = append(loadedPluginsToSync, loadedPlugins...)
|
||||||
totalPlugins += len(loadedPlugins)
|
totalPlugins += len(loadedPlugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.installsRegistrar.Sync(ctx, install.SourcePluginStore, loadedPluginsToSync); err != nil {
|
||||||
|
logger.Error("Syncing plugin installations failed", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start))
|
logger.Info("Plugins loaded", "count", totalPlugins, "duration", time.Since(start))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/apps/plugins/pkg/app/install"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||||
"github.com/grafana/grafana/pkg/plugins/log"
|
"github.com/grafana/grafana/pkg/plugins/log"
|
||||||
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStore_ProvideService(t *testing.T) {
|
func TestStore_ProvideService(t *testing.T) {
|
||||||
@@ -76,7 +78,7 @@ func TestStore_ProvideService(t *testing.T) {
|
|||||||
features = featuremgmt.WithFeatures()
|
features = featuremgmt.WithFeatures()
|
||||||
}
|
}
|
||||||
|
|
||||||
service, err := ProvideService(pluginfakes.NewFakePluginRegistry(), srcs, l, features)
|
service, err := ProvideService(pluginfakes.NewFakePluginRegistry(), srcs, l, installsyncfakes.NewFakeSyncer(), features)
|
||||||
require.Equal(t, tt.expectedLoadOnStartup, service.loadOnStartup)
|
require.Equal(t, tt.expectedLoadOnStartup, service.loadOnStartup)
|
||||||
require.Equal(t, tt.expectedBeforeStart, loadedSrcs)
|
require.Equal(t, tt.expectedBeforeStart, loadedSrcs)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -89,6 +91,47 @@ func TestStore_ProvideService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Plugin installs are synced", func(t *testing.T) {
|
||||||
|
registrar := installsyncfakes.NewFakeSyncer()
|
||||||
|
registered := []*plugins.Plugin{}
|
||||||
|
registrar.SyncFunc = func(ctx context.Context, source install.Source, installedPlugins []*plugins.Plugin) error {
|
||||||
|
registered = append(registered, installedPlugins...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
srcs := &pluginfakes.FakeSourceRegistry{ListFunc: func(_ context.Context) []plugins.PluginSource {
|
||||||
|
return []plugins.PluginSource{
|
||||||
|
&pluginfakes.FakePluginSource{
|
||||||
|
PluginClassFunc: func(ctx context.Context) plugins.Class {
|
||||||
|
return plugins.ClassExternal
|
||||||
|
},
|
||||||
|
DiscoverFunc: func(ctx context.Context) ([]*plugins.FoundBundle, error) {
|
||||||
|
return []*plugins.FoundBundle{
|
||||||
|
{
|
||||||
|
Primary: plugins.FoundPlugin{JSONData: plugins.JSONData{ID: "test-plugin"}},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
DefaultSignatureFunc: func(ctx context.Context) (plugins.Signature, bool) {
|
||||||
|
return plugins.Signature{}, false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
l := &pluginfakes.FakeLoader{
|
||||||
|
LoadFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) {
|
||||||
|
return []*plugins.Plugin{{JSONData: plugins.JSONData{ID: "test-plugin"}}}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
service, err := ProvideService(pluginfakes.NewFakePluginRegistry(), srcs, l, registrar, featuremgmt.WithFeatures())
|
||||||
|
require.NoError(t, err)
|
||||||
|
ctx := context.Background()
|
||||||
|
err = service.StartAsync(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = service.AwaitRunning(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, registered, 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore_Plugin(t *testing.T) {
|
func TestStore_Plugin(t *testing.T) {
|
||||||
@@ -102,7 +145,7 @@ func TestStore_Plugin(t *testing.T) {
|
|||||||
p1.ID: p1,
|
p1.ID: p1,
|
||||||
p2.ID: p2,
|
p2.ID: p2,
|
||||||
},
|
},
|
||||||
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
p, exists := ps.Plugin(context.Background(), p1.ID)
|
p, exists := ps.Plugin(context.Background(), p1.ID)
|
||||||
@@ -132,7 +175,7 @@ func TestStore_Plugins(t *testing.T) {
|
|||||||
p4.ID: p4,
|
p4.ID: p4,
|
||||||
p5.ID: p5,
|
p5.ID: p5,
|
||||||
},
|
},
|
||||||
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ToGrafanaDTO(p1)
|
ToGrafanaDTO(p1)
|
||||||
@@ -176,7 +219,7 @@ func TestStore_Routes(t *testing.T) {
|
|||||||
p5.ID: p5,
|
p5.ID: p5,
|
||||||
p6.ID: p6,
|
p6.ID: p6,
|
||||||
},
|
},
|
||||||
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
sr := func(p *plugins.Plugin) *plugins.StaticRoute {
|
sr := func(p *plugins.Plugin) *plugins.StaticRoute {
|
||||||
@@ -206,7 +249,7 @@ func TestProcessManager_shutdown(t *testing.T) {
|
|||||||
unloaded = true
|
unloaded = true
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
}, &pluginfakes.FakeSourceRegistry{})
|
}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
@@ -239,7 +282,7 @@ func TestProcessManager_shutdown(t *testing.T) {
|
|||||||
UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) {
|
UnloadFunc: func(_ context.Context, plugin *plugins.Plugin) (*plugins.Plugin, error) {
|
||||||
return nil, expectedErr
|
return nil, expectedErr
|
||||||
},
|
},
|
||||||
}, &pluginfakes.FakeSourceRegistry{})
|
}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = ps.stopping(nil)
|
err = ps.stopping(nil)
|
||||||
@@ -259,7 +302,7 @@ func TestStore_availablePlugins(t *testing.T) {
|
|||||||
p1.ID: p1,
|
p1.ID: p1,
|
||||||
p2.ID: p2,
|
p2.ID: p2,
|
||||||
},
|
},
|
||||||
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{})
|
}, &pluginfakes.FakeLoader{}, &pluginfakes.FakeSourceRegistry{}, installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
aps := ps.availablePlugins(context.Background())
|
aps := ps.availablePlugins(context.Background())
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
"github.com/grafana/grafana/pkg/plugins/manager/sources"
|
||||||
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
"github.com/grafana/grafana/pkg/plugins/pluginassets"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/installsync/installsyncfakes"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs"
|
||||||
@@ -64,7 +65,7 @@ func CreateIntegrationTestCtx(t *testing.T, cfg *setting.Cfg, coreRegistry *core
|
|||||||
Terminator: term,
|
Terminator: term,
|
||||||
})
|
})
|
||||||
|
|
||||||
ps, err := pluginstore.NewPluginStoreForTest(reg, l, sources.ProvideService(cfg, pCfg))
|
ps, err := pluginstore.NewPluginStoreForTest(reg, l, sources.ProvideService(cfg, pCfg), installsyncfakes.NewFakeSyncer())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return &IntegrationTestCtx{
|
return &IntegrationTestCtx{
|
||||||
|
|||||||
Reference in New Issue
Block a user