update system metadata service to be observable-first
Signed-off-by: aramissennyeydd <aramis.sennyey@doordash.com>
This commit is contained in:
@@ -198,6 +198,7 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@ export const actionsServiceFactory: ServiceFactory<
|
||||
ActionsService,
|
||||
import { BackstageInstance } from '@backstage/backend-plugin-api';
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { RootConfigService } from '@backstage/backend-plugin-api';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { SystemMetadataService } from '@backstage/backend-plugin-api';
|
||||
@@ -32,9 +33,7 @@ export class DefaultSystemMetadataService implements SystemMetadataService {
|
||||
config: RootConfigService;
|
||||
}): DefaultSystemMetadataService;
|
||||
// (undocumented)
|
||||
introspect(): Promise<{
|
||||
instances: BackstageInstance[];
|
||||
}>;
|
||||
instances(): Observable<BackstageInstance[]>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
|
||||
+152
-92
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createSpecializedBackend } from '@backstage/backend-app-api';
|
||||
import { Backend, createSpecializedBackend } from '@backstage/backend-app-api';
|
||||
import { systemMetadataServiceFactory } from './systemMetadataServiceFactory';
|
||||
import { mockServices } from '@backstage/backend-test-utils';
|
||||
import getPort from 'get-port';
|
||||
@@ -29,111 +29,171 @@ const baseFactories = [
|
||||
];
|
||||
|
||||
describe('SystemMetadataService', () => {
|
||||
it('should list plugins across instances', async () => {
|
||||
const instance1HttpPort = await getPort();
|
||||
const instance2HttpPort = await getPort();
|
||||
const instance1 = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: {
|
||||
port: instance1HttpPort,
|
||||
},
|
||||
},
|
||||
discovery: {
|
||||
instances: [
|
||||
{
|
||||
baseUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
{
|
||||
baseUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
describe('multiple backends testing', () => {
|
||||
let instance1: Backend;
|
||||
let instance2: Backend;
|
||||
let instance1HttpPort: number;
|
||||
let instance2HttpPort: number;
|
||||
|
||||
const configFactory = (port: number) =>
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: {
|
||||
port,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
discovery: {
|
||||
instances: [
|
||||
{
|
||||
baseUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
{
|
||||
baseUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
beforeEach(async () => {
|
||||
instance1HttpPort = await getPort();
|
||||
instance2HttpPort = await getPort();
|
||||
// Setup code for multiple backend instances
|
||||
instance1 = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
configFactory(instance1HttpPort),
|
||||
],
|
||||
});
|
||||
|
||||
instance2 = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
configFactory(instance2HttpPort),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const instance2 = createSpecializedBackend({
|
||||
defaultServiceFactories: [
|
||||
...baseFactories,
|
||||
systemMetadataServiceFactory,
|
||||
mockServices.rootConfig.factory({
|
||||
data: {
|
||||
backend: {
|
||||
listen: {
|
||||
port: instance2HttpPort,
|
||||
it('should list plugins across instances', async () => {
|
||||
instance1.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
// do nothing
|
||||
},
|
||||
},
|
||||
discovery: {
|
||||
instances: [
|
||||
{
|
||||
baseUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
{
|
||||
baseUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
instance2.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test-other',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await instance1.start();
|
||||
await instance2.start();
|
||||
|
||||
const instance1Response = await fetch(
|
||||
`http://localhost:${instance1HttpPort}/.backstage/systemMetadata/v1/features/installed`,
|
||||
);
|
||||
|
||||
expect(instance1Response.status).toBe(200);
|
||||
await expect(instance1Response.json()).resolves.toMatchObject({
|
||||
test: [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
],
|
||||
'test-other': [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const instance2Response = await fetch(
|
||||
`http://localhost:${instance2HttpPort}/.backstage/systemMetadata/v1/features/installed`,
|
||||
);
|
||||
|
||||
expect(instance2Response.status).toBe(200);
|
||||
|
||||
await expect(instance2Response.json()).resolves.toMatchObject({
|
||||
test: [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
],
|
||||
'test-other': [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
instance1.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
it('should list all known instances', async () => {
|
||||
await instance1.start();
|
||||
await instance2.start();
|
||||
|
||||
instance2.add(
|
||||
createBackendPlugin({
|
||||
pluginId: 'test-other',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: {},
|
||||
async init() {
|
||||
// do nothing
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
const instance1Response = await fetch(
|
||||
`http://localhost:${instance1HttpPort}/.backstage/systemMetadata/v1/instances`,
|
||||
);
|
||||
|
||||
await instance1.start();
|
||||
await instance2.start();
|
||||
expect(instance1Response.status).toBe(200);
|
||||
await expect(instance1Response.json()).resolves.toMatchObject({
|
||||
items: [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
{
|
||||
externalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const installedFeatures = await fetch(
|
||||
`http://localhost:${instance1HttpPort}/.backstage/systemMetadata/v1/features/installed`,
|
||||
);
|
||||
const instance2Response = await fetch(
|
||||
`http://localhost:${instance2HttpPort}/.backstage/systemMetadata/v1/instances`,
|
||||
);
|
||||
|
||||
expect(installedFeatures.status).toBe(200);
|
||||
expect(instance2Response.status).toBe(200);
|
||||
await expect(instance2Response.json()).resolves.toMatchObject({
|
||||
items: [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
{
|
||||
externalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
await expect(installedFeatures.json()).resolves.toMatchObject({
|
||||
test: [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance1HttpPort}`,
|
||||
},
|
||||
],
|
||||
'test-other': [
|
||||
{
|
||||
externalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
internalUrl: `http://localhost:${instance2HttpPort}`,
|
||||
},
|
||||
],
|
||||
afterEach(async () => {
|
||||
await instance1.stop();
|
||||
await instance2.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+113
-7
@@ -22,18 +22,126 @@ 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 instances: BackstageInstance[];
|
||||
private instance$: BehaviorSubject<BackstageInstance[]>;
|
||||
constructor(
|
||||
private options: { logger: LoggerService; config: RootConfigService },
|
||||
) {
|
||||
@@ -60,9 +168,9 @@ export class DefaultSystemMetadataService implements SystemMetadataService {
|
||||
}
|
||||
return instances;
|
||||
};
|
||||
this.instances = getInstances();
|
||||
this.instance$ = new BehaviorSubject(getInstances());
|
||||
this.options.config.subscribe?.(() => {
|
||||
this.instances = getInstances();
|
||||
this.instance$.next(getInstances());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,9 +181,7 @@ export class DefaultSystemMetadataService implements SystemMetadataService {
|
||||
return new DefaultSystemMetadataService(pluginEnv);
|
||||
}
|
||||
|
||||
async introspect() {
|
||||
return {
|
||||
instances: this.instances,
|
||||
};
|
||||
instances(): Observable<BackstageInstance[]> {
|
||||
return this.instance$;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-10
@@ -16,7 +16,10 @@
|
||||
|
||||
import { LoggerService } from '@backstage/backend-plugin-api';
|
||||
import { BackendFeatureMeta } from '@backstage/backend-plugin-api/alpha';
|
||||
import type { SystemMetadataService } from '@backstage/backend-plugin-api';
|
||||
import type {
|
||||
BackstageInstance,
|
||||
SystemMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import Router from 'express-promise-router';
|
||||
|
||||
export async function createSystemMetadataRouter(options: {
|
||||
@@ -25,23 +28,22 @@ export async function createSystemMetadataRouter(options: {
|
||||
}) {
|
||||
const { logger, systemMetadata } = options;
|
||||
|
||||
async function getInstances() {
|
||||
const instances = await systemMetadata.introspect();
|
||||
return instances.instances;
|
||||
}
|
||||
let instances: BackstageInstance[] = [];
|
||||
systemMetadata.instances().subscribe({
|
||||
next: value => {
|
||||
instances = value;
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Instances in this system: ${JSON.stringify(await getInstances())}`,
|
||||
);
|
||||
logger.info(`Instances in this system: ${JSON.stringify(instances)}`);
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/instances', async (_, res) => {
|
||||
res.json(await getInstances());
|
||||
res.json({ items: instances });
|
||||
});
|
||||
|
||||
router.get('/features/installed', async (_, res) => {
|
||||
const instances = await getInstances();
|
||||
const featurePromises = await Promise.allSettled(
|
||||
instances.map(async instance => {
|
||||
const response = await fetch(
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isChildPath } from '@backstage/cli-common';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { Knex } from 'knex';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { Permission } from '@backstage/plugin-permission-common';
|
||||
import { PermissionAttributes } from '@backstage/plugin-permission-common';
|
||||
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
|
||||
@@ -762,9 +763,7 @@ export interface ServiceRefOptions<
|
||||
// @public (undocumented)
|
||||
export interface SystemMetadataService {
|
||||
// (undocumented)
|
||||
introspect(): Promise<{
|
||||
instances: BackstageInstance[];
|
||||
}>;
|
||||
instances(): Observable<BackstageInstance[]>;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Observable } from '@backstage/types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -26,5 +28,5 @@ export interface BackstageInstance {
|
||||
* @public
|
||||
*/
|
||||
export interface SystemMetadataService {
|
||||
introspect(): Promise<{ instances: BackstageInstance[] }>;
|
||||
instances(): Observable<BackstageInstance[]>;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"text-extensions": "^2.4.0",
|
||||
"uuid": "^11.0.0",
|
||||
"yn": "^4.0.0",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.20.4"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AuthService } from '@backstage/backend-plugin-api';
|
||||
import { Backend } from '@backstage/backend-app-api';
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { BackstageCredentials } from '@backstage/backend-plugin-api';
|
||||
import { BackstageInstance } from '@backstage/backend-plugin-api';
|
||||
import { BackstageNonePrincipal } from '@backstage/backend-plugin-api';
|
||||
import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api';
|
||||
import { BackstageServicePrincipal } from '@backstage/backend-plugin-api';
|
||||
@@ -397,12 +398,17 @@ export namespace mockServices {
|
||||
partialImpl?: Partial<SchedulerService> | undefined,
|
||||
) => ServiceMock<SchedulerService>;
|
||||
}
|
||||
export function systemMetadata(options: {
|
||||
instances: BackstageInstance[];
|
||||
}): SystemMetadataService;
|
||||
// (undocumented)
|
||||
export namespace systemMetadata {
|
||||
const factory: () => ServiceFactory<
|
||||
const factory: (options: {
|
||||
instances: BackstageInstance[];
|
||||
}) => ServiceFactory<
|
||||
SystemMetadataService,
|
||||
'root',
|
||||
'singleton'
|
||||
'singleton' | 'multiton'
|
||||
>;
|
||||
const mock: (
|
||||
partialImpl?: Partial<SystemMetadataService> | undefined,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 { Observable } from '@backstage/types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
export function createMockObservable<T>(value: T): Observable<T> {
|
||||
return new ObservableImpl(observer => {
|
||||
observer.next(value);
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { BackstageInstance } from '@backstage/backend-plugin-api';
|
||||
import { MockSystemMetadataService } from './MockSystemMetadataService';
|
||||
|
||||
describe('MockSystemMetadataService', () => {
|
||||
it('should return the passed in instances', () => {
|
||||
expect.assertions(1);
|
||||
const instances: BackstageInstance[] = [
|
||||
{ internalUrl: 'localhost:7007', externalUrl: 'external.url' },
|
||||
{ internalUrl: 'localhost:7008', externalUrl: 'other.external.url' },
|
||||
];
|
||||
const service = MockSystemMetadataService.create({ instances });
|
||||
service.instances().subscribe({
|
||||
next: value => {
|
||||
expect(value).toEqual(instances);
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 {
|
||||
BackstageInstance,
|
||||
SystemMetadataService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class MockSystemMetadataService implements SystemMetadataService {
|
||||
#instances: BackstageInstance[];
|
||||
|
||||
constructor(instances: BackstageInstance[]) {
|
||||
this.#instances = instances;
|
||||
}
|
||||
|
||||
public static create(options: { instances: BackstageInstance[] }) {
|
||||
return new MockSystemMetadataService(options.instances);
|
||||
}
|
||||
|
||||
instances(): Observable<BackstageInstance[]> {
|
||||
return new ObservableImpl<BackstageInstance[]>(subscriber => {
|
||||
subscriber.next(this.#instances);
|
||||
subscriber.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,10 @@ import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth
|
||||
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
|
||||
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
|
||||
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
|
||||
import { systemMetadataServiceFactory } from '@backstage/backend-defaults/systemMetadata';
|
||||
import {
|
||||
AuthService,
|
||||
BackstageCredentials,
|
||||
BackstageInstance,
|
||||
BackstageUserInfo,
|
||||
DatabaseService,
|
||||
DiscoveryService,
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
SchedulerService,
|
||||
ServiceFactory,
|
||||
ServiceRef,
|
||||
SystemMetadataService,
|
||||
UserInfoService,
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
@@ -62,6 +63,8 @@ import { simpleMock } from './simpleMock';
|
||||
import { MockSchedulerService } from './MockSchedulerService';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { ObservableConfigProxy } from '../../../config-loader/src/sources/ObservableConfigProxy';
|
||||
import { MockSystemMetadataService } from './MockSystemMetadataService';
|
||||
import { createMockObservable } from './MockObservable';
|
||||
|
||||
/** @internal */
|
||||
function createLoggerMock() {
|
||||
@@ -574,19 +577,33 @@ export namespace mockServices {
|
||||
rootInstanceMetadata,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Creates a functional mock implementation for the
|
||||
* {@link @backstage/backend-plugin-api#coreServices.systemMetadata}.
|
||||
*/
|
||||
export function systemMetadata(options: {
|
||||
instances: BackstageInstance[];
|
||||
}): SystemMetadataService {
|
||||
return MockSystemMetadataService.create(options);
|
||||
}
|
||||
export namespace systemMetadata {
|
||||
/**
|
||||
* Creates a functional mock factory for the
|
||||
* {@link @backstage/backend-plugin-api#coreServices.systemMetadata}.
|
||||
*/
|
||||
export const factory = () => systemMetadataServiceFactory;
|
||||
export const factory = simpleFactoryWithOptions(
|
||||
coreServices.systemMetadata,
|
||||
systemMetadata,
|
||||
);
|
||||
/**
|
||||
* Creates a mock of the
|
||||
* {@link @backstage/backend-events-node#systemMetadata}, optionally
|
||||
* with some given method implementations.
|
||||
*/
|
||||
export const mock = simpleMock(coreServices.systemMetadata, () => ({
|
||||
introspect: jest.fn(),
|
||||
instances: jest
|
||||
.fn()
|
||||
.mockReturnValue(createMockObservable<BackstageInstance[]>([])),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export function simpleMock<TService>(
|
||||
const mock = mockFactory();
|
||||
if (partialImpl) {
|
||||
for (const [key, impl] of Object.entries(partialImpl)) {
|
||||
console.log(key, impl, mock);
|
||||
if (typeof impl === 'function') {
|
||||
(mock as any)[key].mockImplementation(impl);
|
||||
} else {
|
||||
|
||||
@@ -2951,6 +2951,7 @@ __metadata:
|
||||
winston-transport: "npm:^4.5.0"
|
||||
yauzl: "npm:^3.0.0"
|
||||
yn: "npm:^4.0.0"
|
||||
zen-observable: "npm:^0.10.0"
|
||||
zod: "npm:^3.22.4"
|
||||
zod-to-json-schema: "npm:^3.20.4"
|
||||
peerDependencies:
|
||||
@@ -3103,6 +3104,7 @@ __metadata:
|
||||
text-extensions: "npm:^2.4.0"
|
||||
uuid: "npm:^11.0.0"
|
||||
yn: "npm:^4.0.0"
|
||||
zen-observable: "npm:^0.10.0"
|
||||
zod: "npm:^3.22.4"
|
||||
zod-to-json-schema: "npm:^3.20.4"
|
||||
languageName: unknown
|
||||
|
||||
Reference in New Issue
Block a user