diff --git a/.changeset/young-months-admire.md b/.changeset/young-months-admire.md new file mode 100644 index 0000000000..a2062b3f88 --- /dev/null +++ b/.changeset/young-months-admire.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-app-api': minor +'@backstage/backend-plugin-api': minor +--- + +**EXPERIMENTAL**: Adds a new `instanceMetadataService` to hold information about a specific backend instance. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 8e92fc7120..f6d65e883e 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -27,6 +27,7 @@ import { createBackendFeatureLoader, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; +import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; class MockLogger { debug() {} @@ -724,4 +725,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: instanceMetadataServiceRef, + }, + 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..3e7b46fc9e 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 @@ -34,7 +35,11 @@ import type { // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; import { ForwardedError, ConflictError } from '@backstage/errors'; -import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + instanceMetadataServiceRef, + featureDiscoveryServiceRef, + BackendFeatureMeta, +} from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationLogger } from './createInitializationLogger'; @@ -96,6 +101,39 @@ const instanceRegistry = new (class InstanceRegistry { }; })(); +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, + }; + } + // Ignore unknown feature types. + return undefined; + }) + .filter(Boolean) as BackendFeatureMeta[]; + } + return []; + }) + .flat(); + return createServiceFactory({ + service: instanceMetadataServiceRef, + deps: {}, + factory: async () => ({ getInstalledFeatures: () => installedFeatures }), + }); +} + export class BackendInitializer { #startPromise?: Promise; #stopPromise?: Promise; @@ -210,6 +248,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/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index dea357502e..8509d1196d 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -6,6 +6,18 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; +// @alpha (undocumented) +export type BackendFeatureMeta = + | { + type: 'plugin'; + pluginId: string; + } + | { + type: 'module'; + pluginId: string; + moduleId: string; + }; + // @alpha (undocumented) export interface FeatureDiscoveryService { // (undocumented) @@ -21,5 +33,18 @@ export const featureDiscoveryServiceRef: ServiceRef< 'singleton' >; +// @alpha (undocumented) +export interface InstanceMetadataService { + // (undocumented) + getInstalledFeatures: () => BackendFeatureMeta[]; +} + +// @alpha +export const instanceMetadataServiceRef: ServiceRef< + InstanceMetadataService, + 'plugin', + 'singleton' +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-plugin-api/src/alpha.ts b/packages/backend-plugin-api/src/alpha.ts index 3f26c66fe9..1a39fe6c23 100644 --- a/packages/backend-plugin-api/src/alpha.ts +++ b/packages/backend-plugin-api/src/alpha.ts @@ -34,3 +34,19 @@ export const featureDiscoveryServiceRef = id: 'core.featureDiscovery', scope: 'root', }); + +/** + * EXPERIMENTAL: Instance metadata service. + * + * @alpha + */ +export const instanceMetadataServiceRef = createServiceRef< + import('./services/definitions/InstanceMetadataService').InstanceMetadataService +>({ + id: 'core.instanceMetadata', +}); + +export type { + BackendFeatureMeta, + InstanceMetadataService, +} from './services/definitions/InstanceMetadataService'; 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..869d361343 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/** @alpha */ +export type BackendFeatureMeta = + | { + type: 'plugin'; + pluginId: string; + } + | { + type: 'module'; + pluginId: string; + moduleId: string; + }; + +/** @alpha */ +export interface InstanceMetadataService { + getInstalledFeatures: () => BackendFeatureMeta[]; +} diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index d8a4bcb98f..8ae9864a4a 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -59,5 +59,6 @@ backend.add(searchLoader); backend.add(import('@backstage/plugin-techdocs-backend')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); +backend.add(import('./instanceMetadata')); backend.start(); diff --git a/packages/backend/src/instanceMetadata.ts b/packages/backend/src/instanceMetadata.ts new file mode 100644 index 0000000000..dd499aa40a --- /dev/null +++ b/packages/backend/src/instanceMetadata.ts @@ -0,0 +1,38 @@ +/* + * 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'; +import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; + +// Example usage of the instance metadata service to log the installed features. +export default createBackendPlugin({ + pluginId: 'instance-metadata-logging', + register(env) { + env.registerInit({ + deps: { + instanceMetadata: instanceMetadataServiceRef, + logger: coreServices.logger, + }, + async init({ instanceMetadata, logger }) { + logger.info( + `Installed features on this instance: ${instanceMetadata.getInstalledFeatures()}`, + ); + }, + }); + }, +});