update to just config reading-based
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
-10
@@ -1249,15 +1249,5 @@ export interface Config {
|
||||
*/
|
||||
plugins: string[];
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A list of deployed Backstage instances that can be crawled for discovery.
|
||||
*/
|
||||
instances: Array<{
|
||||
/**
|
||||
* The base URL of the instance. All /.backstage/ routes should be accessible.
|
||||
*/
|
||||
baseUrl: string | { internal: string; external: string };
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
"./permissions": "./src/entrypoints/permissions/index.ts",
|
||||
"./rootConfig": "./src/entrypoints/rootConfig/index.ts",
|
||||
"./rootHealth": "./src/entrypoints/rootHealth/index.ts",
|
||||
"./rootSystemMetadata": "./src/entrypoints/rootSystemMetadata/index.ts",
|
||||
"./rootHttpRouter": "./src/entrypoints/rootHttpRouter/index.ts",
|
||||
"./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts",
|
||||
"./rootLogger": "./src/entrypoints/rootLogger/index.ts",
|
||||
"./scheduler": "./src/entrypoints/scheduler/index.ts",
|
||||
"./systemMetadata": "./src/entrypoints/systemMetadata/index.ts",
|
||||
"./urlReader": "./src/entrypoints/urlReader/index.ts",
|
||||
"./userInfo": "./src/entrypoints/userInfo/index.ts",
|
||||
"./alpha": "./src/alpha/index.ts",
|
||||
@@ -92,15 +92,15 @@
|
||||
"rootLifecycle": [
|
||||
"src/entrypoints/rootLifecycle/index.ts"
|
||||
],
|
||||
"rootSystemMetadata": [
|
||||
"src/entrypoints/rootSystemMetadata/index.ts"
|
||||
],
|
||||
"rootLogger": [
|
||||
"src/entrypoints/rootLogger/index.ts"
|
||||
],
|
||||
"scheduler": [
|
||||
"src/entrypoints/scheduler/index.ts"
|
||||
],
|
||||
"systemMetadata": [
|
||||
"src/entrypoints/systemMetadata/index.ts"
|
||||
],
|
||||
"urlReader": [
|
||||
"src/entrypoints/urlReader/index.ts"
|
||||
],
|
||||
@@ -198,7 +198,6 @@
|
||||
"winston-transport": "^4.5.0",
|
||||
"yauzl": "^3.0.0",
|
||||
"yn": "^4.0.0",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.20.4"
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -150,6 +150,11 @@ export class HostDiscovery implements DiscoveryService {
|
||||
throw new Error('Not initialized');
|
||||
};
|
||||
|
||||
#resolutions: Map<
|
||||
string,
|
||||
Set<{ hash: string; target: { internal?: string; external?: string } }>
|
||||
> = new Map();
|
||||
|
||||
static fromConfig(config: RootConfigService, options?: HostDiscoveryOptions) {
|
||||
const discovery = new HostDiscovery(new SrvResolvers());
|
||||
|
||||
@@ -193,12 +198,23 @@ export class HostDiscovery implements DiscoveryService {
|
||||
return await resolver(pluginId);
|
||||
}
|
||||
|
||||
#updateResolvers(config: Config, defaultEndpoints?: HostDiscoveryEndpoint[]) {
|
||||
this.#updateFallbackResolvers(config);
|
||||
this.#updatePluginResolvers(config, defaultEndpoints);
|
||||
async listResolutions() {
|
||||
const _targets: Map<string, { internal?: string; external?: string }[]> =
|
||||
new Map();
|
||||
for (const [pluginId, targets] of this.#resolutions.entries()) {
|
||||
const currentTargets = [...targets.values()].map(({ target }) => ({
|
||||
...target,
|
||||
}));
|
||||
if (_targets.has(pluginId)) {
|
||||
_targets.set(pluginId, [..._targets.get(pluginId)!, ...currentTargets]);
|
||||
} else {
|
||||
_targets.set(pluginId, currentTargets);
|
||||
}
|
||||
}
|
||||
return _targets;
|
||||
}
|
||||
|
||||
#updateFallbackResolvers(config: Config) {
|
||||
getInstanceAddress(config: Config) {
|
||||
const backendBaseUrl = trimEnd(config.getString('backend.baseUrl'), '/');
|
||||
|
||||
const {
|
||||
@@ -220,12 +236,26 @@ export class HostDiscovery implements DiscoveryService {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
|
||||
return {
|
||||
internal: `${protocol}://${host}:${listenPort}`,
|
||||
external: backendBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
#updateResolvers(config: Config, defaultEndpoints?: HostDiscoveryEndpoint[]) {
|
||||
this.#updateFallbackResolvers(config);
|
||||
this.#updatePluginResolvers(config, defaultEndpoints);
|
||||
}
|
||||
|
||||
#updateFallbackResolvers(config: Config) {
|
||||
const { internal, external } = this.getInstanceAddress(config);
|
||||
|
||||
this.#internalFallbackResolver = this.#makeResolver(
|
||||
`${protocol}://${host}:${listenPort}/api/{{pluginId}}`,
|
||||
`${internal}/api/{{pluginId}}`,
|
||||
false,
|
||||
);
|
||||
this.#externalFallbackResolver = this.#makeResolver(
|
||||
`${backendBaseUrl}/api/{{pluginId}}`,
|
||||
`${external}/api/{{pluginId}}`,
|
||||
false,
|
||||
);
|
||||
}
|
||||
@@ -264,6 +294,7 @@ export class HostDiscovery implements DiscoveryService {
|
||||
for (const { target, plugins } of endpoints) {
|
||||
let internalResolver: Resolver | undefined;
|
||||
let externalResolver: Resolver | undefined;
|
||||
this.#addResolution(target, plugins);
|
||||
|
||||
if (typeof target === 'string') {
|
||||
internalResolver = externalResolver = this.#makeResolver(target, false);
|
||||
@@ -293,6 +324,26 @@ export class HostDiscovery implements DiscoveryService {
|
||||
this.#externalResolvers = externalResolvers;
|
||||
}
|
||||
|
||||
#addResolution(
|
||||
target: string | { internal?: string; external?: string },
|
||||
plugins: string[],
|
||||
) {
|
||||
for (const pluginId of plugins) {
|
||||
if (!this.#resolutions.has(pluginId)) {
|
||||
this.#resolutions.set(pluginId, new Set());
|
||||
}
|
||||
const standardizedTarget =
|
||||
typeof target === 'string'
|
||||
? { external: target, internal: target }
|
||||
: target;
|
||||
const matchingResolution = this.#resolutions.get(pluginId)!;
|
||||
const hash = JSON.stringify(standardizedTarget);
|
||||
if (![...matchingResolution.values()].some(e => e.hash === hash)) {
|
||||
matchingResolution.add({ target: standardizedTarget, hash });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#makeResolver(urlPattern: string, allowSrv: boolean): Resolver {
|
||||
const withPluginId = (pluginId: string, url: string) => {
|
||||
return url.replace(
|
||||
|
||||
+5
-5
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Backend, createSpecializedBackend } from '@backstage/backend-app-api';
|
||||
import { systemMetadataServiceFactory } from './systemMetadataServiceFactory';
|
||||
import { rootSystemMetadataServiceFactory } from './rootSystemMetadataServiceFactory';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import getPort from 'get-port';
|
||||
import {
|
||||
@@ -66,7 +66,7 @@ describe('SystemMetadataService', () => {
|
||||
instance1 = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
rootSystemMetadataServiceFactory,
|
||||
configFactory(instance1HttpPort),
|
||||
],
|
||||
});
|
||||
@@ -74,7 +74,7 @@ describe('SystemMetadataService', () => {
|
||||
instance2 = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
rootSystemMetadataServiceFactory,
|
||||
configFactory(instance2HttpPort),
|
||||
],
|
||||
});
|
||||
@@ -169,7 +169,7 @@ describe('SystemMetadataService', () => {
|
||||
instance = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
rootSystemMetadataServiceFactory,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
@@ -236,7 +236,7 @@ describe('SystemMetadataService', () => {
|
||||
instance = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
rootSystemMetadataServiceFactory,
|
||||
configFactory,
|
||||
],
|
||||
});
|
||||
+2
-2
@@ -13,5 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { systemMetadataServiceFactory } from './systemMetadataServiceFactory';
|
||||
export { DefaultSystemMetadataService } from './lib/DefaultSystemMetadataService';
|
||||
export { rootSystemMetadataServiceFactory } from './rootSystemMetadataServiceFactory';
|
||||
export { DefaultRootSystemMetadataService } from './lib/DefaultRootSystemMetadataService';
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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,
|
||||
RootSystemMetadataService,
|
||||
RootSystemMetadataServicePluginInfo,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { HostDiscovery } from '../../discovery';
|
||||
import {} from '@backstage/backend-plugin-api';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export class DefaultRootSystemMetadataService
|
||||
implements RootSystemMetadataService
|
||||
{
|
||||
#hostDiscovery: HostDiscovery;
|
||||
#instanceMetadata: RootInstanceMetadataService;
|
||||
#config: RootConfigService;
|
||||
constructor(options: {
|
||||
logger: LoggerService;
|
||||
config: RootConfigService;
|
||||
instanceMetadata: RootInstanceMetadataService;
|
||||
}) {
|
||||
this.#hostDiscovery = HostDiscovery.fromConfig(options.config, {
|
||||
logger: options.logger,
|
||||
});
|
||||
options.config.subscribe?.(() => {
|
||||
this.#hostDiscovery = HostDiscovery.fromConfig(options.config, {
|
||||
logger: options.logger,
|
||||
});
|
||||
});
|
||||
this.#instanceMetadata = options.instanceMetadata;
|
||||
this.#config = options.config;
|
||||
}
|
||||
|
||||
public static create(pluginEnv: {
|
||||
logger: LoggerService;
|
||||
config: RootConfigService;
|
||||
instanceMetadata: RootInstanceMetadataService;
|
||||
}) {
|
||||
return new DefaultRootSystemMetadataService(pluginEnv);
|
||||
}
|
||||
|
||||
public async getInstalledPlugins(): Promise<
|
||||
RootSystemMetadataServicePluginInfo[]
|
||||
> {
|
||||
const resolutions = await this.#hostDiscovery.listResolutions();
|
||||
const instanceAddress = this.#hostDiscovery.getInstanceAddress(
|
||||
this.#config,
|
||||
);
|
||||
const currentInstance = await this.#instanceMetadata.getInstalledPlugins();
|
||||
for (const plugin of currentInstance) {
|
||||
if (!resolutions.has(plugin.pluginId)) {
|
||||
resolutions.set(plugin.pluginId, []);
|
||||
}
|
||||
resolutions.get(plugin.pluginId)?.push(instanceAddress);
|
||||
}
|
||||
return Array.from(resolutions.entries()).map(([pluginId, targets]) => ({
|
||||
pluginId,
|
||||
hosts: Array.from(targets).filter(
|
||||
(target): target is { external: string; internal: string } =>
|
||||
Object.keys(target).length > 0,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
public async getHosts(): Promise<
|
||||
ReadonlyArray<string | { external: string; internal: string }>
|
||||
> {
|
||||
const resolutions = await this.#hostDiscovery.listResolutions();
|
||||
const hosts = new Set<string | { external: string; internal: string }>();
|
||||
for (const [_, targets] of resolutions.entries()) {
|
||||
for (const target of targets) {
|
||||
hosts.add(target as string | { external: string; internal: string });
|
||||
}
|
||||
}
|
||||
return Array.from(hosts);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import type { RootSystemMetadataService } from '@backstage/backend-plugin-api';
|
||||
import Router from 'express-promise-router';
|
||||
|
||||
export async function createSystemMetadataRouter(options: {
|
||||
logger: LoggerService;
|
||||
systemMetadata: RootSystemMetadataService;
|
||||
}) {
|
||||
const { systemMetadata } = options;
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/hosts', async (_, res) => {
|
||||
const hosts = await systemMetadata.getHosts();
|
||||
res.json({ items: hosts });
|
||||
});
|
||||
|
||||
router.get('/plugins/installed', async (_, res) => {
|
||||
res.json(await systemMetadata.getInstalledPlugins());
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
+7
-5
@@ -18,7 +18,7 @@ import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { DefaultSystemMetadataService } from './lib/DefaultSystemMetadataService';
|
||||
import { DefaultRootSystemMetadataService } from './lib/DefaultRootSystemMetadataService';
|
||||
import { createSystemMetadataRouter } from './lib/createSystemMetadataRouter';
|
||||
|
||||
/**
|
||||
@@ -26,17 +26,19 @@ import { createSystemMetadataRouter } from './lib/createSystemMetadataRouter';
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const systemMetadataServiceFactory = createServiceFactory({
|
||||
service: coreServices.systemMetadata,
|
||||
export const rootSystemMetadataServiceFactory = createServiceFactory({
|
||||
service: coreServices.rootSystemMetadata,
|
||||
deps: {
|
||||
logger: coreServices.rootLogger,
|
||||
config: coreServices.rootConfig,
|
||||
httpRouter: coreServices.rootHttpRouter,
|
||||
instanceMetadata: coreServices.rootInstanceMetadata,
|
||||
},
|
||||
async factory({ logger, config, httpRouter }) {
|
||||
const systemMetadata = DefaultSystemMetadataService.create({
|
||||
async factory({ logger, config, httpRouter, instanceMetadata }) {
|
||||
const systemMetadata = DefaultRootSystemMetadataService.create({
|
||||
logger,
|
||||
config,
|
||||
instanceMetadata,
|
||||
});
|
||||
|
||||
const router = await createSystemMetadataRouter({ systemMetadata, logger });
|
||||
-187
@@ -1,187 +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 {
|
||||
LoggerService,
|
||||
RootConfigService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
BackstageInstance,
|
||||
SystemMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import z from 'zod';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
const targetObjectSchema = z.object({
|
||||
internal: z.string(),
|
||||
external: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* A basic implementation of ReactiveX behavior subjects.
|
||||
*
|
||||
* A subject is a convenient way to create an observable when you want
|
||||
* to fan out a single value to all subscribers.
|
||||
*
|
||||
* The BehaviorSubject will emit the most recently emitted value or error
|
||||
* whenever a new observer subscribes to the subject.
|
||||
*
|
||||
* See http://reactivex.io/documentation/subject.html
|
||||
*
|
||||
* FORKED FROM core-app-api - where should this live?
|
||||
*/
|
||||
|
||||
export class BehaviorSubject<T>
|
||||
implements Observable<T>, ZenObservable.SubscriptionObserver<T>
|
||||
{
|
||||
private isClosed: boolean;
|
||||
private currentValue: T;
|
||||
private terminatingError: Error | undefined;
|
||||
private readonly observable: Observable<T>;
|
||||
|
||||
constructor(value: T) {
|
||||
this.isClosed = false;
|
||||
this.currentValue = value;
|
||||
this.terminatingError = undefined;
|
||||
this.observable = new ObservableImpl<T>(subscriber => {
|
||||
if (this.isClosed) {
|
||||
if (this.terminatingError) {
|
||||
subscriber.error(this.terminatingError);
|
||||
} else {
|
||||
subscriber.complete();
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
subscriber.next(this.currentValue);
|
||||
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private readonly subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<T>
|
||||
>();
|
||||
|
||||
[Symbol.observable]() {
|
||||
return this;
|
||||
}
|
||||
|
||||
get closed() {
|
||||
return this.isClosed;
|
||||
}
|
||||
|
||||
next(value: T) {
|
||||
if (this.isClosed) {
|
||||
throw new Error('BehaviorSubject is closed');
|
||||
}
|
||||
this.currentValue = value;
|
||||
this.subscribers.forEach(subscriber => subscriber.next(value));
|
||||
}
|
||||
|
||||
error(error: Error) {
|
||||
if (this.isClosed) {
|
||||
throw new Error('BehaviorSubject is closed');
|
||||
}
|
||||
this.isClosed = true;
|
||||
this.terminatingError = error;
|
||||
this.subscribers.forEach(subscriber => subscriber.error(error));
|
||||
}
|
||||
|
||||
complete() {
|
||||
if (this.isClosed) {
|
||||
throw new Error('BehaviorSubject is closed');
|
||||
}
|
||||
this.isClosed = true;
|
||||
this.subscribers.forEach(subscriber => subscriber.complete());
|
||||
}
|
||||
|
||||
subscribe(observer: ZenObservable.Observer<T>): ZenObservable.Subscription;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: any) => void,
|
||||
onComplete?: () => void,
|
||||
): ZenObservable.Subscription;
|
||||
subscribe(
|
||||
onNext: ZenObservable.Observer<T> | ((value: T) => void),
|
||||
onError?: (error: any) => void,
|
||||
onComplete?: () => void,
|
||||
): ZenObservable.Subscription {
|
||||
const observer =
|
||||
typeof onNext === 'function'
|
||||
? {
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
}
|
||||
: onNext;
|
||||
|
||||
return this.observable.subscribe(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export class DefaultSystemMetadataService implements SystemMetadataService {
|
||||
private instance$: BehaviorSubject<BackstageInstance[]>;
|
||||
constructor(
|
||||
private options: { logger: LoggerService; config: RootConfigService },
|
||||
) {
|
||||
const getInstances = () => {
|
||||
const endpoints =
|
||||
options.config.getOptionalConfigArray('discovery.instances') ?? [];
|
||||
const instances: BackstageInstance[] = [];
|
||||
for (const endpoint of endpoints) {
|
||||
const baseUrl = endpoint.getOptional('baseUrl');
|
||||
if (baseUrl) {
|
||||
if (typeof baseUrl === 'string') {
|
||||
instances.push({ internalUrl: baseUrl, externalUrl: baseUrl });
|
||||
} else {
|
||||
const parseAttempt = targetObjectSchema.safeParse(baseUrl);
|
||||
if (parseAttempt.success) {
|
||||
const { internal, external } = parseAttempt.data;
|
||||
instances.push({
|
||||
internalUrl: internal,
|
||||
externalUrl: external,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return instances;
|
||||
};
|
||||
this.instance$ = new BehaviorSubject(getInstances());
|
||||
this.options.config.subscribe?.(() => {
|
||||
this.instance$.next(getInstances());
|
||||
});
|
||||
}
|
||||
|
||||
public static create(pluginEnv: {
|
||||
logger: LoggerService;
|
||||
config: RootConfigService;
|
||||
}) {
|
||||
return new DefaultSystemMetadataService(pluginEnv);
|
||||
}
|
||||
|
||||
instances(): Observable<BackstageInstance[]> {
|
||||
return this.instance$;
|
||||
}
|
||||
}
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* 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 { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { BackendFeatureMeta } from '@backstage/backend-plugin-api/alpha';
|
||||
import type {
|
||||
BackstageInstance,
|
||||
SystemMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import Router from 'express-promise-router';
|
||||
|
||||
export async function createSystemMetadataRouter(options: {
|
||||
logger: LoggerService;
|
||||
systemMetadata: SystemMetadataService;
|
||||
}) {
|
||||
const { logger, systemMetadata } = options;
|
||||
|
||||
let instances: BackstageInstance[] = [];
|
||||
systemMetadata.instances().subscribe({
|
||||
next: value => {
|
||||
instances = value;
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Instances in this system: ${JSON.stringify(instances)}`);
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/instances', async (_, res) => {
|
||||
res.json({ items: instances });
|
||||
});
|
||||
|
||||
router.get('/features/installed', async (_, res) => {
|
||||
const featurePromises = await Promise.allSettled(
|
||||
instances.map(async instance => {
|
||||
const response = await fetch(
|
||||
`${instance.internalUrl}/.backstage/instanceMetadata/v1/features/installed`,
|
||||
);
|
||||
if (response.ok) {
|
||||
return { instance, response: await response.json() };
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to fetch installed features from ${instance.internalUrl}`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
const pluginByInstance: Record<
|
||||
string,
|
||||
{ internalUrl: string; externalUrl: string }[]
|
||||
> = {};
|
||||
for (const result of featurePromises) {
|
||||
if (result.status !== 'fulfilled') {
|
||||
logger.error(`Failed to fetch installed features: ${result.reason}`);
|
||||
continue;
|
||||
}
|
||||
const instance = result.value.instance;
|
||||
const installedFeatures = result.value.response
|
||||
.items as BackendFeatureMeta[];
|
||||
for (const feature of installedFeatures) {
|
||||
if (feature.type === 'plugin') {
|
||||
if (!pluginByInstance[feature.pluginId]) {
|
||||
pluginByInstance[feature.pluginId] = [];
|
||||
}
|
||||
pluginByInstance[feature.pluginId].push(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.json(pluginByInstance);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user