From a17d9df2eeacca2f603601492638c508a48b8e4c Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 3 Sep 2025 09:48:34 -0400 Subject: [PATCH 01/18] feat: promote instance metadata to stable Signed-off-by: aramissennyeydd --- .changeset/honest-pandas-win.md | 5 + .changeset/moody-plums-add.md | 5 + .changeset/open-items-open.md | 5 + packages/backend-app-api/package.json | 3 +- .../src/wiring/BackendInitializer.test.ts | 31 +++--- .../src/wiring/BackendInitializer.ts | 102 +++++++++--------- .../backend-plugin-api/src/alpha/index.ts | 11 +- packages/backend-plugin-api/src/alpha/refs.ts | 9 -- .../definitions}/InstanceMetadataService.ts | 19 ++-- .../src/services/definitions/coreServices.ts | 11 ++ .../src/services/definitions/index.ts | 4 + packages/backend/src/instanceMetadata.ts | 10 +- plugins/gateway-backend/src/plugin.ts | 3 +- plugins/gateway-backend/src/router.ts | 12 +-- yarn.lock | 1 + 15 files changed, 119 insertions(+), 112 deletions(-) create mode 100644 .changeset/honest-pandas-win.md create mode 100644 .changeset/moody-plums-add.md create mode 100644 .changeset/open-items-open.md rename packages/backend-plugin-api/src/{alpha => services/definitions}/InstanceMetadataService.ts (73%) diff --git a/.changeset/honest-pandas-win.md b/.changeset/honest-pandas-win.md new file mode 100644 index 0000000000..a9dc5d7e9d --- /dev/null +++ b/.changeset/honest-pandas-win.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +Promote `instanceMetadata` service to main entrypoint. diff --git a/.changeset/moody-plums-add.md b/.changeset/moody-plums-add.md new file mode 100644 index 0000000000..a88e11b347 --- /dev/null +++ b/.changeset/moody-plums-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-gateway-backend': minor +--- + +Update usage of the `instanceMetadata` service. diff --git a/.changeset/open-items-open.md b/.changeset/open-items-open.md new file mode 100644 index 0000000000..eafce6bd60 --- /dev/null +++ b/.changeset/open-items-open.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +Updates API for `instanceMetadata` service to return a list of plugins not features. Also adds an HTTP endpoint that returns information about the current instance. diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 1613e05cc0..0261845093 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -48,7 +48,8 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/errors": "workspace:^" + "@backstage/errors": "workspace:^", + "express-promise-router": "^4.1.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 9e8a6579fd..312511f4a9 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -22,9 +22,9 @@ import { createExtensionPoint, createBackendFeatureLoader, ServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; -import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; import { mockServices } from '@backstage/backend-test-utils'; const baseFactories = [ @@ -32,6 +32,9 @@ const baseFactories = [ mockServices.lifecycle.factory(), mockServices.rootLogger.factory(), mockServices.logger.factory(), + mockServices.rootConfig.factory(), + mockServices.rootHttpRouter.mock().factory, + mockServices.rootHealth.factory(), ]; function mkNoopFactory(ref: ServiceRef<{}, 'plugin'>) { @@ -1074,7 +1077,7 @@ describe('BackendInitializer', () => { }); it('should properly add plugins + modules to the instance metadata service', async () => { - expect.assertions(2); + expect.assertions(1); const backend = new BackendInitializer(baseFactories); const plugin = createBackendPlugin({ pluginId: 'test', @@ -1090,31 +1093,23 @@ describe('BackendInitializer', () => { register(reg) { reg.registerInit({ deps: { - instanceMetadata: instanceMetadataServiceRef, + instanceMetadata: coreServices.instanceMetadata, }, async init({ instanceMetadata }) { - expect(instanceMetadata.getInstalledFeatures()).toEqual([ + expect(instanceMetadata.getInstalledPlugins()).toEqual([ { pluginId: 'test', - type: 'plugin', - }, - { - pluginId: 'test', - moduleId: 'test', - type: 'module', + modules: [ + { + moduleId: 'test', + }, + ], }, { pluginId: 'instance-metadata', - type: 'plugin', + modules: [], }, ]); - expect(instanceMetadata.getInstalledFeatures().map(String)).toEqual( - [ - 'plugin{pluginId=test}', - 'module{moduleId=test,pluginId=test}', - 'plugin{pluginId=instance-metadata}', - ], - ); }, }); }, diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 09b22e3a7a..75d5fd5a70 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -36,14 +36,13 @@ 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, assertError } from '@backstage/errors'; -import { - instanceMetadataServiceRef, - BackendFeatureMeta, -} from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationLogger } from './createInitializationLogger'; import { unwrapFeature } from './helpers'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import type { BackendPlugin } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService'; +import Router from 'express-promise-router'; export interface BackendRegisterInit { consumes: Set; @@ -104,53 +103,58 @@ 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 Object.defineProperty( - { - type: 'plugin', - pluginId: feature.pluginId, - }, - 'toString', - { - enumerable: false, - configurable: true, - value: () => `plugin{pluginId=${feature.pluginId}}`, - }, - ); - } else if (feature.type === 'module') { - return Object.defineProperty( - { - type: 'module', - pluginId: feature.pluginId, - moduleId: feature.moduleId, - }, - 'toString', - { - enumerable: false, - configurable: true, - value: () => - `module{moduleId=${feature.moduleId},pluginId=${feature.pluginId}}`, - }, - ); - } - // Ignore unknown feature types. - return undefined; - }) - .filter(Boolean) as BackendFeatureMeta[]; + const installedPlugins: { [pluginId: string]: BackendPlugin } = {}; + for (const registration of registrations) { + if (registration.featureType === 'registrations') { + for (const feature of registration.getRegistrations()) { + if (feature.type === 'plugin') { + if (!installedPlugins[feature.pluginId]) { + installedPlugins[feature.pluginId] = { + pluginId: feature.pluginId, + modules: [], + }; + } + } else if (feature.type === 'module') { + if (!installedPlugins[feature.pluginId]) { + installedPlugins[feature.pluginId] = { + pluginId: feature.pluginId, + modules: [], + }; + } + installedPlugins[feature.pluginId].modules.push({ + moduleId: feature.moduleId, + }); + } } - return []; - }) - .flat(); + } + } return createServiceFactory({ - service: instanceMetadataServiceRef, - deps: {}, - factory: async () => ({ getInstalledFeatures: () => installedFeatures }), + service: coreServices.instanceMetadata, + deps: { + httpRouter: coreServices.rootHttpRouter, + logger: coreServices.rootLogger, + }, + factory: async ({ logger, httpRouter }) => { + const instanceMetadata = { + getInstalledPlugins: () => Object.values(installedPlugins), + }; + + logger.info( + `Installed plugins on this instance: ${instanceMetadata + .getInstalledPlugins() + .map(p => p.pluginId) + .join(', ')}`, + ); + + const router = Router(); + + router.get('/info', (_, res) => { + res.json({ plugins: Object.values(installedPlugins) }); + }); + + httpRouter.use('/.backstage/instanceMetadata/v1', router); + return instanceMetadata; + }, }); } diff --git a/packages/backend-plugin-api/src/alpha/index.ts b/packages/backend-plugin-api/src/alpha/index.ts index 5bb69eb4e2..b1edd68adc 100644 --- a/packages/backend-plugin-api/src/alpha/index.ts +++ b/packages/backend-plugin-api/src/alpha/index.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -export type { - BackendFeatureMeta, - InstanceMetadataService, -} from './InstanceMetadataService'; - export type { ActionsRegistryService, ActionsRegistryActionOptions, @@ -27,8 +22,4 @@ export type { export type { ActionsService, ActionsServiceAction } from './ActionsService'; -export { - actionsRegistryServiceRef, - actionsServiceRef, - instanceMetadataServiceRef, -} from './refs'; +export { actionsRegistryServiceRef, actionsServiceRef } from './refs'; diff --git a/packages/backend-plugin-api/src/alpha/refs.ts b/packages/backend-plugin-api/src/alpha/refs.ts index 81996679f7..cfbb215615 100644 --- a/packages/backend-plugin-api/src/alpha/refs.ts +++ b/packages/backend-plugin-api/src/alpha/refs.ts @@ -16,15 +16,6 @@ import { createServiceRef } from '@backstage/backend-plugin-api'; -/** - * @alpha - */ -export const instanceMetadataServiceRef = createServiceRef< - import('./InstanceMetadataService').InstanceMetadataService ->({ - id: 'core.instanceMetadata', -}); - /** * Service for calling distributed actions * diff --git a/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts similarity index 73% rename from packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts rename to packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts index 869d361343..2a42631ec7 100644 --- a/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts @@ -14,19 +14,14 @@ * limitations under the License. */ -/** @alpha */ -export type BackendFeatureMeta = - | { - type: 'plugin'; - pluginId: string; - } - | { - type: 'module'; - pluginId: string; - moduleId: string; - }; +export interface BackendPlugin { + pluginId: string; + modules: { + moduleId: string; + }[]; +} /** @alpha */ export interface InstanceMetadataService { - getInstalledFeatures: () => BackendFeatureMeta[]; + getInstalledPlugins: () => readonly BackendPlugin[]; } diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index 8c8c6c0f82..b8f4494f74 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -277,4 +277,15 @@ export namespace coreServices { export const urlReader = createServiceRef< import('./UrlReaderService').UrlReaderService >({ id: 'core.urlReader' }); + + /** + * Information about the current Backstage instance. + * + * @public + */ + export const instanceMetadata = createServiceRef< + import('./InstanceMetadataService').InstanceMetadataService + >({ + id: 'core.instanceMetadata', + }); } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index c811a513a8..2c606bc370 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -85,4 +85,8 @@ export type { UrlReaderServiceSearchResponseFile, } from './UrlReaderService'; export type { BackstageUserInfo, UserInfoService } from './UserInfoService'; +export type { + InstanceMetadataService, + BackendPlugin, +} from './InstanceMetadataService'; export { coreServices } from './coreServices'; diff --git a/packages/backend/src/instanceMetadata.ts b/packages/backend/src/instanceMetadata.ts index 5f835f3117..463b2c317d 100644 --- a/packages/backend/src/instanceMetadata.ts +++ b/packages/backend/src/instanceMetadata.ts @@ -17,21 +17,21 @@ 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. +// Example usage of the instance metadata service to log the installed plugins. export default createBackendPlugin({ pluginId: 'instance-metadata-logging', register(env) { env.registerInit({ deps: { - instanceMetadata: instanceMetadataServiceRef, + instanceMetadata: coreServices.instanceMetadata, logger: coreServices.logger, }, async init({ instanceMetadata, logger }) { logger.info( - `Installed features on this instance: ${instanceMetadata - .getInstalledFeatures() + `Installed plugins on this instance: ${instanceMetadata + .getInstalledPlugins() + .map(e => e.pluginId) .join(', ')}`, ); }, diff --git a/plugins/gateway-backend/src/plugin.ts b/plugins/gateway-backend/src/plugin.ts index 94b979d1bc..391efb6852 100644 --- a/plugins/gateway-backend/src/plugin.ts +++ b/plugins/gateway-backend/src/plugin.ts @@ -18,7 +18,6 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { createRouter } from './router'; -import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; import { Handler } from 'express'; /** @@ -33,7 +32,7 @@ export const gatewayPlugin = createBackendPlugin({ deps: { logger: coreServices.logger, rootHttpRouter: coreServices.rootHttpRouter, - instanceMeta: instanceMetadataServiceRef, + instanceMeta: coreServices.instanceMetadata, discovery: coreServices.discovery, }, async init({ logger, discovery, instanceMeta, rootHttpRouter }) { diff --git a/plugins/gateway-backend/src/router.ts b/plugins/gateway-backend/src/router.ts index 74b3c6e066..4b041ef06d 100644 --- a/plugins/gateway-backend/src/router.ts +++ b/plugins/gateway-backend/src/router.ts @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DiscoveryService, LoggerService } from '@backstage/backend-plugin-api'; -import { InstanceMetadataService } from '@backstage/backend-plugin-api/alpha'; +import { + DiscoveryService, + InstanceMetadataService, + LoggerService, +} from '@backstage/backend-plugin-api'; import { Request, Response, NextFunction } from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; import { context } from '@opentelemetry/api'; @@ -29,10 +32,7 @@ export function createRouter({ logger: LoggerService; }) { const localPluginIds = new Set( - instanceMeta - .getInstalledFeatures() - .filter(f => f.type === 'plugin') - .map(f => f.pluginId), + instanceMeta.getInstalledPlugins().map(f => f.pluginId), ); const proxy = createProxyMiddleware({ diff --git a/yarn.lock b/yarn.lock index 9c96dcf272..c4d760f119 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,6 +2518,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" + express-promise-router: "npm:^4.1.0" languageName: unknown linkType: soft From 0102b3e5a1d2e28f4ba8f03e677ec5d25a19d86a Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 3 Sep 2025 10:45:17 -0400 Subject: [PATCH 02/18] address PR feedback and fix api reports Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.ts | 6 +++-- .../backend-plugin-api/report-alpha.api.md | 25 ------------------- packages/backend-plugin-api/report.api.md | 21 ++++++++++++++++ .../definitions/InstanceMetadataService.ts | 7 +++--- .../src/services/definitions/index.ts | 2 +- .../src/services/mockServices.ts | 12 +++++++++ 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 75d5fd5a70..419b7f4d9d 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -41,7 +41,7 @@ import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationLogger } from './createInitializationLogger'; import { unwrapFeature } from './helpers'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import type { BackendPlugin } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService'; +import type { InstanceMetadataServicePluginInfo } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService'; import Router from 'express-promise-router'; export interface BackendRegisterInit { @@ -103,7 +103,9 @@ const instanceRegistry = new (class InstanceRegistry { function createInstanceMetadataServiceFactory( registrations: InternalBackendRegistrations[], ) { - const installedPlugins: { [pluginId: string]: BackendPlugin } = {}; + const installedPlugins: { + [pluginId: string]: InstanceMetadataServicePluginInfo; + } = {}; for (const registration of registrations) { if (registration.featureType === 'registrations') { for (const feature of registration.getRegistrations()) { diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index 9be7a29ed5..cefed593c0 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -103,30 +103,5 @@ export const actionsServiceRef: ServiceRef< 'singleton' >; -// @alpha (undocumented) -export type BackendFeatureMeta = - | { - type: 'plugin'; - pluginId: string; - } - | { - type: 'module'; - pluginId: string; - moduleId: string; - }; - -// @alpha (undocumented) -export interface InstanceMetadataService { - // (undocumented) - getInstalledFeatures: () => BackendFeatureMeta[]; -} - -// @alpha (undocumented) -export const instanceMetadataServiceRef: ServiceRef< - InstanceMetadataService, - 'plugin', - 'singleton' ->; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 52843dec96..ebc544deed 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -232,6 +232,11 @@ export namespace coreServices { const rootLogger: ServiceRef; const scheduler: ServiceRef; const urlReader: ServiceRef; + const instanceMetadata: ServiceRef< + InstanceMetadataService, + 'plugin', + 'singleton' + >; } // @public @@ -416,6 +421,22 @@ export interface HttpRouterServiceAuthPolicy { path: string; } +// @public (undocumented) +export interface InstanceMetadataService { + // (undocumented) + getInstalledPlugins: () => readonly InstanceMetadataServicePluginInfo[]; +} + +// @public (undocumented) +export interface InstanceMetadataServicePluginInfo { + // (undocumented) + modules: { + moduleId: string; + }[]; + // (undocumented) + pluginId: string; +} + export { isChildPath }; // @public diff --git a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts index 2a42631ec7..78d6f8356c 100644 --- a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts @@ -14,14 +14,15 @@ * limitations under the License. */ -export interface BackendPlugin { +/** @public */ +export interface InstanceMetadataServicePluginInfo { pluginId: string; modules: { moduleId: string; }[]; } -/** @alpha */ +/** @public */ export interface InstanceMetadataService { - getInstalledPlugins: () => readonly BackendPlugin[]; + getInstalledPlugins: () => readonly InstanceMetadataServicePluginInfo[]; } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 2c606bc370..111bd86ba9 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -87,6 +87,6 @@ export type { export type { BackstageUserInfo, UserInfoService } from './UserInfoService'; export type { InstanceMetadataService, - BackendPlugin, + InstanceMetadataServicePluginInfo, } from './InstanceMetadataService'; export { coreServices } from './coreServices'; diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index f7d57d3fb1..d7c0474838 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -34,6 +34,7 @@ import { DatabaseService, DiscoveryService, HttpAuthService, + InstanceMetadataService, LoggerService, PermissionsService, RootConfigService, @@ -556,4 +557,15 @@ export namespace mockServices { subscribe: jest.fn(), })); } + + export function instanceMetadata(): InstanceMetadataService { + return { + getInstalledPlugins: () => [], + }; + } + export namespace instanceMetadata { + export const mock = simpleMock(coreServices.instanceMetadata, () => ({ + getInstalledPlugins: jest.fn(), + })); + } } From e6e0c8bb39ff139ab6b6f73b6ae4aee68abe78cd Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Fri, 26 Sep 2025 08:37:25 -0400 Subject: [PATCH 03/18] fix tests Signed-off-by: aramissennyeydd --- packages/backend-test-utils/src/services/mockServices.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index d7c0474838..bdf55930dd 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -567,5 +567,9 @@ export namespace mockServices { export const mock = simpleMock(coreServices.instanceMetadata, () => ({ getInstalledPlugins: jest.fn(), })); + export const factory = simpleFactoryWithOptions( + coreServices.instanceMetadata, + instanceMetadata, + ); } } From 374ac99a6fe82539ddc6a41684e21943f6b02724 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Fri, 26 Sep 2025 10:09:42 -0400 Subject: [PATCH 04/18] fix api report Signed-off-by: aramissennyeydd --- packages/backend-test-utils/report.api.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 02fa9d8587..8650036014 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -23,6 +23,7 @@ import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; +import { InstanceMetadataService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import Keyv from 'keyv'; import { Knex } from 'knex'; @@ -262,6 +263,21 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) + export function instanceMetadata(): InstanceMetadataService; + // (undocumented) + export namespace instanceMetadata { + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + const // (undocumented) + factory: () => ServiceFactory< + InstanceMetadataService, + 'plugin', + 'singleton' | 'multiton' + >; + } + // (undocumented) export namespace lifecycle { const // (undocumented) factory: () => ServiceFactory; From f9f600e951c95d60e94a032d28721c313c89e9ca Mon Sep 17 00:00:00 2001 From: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:53:06 -0400 Subject: [PATCH 05/18] Update .changeset/open-items-open.md Signed-off-by: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> --- .changeset/open-items-open.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/open-items-open.md b/.changeset/open-items-open.md index eafce6bd60..12fcefab4a 100644 --- a/.changeset/open-items-open.md +++ b/.changeset/open-items-open.md @@ -2,4 +2,4 @@ '@backstage/backend-app-api': minor --- -Updates API for `instanceMetadata` service to return a list of plugins not features. Also adds an HTTP endpoint that returns information about the current instance. +Updates API for `instanceMetadata` service to return a list of plugins not features. From 3e19bd3667e529194974a7e0dd64c718590bbec1 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Fri, 26 Sep 2025 10:55:01 -0400 Subject: [PATCH 06/18] remove http routes Signed-off-by: aramissennyeydd --- packages/backend-app-api/package.json | 3 +-- .../src/wiring/BackendInitializer.test.ts | 3 --- .../backend-app-api/src/wiring/BackendInitializer.ts | 12 +----------- yarn.lock | 1 - 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 0261845093..1613e05cc0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -48,8 +48,7 @@ "dependencies": { "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", - "@backstage/errors": "workspace:^", - "express-promise-router": "^4.1.0" + "@backstage/errors": "workspace:^" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 312511f4a9..18b0adaee7 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -32,9 +32,6 @@ const baseFactories = [ mockServices.lifecycle.factory(), mockServices.rootLogger.factory(), mockServices.logger.factory(), - mockServices.rootConfig.factory(), - mockServices.rootHttpRouter.mock().factory, - mockServices.rootHealth.factory(), ]; function mkNoopFactory(ref: ServiceRef<{}, 'plugin'>) { diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 419b7f4d9d..6d3939a4b5 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -42,7 +42,6 @@ import { createInitializationLogger } from './createInitializationLogger'; import { unwrapFeature } from './helpers'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InstanceMetadataServicePluginInfo } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService'; -import Router from 'express-promise-router'; export interface BackendRegisterInit { consumes: Set; @@ -133,10 +132,9 @@ function createInstanceMetadataServiceFactory( return createServiceFactory({ service: coreServices.instanceMetadata, deps: { - httpRouter: coreServices.rootHttpRouter, logger: coreServices.rootLogger, }, - factory: async ({ logger, httpRouter }) => { + factory: async ({ logger }) => { const instanceMetadata = { getInstalledPlugins: () => Object.values(installedPlugins), }; @@ -147,14 +145,6 @@ function createInstanceMetadataServiceFactory( .map(p => p.pluginId) .join(', ')}`, ); - - const router = Router(); - - router.get('/info', (_, res) => { - res.json({ plugins: Object.values(installedPlugins) }); - }); - - httpRouter.use('/.backstage/instanceMetadata/v1', router); return instanceMetadata; }, }); diff --git a/yarn.lock b/yarn.lock index c4d760f119..9c96dcf272 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,7 +2518,6 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" - express-promise-router: "npm:^4.1.0" languageName: unknown linkType: soft From 92f582349847800e31763548ef6c40086a1fbd11 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 29 Sep 2025 10:19:47 -0400 Subject: [PATCH 07/18] add docs Signed-off-by: aramissennyeydd --- .../core-services/instance-metadata.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/backend-system/core-services/instance-metadata.md diff --git a/docs/backend-system/core-services/instance-metadata.md b/docs/backend-system/core-services/instance-metadata.md new file mode 100644 index 0000000000..8a5818e2bc --- /dev/null +++ b/docs/backend-system/core-services/instance-metadata.md @@ -0,0 +1,44 @@ +--- +id: instance-metadata +title: Instance Metadata Service +sidebar_label: Instance Metadata +description: Documentation for the Instance Metadata service +--- + +The instance metadata service provides information about the running Backstage backend instance. Currently, it provides a list of all installed backend plugins. + +:::note Note + +The instance metadata service only provides information about the specific Backstage instance you're running on. In more complex deployments with multiple Backstage instances, this service will not provide a complete list of all plugins across all instances. + +::: + +## Using the service + +The following example shows how to use the instance metadata service in your `example` backend plugin to access the list of installed backend plugins. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + instanceMetadata: coreServices.instanceMetadata, + }, + async init({ instanceMetadata }) { + const plugins = instanceMetadata.getInstalledPlugins(); + console.log('Installed plugins:', plugins); + }, + }); + }, +}); +``` + +## Dynamic plugin registration + +The instance metadata service picks up plugins that are registered at start time through a `backend.start()` call. You need to restart the running backend instance to pick up newly installed plugins. From 6cf2ba39d94139d38845cf922a1b565c11434405 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 29 Sep 2025 10:20:14 -0400 Subject: [PATCH 08/18] make async and readonly Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.test.ts | 27 ++++++++++++++++++- .../src/wiring/BackendInitializer.ts | 12 ++++++--- .../backend-app-api/src/wiring/helpers.ts | 26 ++++++++++++++++++ .../definitions/InstanceMetadataService.ts | 9 ++++++- plugins/gateway-backend/src/plugin.ts | 4 +-- plugins/gateway-backend/src/router.ts | 7 +++-- 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 18b0adaee7..3ce4ba2ee5 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -1093,7 +1093,9 @@ describe('BackendInitializer', () => { instanceMetadata: coreServices.instanceMetadata, }, async init({ instanceMetadata }) { - expect(instanceMetadata.getInstalledPlugins()).toEqual([ + await expect( + instanceMetadata.getInstalledPlugins(), + ).resolves.toEqual([ { pluginId: 'test', modules: [ @@ -1127,6 +1129,29 @@ describe('BackendInitializer', () => { await backend.start(); }); + it('should prevent writes to the instance metadata service', async () => { + expect.assertions(1); + const backend = new BackendInitializer(baseFactories); + const plugin = createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { + instanceMetadata: coreServices.instanceMetadata, + }, + async init({ instanceMetadata }) { + const plugins = await instanceMetadata.getInstalledPlugins(); + await expect(() => { + (plugins[0] as any).pluginId = 'foo'; + }).toThrow(/Cannot assign to read only property/); + }, + }); + }, + }); + backend.add(plugin); + await backend.start(); + }); + it('should properly wait for all modules that consume an extension point to really finish, before starting the module that provides that extension point', async () => { expect.assertions(3); const backend = new BackendInitializer(baseFactories); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 6d3939a4b5..080f6c7c69 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -39,7 +39,7 @@ import { ForwardedError, ConflictError, assertError } from '@backstage/errors'; import { DependencyGraph } from '../lib/DependencyGraph'; import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationLogger } from './createInitializationLogger'; -import { unwrapFeature } from './helpers'; +import { deepFreeze, unwrapFeature } from './helpers'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { InstanceMetadataServicePluginInfo } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService'; @@ -135,13 +135,17 @@ function createInstanceMetadataServiceFactory( logger: coreServices.rootLogger, }, factory: async ({ logger }) => { + const readonlyInstalledPlugins = deepFreeze( + Object.values(installedPlugins), + ); const instanceMetadata = { - getInstalledPlugins: () => Object.values(installedPlugins), + getInstalledPlugins: () => Promise.resolve(readonlyInstalledPlugins), }; + const plugins = await instanceMetadata.getInstalledPlugins(); + logger.info( - `Installed plugins on this instance: ${instanceMetadata - .getInstalledPlugins() + `Installed plugins on this instance: ${plugins .map(p => p.pluginId) .join(', ')}`, ); diff --git a/packages/backend-app-api/src/wiring/helpers.ts b/packages/backend-app-api/src/wiring/helpers.ts index ffb7e3b079..e67c9a1125 100644 --- a/packages/backend-app-api/src/wiring/helpers.ts +++ b/packages/backend-app-api/src/wiring/helpers.ts @@ -34,3 +34,29 @@ export function unwrapFeature( return feature; } + +/** @internal */ +export type DeepReadonly = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; +}; + +/** + * Deeply freezes an object by recursively freezing all of its properties. + * + * - https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 + * - https://stackoverflow.com/a/69656011 + * + * FIXME Should be part of Lodash and related: https://github.com/Maggi64/moderndash/issues/139 + * + * Does not work with Set and Map: https://stackoverflow.com/q/31509175 + */ +export function deepFreeze< + T, + // Can cause: "Type instantiation is excessively deep and possibly infinite." +>(obj: T) { + // @ts-expect-error + Object.values(obj).forEach( + value => Object.isFrozen(value) || deepFreeze(value), + ); + return Object.freeze(obj) as DeepReadonly; +} diff --git a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts index 78d6f8356c..7e3b3b03d1 100644 --- a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts @@ -22,7 +22,14 @@ export interface InstanceMetadataServicePluginInfo { }[]; } +/** @internal */ +export type DeepReadonly = { + readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; +}; + /** @public */ export interface InstanceMetadataService { - getInstalledPlugins: () => readonly InstanceMetadataServicePluginInfo[]; + getInstalledPlugins: () => Promise< + ReadonlyArray> + >; } diff --git a/plugins/gateway-backend/src/plugin.ts b/plugins/gateway-backend/src/plugin.ts index 391efb6852..efd7a372de 100644 --- a/plugins/gateway-backend/src/plugin.ts +++ b/plugins/gateway-backend/src/plugin.ts @@ -38,11 +38,11 @@ export const gatewayPlugin = createBackendPlugin({ async init({ logger, discovery, instanceMeta, rootHttpRouter }) { rootHttpRouter.use( '/api/:pluginId', - createRouter({ + (await createRouter({ discovery, instanceMeta, logger, - }) as Handler, + })) as Handler, ); }, }); diff --git a/plugins/gateway-backend/src/router.ts b/plugins/gateway-backend/src/router.ts index 4b041ef06d..7f8c898761 100644 --- a/plugins/gateway-backend/src/router.ts +++ b/plugins/gateway-backend/src/router.ts @@ -23,7 +23,7 @@ import { createProxyMiddleware } from 'http-proxy-middleware'; import { context } from '@opentelemetry/api'; import { getRPCMetadata } from '@opentelemetry/core'; -export function createRouter({ +export async function createRouter({ discovery, instanceMeta, }: { @@ -31,9 +31,8 @@ export function createRouter({ instanceMeta: InstanceMetadataService; logger: LoggerService; }) { - const localPluginIds = new Set( - instanceMeta.getInstalledPlugins().map(f => f.pluginId), - ); + const plugins = await instanceMeta.getInstalledPlugins(); + const localPluginIds = new Set(plugins.map(f => f.pluginId)); const proxy = createProxyMiddleware({ changeOrigin: true, From 99ecdbaf957af871ca32923a99725386a535a481 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 29 Sep 2025 10:25:38 -0400 Subject: [PATCH 09/18] overzealous comment Signed-off-by: aramissennyeydd --- packages/backend-app-api/src/wiring/helpers.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/backend-app-api/src/wiring/helpers.ts b/packages/backend-app-api/src/wiring/helpers.ts index e67c9a1125..1be7f62da2 100644 --- a/packages/backend-app-api/src/wiring/helpers.ts +++ b/packages/backend-app-api/src/wiring/helpers.ts @@ -42,18 +42,11 @@ export type DeepReadonly = { /** * Deeply freezes an object by recursively freezing all of its properties. - * - * - https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 - * - https://stackoverflow.com/a/69656011 - * - * FIXME Should be part of Lodash and related: https://github.com/Maggi64/moderndash/issues/139 - * - * Does not work with Set and Map: https://stackoverflow.com/q/31509175 + * From https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 + + * https://stackoverflow.com/a/69656011 */ -export function deepFreeze< - T, +export function deepFreeze(obj: T) { // Can cause: "Type instantiation is excessively deep and possibly infinite." ->(obj: T) { // @ts-expect-error Object.values(obj).forEach( value => Object.isFrozen(value) || deepFreeze(value), From 3bdbba8c00d7868fd779a30ae91c61f2821e3e0c Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 29 Sep 2025 10:53:23 -0400 Subject: [PATCH 10/18] fix api reports Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.ts | 6 +++++- packages/backend-plugin-api/report.api.md | 10 ++++++---- .../services/definitions/InstanceMetadataService.ts | 13 ++++--------- .../backend-test-utils/src/services/mockServices.ts | 2 +- packages/backend/src/instanceMetadata.ts | 4 ++-- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 080f6c7c69..cf27f4dff9 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -122,7 +122,11 @@ function createInstanceMetadataServiceFactory( modules: [], }; } - installedPlugins[feature.pluginId].modules.push({ + ( + installedPlugins[feature.pluginId].modules as Array<{ + moduleId: string; + }> + ).push({ moduleId: feature.moduleId, }); } diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index ebc544deed..fdb30434ed 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -424,17 +424,19 @@ export interface HttpRouterServiceAuthPolicy { // @public (undocumented) export interface InstanceMetadataService { // (undocumented) - getInstalledPlugins: () => readonly InstanceMetadataServicePluginInfo[]; + getInstalledPlugins: () => Promise< + ReadonlyArray + >; } // @public (undocumented) export interface InstanceMetadataServicePluginInfo { // (undocumented) - modules: { + readonly modules: ReadonlyArray<{ moduleId: string; - }[]; + }>; // (undocumented) - pluginId: string; + readonly pluginId: string; } export { isChildPath }; diff --git a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts index 7e3b3b03d1..49c4dfe765 100644 --- a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts @@ -16,20 +16,15 @@ /** @public */ export interface InstanceMetadataServicePluginInfo { - pluginId: string; - modules: { + readonly pluginId: string; + readonly modules: ReadonlyArray<{ moduleId: string; - }[]; + }>; } -/** @internal */ -export type DeepReadonly = { - readonly [K in keyof T]: T[K] extends object ? DeepReadonly : T[K]; -}; - /** @public */ export interface InstanceMetadataService { getInstalledPlugins: () => Promise< - ReadonlyArray> + ReadonlyArray >; } diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index bdf55930dd..2782724d11 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -560,7 +560,7 @@ export namespace mockServices { export function instanceMetadata(): InstanceMetadataService { return { - getInstalledPlugins: () => [], + getInstalledPlugins: () => Promise.resolve([]), }; } export namespace instanceMetadata { diff --git a/packages/backend/src/instanceMetadata.ts b/packages/backend/src/instanceMetadata.ts index 463b2c317d..4f12fc889d 100644 --- a/packages/backend/src/instanceMetadata.ts +++ b/packages/backend/src/instanceMetadata.ts @@ -28,9 +28,9 @@ export default createBackendPlugin({ logger: coreServices.logger, }, async init({ instanceMetadata, logger }) { + const plugins = await instanceMetadata.getInstalledPlugins(); logger.info( - `Installed plugins on this instance: ${instanceMetadata - .getInstalledPlugins() + `Installed plugins on this instance: ${plugins .map(e => e.pluginId) .join(', ')}`, ); From 4e245210fb6892c931bb4284de982dc8af948fb9 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 12:26:39 -0400 Subject: [PATCH 11/18] add back the alpha entrypoint and update new service to Root... Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.ts | 61 +++++++++++-------- .../src/alpha/InstanceMetadataService.ts | 32 ++++++++++ .../backend-plugin-api/src/alpha/index.ts | 11 +++- packages/backend-plugin-api/src/alpha/refs.ts | 9 +++ ...vice.ts => RootInstanceMetadataService.ts} | 6 +- .../src/services/definitions/coreServices.ts | 6 +- .../src/services/definitions/index.ts | 6 +- 7 files changed, 95 insertions(+), 36 deletions(-) create mode 100644 packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts rename packages/backend-plugin-api/src/services/definitions/{InstanceMetadataService.ts => RootInstanceMetadataService.ts} (82%) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index cf27f4dff9..2f3a11b7e0 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -40,8 +40,8 @@ import { DependencyGraph } from '../lib/DependencyGraph'; import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationLogger } from './createInitializationLogger'; import { deepFreeze, unwrapFeature } from './helpers'; -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import type { InstanceMetadataServicePluginInfo } from '../../../backend-plugin-api/src/services/definitions/InstanceMetadataService'; +import type { RootInstanceMetadataServicePluginInfo } from '@backstage/backend-plugin-api'; +import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; export interface BackendRegisterInit { consumes: Set; @@ -99,31 +99,30 @@ const instanceRegistry = new (class InstanceRegistry { }; })(); -function createInstanceMetadataServiceFactory( +function createRootInstanceMetadataServiceFactory( registrations: InternalBackendRegistrations[], ) { - const installedPlugins: { - [pluginId: string]: InstanceMetadataServicePluginInfo; - } = {}; + const installedPlugins: Map = + new Map(); for (const registration of registrations) { if (registration.featureType === 'registrations') { for (const feature of registration.getRegistrations()) { if (feature.type === 'plugin') { - if (!installedPlugins[feature.pluginId]) { - installedPlugins[feature.pluginId] = { + if (!installedPlugins.get(feature.pluginId)) { + installedPlugins.set(feature.pluginId, { pluginId: feature.pluginId, modules: [], - }; + }); } } else if (feature.type === 'module') { - if (!installedPlugins[feature.pluginId]) { - installedPlugins[feature.pluginId] = { + if (!installedPlugins.get(feature.pluginId)) { + installedPlugins.set(feature.pluginId, { pluginId: feature.pluginId, modules: [], - }; + }); } ( - installedPlugins[feature.pluginId].modules as Array<{ + installedPlugins.get(feature.pluginId)!.modules as Array<{ moduleId: string; }> ).push({ @@ -134,11 +133,9 @@ function createInstanceMetadataServiceFactory( } } return createServiceFactory({ - service: coreServices.instanceMetadata, - deps: { - logger: coreServices.rootLogger, - }, - factory: async ({ logger }) => { + service: coreServices.rootInstanceMetadata, + deps: {}, + factory: async () => { const readonlyInstalledPlugins = deepFreeze( Object.values(installedPlugins), ); @@ -146,18 +143,29 @@ function createInstanceMetadataServiceFactory( getInstalledPlugins: () => Promise.resolve(readonlyInstalledPlugins), }; - const plugins = await instanceMetadata.getInstalledPlugins(); - - logger.info( - `Installed plugins on this instance: ${plugins - .map(p => p.pluginId) - .join(', ')}`, - ); return instanceMetadata; }, }); } +function createDeprecatedInstanceMetadataServiceFactory() { + return createServiceFactory({ + service: instanceMetadataServiceRef, + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + factory: async ({ instanceMetadata }) => { + const plugins = await instanceMetadata.getInstalledPlugins(); + const service = { + getInstalledFeatures: () => + plugins.map(e => ({ type: 'plugin' as const, pluginId: e.pluginId })), + }; + + return service; + }, + }); +} + export class BackendInitializer { #startPromise?: Promise; #stopPromise?: Promise; @@ -259,8 +267,9 @@ export class BackendInitializer { await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); this.#serviceRegistry.add( - createInstanceMetadataServiceFactory(this.#registrations), + createRootInstanceMetadataServiceFactory(this.#registrations), ); + this.#serviceRegistry.add(createDeprecatedInstanceMetadataServiceFactory()); // This makes sure that any uncaught errors or unhandled rejections are // caught and logged, rather than terminating the process. We register these diff --git a/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts b/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts new file mode 100644 index 0000000000..a82a261d9d --- /dev/null +++ b/packages/backend-plugin-api/src/alpha/InstanceMetadataService.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2025 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-plugin-api/src/alpha/index.ts b/packages/backend-plugin-api/src/alpha/index.ts index b1edd68adc..5bb69eb4e2 100644 --- a/packages/backend-plugin-api/src/alpha/index.ts +++ b/packages/backend-plugin-api/src/alpha/index.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +export type { + BackendFeatureMeta, + InstanceMetadataService, +} from './InstanceMetadataService'; + export type { ActionsRegistryService, ActionsRegistryActionOptions, @@ -22,4 +27,8 @@ export type { export type { ActionsService, ActionsServiceAction } from './ActionsService'; -export { actionsRegistryServiceRef, actionsServiceRef } from './refs'; +export { + actionsRegistryServiceRef, + actionsServiceRef, + instanceMetadataServiceRef, +} from './refs'; diff --git a/packages/backend-plugin-api/src/alpha/refs.ts b/packages/backend-plugin-api/src/alpha/refs.ts index cfbb215615..81996679f7 100644 --- a/packages/backend-plugin-api/src/alpha/refs.ts +++ b/packages/backend-plugin-api/src/alpha/refs.ts @@ -16,6 +16,15 @@ import { createServiceRef } from '@backstage/backend-plugin-api'; +/** + * @alpha + */ +export const instanceMetadataServiceRef = createServiceRef< + import('./InstanceMetadataService').InstanceMetadataService +>({ + id: 'core.instanceMetadata', +}); + /** * Service for calling distributed actions * diff --git a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/RootInstanceMetadataService.ts similarity index 82% rename from packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts rename to packages/backend-plugin-api/src/services/definitions/RootInstanceMetadataService.ts index 49c4dfe765..c324fed331 100644 --- a/packages/backend-plugin-api/src/services/definitions/InstanceMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/RootInstanceMetadataService.ts @@ -15,7 +15,7 @@ */ /** @public */ -export interface InstanceMetadataServicePluginInfo { +export interface RootInstanceMetadataServicePluginInfo { readonly pluginId: string; readonly modules: ReadonlyArray<{ moduleId: string; @@ -23,8 +23,8 @@ export interface InstanceMetadataServicePluginInfo { } /** @public */ -export interface InstanceMetadataService { +export interface RootInstanceMetadataService { getInstalledPlugins: () => Promise< - ReadonlyArray + ReadonlyArray >; } diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index b8f4494f74..62f2f848fc 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -283,9 +283,9 @@ export namespace coreServices { * * @public */ - export const instanceMetadata = createServiceRef< - import('./InstanceMetadataService').InstanceMetadataService + export const rootInstanceMetadata = createServiceRef< + import('./RootInstanceMetadataService').RootInstanceMetadataService >({ - id: 'core.instanceMetadata', + id: 'core.rootInstanceMetadata', }); } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 111bd86ba9..6bcee4b043 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -86,7 +86,7 @@ export type { } from './UrlReaderService'; export type { BackstageUserInfo, UserInfoService } from './UserInfoService'; export type { - InstanceMetadataService, - InstanceMetadataServicePluginInfo, -} from './InstanceMetadataService'; + RootInstanceMetadataService, + RootInstanceMetadataServicePluginInfo, +} from './RootInstanceMetadataService'; export { coreServices } from './coreServices'; From 468f2d8ca7b98a66e4f11e0a4b1baacff74856eb Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 12:38:35 -0400 Subject: [PATCH 12/18] move alpha instance to default service factories Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.ts | 19 ---------- .../backend-defaults/src/CreateBackend.ts | 2 + .../entrypoints/instanceMetadata/index.ts | 17 +++++++++ .../instanceMetadataServiceFactory.ts | 37 +++++++++++++++++++ packages/backend-defaults/src/alpha/index.ts | 1 + .../src/services/mockServices.ts | 12 +++--- 6 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/index.ts create mode 100644 packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 2f3a11b7e0..ee62ef966d 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -148,24 +148,6 @@ function createRootInstanceMetadataServiceFactory( }); } -function createDeprecatedInstanceMetadataServiceFactory() { - return createServiceFactory({ - service: instanceMetadataServiceRef, - deps: { - instanceMetadata: coreServices.rootInstanceMetadata, - }, - factory: async ({ instanceMetadata }) => { - const plugins = await instanceMetadata.getInstalledPlugins(); - const service = { - getInstalledFeatures: () => - plugins.map(e => ({ type: 'plugin' as const, pluginId: e.pluginId })), - }; - - return service; - }, - }); -} - export class BackendInitializer { #startPromise?: Promise; #stopPromise?: Promise; @@ -269,7 +251,6 @@ export class BackendInitializer { this.#serviceRegistry.add( createRootInstanceMetadataServiceFactory(this.#registrations), ); - this.#serviceRegistry.add(createDeprecatedInstanceMetadataServiceFactory()); // This makes sure that any uncaught errors or unhandled rejections are // caught and logged, rather than terminating the process. We register these diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 4b201fded3..44df59b5f1 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -38,6 +38,7 @@ import { eventsServiceFactory } from '@backstage/plugin-events-node'; import { actionsRegistryServiceFactory, actionsServiceFactory, + instanceMetadataServiceFactory, } from '@backstage/backend-defaults/alpha'; export const defaultServiceFactories = [ @@ -65,6 +66,7 @@ export const defaultServiceFactories = [ // alpha services actionsRegistryServiceFactory, actionsServiceFactory, + instanceMetadataServiceFactory, ]; /** diff --git a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/index.ts b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/index.ts new file mode 100644 index 0000000000..00ee75400a --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025 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 { instanceMetadataServiceFactory } from './instanceMetadataServiceFactory'; diff --git a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts new file mode 100644 index 0000000000..f5e3af503b --- /dev/null +++ b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; + +export const instanceMetadataServiceFactory = createServiceFactory({ + service: instanceMetadataServiceRef, + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + factory: async ({ instanceMetadata }) => { + const plugins = await instanceMetadata.getInstalledPlugins(); + const service = { + getInstalledFeatures: () => + plugins.map(e => ({ type: 'plugin' as const, pluginId: e.pluginId })), + }; + + return service; + }, +}); diff --git a/packages/backend-defaults/src/alpha/index.ts b/packages/backend-defaults/src/alpha/index.ts index 13bb439acd..744d5bc552 100644 --- a/packages/backend-defaults/src/alpha/index.ts +++ b/packages/backend-defaults/src/alpha/index.ts @@ -15,3 +15,4 @@ */ export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry'; export { actionsServiceFactory } from './entrypoints/actions'; +export { instanceMetadataServiceFactory } from './entrypoints/instanceMetadata'; diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index 2782724d11..9ecb59a35b 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -34,7 +34,7 @@ import { DatabaseService, DiscoveryService, HttpAuthService, - InstanceMetadataService, + RootInstanceMetadataService, LoggerService, PermissionsService, RootConfigService, @@ -558,18 +558,18 @@ export namespace mockServices { })); } - export function instanceMetadata(): InstanceMetadataService { + export function rootInstanceMetadata(): RootInstanceMetadataService { return { getInstalledPlugins: () => Promise.resolve([]), }; } - export namespace instanceMetadata { - export const mock = simpleMock(coreServices.instanceMetadata, () => ({ + export namespace rootInstanceMetadata { + export const mock = simpleMock(coreServices.rootInstanceMetadata, () => ({ getInstalledPlugins: jest.fn(), })); export const factory = simpleFactoryWithOptions( - coreServices.instanceMetadata, - instanceMetadata, + coreServices.rootInstanceMetadata, + rootInstanceMetadata, ); } } From 5e93cfcff29909b8af1f2ca7a13bd2147bd2a448 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 12:38:52 -0400 Subject: [PATCH 13/18] fix test issues caused by rename Signed-off-by: aramissennyeydd --- .../backend-app-api/src/wiring/BackendInitializer.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 3ce4ba2ee5..b4ec19a4ad 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -1090,7 +1090,7 @@ describe('BackendInitializer', () => { register(reg) { reg.registerInit({ deps: { - instanceMetadata: coreServices.instanceMetadata, + instanceMetadata: coreServices.rootInstanceMetadata, }, async init({ instanceMetadata }) { await expect( @@ -1137,7 +1137,7 @@ describe('BackendInitializer', () => { register(reg) { reg.registerInit({ deps: { - instanceMetadata: coreServices.instanceMetadata, + instanceMetadata: coreServices.rootInstanceMetadata, }, async init({ instanceMetadata }) { const plugins = await instanceMetadata.getInstalledPlugins(); From ef642fffbefa6d6e16939e922a2c3c3bd4870f2e Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 13:35:47 -0400 Subject: [PATCH 14/18] fix api reports Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.ts | 1 - packages/backend-defaults/report-alpha.api.md | 8 ++++ .../instanceMetadataServiceFactory.ts | 4 ++ .../backend-plugin-api/report-alpha.api.md | 25 ++++++++++++ packages/backend-plugin-api/report.api.md | 40 +++++++++---------- packages/backend-test-utils/report.api.md | 32 +++++++-------- packages/backend/src/instanceMetadata.ts | 2 +- plugins/gateway-backend/src/plugin.ts | 2 +- plugins/gateway-backend/src/router.ts | 4 +- 9 files changed, 77 insertions(+), 41 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ee62ef966d..bc778b6589 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -41,7 +41,6 @@ import { ServiceRegistry } from './ServiceRegistry'; import { createInitializationLogger } from './createInitializationLogger'; import { deepFreeze, unwrapFeature } from './helpers'; import type { RootInstanceMetadataServicePluginInfo } from '@backstage/backend-plugin-api'; -import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; export interface BackendRegisterInit { consumes: Set; diff --git a/packages/backend-defaults/report-alpha.api.md b/packages/backend-defaults/report-alpha.api.md index 58277b5e1e..2ca3525946 100644 --- a/packages/backend-defaults/report-alpha.api.md +++ b/packages/backend-defaults/report-alpha.api.md @@ -5,6 +5,7 @@ ```ts import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha'; import { ActionsService } from '@backstage/backend-plugin-api/alpha'; +import { InstanceMetadataService } from '@backstage/backend-plugin-api/alpha'; import { ServiceFactory } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -21,5 +22,12 @@ export const actionsServiceFactory: ServiceFactory< 'singleton' >; +// @alpha @deprecated (undocumented) +export const instanceMetadataServiceFactory: ServiceFactory< + InstanceMetadataService, + 'plugin', + 'singleton' +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts index f5e3af503b..a50c8f0058 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts @@ -20,6 +20,10 @@ import { } from '@backstage/backend-plugin-api'; import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; +/** + * @alpha + * @deprecated use {@link @backstage/backend-plugin-api#coreServices.rootInstanceMetadata} instead + */ export const instanceMetadataServiceFactory = createServiceFactory({ service: instanceMetadataServiceRef, deps: { diff --git a/packages/backend-plugin-api/report-alpha.api.md b/packages/backend-plugin-api/report-alpha.api.md index cefed593c0..9be7a29ed5 100644 --- a/packages/backend-plugin-api/report-alpha.api.md +++ b/packages/backend-plugin-api/report-alpha.api.md @@ -103,5 +103,30 @@ export const actionsServiceRef: ServiceRef< 'singleton' >; +// @alpha (undocumented) +export type BackendFeatureMeta = + | { + type: 'plugin'; + pluginId: string; + } + | { + type: 'module'; + pluginId: string; + moduleId: string; + }; + +// @alpha (undocumented) +export interface InstanceMetadataService { + // (undocumented) + getInstalledFeatures: () => BackendFeatureMeta[]; +} + +// @alpha (undocumented) +export const instanceMetadataServiceRef: ServiceRef< + InstanceMetadataService, + 'plugin', + 'singleton' +>; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index fdb30434ed..cce3221c80 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -232,8 +232,8 @@ export namespace coreServices { const rootLogger: ServiceRef; const scheduler: ServiceRef; const urlReader: ServiceRef; - const instanceMetadata: ServiceRef< - InstanceMetadataService, + const rootInstanceMetadata: ServiceRef< + RootInstanceMetadataService, 'plugin', 'singleton' >; @@ -421,24 +421,6 @@ export interface HttpRouterServiceAuthPolicy { path: string; } -// @public (undocumented) -export interface InstanceMetadataService { - // (undocumented) - getInstalledPlugins: () => Promise< - ReadonlyArray - >; -} - -// @public (undocumented) -export interface InstanceMetadataServicePluginInfo { - // (undocumented) - readonly modules: ReadonlyArray<{ - moduleId: string; - }>; - // (undocumented) - readonly pluginId: string; -} - export { isChildPath }; // @public @@ -601,6 +583,24 @@ export interface RootHttpRouterService { use(path: string, handler: Handler): void; } +// @public (undocumented) +export interface RootInstanceMetadataService { + // (undocumented) + getInstalledPlugins: () => Promise< + ReadonlyArray + >; +} + +// @public (undocumented) +export interface RootInstanceMetadataServicePluginInfo { + // (undocumented) + readonly modules: ReadonlyArray<{ + moduleId: string; + }>; + // (undocumented) + readonly pluginId: string; +} + // @public export interface RootLifecycleService extends LifecycleService { // (undocumented) diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 8650036014..ebe5eaaf15 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -23,7 +23,6 @@ import { ExtendedHttpServer } from '@backstage/backend-defaults/rootHttpRouter'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; import { HttpRouterService } from '@backstage/backend-plugin-api'; -import { InstanceMetadataService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; import Keyv from 'keyv'; import { Knex } from 'knex'; @@ -36,6 +35,7 @@ import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootHealthService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; +import { RootInstanceMetadataService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; @@ -263,21 +263,6 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) - export function instanceMetadata(): InstanceMetadataService; - // (undocumented) - export namespace instanceMetadata { - const // (undocumented) - mock: ( - partialImpl?: Partial | undefined, - ) => ServiceMock; - const // (undocumented) - factory: () => ServiceFactory< - InstanceMetadataService, - 'plugin', - 'singleton' | 'multiton' - >; - } - // (undocumented) export namespace lifecycle { const // (undocumented) factory: () => ServiceFactory; @@ -360,6 +345,21 @@ export namespace mockServices { ) => ServiceMock; } // (undocumented) + export function rootInstanceMetadata(): RootInstanceMetadataService; + // (undocumented) + export namespace rootInstanceMetadata { + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + const // (undocumented) + factory: () => ServiceFactory< + RootInstanceMetadataService, + 'plugin', + 'singleton' | 'multiton' + >; + } + // (undocumented) export namespace rootLifecycle { const // (undocumented) factory: () => ServiceFactory; diff --git a/packages/backend/src/instanceMetadata.ts b/packages/backend/src/instanceMetadata.ts index 4f12fc889d..026fc6fd02 100644 --- a/packages/backend/src/instanceMetadata.ts +++ b/packages/backend/src/instanceMetadata.ts @@ -24,7 +24,7 @@ export default createBackendPlugin({ register(env) { env.registerInit({ deps: { - instanceMetadata: coreServices.instanceMetadata, + instanceMetadata: coreServices.rootInstanceMetadata, logger: coreServices.logger, }, async init({ instanceMetadata, logger }) { diff --git a/plugins/gateway-backend/src/plugin.ts b/plugins/gateway-backend/src/plugin.ts index efd7a372de..6facc1ddea 100644 --- a/plugins/gateway-backend/src/plugin.ts +++ b/plugins/gateway-backend/src/plugin.ts @@ -32,7 +32,7 @@ export const gatewayPlugin = createBackendPlugin({ deps: { logger: coreServices.logger, rootHttpRouter: coreServices.rootHttpRouter, - instanceMeta: coreServices.instanceMetadata, + instanceMeta: coreServices.rootInstanceMetadata, discovery: coreServices.discovery, }, async init({ logger, discovery, instanceMeta, rootHttpRouter }) { diff --git a/plugins/gateway-backend/src/router.ts b/plugins/gateway-backend/src/router.ts index 7f8c898761..8263fb27ab 100644 --- a/plugins/gateway-backend/src/router.ts +++ b/plugins/gateway-backend/src/router.ts @@ -15,7 +15,7 @@ */ import { DiscoveryService, - InstanceMetadataService, + RootInstanceMetadataService, LoggerService, } from '@backstage/backend-plugin-api'; import { Request, Response, NextFunction } from 'express'; @@ -28,7 +28,7 @@ export async function createRouter({ instanceMeta, }: { discovery: DiscoveryService; - instanceMeta: InstanceMetadataService; + instanceMeta: RootInstanceMetadataService; logger: LoggerService; }) { const plugins = await instanceMeta.getInstalledPlugins(); From 40104ba92ac982dd0b75a8e25121901fd48ccf82 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 15:42:13 -0400 Subject: [PATCH 15/18] fix test case regression caused by move to map Signed-off-by: aramissennyeydd --- packages/backend-app-api/src/wiring/BackendInitializer.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index bc778b6589..0808846d9c 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -135,9 +135,11 @@ function createRootInstanceMetadataServiceFactory( service: coreServices.rootInstanceMetadata, deps: {}, factory: async () => { - const readonlyInstalledPlugins = deepFreeze( - Object.values(installedPlugins), - ); + console.log(installedPlugins); + const readonlyInstalledPlugins = deepFreeze([ + ...installedPlugins.values(), + ]); + console.log(readonlyInstalledPlugins, Object.values(installedPlugins)); const instanceMetadata = { getInstalledPlugins: () => Promise.resolve(readonlyInstalledPlugins), }; From 9ea2c4fb359d0c8b5acf96d0eace2d079ecb7c62 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 16:24:05 -0400 Subject: [PATCH 16/18] add missing modules Signed-off-by: aramissennyeydd --- .../instanceMetadataServiceFactory.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts index a50c8f0058..bd2d427a59 100644 --- a/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts +++ b/packages/backend-defaults/src/alpha/entrypoints/instanceMetadata/instanceMetadataServiceFactory.ts @@ -18,7 +18,11 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; -import { instanceMetadataServiceRef } from '@backstage/backend-plugin-api/alpha'; +import { + BackendFeatureMeta, + InstanceMetadataService, + instanceMetadataServiceRef, +} from '@backstage/backend-plugin-api/alpha'; /** * @alpha @@ -31,9 +35,22 @@ export const instanceMetadataServiceFactory = createServiceFactory({ }, factory: async ({ instanceMetadata }) => { const plugins = await instanceMetadata.getInstalledPlugins(); - const service = { - getInstalledFeatures: () => - plugins.map(e => ({ type: 'plugin' as const, pluginId: e.pluginId })), + const features: BackendFeatureMeta[] = []; + for (const plugin of plugins) { + features.push({ + type: 'plugin' as const, + pluginId: plugin.pluginId, + }); + for (const module of plugin.modules) { + features.push({ + type: 'module' as const, + pluginId: plugin.pluginId, + moduleId: module.moduleId, + }); + } + } + const service: InstanceMetadataService = { + getInstalledFeatures: () => features, }; return service; From 769e2b7880d91aae2310d1147425a19164d61ff1 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Wed, 22 Oct 2025 22:31:04 -0400 Subject: [PATCH 17/18] rework logic to explicitly rely on the list of plugins to create the map keys Signed-off-by: aramissennyeydd --- .../src/wiring/BackendInitializer.test.ts | 38 +++++++++++++ .../src/wiring/BackendInitializer.ts | 54 +++++++++---------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index b4ec19a4ad..a751beb9c3 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -1129,6 +1129,44 @@ describe('BackendInitializer', () => { await backend.start(); }); + it('should ignore modules that do not have a matching plugin', async () => { + expect.assertions(1); + const backend = new BackendInitializer(baseFactories); + const instanceMetadataPlugin = createBackendPlugin({ + pluginId: 'instance-metadata', + register(reg) { + reg.registerInit({ + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + async init({ instanceMetadata }) { + await expect( + instanceMetadata.getInstalledPlugins(), + ).resolves.toEqual([ + { + pluginId: 'instance-metadata', + modules: [], + }, + ]); + }, + }); + }, + }); + const module = createBackendModule({ + pluginId: 'test', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + }); + backend.add(module); + backend.add(instanceMetadataPlugin); + await backend.start(); + }); + it('should prevent writes to the instance metadata service', async () => { expect.assertions(1); const backend = new BackendInitializer(baseFactories); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 0808846d9c..0e67350076 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -99,38 +99,38 @@ const instanceRegistry = new (class InstanceRegistry { })(); function createRootInstanceMetadataServiceFactory( - registrations: InternalBackendRegistrations[], + rawRegistrations: InternalBackendRegistrations[], ) { const installedPlugins: Map = new Map(); - for (const registration of registrations) { - if (registration.featureType === 'registrations') { - for (const feature of registration.getRegistrations()) { - if (feature.type === 'plugin') { - if (!installedPlugins.get(feature.pluginId)) { - installedPlugins.set(feature.pluginId, { - pluginId: feature.pluginId, - modules: [], - }); - } - } else if (feature.type === 'module') { - if (!installedPlugins.get(feature.pluginId)) { - installedPlugins.set(feature.pluginId, { - pluginId: feature.pluginId, - modules: [], - }); - } - ( - installedPlugins.get(feature.pluginId)!.modules as Array<{ - moduleId: string; - }> - ).push({ - moduleId: feature.moduleId, - }); - } - } + const registrations = rawRegistrations + .filter(registration => registration.featureType === 'registrations') + .flatMap(registration => registration.getRegistrations()); + const plugins = registrations.filter( + registration => registration.type === 'plugin', + ); + const modules = registrations.filter( + registration => registration.type === 'module', + ); + for (const plugin of plugins) { + const { pluginId } = plugin; + if (!installedPlugins.get(pluginId)) { + installedPlugins.set(pluginId, { + pluginId, + modules: [], + }); } } + for (const module of modules) { + const { pluginId, moduleId } = module; + const installedPlugin = installedPlugins.get(pluginId); + if (installedPlugin) { + (installedPlugin.modules as Array<{ moduleId: string }>).push({ + moduleId, + }); + } + } + return createServiceFactory({ service: coreServices.rootInstanceMetadata, deps: {}, From e743d11fadaed17ca7867da4df1a49425146da19 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Thu, 30 Oct 2025 08:54:29 -0400 Subject: [PATCH 18/18] update docs to use root Signed-off-by: aramissennyeydd --- .../core-services/instance-metadata.md | 44 ------------------- .../core-services/root-instance-metadata.md | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 44 deletions(-) delete mode 100644 docs/backend-system/core-services/instance-metadata.md create mode 100644 docs/backend-system/core-services/root-instance-metadata.md diff --git a/docs/backend-system/core-services/instance-metadata.md b/docs/backend-system/core-services/instance-metadata.md deleted file mode 100644 index 8a5818e2bc..0000000000 --- a/docs/backend-system/core-services/instance-metadata.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -id: instance-metadata -title: Instance Metadata Service -sidebar_label: Instance Metadata -description: Documentation for the Instance Metadata service ---- - -The instance metadata service provides information about the running Backstage backend instance. Currently, it provides a list of all installed backend plugins. - -:::note Note - -The instance metadata service only provides information about the specific Backstage instance you're running on. In more complex deployments with multiple Backstage instances, this service will not provide a complete list of all plugins across all instances. - -::: - -## Using the service - -The following example shows how to use the instance metadata service in your `example` backend plugin to access the list of installed backend plugins. - -```ts -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; - -createBackendPlugin({ - pluginId: 'example', - register(env) { - env.registerInit({ - deps: { - instanceMetadata: coreServices.instanceMetadata, - }, - async init({ instanceMetadata }) { - const plugins = instanceMetadata.getInstalledPlugins(); - console.log('Installed plugins:', plugins); - }, - }); - }, -}); -``` - -## Dynamic plugin registration - -The instance metadata service picks up plugins that are registered at start time through a `backend.start()` call. You need to restart the running backend instance to pick up newly installed plugins. diff --git a/docs/backend-system/core-services/root-instance-metadata.md b/docs/backend-system/core-services/root-instance-metadata.md new file mode 100644 index 0000000000..109042f1a5 --- /dev/null +++ b/docs/backend-system/core-services/root-instance-metadata.md @@ -0,0 +1,44 @@ +--- +id: root-instance-metadata +title: Root Instance Metadata Service +sidebar_label: Root Instance Metadata +description: Documentation for the Root Instance Metadata service +--- + +The root instance metadata service provides information about the running Backstage backend instance. Currently, it provides a list of all installed backend plugins. + +:::note Note + +The root instance metadata service only provides information about the specific Backstage instance you're running on. In more complex deployments with multiple Backstage instances, this service will not provide a complete list of all plugins across all instances. + +::: + +## Using the service + +The following example shows how to use the root instance metadata service in your `example` backend plugin to access the list of installed backend plugins. + +```ts +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; + +createBackendPlugin({ + pluginId: 'example', + register(env) { + env.registerInit({ + deps: { + instanceMetadata: coreServices.rootInstanceMetadata, + }, + async init({ instanceMetadata }) { + const plugins = instanceMetadata.getInstalledPlugins(); + console.log('Installed plugins:', plugins); + }, + }); + }, +}); +``` + +## Dynamic plugin registration + +The root instance metadata service picks up plugins that are registered at start time through a `backend.start()` call. You need to restart the running backend instance to pick up newly installed plugins.