From 0d4884cb3bd3684c4ffcc95c74ebece82d73b7ae Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Mon, 12 May 2025 09:45:56 +0200 Subject: [PATCH] Advisor: Avoid one GCOM request per plugin (#105180) --- .../pkg/app/checks/authchecks/check.go | 4 + .../pkg/app/checks/datasourcecheck/check.go | 23 ++++-- .../app/checks/datasourcecheck/check_test.go | 41 ++++++---- apps/advisor/pkg/app/checks/ifaces.go | 3 + .../pkg/app/checks/plugincheck/check.go | 46 +++++------ .../pkg/app/checks/plugincheck/check_test.go | 76 +++++-------------- apps/advisor/pkg/app/utils.go | 8 ++ apps/advisor/pkg/app/utils_test.go | 4 + pkg/plugins/manager/fakes/fakes.go | 4 + pkg/plugins/repo/ifaces.go | 4 +- pkg/plugins/repo/models.go | 7 +- pkg/plugins/repo/service.go | 10 +-- pkg/plugins/repo/service_test.go | 22 ++++-- 13 files changed, 137 insertions(+), 115 deletions(-) diff --git a/apps/advisor/pkg/app/checks/authchecks/check.go b/apps/advisor/pkg/app/checks/authchecks/check.go index 69513bfed90..d20dcd27621 100644 --- a/apps/advisor/pkg/app/checks/authchecks/check.go +++ b/apps/advisor/pkg/app/checks/authchecks/check.go @@ -28,6 +28,10 @@ func (c *check) ID() string { return CheckID } +func (c *check) Init(ctx context.Context) error { + return nil +} + func (c *check) Steps() []checks.Step { return []checks.Step{ &listFormatValidation{}, diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index c5e28621162..fe45d5e7365 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -32,6 +32,7 @@ type check struct { PluginClient plugins.Client PluginRepo repo.Service GrafanaVersion string + pluginIndex map[string]repo.PluginInfo } func New( @@ -79,6 +80,19 @@ func (c *check) ID() string { return CheckID } +func (c *check) Init(ctx context.Context) error { + compatOpts := repo.NewCompatOpts(c.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) + plugins, err := c.PluginRepo.GetPluginsInfo(ctx, compatOpts) + if err != nil { + return err + } + c.pluginIndex = make(map[string]repo.PluginInfo) + for _, p := range plugins { + c.pluginIndex[p.Slug] = p + } + return nil +} + func (c *check) Steps() []checks.Step { return []checks.Step{ &uidValidationStep{}, @@ -88,8 +102,8 @@ func (c *check) Steps() []checks.Step { }, &missingPluginStep{ PluginStore: c.PluginStore, - PluginRepo: c.PluginRepo, GrafanaVersion: c.GrafanaVersion, + pluginIndex: c.pluginIndex, }, } } @@ -207,8 +221,8 @@ func (s *healthCheckStep) Run(ctx context.Context, log logging.Logger, obj *advi type missingPluginStep struct { PluginStore pluginstore.Store - PluginRepo repo.Service GrafanaVersion string + pluginIndex map[string]repo.PluginInfo } func (s *missingPluginStep) Title() string { @@ -241,9 +255,8 @@ func (s *missingPluginStep) Run(ctx context.Context, log logging.Logger, obj *ad Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), }, } - compatOpts := repo.NewCompatOpts(s.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) - _, err := s.PluginRepo.PluginInfo(ctx, ds.Type, compatOpts) - if err == nil { + _, ok := s.pluginIndex[ds.Type] + if ok { // Plugin is available in the repo links = append(links, advisor.CheckErrorLink{ Message: "Install plugin", diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go index 4e6ab2e8eac..f7f834a7b1b 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -2,7 +2,6 @@ package datasourcecheck import ( "context" - "errors" "testing" "github.com/grafana/grafana-app-sdk/logging" @@ -26,6 +25,10 @@ func runChecks(check *check) ([]advisor.CheckReportFailure, error) { } failures := []advisor.CheckReportFailure{} + err = check.Init(ctx) + if err != nil { + return nil, err + } for _, step := range check.Steps() { for _, item := range items { stepFailures, err := step.Run(ctx, logging.DefaultLogger, &advisor.CheckSpec{}, item) @@ -51,7 +54,10 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} - mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ + {ID: 1, Slug: "prometheus", Status: "active"}, + {ID: 2, Slug: "mysql", Status: "active"}, + }} mockPluginStore := &MockPluginStore{exists: true} check := &check{ @@ -75,7 +81,9 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} - mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ + {ID: 1, Slug: "prometheus", Status: "active"}, + }} mockPluginStore := &MockPluginStore{exists: true} check := &check{ @@ -100,7 +108,9 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}} - mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ + {ID: 1, Slug: "prometheus", Status: "active"}, + }} mockPluginStore := &MockPluginStore{exists: true} check := &check{ @@ -124,7 +134,9 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{err: plugins.ErrMethodNotImplemented} - mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ + {ID: 1, Slug: "prometheus", Status: "active"}, + }} mockPluginStore := &MockPluginStore{exists: true} check := &check{ @@ -147,7 +159,9 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{err: plugins.ErrPluginNotRegistered} - mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ + {ID: 1, Slug: "prometheus", Status: "active"}, + }} mockPluginStore := &MockPluginStore{exists: true} check := &check{ @@ -171,7 +185,9 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} - mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{ + {ID: 1, Slug: "prometheus", Status: "active"}, + }} mockPluginStore := &MockPluginStore{exists: false} check := &check{ @@ -196,7 +212,7 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} - mockPluginRepo := &MockPluginRepo{exists: false} + mockPluginRepo := &MockPluginRepo{plugins: []repo.PluginInfo{}} mockPluginStore := &MockPluginStore{exists: false} check := &check{ @@ -257,12 +273,9 @@ func (m *MockPluginStore) Plugin(context.Context, string) (pluginstore.Plugin, b type MockPluginRepo struct { repo.Service - exists bool + plugins []repo.PluginInfo } -func (m *MockPluginRepo) PluginInfo(context.Context, string, repo.CompatOpts) (*repo.PluginInfo, error) { - if !m.exists { - return nil, errors.New("plugin not found") - } - return &repo.PluginInfo{}, nil +func (m *MockPluginRepo) GetPluginsInfo(context.Context, repo.CompatOpts) ([]repo.PluginInfo, error) { + return m.plugins, nil } diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go index 9e9f8529526..06ebcb53436 100644 --- a/apps/advisor/pkg/app/checks/ifaces.go +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -17,6 +17,9 @@ type Check interface { Items(ctx context.Context) ([]any, error) // Steps returns the list of steps that will be executed Steps() []Step + // Init initializes the check. It's called before running the steps and should be idempotent. + // The result should not be cached, it should be initialized from scratch. + Init(ctx context.Context) error } // Step is a single step in a check, including its metadata diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 91e9bcbc211..68dc5a8eaf3 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -5,7 +5,6 @@ import ( "fmt" sysruntime "runtime" - "github.com/Masterminds/semver/v3" "github.com/grafana/grafana-app-sdk/logging" advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" "github.com/grafana/grafana/apps/advisor/pkg/app/checks" @@ -39,6 +38,7 @@ type check struct { PluginRepo repo.Service updateChecker pluginchecker.PluginUpdateChecker GrafanaVersion string + pluginIndex map[string]repo.PluginInfo } func (c *check) ID() string { @@ -62,25 +62,38 @@ func (c *check) Item(ctx context.Context, id string) (any, error) { return p, nil } +func (c *check) Init(ctx context.Context) error { + compatOpts := repo.NewCompatOpts(c.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) + plugins, err := c.PluginRepo.GetPluginsInfo(ctx, compatOpts) + if err != nil { + return err + } + c.pluginIndex = make(map[string]repo.PluginInfo) + for _, p := range plugins { + c.pluginIndex[p.Slug] = p + } + return nil +} + func (c *check) Steps() []checks.Step { return []checks.Step{ &deprecationStep{ - PluginRepo: c.PluginRepo, GrafanaVersion: c.GrafanaVersion, updateChecker: c.updateChecker, + pluginIndex: c.pluginIndex, }, &updateStep{ - PluginRepo: c.PluginRepo, GrafanaVersion: c.GrafanaVersion, updateChecker: c.updateChecker, + pluginIndex: c.pluginIndex, }, } } type deprecationStep struct { - PluginRepo repo.Service GrafanaVersion string updateChecker pluginchecker.PluginUpdateChecker + pluginIndex map[string]repo.PluginInfo } func (s *deprecationStep) Title() string { @@ -111,9 +124,8 @@ func (s *deprecationStep) Run(ctx context.Context, log logging.Logger, _ *adviso } // Check if plugin is deprecated - compatOpts := repo.NewCompatOpts(s.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) - i, err := s.PluginRepo.PluginInfo(ctx, p.ID, compatOpts) - if err != nil { + i, ok := s.pluginIndex[p.ID] + if !ok { // Unable to check deprecation status return nil, nil } @@ -135,9 +147,9 @@ func (s *deprecationStep) Run(ctx context.Context, log logging.Logger, _ *adviso } type updateStep struct { - PluginRepo repo.Service GrafanaVersion string updateChecker pluginchecker.PluginUpdateChecker + pluginIndex map[string]repo.PluginInfo } func (s *updateStep) Title() string { @@ -167,13 +179,12 @@ func (s *updateStep) Run(ctx context.Context, log logging.Logger, _ *advisor.Che } // Check if plugin has a newer version available - compatOpts := repo.NewCompatOpts(s.GrafanaVersion, sysruntime.GOOS, sysruntime.GOARCH) - info, err := s.PluginRepo.GetPluginArchiveInfo(ctx, p.ID, "", compatOpts) - if err != nil { + info, ok := s.pluginIndex[p.ID] + if !ok { // Unable to check updates return nil, nil } - if hasUpdate(p, info) { + if s.updateChecker.CanUpdate(p.ID, p.Info.Version, info.Version, false) { return []advisor.CheckReportFailure{checks.NewCheckReportFailure( advisor.CheckReportFailureSeverityLow, s.ID(), @@ -190,14 +201,3 @@ func (s *updateStep) Run(ctx context.Context, log logging.Logger, _ *advisor.Che return nil, nil } - -func hasUpdate(current pluginstore.Plugin, latest *repo.PluginArchiveInfo) bool { - // If both versions are semver-valid, compare them - v1, err1 := semver.NewVersion(current.Info.Version) - v2, err2 := semver.NewVersion(latest.Version) - if err1 == nil && err2 == nil { - return v1.LessThan(v2) - } - // In other case, assume that a different latest version will always be newer - return current.Info.Version != latest.Version -} diff --git a/apps/advisor/pkg/app/checks/plugincheck/check_test.go b/apps/advisor/pkg/app/checks/plugincheck/check_test.go index b85f33db402..3bd0a97b62e 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check_test.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check_test.go @@ -19,8 +19,7 @@ func TestRun(t *testing.T) { tests := []struct { name string plugins []pluginstore.Plugin - pluginInfo map[string]*repo.PluginInfo - pluginArchives map[string]*repo.PluginArchiveInfo + pluginInfo []repo.PluginInfo pluginPreinstalled []string pluginManaged []string pluginProvisioned []string @@ -36,11 +35,8 @@ func TestRun(t *testing.T) { plugins: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "plugin1", Name: "Plugin 1", Info: plugins.Info{Version: "1.0.0"}}}, }, - pluginInfo: map[string]*repo.PluginInfo{ - "plugin1": {Status: "deprecated"}, - }, - pluginArchives: map[string]*repo.PluginArchiveInfo{ - "plugin1": {Version: "1.0.0"}, + pluginInfo: []repo.PluginInfo{ + {Status: "deprecated", Slug: "plugin1", Version: "1.0.0"}, }, expectedFailures: []advisor.CheckReportFailure{ { @@ -62,11 +58,8 @@ func TestRun(t *testing.T) { plugins: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "plugin2", Name: "Plugin 2", Info: plugins.Info{Version: "1.0.0"}}}, }, - pluginInfo: map[string]*repo.PluginInfo{ - "plugin2": {Status: "active"}, - }, - pluginArchives: map[string]*repo.PluginArchiveInfo{ - "plugin2": {Version: "1.1.0"}, + pluginInfo: []repo.PluginInfo{ + {Status: "active", Slug: "plugin2", Version: "1.1.0"}, }, expectedFailures: []advisor.CheckReportFailure{ { @@ -88,37 +81,18 @@ func TestRun(t *testing.T) { plugins: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "plugin2", Name: "Plugin 2", Info: plugins.Info{Version: "alpha"}}}, }, - pluginInfo: map[string]*repo.PluginInfo{ - "plugin2": {Status: "active"}, - }, - pluginArchives: map[string]*repo.PluginArchiveInfo{ - "plugin2": {Version: "beta"}, - }, - expectedFailures: []advisor.CheckReportFailure{ - { - Severity: advisor.CheckReportFailureSeverityLow, - StepID: "update", - Item: "Plugin 2", - ItemID: "plugin2", - Links: []advisor.CheckErrorLink{ - { - Url: "/plugins/plugin2?page=version-history", - Message: "Upgrade", - }, - }, - }, + pluginInfo: []repo.PluginInfo{ + {Status: "active", Slug: "plugin2", Version: "beta"}, }, + expectedFailures: []advisor.CheckReportFailure{}, // Cannot be compared because the version is not semver }, { name: "Plugin pinned", plugins: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "plugin3", Name: "Plugin 3", Info: plugins.Info{Version: "1.0.0"}}}, }, - pluginInfo: map[string]*repo.PluginInfo{ - "plugin3": {Status: "deprecated"}, // This should be ignored - }, - pluginArchives: map[string]*repo.PluginArchiveInfo{ - "plugin3": {Version: "1.1.0"}, + pluginInfo: []repo.PluginInfo{ + {Status: "deprecated", Slug: "plugin3"}, // This should be ignored }, pluginPreinstalled: []string{"plugin3"}, expectedFailures: []advisor.CheckReportFailure{}, @@ -128,11 +102,8 @@ func TestRun(t *testing.T) { plugins: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "plugin4", Name: "Plugin 4", Info: plugins.Info{Version: "1.0.0"}}}, }, - pluginInfo: map[string]*repo.PluginInfo{ - "plugin4": {Status: "deprecated"}, // This should be ignored - }, - pluginArchives: map[string]*repo.PluginArchiveInfo{ - "plugin4": {Version: "1.1.0"}, + pluginInfo: []repo.PluginInfo{ + {Status: "deprecated", Slug: "plugin4", Version: "1.1.0"}, // This should be ignored }, pluginManaged: []string{"plugin4"}, expectedFailures: []advisor.CheckReportFailure{}, @@ -142,11 +113,8 @@ func TestRun(t *testing.T) { plugins: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "plugin5", Name: "Plugin 5", Info: plugins.Info{Version: "1.0.0"}}}, }, - pluginInfo: map[string]*repo.PluginInfo{ - "plugin5": {Status: "deprecated"}, // This should be ignored - }, - pluginArchives: map[string]*repo.PluginArchiveInfo{ - "plugin5": {Version: "1.1.0"}, + pluginInfo: []repo.PluginInfo{ + {Status: "deprecated", Slug: "plugin5", Version: "1.1.0"}, // This should be ignored }, pluginProvisioned: []string{"plugin5"}, expectedFailures: []advisor.CheckReportFailure{}, @@ -157,8 +125,7 @@ func TestRun(t *testing.T) { t.Run(tt.name, func(t *testing.T) { pluginStore := &mockPluginStore{plugins: tt.plugins} pluginRepo := &mockPluginRepo{ - pluginInfo: tt.pluginInfo, - pluginArchiveInfo: tt.pluginArchives, + pluginInfo: tt.pluginInfo, } pluginPreinstall := &mockPluginPreinstall{pinned: tt.pluginPreinstalled} managedPlugins := &mockManagedPlugins{managed: tt.pluginManaged} @@ -169,6 +136,8 @@ func TestRun(t *testing.T) { items, err := check.Items(context.Background()) assert.NoError(t, err) failures := []advisor.CheckReportFailure{} + err = check.Init(context.Background()) + assert.NoError(t, err) for _, step := range check.Steps() { for _, item := range items { stepFailures, err := step.Run(context.Background(), logging.DefaultLogger, &advisor.CheckSpec{}, item) @@ -196,16 +165,11 @@ func (m *mockPluginStore) Plugins(ctx context.Context, t ...plugins.Type) []plug type mockPluginRepo struct { repo.Service - pluginInfo map[string]*repo.PluginInfo - pluginArchiveInfo map[string]*repo.PluginArchiveInfo + pluginInfo []repo.PluginInfo } -func (m *mockPluginRepo) PluginInfo(ctx context.Context, id string, compatOpts repo.CompatOpts) (*repo.PluginInfo, error) { - return m.pluginInfo[id], nil -} - -func (m *mockPluginRepo) GetPluginArchiveInfo(ctx context.Context, id, version string, opts repo.CompatOpts) (*repo.PluginArchiveInfo, error) { - return m.pluginArchiveInfo[id], nil +func (m *mockPluginRepo) GetPluginsInfo(ctx context.Context, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error) { + return m.pluginInfo, nil } type mockPluginPreinstall struct { diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go index fefe646a421..ac671cc58bc 100644 --- a/apps/advisor/pkg/app/utils.go +++ b/apps/advisor/pkg/app/utils.go @@ -42,6 +42,10 @@ func processCheck(ctx context.Context, log logging.Logger, client resource.Clien return fmt.Errorf("invalid object type") } // Get the items to check + err := check.Init(ctx) + if err != nil { + return fmt.Errorf("error initializing check: %w", err) + } items, err := check.Items(ctx) if err != nil { setErr := checks.SetStatusAnnotation(ctx, client, obj, checks.StatusAnnotationError) @@ -95,6 +99,10 @@ func processCheckRetry(ctx context.Context, log logging.Logger, client resource. return fmt.Errorf("invalid object type") } // Get the items to check + err := check.Init(ctx) + if err != nil { + return fmt.Errorf("error initializing check: %w", err) + } item, err := check.Item(ctx, itemToRetry) if err != nil { setErr := checks.SetStatusAnnotation(ctx, client, obj, checks.StatusAnnotationError) diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go index f97ee3778ab..37015cf419a 100644 --- a/apps/advisor/pkg/app/utils_test.go +++ b/apps/advisor/pkg/app/utils_test.go @@ -252,6 +252,10 @@ func (m *mockCheck) Item(ctx context.Context, id string) (any, error) { return m.items[0], nil } +func (m *mockCheck) Init(ctx context.Context) error { + return nil +} + func (m *mockCheck) Steps() []checks.Step { return []checks.Step{ &mockStep{err: m.err, panics: m.runPanics}, diff --git a/pkg/plugins/manager/fakes/fakes.go b/pkg/plugins/manager/fakes/fakes.go index 9eee0056d2e..eb9ecae8cfc 100644 --- a/pkg/plugins/manager/fakes/fakes.go +++ b/pkg/plugins/manager/fakes/fakes.go @@ -276,6 +276,10 @@ func (r *FakePluginRepo) PluginInfo(ctx context.Context, pluginID string, compat return &repo.PluginInfo{}, nil } +func (r *FakePluginRepo) GetPluginsInfo(ctx context.Context, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error) { + return []repo.PluginInfo{}, nil +} + type fakeTracerProvider struct { noop.TracerProvider } diff --git a/pkg/plugins/repo/ifaces.go b/pkg/plugins/repo/ifaces.go index 3674540d90b..3ca6f6b100b 100644 --- a/pkg/plugins/repo/ifaces.go +++ b/pkg/plugins/repo/ifaces.go @@ -16,8 +16,8 @@ type Service interface { GetPluginArchiveInfo(ctx context.Context, pluginID, version string, opts CompatOpts) (*PluginArchiveInfo, error) // PluginVersion will return plugin version based on the requested information. PluginVersion(ctx context.Context, pluginID, version string, compatOpts CompatOpts) (VersionData, error) - // PluginInfo will return generic plugin information from grafana.com/api/plugins. - PluginInfo(ctx context.Context, pluginID string, compatOpts CompatOpts) (*PluginInfo, error) + // GetPluginsInfo will return a list of plugins from grafana.com/api/plugins. + GetPluginsInfo(ctx context.Context, compatOpts CompatOpts) ([]PluginInfo, error) } type CompatOpts struct { diff --git a/pkg/plugins/repo/models.go b/pkg/plugins/repo/models.go index 2b3e7123124..43f90d4f277 100644 --- a/pkg/plugins/repo/models.go +++ b/pkg/plugins/repo/models.go @@ -35,7 +35,8 @@ type ArchMeta struct { // PluginInfo is (a subset of) the JSON response from grafana.com/api/plugins/$pluginID type PluginInfo struct { - ID int `json:"id"` - Status string `json:"status"` - Slug string `json:"slug"` + ID int `json:"id"` + Status string `json:"status"` + Slug string `json:"slug"` + Version string `json:"version"` } diff --git a/pkg/plugins/repo/service.go b/pkg/plugins/repo/service.go index aa8c2a1b7f0..2bdd1d77c09 100644 --- a/pkg/plugins/repo/service.go +++ b/pkg/plugins/repo/service.go @@ -132,25 +132,25 @@ func (m *Manager) grafanaCompatiblePluginVersions(ctx context.Context, pluginID return v.Versions, nil } -func (m *Manager) PluginInfo(ctx context.Context, pluginID string, compatOpts CompatOpts) (*PluginInfo, error) { +func (m *Manager) GetPluginsInfo(ctx context.Context, compatOpts CompatOpts) ([]PluginInfo, error) { u, err := url.Parse(m.client.grafanaComAPIURL) if err != nil { return nil, err } - u.Path = path.Join(u.Path, pluginID) - body, err := m.client.SendReq(ctx, u, compatOpts) if err != nil { return nil, err } - var v PluginInfo + var v struct { + Items []PluginInfo `json:"items"` + } err = json.Unmarshal(body, &v) if err != nil { m.log.Error("Failed to unmarshal plugin repo response", "error", err) return nil, err } - return &v, nil + return v.Items, nil } diff --git a/pkg/plugins/repo/service_test.go b/pkg/plugins/repo/service_test.go index 613468b39ef..4dae0b1ac0e 100644 --- a/pkg/plugins/repo/service_test.go +++ b/pkg/plugins/repo/service_test.go @@ -108,16 +108,17 @@ func TestGetPluginArchive(t *testing.T) { } } -func TestPluginInfo(t *testing.T) { +func TestPluginIndex(t *testing.T) { const ( pluginID = "grafana-test-datasource" ) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, fmt.Sprintf("/%s", pluginID), r.URL.Path) + require.Equal(t, "12.0.0", r.Header.Get("grafana-version")) + require.Equal(t, "grafana 12.0.0", r.Header.Get("User-Agent")) w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json") - _, _ = fmt.Fprintf(w, `{ "id": 1, "slug": "%s", "status": "active" }`, pluginID) + _, _ = fmt.Fprintf(w, `{"items": [{ "id": 1, "slug": "%s", "status": "active" }]}`, pluginID) })) t.Cleanup(srv.Close) @@ -126,11 +127,18 @@ func TestPluginInfo(t *testing.T) { BaseURL: srv.URL, Logger: log.NewTestPrettyLogger(), }) - pi, err := m.PluginInfo(context.Background(), pluginID, CompatOpts{}) + pi, err := m.GetPluginsInfo(context.Background(), CompatOpts{ + grafanaVersion: "12.0.0", + system: SystemCompatOpts{ + os: "darwin", + arch: "amd64", + }, + }) require.NoError(t, err) - require.Equal(t, 1, pi.ID) - require.Equal(t, pluginID, pi.Slug) - require.Equal(t, "active", pi.Status) + require.Len(t, pi, 1) + require.Equal(t, 1, pi[0].ID) + require.Equal(t, pluginID, pi[0].Slug) + require.Equal(t, "active", pi[0].Status) } func verifyArchive(t *testing.T, archive *PluginArchive) {