Merge pull request #31053 from backstage/sennyeya/instance-metadata-update
feat: promote instance metadata
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-plugin-api': minor
|
||||
---
|
||||
|
||||
Promote `instanceMetadata` service to main entrypoint.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-gateway-backend': minor
|
||||
---
|
||||
|
||||
Update usage of the `instanceMetadata` service.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': minor
|
||||
---
|
||||
|
||||
Updates API for `instanceMetadata` service to return a list of plugins not features.
|
||||
@@ -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.
|
||||
@@ -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 = [
|
||||
@@ -1110,7 +1110,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',
|
||||
@@ -1126,31 +1126,25 @@ describe('BackendInitializer', () => {
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
instanceMetadata: instanceMetadataServiceRef,
|
||||
instanceMetadata: coreServices.rootInstanceMetadata,
|
||||
},
|
||||
async init({ instanceMetadata }) {
|
||||
expect(instanceMetadata.getInstalledFeatures()).toEqual([
|
||||
await expect(
|
||||
instanceMetadata.getInstalledPlugins(),
|
||||
).resolves.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}',
|
||||
],
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -1171,6 +1165,67 @@ 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);
|
||||
const plugin = createBackendPlugin({
|
||||
pluginId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
instanceMetadata: coreServices.rootInstanceMetadata,
|
||||
},
|
||||
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);
|
||||
|
||||
@@ -36,14 +36,11 @@ import type {
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types';
|
||||
import { ForwardedError, ConflictError, 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';
|
||||
import { deepFreeze, unwrapFeature } from './helpers';
|
||||
import type { RootInstanceMetadataServicePluginInfo } from '@backstage/backend-plugin-api';
|
||||
|
||||
export interface BackendRegisterInit {
|
||||
consumes: Set<ServiceOrExtensionPoint>;
|
||||
@@ -101,56 +98,54 @@ const instanceRegistry = new (class InstanceRegistry {
|
||||
};
|
||||
})();
|
||||
|
||||
function createInstanceMetadataServiceFactory(
|
||||
registrations: InternalBackendRegistrations[],
|
||||
function createRootInstanceMetadataServiceFactory(
|
||||
rawRegistrations: 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[];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.flat();
|
||||
const installedPlugins: Map<string, RootInstanceMetadataServicePluginInfo> =
|
||||
new Map();
|
||||
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: instanceMetadataServiceRef,
|
||||
service: coreServices.rootInstanceMetadata,
|
||||
deps: {},
|
||||
factory: async () => ({ getInstalledFeatures: () => installedFeatures }),
|
||||
factory: async () => {
|
||||
console.log(installedPlugins);
|
||||
const readonlyInstalledPlugins = deepFreeze([
|
||||
...installedPlugins.values(),
|
||||
]);
|
||||
console.log(readonlyInstalledPlugins, Object.values(installedPlugins));
|
||||
const instanceMetadata = {
|
||||
getInstalledPlugins: () => Promise.resolve(readonlyInstalledPlugins),
|
||||
};
|
||||
|
||||
return instanceMetadata;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -255,7 +250,7 @@ export class BackendInitializer {
|
||||
await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders);
|
||||
|
||||
this.#serviceRegistry.add(
|
||||
createInstanceMetadataServiceFactory(this.#registrations),
|
||||
createRootInstanceMetadataServiceFactory(this.#registrations),
|
||||
);
|
||||
|
||||
// This makes sure that any uncaught errors or unhandled rejections are
|
||||
|
||||
@@ -34,3 +34,22 @@ export function unwrapFeature(
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type DeepReadonly<T> = {
|
||||
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
||||
};
|
||||
|
||||
/**
|
||||
* Deeply freezes an object by recursively freezing all of its properties.
|
||||
* From https://gist.github.com/tkrotoff/e997cd6ff8d6cf6e51e6bb6146407fc3 +
|
||||
* https://stackoverflow.com/a/69656011
|
||||
*/
|
||||
export function deepFreeze<T>(obj: T) {
|
||||
// Can cause: "Type instantiation is excessively deep and possibly infinite."
|
||||
// @ts-expect-error
|
||||
Object.values(obj).forEach(
|
||||
value => Object.isFrozen(value) || deepFreeze(value),
|
||||
);
|
||||
return Object.freeze(obj) as DeepReadonly<T>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 {
|
||||
BackendFeatureMeta,
|
||||
InstanceMetadataService,
|
||||
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: {
|
||||
instanceMetadata: coreServices.rootInstanceMetadata,
|
||||
},
|
||||
factory: async ({ instanceMetadata }) => {
|
||||
const plugins = await instanceMetadata.getInstalledPlugins();
|
||||
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;
|
||||
},
|
||||
});
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry';
|
||||
export { actionsServiceFactory } from './entrypoints/actions';
|
||||
export { instanceMetadataServiceFactory } from './entrypoints/instanceMetadata';
|
||||
|
||||
@@ -232,6 +232,11 @@ export namespace coreServices {
|
||||
const rootLogger: ServiceRef<RootLoggerService, 'root', 'singleton'>;
|
||||
const scheduler: ServiceRef<SchedulerService, 'plugin', 'singleton'>;
|
||||
const urlReader: ServiceRef<UrlReaderService, 'plugin', 'singleton'>;
|
||||
const rootInstanceMetadata: ServiceRef<
|
||||
RootInstanceMetadataService,
|
||||
'plugin',
|
||||
'singleton'
|
||||
>;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -578,6 +583,24 @@ export interface RootHttpRouterService {
|
||||
use(path: string, handler: Handler): void;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootInstanceMetadataService {
|
||||
// (undocumented)
|
||||
getInstalledPlugins: () => Promise<
|
||||
ReadonlyArray<RootInstanceMetadataServicePluginInfo>
|
||||
>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootInstanceMetadataServicePluginInfo {
|
||||
// (undocumented)
|
||||
readonly modules: ReadonlyArray<{
|
||||
moduleId: string;
|
||||
}>;
|
||||
// (undocumented)
|
||||
readonly pluginId: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface RootLifecycleService extends LifecycleService {
|
||||
// (undocumented)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
* 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.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export interface RootInstanceMetadataServicePluginInfo {
|
||||
readonly pluginId: string;
|
||||
readonly modules: ReadonlyArray<{
|
||||
moduleId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface RootInstanceMetadataService {
|
||||
getInstalledPlugins: () => Promise<
|
||||
ReadonlyArray<RootInstanceMetadataServicePluginInfo>
|
||||
>;
|
||||
}
|
||||
@@ -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 rootInstanceMetadata = createServiceRef<
|
||||
import('./RootInstanceMetadataService').RootInstanceMetadataService
|
||||
>({
|
||||
id: 'core.rootInstanceMetadata',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,4 +85,8 @@ export type {
|
||||
UrlReaderServiceSearchResponseFile,
|
||||
} from './UrlReaderService';
|
||||
export type { BackstageUserInfo, UserInfoService } from './UserInfoService';
|
||||
export type {
|
||||
RootInstanceMetadataService,
|
||||
RootInstanceMetadataServicePluginInfo,
|
||||
} from './RootInstanceMetadataService';
|
||||
export { coreServices } from './coreServices';
|
||||
|
||||
@@ -35,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';
|
||||
@@ -344,6 +345,21 @@ export namespace mockServices {
|
||||
) => ServiceMock<RootHttpRouterService>;
|
||||
}
|
||||
// (undocumented)
|
||||
export function rootInstanceMetadata(): RootInstanceMetadataService;
|
||||
// (undocumented)
|
||||
export namespace rootInstanceMetadata {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?: Partial<RootInstanceMetadataService> | undefined,
|
||||
) => ServiceMock<RootInstanceMetadataService>;
|
||||
const // (undocumented)
|
||||
factory: () => ServiceFactory<
|
||||
RootInstanceMetadataService,
|
||||
'plugin',
|
||||
'singleton' | 'multiton'
|
||||
>;
|
||||
}
|
||||
// (undocumented)
|
||||
export namespace rootLifecycle {
|
||||
const // (undocumented)
|
||||
factory: () => ServiceFactory<RootLifecycleService, 'root', 'singleton'>;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
DatabaseService,
|
||||
DiscoveryService,
|
||||
HttpAuthService,
|
||||
RootInstanceMetadataService,
|
||||
LoggerService,
|
||||
PermissionsService,
|
||||
RootConfigService,
|
||||
@@ -556,4 +557,19 @@ export namespace mockServices {
|
||||
subscribe: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
export function rootInstanceMetadata(): RootInstanceMetadataService {
|
||||
return {
|
||||
getInstalledPlugins: () => Promise.resolve([]),
|
||||
};
|
||||
}
|
||||
export namespace rootInstanceMetadata {
|
||||
export const mock = simpleMock(coreServices.rootInstanceMetadata, () => ({
|
||||
getInstalledPlugins: jest.fn(),
|
||||
}));
|
||||
export const factory = simpleFactoryWithOptions(
|
||||
coreServices.rootInstanceMetadata,
|
||||
rootInstanceMetadata,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.rootInstanceMetadata,
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async init({ instanceMetadata, logger }) {
|
||||
const plugins = await instanceMetadata.getInstalledPlugins();
|
||||
logger.info(
|
||||
`Installed features on this instance: ${instanceMetadata
|
||||
.getInstalledFeatures()
|
||||
`Installed plugins on this instance: ${plugins
|
||||
.map(e => e.pluginId)
|
||||
.join(', ')}`,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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,17 +32,17 @@ export const gatewayPlugin = createBackendPlugin({
|
||||
deps: {
|
||||
logger: coreServices.logger,
|
||||
rootHttpRouter: coreServices.rootHttpRouter,
|
||||
instanceMeta: instanceMetadataServiceRef,
|
||||
instanceMeta: coreServices.rootInstanceMetadata,
|
||||
discovery: coreServices.discovery,
|
||||
},
|
||||
async init({ logger, discovery, instanceMeta, rootHttpRouter }) {
|
||||
rootHttpRouter.use(
|
||||
'/api/:pluginId',
|
||||
createRouter({
|
||||
(await createRouter({
|
||||
discovery,
|
||||
instanceMeta,
|
||||
logger,
|
||||
}) as Handler,
|
||||
})) as Handler,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,27 +13,26 @@
|
||||
* 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,
|
||||
RootInstanceMetadataService,
|
||||
LoggerService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
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,
|
||||
}: {
|
||||
discovery: DiscoveryService;
|
||||
instanceMeta: InstanceMetadataService;
|
||||
instanceMeta: RootInstanceMetadataService;
|
||||
logger: LoggerService;
|
||||
}) {
|
||||
const localPluginIds = new Set(
|
||||
instanceMeta
|
||||
.getInstalledFeatures()
|
||||
.filter(f => f.type === 'plugin')
|
||||
.map(f => f.pluginId),
|
||||
);
|
||||
const plugins = await instanceMeta.getInstalledPlugins();
|
||||
const localPluginIds = new Set(plugins.map(f => f.pluginId));
|
||||
|
||||
const proxy = createProxyMiddleware({
|
||||
changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user