Merge pull request #28129 from backstage/sennyeya/deployment-discovery
feat: add a new system metadata service
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user