Merge pull request #28129 from backstage/sennyeya/deployment-discovery
feat: add a new system metadata service
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-defaults': patch
|
||||
'@backstage/backend-plugin-api': minor
|
||||
---
|
||||
|
||||
Adds a new experimental `RootSystemMetadataService` for tracking the collection of Backstage instances that may be deployed at any one time. It currently offers a single API, `getInstalledPlugins` that returns a list of installed plugins based on config you have set up in `discovery.endpoints` as well as the plugins installed on the instance you're calling the API with. It does not handle wildcard values or fallback values. The intention is for this plugin to provide plugin authors with a simple interface to fetch a trustworthy list of all installed plugins.
|
||||
@@ -214,6 +214,7 @@
|
||||
"@types/yauzl": "^2.10.0",
|
||||
"aws-sdk-client-mock": "^4.0.0",
|
||||
"better-sqlite3": "^12.0.0",
|
||||
"get-port": "^5.1.1",
|
||||
"http-errors": "^2.0.0",
|
||||
"msw": "^1.0.0",
|
||||
"node-mocks-http": "^1.0.0",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
```ts
|
||||
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
|
||||
import { RootSystemMetadataService } 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
|
||||
export const rootSystemMetadataServiceFactory: ServiceFactory<
|
||||
RootSystemMetadataService,
|
||||
'root',
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import Router from 'express-promise-router';
|
||||
import request from 'supertest';
|
||||
import { rootSystemMetadataServiceFactory } from './rootSystemMetadataServiceFactory';
|
||||
import { rootSystemMetadataServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
describe('SystemMetadataService', () => {
|
||||
describe('returns plugins from config', () => {
|
||||
const testPlugin = createBackendPlugin({
|
||||
pluginId: 'test-plugin',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {
|
||||
systemMetadata: rootSystemMetadataServiceRef,
|
||||
rootHttpRouter: coreServices.rootHttpRouter,
|
||||
},
|
||||
init: async ({ systemMetadata, rootHttpRouter }) => {
|
||||
const router = Router();
|
||||
router.use('/plugins', async (_, res) => {
|
||||
res.json(await systemMetadata.getInstalledPlugins());
|
||||
});
|
||||
rootHttpRouter.use('/systemMetadata', router);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
it('should list all known instances', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
testPlugin,
|
||||
rootSystemMetadataServiceFactory,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: {
|
||||
port: 0,
|
||||
},
|
||||
baseUrl: `http://localhost:0`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const instanceResponse = await request(server).get(
|
||||
`/systemMetadata/plugins`,
|
||||
);
|
||||
|
||||
expect(instanceResponse.status).toBe(200);
|
||||
expect(instanceResponse.body).toMatchObject([
|
||||
{
|
||||
pluginId: 'test-plugin',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should react to config updates', async () => {
|
||||
const config = mockServices.rootConfig({
|
||||
data: {
|
||||
backend: {
|
||||
listen: {
|
||||
port: 0,
|
||||
},
|
||||
baseUrl: `http://localhost:0`,
|
||||
},
|
||||
},
|
||||
});
|
||||
const configFactory = createServiceFactory({
|
||||
service: coreServices.rootConfig,
|
||||
deps: {},
|
||||
factory: () => config,
|
||||
});
|
||||
const { server } = await startTestBackend({
|
||||
features: [testPlugin, configFactory, rootSystemMetadataServiceFactory],
|
||||
});
|
||||
|
||||
const initialResponse = await request(server).get(
|
||||
`/systemMetadata/plugins`,
|
||||
);
|
||||
|
||||
expect(initialResponse.status).toBe(200);
|
||||
expect(initialResponse.body).toMatchObject([
|
||||
{
|
||||
pluginId: 'test-plugin',
|
||||
},
|
||||
]);
|
||||
|
||||
config.update({
|
||||
data: {
|
||||
backend: {
|
||||
listen: {
|
||||
port: 0,
|
||||
},
|
||||
baseUrl: `http://localhost:0`,
|
||||
},
|
||||
discovery: {
|
||||
endpoints: [
|
||||
{
|
||||
target: `http://test.internal`,
|
||||
plugins: ['your-new-plugin'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const responseAfterUpdate = await request(server).get(
|
||||
`/systemMetadata/plugins`,
|
||||
);
|
||||
|
||||
expect(responseAfterUpdate.status).toBe(200);
|
||||
expect(responseAfterUpdate.body).toMatchObject([
|
||||
{
|
||||
pluginId: 'your-new-plugin',
|
||||
},
|
||||
{
|
||||
pluginId: 'test-plugin',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { rootSystemMetadataServiceFactory } from './rootSystemMetadataServiceFactory';
|
||||
export { DefaultRootSystemMetadataService } from './lib/DefaultRootSystemMetadataService';
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
RootInstanceMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
RootSystemMetadataService,
|
||||
RootSystemMetadataServicePluginInfo,
|
||||
} from '@backstage/backend-plugin-api/alpha';
|
||||
import { getEndpoints } from '../../../../entrypoints/discovery/parsing';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
function getPlugins(config: Config): string[] {
|
||||
const endpoints = getEndpoints(config);
|
||||
return Array.from(new Set(endpoints.flatMap(endpoint => endpoint.plugins)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export class DefaultRootSystemMetadataService
|
||||
implements RootSystemMetadataService
|
||||
{
|
||||
#plugins: string[];
|
||||
#instanceMetadata: RootInstanceMetadataService;
|
||||
constructor(options: {
|
||||
logger: LoggerService;
|
||||
config: RootConfigService;
|
||||
instanceMetadata: RootInstanceMetadataService;
|
||||
}) {
|
||||
const { config } = options;
|
||||
this.#plugins = getPlugins(config);
|
||||
config.subscribe?.(() => {
|
||||
this.#plugins = getPlugins(config);
|
||||
});
|
||||
this.#instanceMetadata = options.instanceMetadata;
|
||||
}
|
||||
|
||||
public static create(pluginEnv: {
|
||||
logger: LoggerService;
|
||||
config: RootConfigService;
|
||||
instanceMetadata: RootInstanceMetadataService;
|
||||
}) {
|
||||
return new DefaultRootSystemMetadataService(pluginEnv);
|
||||
}
|
||||
|
||||
public async getInstalledPlugins(): Promise<
|
||||
RootSystemMetadataServicePluginInfo[]
|
||||
> {
|
||||
const plugins = this.#plugins.map(pluginId => ({ pluginId }));
|
||||
|
||||
for (const plugin of await this.#instanceMetadata.getInstalledPlugins()) {
|
||||
plugins.push({ pluginId: plugin.pluginId });
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { DefaultRootSystemMetadataService } from './lib/DefaultRootSystemMetadataService';
|
||||
import { rootSystemMetadataServiceRef } from '@backstage/backend-plugin-api/alpha';
|
||||
|
||||
/**
|
||||
* Metadata about an entire Backstage system, a collection of Backstage instances.
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const rootSystemMetadataServiceFactory = createServiceFactory({
|
||||
service: rootSystemMetadataServiceRef,
|
||||
deps: {
|
||||
logger: coreServices.rootLogger,
|
||||
config: coreServices.rootConfig,
|
||||
instanceMetadata: coreServices.rootInstanceMetadata,
|
||||
},
|
||||
async factory({ logger, config, instanceMetadata }) {
|
||||
return DefaultRootSystemMetadataService.create({
|
||||
logger,
|
||||
config,
|
||||
instanceMetadata,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export { actionsRegistryServiceFactory } from './entrypoints/actionsRegistry';
|
||||
export { actionsServiceFactory } from './entrypoints/actions';
|
||||
export { rootSystemMetadataServiceFactory } from './entrypoints/rootSystemMetadata';
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { readHttpServerOptions } from '../rootHttpRouter/http/config';
|
||||
import { SrvResolvers } from './SrvResolvers';
|
||||
import { trimEnd } from 'lodash';
|
||||
import { getEndpoints } from './parsing';
|
||||
|
||||
type Resolver = (pluginId: string) => Promise<string>;
|
||||
|
||||
@@ -238,25 +239,7 @@ export class HostDiscovery implements DiscoveryService {
|
||||
const endpoints = defaultEndpoints?.slice() ?? [];
|
||||
|
||||
// Allow config to override the default endpoints
|
||||
const endpointConfigs = config.getOptionalConfigArray(
|
||||
'discovery.endpoints',
|
||||
);
|
||||
for (const endpointConfig of endpointConfigs ?? []) {
|
||||
if (typeof endpointConfig.get('target') === 'string') {
|
||||
endpoints.push({
|
||||
target: endpointConfig.getString('target'),
|
||||
plugins: endpointConfig.getStringArray('plugins'),
|
||||
});
|
||||
} else {
|
||||
endpoints.push({
|
||||
target: {
|
||||
internal: endpointConfig.getOptionalString('target.internal'),
|
||||
external: endpointConfig.getOptionalString('target.external'),
|
||||
},
|
||||
plugins: endpointConfig.getStringArray('plugins'),
|
||||
});
|
||||
}
|
||||
}
|
||||
endpoints.push(...getEndpoints(config));
|
||||
|
||||
// Build up a new set of resolvers
|
||||
const internalResolvers: Map<string, Resolver> = new Map();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { Config } from '@backstage/config';
|
||||
import type { HostDiscoveryEndpoint } from './HostDiscovery';
|
||||
|
||||
export function getEndpoints(config: Config): HostDiscoveryEndpoint[] {
|
||||
const endpoints: HostDiscoveryEndpoint[] = [];
|
||||
// Allow config to override the default endpoints
|
||||
const endpointConfigs = config.getOptionalConfigArray('discovery.endpoints');
|
||||
for (const endpointConfig of endpointConfigs ?? []) {
|
||||
if (typeof endpointConfig.get('target') === 'string') {
|
||||
endpoints.push({
|
||||
target: endpointConfig.getString('target'),
|
||||
plugins: endpointConfig.getStringArray('plugins'),
|
||||
});
|
||||
} else {
|
||||
endpoints.push({
|
||||
target: {
|
||||
internal: endpointConfig.getOptionalString('target.internal'),
|
||||
external: endpointConfig.getOptionalString('target.external'),
|
||||
},
|
||||
plugins: endpointConfig.getStringArray('plugins'),
|
||||
});
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
@@ -103,5 +103,26 @@ export const actionsServiceRef: ServiceRef<
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootSystemMetadataService {
|
||||
// (undocumented)
|
||||
getInstalledPlugins: () => Promise<
|
||||
ReadonlyArray<RootSystemMetadataServicePluginInfo>
|
||||
>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface RootSystemMetadataServicePluginInfo {
|
||||
// (undocumented)
|
||||
readonly pluginId: string;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const rootSystemMetadataServiceRef: ServiceRef<
|
||||
RootSystemMetadataService,
|
||||
'root',
|
||||
'singleton'
|
||||
>;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 RootSystemMetadataServicePluginInfo {
|
||||
readonly pluginId: string;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface RootSystemMetadataService {
|
||||
getInstalledPlugins: () => Promise<
|
||||
ReadonlyArray<RootSystemMetadataServicePluginInfo>
|
||||
>;
|
||||
}
|
||||
@@ -14,6 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
RootSystemMetadataServicePluginInfo,
|
||||
RootSystemMetadataService,
|
||||
} from './RootSystemMetadataService';
|
||||
|
||||
export type {
|
||||
ActionsRegistryService,
|
||||
ActionsRegistryActionOptions,
|
||||
@@ -22,4 +27,8 @@ export type {
|
||||
|
||||
export type { ActionsService, ActionsServiceAction } from './ActionsService';
|
||||
|
||||
export { actionsRegistryServiceRef, actionsServiceRef } from './refs';
|
||||
export {
|
||||
actionsRegistryServiceRef,
|
||||
actionsServiceRef,
|
||||
rootSystemMetadataServiceRef,
|
||||
} from './refs';
|
||||
|
||||
@@ -45,3 +45,14 @@ export const actionsRegistryServiceRef = createServiceRef<
|
||||
>({
|
||||
id: 'alpha.core.actionsRegistry',
|
||||
});
|
||||
|
||||
/**
|
||||
* Read information about your current Backstage deployment.
|
||||
* @alpha
|
||||
*/
|
||||
export const rootSystemMetadataServiceRef = createServiceRef<
|
||||
import('./RootSystemMetadataService').RootSystemMetadataService
|
||||
>({
|
||||
id: 'alpha.core.rootSystemMetadata',
|
||||
scope: 'root',
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import { rootSystemMetadataServiceFactory } from '@backstage/backend-defaults/alpha';
|
||||
import {
|
||||
coreServices,
|
||||
createBackendFeatureLoader,
|
||||
@@ -69,7 +70,7 @@ backend.add(searchLoader);
|
||||
backend.add(import('@backstage/plugin-techdocs-backend'));
|
||||
backend.add(import('@backstage/plugin-signals-backend'));
|
||||
backend.add(import('@backstage/plugin-notifications-backend'));
|
||||
backend.add(import('./instanceMetadata'));
|
||||
backend.add(rootSystemMetadataServiceFactory);
|
||||
|
||||
backend.add(import('@backstage/plugin-events-backend-module-google-pubsub'));
|
||||
backend.add(import('@backstage/plugin-mcp-actions-backend'));
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
// 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: coreServices.rootInstanceMetadata,
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async init({ instanceMetadata, logger }) {
|
||||
const plugins = await instanceMetadata.getInstalledPlugins();
|
||||
logger.info(
|
||||
`Installed plugins on this instance: ${plugins
|
||||
.map(e => e.pluginId)
|
||||
.join(', ')}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user