diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 606661af2a3..32ad6215875 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -17,6 +17,7 @@ import { getExposedComponentPluginDependencies, getAppPluginConfigs, getAppPluginIdFromExposedComponentId, + getAppPluginDependencies, } from './utils'; jest.mock('app/features/plugins/pluginSettings', () => ({ @@ -879,4 +880,93 @@ describe('Plugin Extensions / Utils', () => { expect(appPluginIds).toEqual(['myorg-second-app', 'myorg-fourth-app', 'myorg-fifth-app']); }); }); + + describe('getAppPluginDependencies()', () => { + const originalApps = config.apps; + const genereicAppPluginConfig = { + path: '', + version: '', + preload: false, + angular: { + detected: false, + hideDeprecation: false, + }, + loadingStrategy: PluginLoadingStrategy.fetch, + dependencies: { + grafanaVersion: '8.0.0', + plugins: [], + extensions: { + exposedComponents: [], + }, + }, + extensions: { + addedLinks: [], + addedComponents: [], + exposedComponents: [], + extensionPoints: [], + }, + }; + + afterEach(() => { + config.apps = originalApps; + }); + + test('should not end up in an infinite loop if there are circular dependencies', () => { + config.apps = { + 'myorg-first-app': { + ...genereicAppPluginConfig, + id: 'myorg-first-app', + }, + 'myorg-second-app': { + ...genereicAppPluginConfig, + id: 'myorg-second-app', + dependencies: { + ...genereicAppPluginConfig.dependencies, + extensions: { + exposedComponents: ['myorg-third-app/link/v1'], + }, + }, + }, + 'myorg-third-app': { + ...genereicAppPluginConfig, + id: 'myorg-third-app', + dependencies: { + ...genereicAppPluginConfig.dependencies, + extensions: { + exposedComponents: ['myorg-second-app/link/v1'], + }, + }, + }, + }; + + const appPluginIds = getAppPluginDependencies('myorg-second-app'); + + expect(appPluginIds).toEqual(['myorg-third-app']); + }); + + test('should not end up in an infinite loop if a plugin depends on itself', () => { + config.apps = { + 'myorg-first-app': { + ...genereicAppPluginConfig, + id: 'myorg-first-app', + }, + 'myorg-second-app': { + ...genereicAppPluginConfig, + id: 'myorg-second-app', + dependencies: { + ...genereicAppPluginConfig.dependencies, + extensions: { + // Not a valid scenario! + // (As this is sometimes happening out in the wild, we thought it's better to also cover it with a test-case.) + exposedComponents: ['myorg-second-app/link/v1'], + }, + }, + }, + }; + + const appPluginIds = getAppPluginDependencies('myorg-second-app'); + + expect(appPluginIds).toEqual([]); + }); + }); }); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index bb339ffcf38..ff7157c46b5 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -459,18 +459,28 @@ export const getExposedComponentPluginDependencies = (exposedComponentId: string // Returns a list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions` // metadata field. (For example the plugins that expose components that the app depends on.) // Heads up! This is a recursive function. -export const getAppPluginDependencies = (pluginId: string): string[] => { +export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => { if (!config.apps[pluginId]) { return []; } + // Prevent infinite recursion (it would happen if there is a circular dependency between app plugins) + if (visited.includes(pluginId)) { + return []; + } + const pluginIdDependencies = config.apps[pluginId].dependencies.extensions.exposedComponents.map( getAppPluginIdFromExposedComponentId ); - return pluginIdDependencies.reduce((acc, pluginId) => { - return [...acc, ...getAppPluginDependencies(pluginId)]; - }, pluginIdDependencies); + return ( + pluginIdDependencies + .reduce((acc, _pluginId) => { + return [...acc, ...getAppPluginDependencies(_pluginId, [...visited, pluginId])]; + }, pluginIdDependencies) + // We don't want the plugin to "depend on itself" + .filter((id) => id !== pluginId) + ); }; // Returns a list of app plugins that has to be loaded before core Grafana could finish the initialization.