feat: promote instance metadata to stable

Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
aramissennyeydd
2025-09-03 09:48:34 -04:00
committed by Fredrik Adelöw
parent 6fb48a2754
commit a17d9df2ee
15 changed files with 119 additions and 112 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-plugin-api': minor
---
Promote `instanceMetadata` service to main entrypoint.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-gateway-backend': minor
---
Update usage of the `instanceMetadata` service.
+5
View File
@@ -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.
+2 -1
View File
@@ -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:^",
@@ -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}',
],
);
},
});
},
@@ -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<ServiceOrExtensionPoint>;
@@ -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;
},
});
}
+1 -10
View File
@@ -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';
@@ -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
*
@@ -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[];
}
@@ -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',
});
}
@@ -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';
+5 -5
View File
@@ -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(', ')}`,
);
},
+1 -2
View File
@@ -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 }) {
+6 -6
View File
@@ -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({
+1
View File
@@ -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