diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 8a55a128763..45f92e05a43 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -122,6 +122,7 @@ export class GrafanaBootConfig implements GrafanaConfig { pluginAdminEnabled = true; pluginAdminExternalManageEnabled = false; pluginCatalogHiddenPlugins: string[] = []; + pluginCatalogManagedPlugins: string[] = []; pluginsCDNBaseURL = ''; expressionsEnabled = false; customTheme?: undefined; diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index 60a4964b56e..a07a5e4358c 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -227,6 +227,7 @@ type FrontendSettingsDTO struct { PluginAdminEnabled bool `json:"pluginAdminEnabled"` PluginAdminExternalManageEnabled bool `json:"pluginAdminExternalManageEnabled"` PluginCatalogHiddenPlugins []string `json:"pluginCatalogHiddenPlugins"` + PluginCatalogManagedPlugins []string `json:"pluginCatalogManagedPlugins"` ExpressionsEnabled bool `json:"expressionsEnabled"` AwsAllowedAuthProviders []string `json:"awsAllowedAuthProviders"` AwsAssumeRoleEnabled bool `json:"awsAssumeRoleEnabled"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 750ec9a9f11..90b3e5317e1 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -266,6 +266,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro PluginAdminEnabled: hs.Cfg.PluginAdminEnabled, PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled, PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins, + PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()), ExpressionsEnabled: hs.Cfg.ExpressionsEnabled, AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders, AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled, diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index da298c3f0f4..6f3f9ad5c68 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/rendering" @@ -77,8 +78,9 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, PluginSettings: cfg.PluginSettings, }), - namespacer: request.GetNamespaceMapper(cfg), - SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}), + namespacer: request.GetNamespaceMapper(cfg), + SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}), + managedPluginsService: managedplugins.NewNoop(), } m := web.New() diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 9f03b7f833e..554d51b9099 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -25,10 +25,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/grafana/grafana/pkg/services/anonymous" - grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" httpstatic "github.com/grafana/grafana/pkg/api/static" @@ -48,7 +44,10 @@ import ( "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/anonymous" "github.com/grafana/grafana/pkg/services/apikey" + grafanaapiserver "github.com/grafana/grafana/pkg/services/apiserver" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/cleanup" @@ -78,6 +77,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/plugindashboards" + "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" @@ -195,6 +195,7 @@ type HTTPServer struct { apiKeyService apikey.Service kvStore kvstore.KVStore pluginsCDNService *pluginscdn.Service + managedPluginsService managedplugins.Manager userService user.Service tempUserService tempUser.Service @@ -254,7 +255,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, - starService star.Service, csrfService csrf.Service, + starService star.Service, csrfService csrf.Service, managedPlugins managedplugins.Manager, playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, secretsService secrets.Service, secretsPluginMigrator spm.SecretMigrationProvider, secretsStore secretsKV.SecretsKVStore, @@ -359,6 +360,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi statsService: statsService, authnService: authnService, pluginsCDNService: pluginsCDNService, + managedPluginsService: managedPlugins, starApi: starApi, promRegister: promRegister, promGatherer: promGatherer, diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 3258000d063..b29b0fdfa00 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -12,12 +12,12 @@ import ( "strings" "testing" + "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" @@ -39,6 +39,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" + "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginerrs" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" @@ -99,6 +100,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) { ID: pluginID, }, }) + hs.managedPluginsService = managedplugins.NewNoop() expectedIdentity := &authn.Identity{ OrgID: tc.permissionOrg, @@ -641,6 +643,7 @@ func Test_PluginsList_AccessControl(t *testing.T) { hs.PluginSettings = &pluginSettings hs.pluginStore = pluginstore.New(pluginRegistry, &fakes.FakeLoader{}) hs.pluginFileStore = filestore.ProvideService(pluginRegistry) + hs.managedPluginsService = managedplugins.NewNoop() var err error hs.pluginsUpdateChecker, err = updatechecker.ProvidePluginsService(hs.Cfg, nil, tracing.InitializeTracerForTest()) require.NoError(t, err) diff --git a/pkg/services/pluginsintegration/managedplugins/managed.go b/pkg/services/pluginsintegration/managedplugins/managed.go new file mode 100644 index 00000000000..1e3c4a6f369 --- /dev/null +++ b/pkg/services/pluginsintegration/managedplugins/managed.go @@ -0,0 +1,19 @@ +package managedplugins + +import "context" + +type Manager interface { + ManagedPlugins(ctx context.Context) []string +} + +var _ Manager = (*Noop)(nil) + +type Noop struct{} + +func NewNoop() *Noop { + return &Noop{} +} + +func (s *Noop) ManagedPlugins(_ context.Context) []string { + return []string{} +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 6963438dd8a..64262eecede 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -41,6 +41,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/loader" + "github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins" "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" @@ -132,10 +133,12 @@ var WireExtensionSet = wire.NewSet( wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)), signature.ProvideOSSAuthorizer, wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), - wire.Bind(new(finder.Finder), new(*finder.Local)), finder.ProvideLocalFinder, + wire.Bind(new(finder.Finder), new(*finder.Local)), ProvideClientDecorator, wire.Bind(new(plugins.Client), new(*client.Decorator)), + managedplugins.NewNoop, + wire.Bind(new(managedplugins.Manager), new(*managedplugins.Noop)), ) func ProvideClientDecorator( diff --git a/pkg/services/pluginsintegration/pluginstore/plugins.go b/pkg/services/pluginsintegration/pluginstore/plugins.go index 273e20ef3e9..194ba92ade4 100644 --- a/pkg/services/pluginsintegration/pluginstore/plugins.go +++ b/pkg/services/pluginsintegration/pluginstore/plugins.go @@ -74,7 +74,6 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin { Module: p.Module, BaseURL: p.BaseURL, ExternalService: p.ExternalService, - - Angular: p.Angular, + Angular: p.Angular, } } diff --git a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts index e4c0bf11c3d..4372f87602f 100644 --- a/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts +++ b/public/app/features/plugins/admin/__mocks__/catalogPlugin.mock.ts @@ -20,6 +20,7 @@ export default { isDisabled: false, isDeprecated: false, isPublished: true, + isManaged: false, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2093, diff --git a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx index 40d747aefea..0a67af40b99 100644 --- a/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx +++ b/public/app/features/plugins/admin/components/GetStartedWithPlugin/GetStartedWithDataSource.test.tsx @@ -32,6 +32,7 @@ const plugin: CatalogPlugin = { isDisabled: false, isDeprecated: false, isPublished: true, + isManaged: false, }; describe('GetStartedWithDataSource', () => { diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx index 3894a2c05b8..258f5895d9d 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.test.tsx @@ -32,6 +32,7 @@ const plugin: CatalogPlugin = { isDisabled: false, isDeprecated: false, isPublished: true, + isManaged: false, }; function setup(opts: { angularSupportEnabled: boolean; angularDetected: boolean }) { @@ -242,4 +243,15 @@ describe('InstallControlsButton', () => { expect(button).toBeEnabled(); }); }); + + describe('update button', () => { + it('should be hidden when plugin is managed', () => { + render( + + + + ); + expect(screen.queryByText('Update')).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx index 671a5b2e8ab..d7babbf3013 100644 --- a/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx +++ b/public/app/features/plugins/admin/components/InstallControls/InstallControlsButton.tsx @@ -152,9 +152,11 @@ export function InstallControlsButton({ return ( - + {!plugin.isManaged && ( + + )} diff --git a/public/app/features/plugins/admin/components/PluginList.test.tsx b/public/app/features/plugins/admin/components/PluginList.test.tsx index 3d8c67c9765..e3fd9c61eed 100644 --- a/public/app/features/plugins/admin/components/PluginList.test.tsx +++ b/public/app/features/plugins/admin/components/PluginList.test.tsx @@ -47,6 +47,7 @@ const getMockPlugin = (id: string): CatalogPlugin => { isDisabled: false, isDeprecated: false, isPublished: true, + isManaged: false, }; }; diff --git a/public/app/features/plugins/admin/components/PluginListItem.test.tsx b/public/app/features/plugins/admin/components/PluginListItem.test.tsx index 8aa31776608..9a609f4be84 100644 --- a/public/app/features/plugins/admin/components/PluginListItem.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItem.test.tsx @@ -57,6 +57,7 @@ describe('PluginListItem', () => { isDisabled: false, isDeprecated: false, isPublished: true, + isManaged: false, }; /** As Grid */ diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx index 2864fe38af1..251ab8df195 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.test.tsx @@ -33,6 +33,7 @@ describe('PluginListItemBadges', () => { isDisabled: false, isDeprecated: false, isPublished: true, + isManaged: false, }; afterEach(() => { @@ -76,6 +77,13 @@ describe('PluginListItemBadges', () => { expect(screen.getByText(/update available/i)).toBeVisible(); }); + it('does not render an upgrade badge (when plugin has an available update and is managed)', () => { + render( + + ); + expect(screen.queryByText(/update available/i)).toBeNull(); + }); + it('renders an angular badge (when plugin is angular)', () => { render(); expect(screen.getByText(/angular/i)).toBeVisible(); diff --git a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx index eb381aec070..bc533a791a2 100644 --- a/public/app/features/plugins/admin/components/PluginListItemBadges.tsx +++ b/public/app/features/plugins/admin/components/PluginListItemBadges.tsx @@ -24,7 +24,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) { {plugin.isDisabled && } - {hasUpdate && } + {hasUpdate && !plugin.isManaged && } {plugin.angularDetected && } ); @@ -36,7 +36,7 @@ export function PluginListItemBadges({ plugin }: PluginBadgeType) { {plugin.isDisabled && } {plugin.isDeprecated && } {plugin.isInstalled && } - {hasUpdate && } + {hasUpdate && !plugin.isManaged && } {plugin.angularDetected && } ); diff --git a/public/app/features/plugins/admin/helpers.test.ts b/public/app/features/plugins/admin/helpers.test.ts index ff4d8eca75e..dfadd646071 100644 --- a/public/app/features/plugins/admin/helpers.test.ts +++ b/public/app/features/plugins/admin/helpers.test.ts @@ -203,6 +203,7 @@ describe('Plugins/Helpers', () => { isInstalled: false, isDeprecated: false, isPublished: true, + isManaged: false, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2111, @@ -280,6 +281,7 @@ describe('Plugins/Helpers', () => { isInstalled: true, isPublished: false, isDeprecated: false, + isManaged: false, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0, @@ -332,6 +334,7 @@ describe('Plugins/Helpers', () => { isInstalled: true, isPublished: true, isDeprecated: false, + isManaged: false, name: 'Zabbix', orgName: 'Alexander Zobnin', popularity: 0.2111, diff --git a/public/app/features/plugins/admin/helpers.ts b/public/app/features/plugins/admin/helpers.ts index 2e9d9982e40..88416895c87 100644 --- a/public/app/features/plugins/admin/helpers.ts +++ b/public/app/features/plugins/admin/helpers.ts @@ -142,6 +142,7 @@ export function mapRemoteToCatalog(plugin: RemotePlugin, error?: PluginError): C isPublished: true, isInstalled: isDisabled, isDisabled: isDisabled, + isManaged: isManagedPlugin(id), isDeprecated: status === RemotePluginStatus.Deprecated, isCore: plugin.internal, isDev: false, @@ -191,6 +192,7 @@ export function mapLocalToCatalog(plugin: LocalPlugin, error?: PluginError): Cat isDeprecated: false, isDev: Boolean(dev), isEnterprise: false, + isManaged: isManagedPlugin(id), type, error: error?.errorCode, accessControl: accessControl, @@ -238,6 +240,7 @@ export function mapToCatalogPlugin(local?: LocalPlugin, remote?: RemotePlugin, e isDisabled: isDisabled, isDeprecated: remote?.status === RemotePluginStatus.Deprecated, isPublished: true, + isManaged: isManagedPlugin(id), // TODO name: remote?.name || local?.name || '', // TODO @@ -373,6 +376,12 @@ function isNotHiddenByConfig(id: string) { return !pluginCatalogHiddenPlugins.includes(id); } +export function isManagedPlugin(id: string) { + const { pluginCatalogManagedPlugins }: { pluginCatalogManagedPlugins: string[] } = config; + + return pluginCatalogManagedPlugins?.includes(id); +} + function isDisabledSecretsPlugin(type?: PluginType): boolean { return type === PluginType.secretsmanager && !config.secretsManagerPluginEnabled; } diff --git a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx index eb9c70b3051..e50b393e022 100644 --- a/public/app/features/plugins/admin/hooks/usePluginInfo.tsx +++ b/public/app/features/plugins/admin/hooks/usePluginInfo.tsx @@ -25,10 +25,17 @@ export const usePluginInfo = (plugin?: CatalogPlugin): PageInfoItem[] => { } if (Boolean(version)) { - info.push({ - label: 'Version', - value: version, - }); + if (plugin.isManaged) { + info.push({ + label: 'Version', + value: 'Managed by Grafana', + }); + } else { + info.push({ + label: 'Version', + value: version, + }); + } } if (Boolean(plugin.orgName)) { diff --git a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx index 6a656c03064..e13a855336b 100644 --- a/public/app/features/plugins/admin/pages/PluginDetails.test.tsx +++ b/public/app/features/plugins/admin/pages/PluginDetails.test.tsx @@ -315,6 +315,17 @@ describe('Plugin details page', () => { expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); }); + it('should not display an update button for a plugin that is managed', async () => { + const { queryByRole } = renderPluginDetails({ id, isInstalled: true, hasUpdate: true, isManaged: true }); + + // Does not display an "update" button + expect(await queryByRole('button', { name: /update/i })).not.toBeInTheDocument(); + expect(queryByRole('button', { name: /uninstall/i })).toBeInTheDocument(); + + // Does not display "install" button + expect(queryByRole('button', { name: /^install/i })).not.toBeInTheDocument(); + }); + it('should display an install button for enterprise plugins if license is valid', async () => { config.licenseInfo.enabledFeatures = { 'enterprise.plugins': true }; diff --git a/public/app/features/plugins/admin/types.ts b/public/app/features/plugins/admin/types.ts index 734bc504953..aa6b41c7737 100644 --- a/public/app/features/plugins/admin/types.ts +++ b/public/app/features/plugins/admin/types.ts @@ -44,6 +44,7 @@ export interface CatalogPlugin extends WithAccessControlMetadata { isInstalled: boolean; isDisabled: boolean; isDeprecated: boolean; + isManaged: boolean; // Indicates that the plugin version is managed by Grafana // `isPublished` is TRUE if the plugin is published to grafana.com isPublished: boolean; name: string;