diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 8e92fc7120..737aba4743 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -724,4 +724,59 @@ describe('BackendInitializer', () => { await init.start(); }); + + it('should properly add plugins + modules to the instance metadata service', async () => { + expect.assertions(1); + const backend = new BackendInitializer(baseFactories); + const plugin = createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }); + const instanceMetadataPlugin = createBackendPlugin({ + pluginId: 'instance-metadata', + register(reg) { + reg.registerInit({ + deps: { + instanceMetadata: coreServices.EXPERIMENTAL_instanceMetadata, + }, + async init({ instanceMetadata }) { + expect(instanceMetadata.getInstalledFeatures()).toEqual([ + { + pluginId: 'test', + type: 'plugin', + }, + { + pluginId: 'test', + moduleId: 'test', + type: 'module', + }, + { + pluginId: 'instance-metadata', + type: 'plugin', + }, + ]); + }, + }); + }, + }); + const module = createBackendModule({ + pluginId: 'test', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }); + backend.add(plugin); + backend.add(module); + backend.add(instanceMetadataPlugin); + await backend.start(); + }); }); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 3ea1200898..cfbaea462c 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -22,6 +22,7 @@ import { ServiceFactory, LifecycleService, RootLifecycleService, + createServiceFactory, } from '@backstage/backend-plugin-api'; import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication @@ -96,6 +97,49 @@ const instanceRegistry = new (class InstanceRegistry { }; })(); +type BackendFeatureMeta = + | { + type: 'plugin'; + pluginId: string; + } + | { + type: 'module'; + pluginId: string; + moduleId: string; + }; + +function createInstanceMetadataServiceFactory( + registrations: InternalBackendRegistrations[], +) { + const installedFeatures = registrations + .map(registration => { + if (registration.featureType === 'registrations') { + return registration + .getRegistrations() + .map(feature => { + if (feature.type === 'plugin') { + return { type: 'plugin', pluginId: feature.pluginId }; + } else if (feature.type === 'module') { + return { + type: 'module', + pluginId: feature.pluginId, + moduleId: feature.moduleId, + }; + } + return undefined; + }) + .filter(Boolean) as BackendFeatureMeta[]; + } + return []; + }) + .flat(); + return createServiceFactory({ + service: coreServices.EXPERIMENTAL_instanceMetadata, + deps: {}, + factory: async () => ({ getInstalledFeatures: () => installedFeatures }), + }); +} + export class BackendInitializer { #startPromise?: Promise; #stopPromise?: Promise; @@ -210,6 +254,10 @@ export class BackendInitializer { await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); + this.#serviceRegistry.add( + createInstanceMetadataServiceFactory(this.#registrations), + ); + // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); diff --git a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts new file mode 100644 index 0000000000..c9eea973bd --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type BackendFeatureMeta = + | { + type: 'plugin'; + pluginId: string; + } + | { + type: 'module'; + pluginId: string; + moduleId: string; + }; + +export interface InstanceMetadataService { + getInstalledFeatures: () => BackendFeatureMeta[]; +} diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index f87bff023c..06e8905614 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -251,4 +251,15 @@ export namespace coreServices { export const urlReader = createServiceRef< import('./UrlReaderService').UrlReaderService >({ id: 'core.urlReader' }); + + /** + * EXPERIMENTAL: Instance metadata service. + * + * @public + */ + export const EXPERIMENTAL_instanceMetadata = createServiceRef< + import('./InstanceMetadataService').InstanceMetadataService + >({ + id: 'core.instanceMetadata', + }); } diff --git a/packages/backend/src/instanceMetadata.ts b/packages/backend/src/instanceMetadata.ts new file mode 100644 index 0000000000..2e6c4e2035 --- /dev/null +++ b/packages/backend/src/instanceMetadata.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +export default createBackendPlugin({ + pluginId: 'instance-metadata-logging', + register(env) { + env.registerInit({ + deps: { + instanceMetadata: coreServices.EXPERIMENTAL_instanceMetadata, + }, + async init({ instanceMetadata }) { + console.log('metadata', instanceMetadata.getInstalledFeatures()); + }, + }); + }, +});