Merge pull request #23939 from backstage/rugvip/no-mod

backend-app-api: stop loading modules if parent plugin is not present
This commit is contained in:
Ben Lambert
2024-04-03 12:56:11 +02:00
committed by GitHub
6 changed files with 71 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': minor
---
**BREAKING**: Modules are no longer loaded unless the plugin that they extend is present.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
`startTestBackend` will now add placeholder plugins when a modules are provided without their parent plugin.
@@ -280,7 +280,7 @@ describe('featureDiscoveryServiceFactory', () => {
'detected-module',
'detected-plugin-with-alpha',
],
exclude: ['detected-plugin'],
exclude: ['detected-module'],
},
},
},
@@ -288,9 +288,9 @@ describe('featureDiscoveryServiceFactory', () => {
],
});
expect(mock.warn).not.toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin');
expect(mock.warn).not.toHaveBeenCalledWith('detected-library');
expect(mock.warn).toHaveBeenCalledWith('detected-module');
expect(mock.warn).not.toHaveBeenCalledWith('detected-module');
expect(mock.warn).toHaveBeenCalledWith('detected-plugin-with-alpha');
});
@@ -60,6 +60,16 @@ const baseFactories = [
loggerServiceFactory(),
];
const testPlugin = createBackendPlugin({
pluginId: 'test',
register(reg) {
reg.registerInit({
deps: {},
async init() {},
});
},
})();
describe('BackendInitializer', () => {
it('should initialize root scoped services', async () => {
const rootFactory = jest.fn();
@@ -99,6 +109,7 @@ describe('BackendInitializer', () => {
});
const init = new BackendInitializer(baseFactories);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
@@ -172,6 +183,7 @@ describe('BackendInitializer', () => {
it('should forward errors when modules fail to start', async () => {
const init = new BackendInitializer([]);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
@@ -222,6 +234,7 @@ describe('BackendInitializer', () => {
it('should reject duplicate modules', async () => {
const init = new BackendInitializer([]);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
@@ -262,6 +275,7 @@ describe('BackendInitializer', () => {
factory: () => new MockLogger(),
})(),
]);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test',
@@ -308,9 +322,10 @@ describe('BackendInitializer', () => {
},
})(),
);
init.add(testPlugin);
init.add(
createBackendModule({
pluginId: 'test-b',
pluginId: 'test',
moduleId: 'mod',
register(reg) {
reg.registerInit({
@@ -321,7 +336,7 @@ describe('BackendInitializer', () => {
})(),
);
await expect(init.start()).rejects.toThrow(
"Illegal dependency: Module 'mod' for plugin 'test-b' attempted to depend on extension point 'a' for plugin 'test-a'. Extension points can only be used within their plugin's scope.",
"Illegal dependency: Module 'mod' for plugin 'test' attempted to depend on extension point 'a' for plugin 'test-a'. Extension points can only be used within their plugin's scope.",
);
});
});
@@ -228,9 +228,7 @@ export class BackendInitializer {
}
}
const allPluginIds = [
...new Set([...pluginInits.keys(), ...moduleInits.keys()]),
];
const allPluginIds = [...pluginInits.keys()];
// All plugins are initialized in parallel
await Promise.all(
@@ -29,6 +29,7 @@ import {
ExtensionPoint,
coreServices,
createBackendModule,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { mockServices } from '../services';
import { ConfigReader } from '@backstage/config';
@@ -84,6 +85,42 @@ export const defaultServiceFactories = [
mockServices.urlReader.factory(),
];
/**
* Given a set of features, return an array of plugins that ensures that each
* module in the provided set of features has a corresponding plugin.
* @internal
*/
function createPluginsForOrphanModules(features: Array<BackendFeature>) {
const pluginIds = new Set<string>();
const modulePluginIds = new Set<string>();
for (const feature of features) {
if (isInternalBackendFeature(feature)) {
const registrations = feature.getRegistrations();
for (const registration of registrations) {
if (registration.type === 'plugin') {
pluginIds.add(registration.pluginId);
} else if (registration.type === 'module') {
modulePluginIds.add(registration.pluginId);
}
}
}
}
for (const pluginId of pluginIds) {
modulePluginIds.delete(pluginId);
}
return Array.from(modulePluginIds).map(pluginId =>
createBackendPlugin({
pluginId,
register(reg) {
reg.registerInit({ deps: {}, async init() {} });
},
}),
);
}
/**
* Given a set of extension points and features, find the extension
* points that we mock and tie them to the correct plugin ID.
@@ -277,7 +314,9 @@ export async function startTestBackend<TExtensionPoints extends any[]>(
for (const m of createExtensionPointTestModules(features, extensionPoints)) {
backend.add(m);
}
for (const p of createPluginsForOrphanModules(features)) {
backend.add(p);
}
for (const feature of features) {
backend.add(feature);
}