Merge pull request #13347 from backstage/jhaals/catalogService
catalog-node: Export experimental catalogServiceRef
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-node': patch
|
||||
---
|
||||
|
||||
Adds experimental `catalogServiceRef` for obtaining a `CatalogClient` in the new backend system.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Updated `ServiceRegistry` to not initialize factories more than once.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Added alpha `scaffolderPlugin` to be used with experimental backend system.
|
||||
@@ -17,6 +17,7 @@
|
||||
import {
|
||||
createServiceRef,
|
||||
createServiceFactory,
|
||||
ServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ServiceRegistry } from './ServiceRegistry';
|
||||
|
||||
@@ -170,4 +171,53 @@ describe('ServiceRegistry', () => {
|
||||
expect(await factoryB('catalog')).toBe(await factoryB('catalog'));
|
||||
expect(await factoryA('catalog')).not.toBe(await factoryB('catalog'));
|
||||
});
|
||||
|
||||
it('should only call each default factory loader once', async () => {
|
||||
const factoryLoader = jest.fn(async (service: ServiceRef<void>) =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {},
|
||||
factory: async () => async () => {},
|
||||
}),
|
||||
);
|
||||
const ref = createServiceRef<void>({
|
||||
id: '1',
|
||||
defaultFactory: factoryLoader,
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([]);
|
||||
const factory = registry.get(ref)!;
|
||||
await Promise.all([
|
||||
expect(factory('catalog')).resolves.toBeUndefined(),
|
||||
expect(factory('catalog')).resolves.toBeUndefined(),
|
||||
]);
|
||||
expect(factoryLoader).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call factory functions more than once', async () => {
|
||||
const innerFactory = jest.fn(async (pluginId: string) => {
|
||||
return { x: 1, pluginId };
|
||||
});
|
||||
const factory = jest.fn(async () => innerFactory);
|
||||
const myFactory = createServiceFactory({
|
||||
service: ref1,
|
||||
deps: {},
|
||||
factory,
|
||||
});
|
||||
|
||||
const registry = new ServiceRegistry([myFactory]);
|
||||
|
||||
await Promise.all([
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('catalog')!,
|
||||
registry.get(ref1)!('scaffolder')!,
|
||||
registry.get(ref1)!('scaffolder')!,
|
||||
]);
|
||||
|
||||
expect(factory).toHaveBeenCalledTimes(1);
|
||||
expect(innerFactory).toHaveBeenCalledTimes(2);
|
||||
expect(innerFactory).toHaveBeenCalledWith('catalog');
|
||||
expect(innerFactory).toHaveBeenCalledWith('scaffolder');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,14 @@ import {
|
||||
|
||||
export class ServiceRegistry {
|
||||
readonly #providedFactories: Map<string, ServiceFactory>;
|
||||
readonly #loadedDefaultFactories: Map<Function, ServiceFactory>;
|
||||
readonly #implementations: Map<ServiceFactory, Map<string, unknown>>;
|
||||
readonly #loadedDefaultFactories: Map<Function, Promise<ServiceFactory>>;
|
||||
readonly #implementations: Map<
|
||||
ServiceFactory,
|
||||
{
|
||||
factoryFunc: Promise<FactoryFunc<unknown>>;
|
||||
byPlugin: Map<string, Promise<unknown>>;
|
||||
}
|
||||
>;
|
||||
|
||||
constructor(factories: ServiceFactory<any>[]) {
|
||||
this.#providedFactories = new Map(factories.map(f => [f.service.id, f]));
|
||||
@@ -41,35 +47,38 @@ export class ServiceRegistry {
|
||||
if (!factory) {
|
||||
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!);
|
||||
if (!loadedFactory) {
|
||||
loadedFactory = (await defaultFactory!(ref)) as ServiceFactory;
|
||||
loadedFactory = defaultFactory!(ref) as Promise<ServiceFactory>;
|
||||
this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory);
|
||||
}
|
||||
factory = loadedFactory;
|
||||
// NOTE: This await is safe as long as #providedFactories is not mutated.
|
||||
factory = await loadedFactory;
|
||||
}
|
||||
|
||||
let implementations = this.#implementations.get(factory);
|
||||
if (implementations) {
|
||||
if (implementations.has(pluginId)) {
|
||||
return implementations.get(pluginId) as T;
|
||||
}
|
||||
} else {
|
||||
implementations = new Map();
|
||||
this.#implementations.set(factory, implementations);
|
||||
let implementation = this.#implementations.get(factory);
|
||||
if (!implementation) {
|
||||
const factoryDeps = Object.fromEntries(
|
||||
Object.entries(factory.deps).map(([name, serviceRef]) => [
|
||||
name,
|
||||
this.get(serviceRef)!, // TODO: throw
|
||||
]),
|
||||
);
|
||||
|
||||
implementation = {
|
||||
factoryFunc: factory.factory(factoryDeps),
|
||||
byPlugin: new Map(),
|
||||
};
|
||||
|
||||
this.#implementations.set(factory, implementation);
|
||||
}
|
||||
|
||||
const factoryDeps = Object.fromEntries(
|
||||
Object.entries(factory.deps).map(([name, serviceRef]) => [
|
||||
name,
|
||||
this.get(serviceRef)!, // TODO: throw
|
||||
]),
|
||||
);
|
||||
let result = implementation.byPlugin.get(pluginId) as Promise<any>;
|
||||
if (!result) {
|
||||
result = implementation.factoryFunc.then(func => func(pluginId));
|
||||
|
||||
const factoryFunc = await factory.factory(factoryDeps);
|
||||
const implementation = await factoryFunc(pluginId);
|
||||
implementation.byPlugin.set(pluginId, result);
|
||||
}
|
||||
|
||||
implementations.set(pluginId, implementation);
|
||||
|
||||
return implementation as T;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
```ts
|
||||
/// <reference types="node" />
|
||||
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
import { CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface CatalogProcessingExtensionPoint {
|
||||
@@ -106,6 +108,9 @@ export type CatalogProcessorResult =
|
||||
| CatalogProcessorErrorResult
|
||||
| CatalogProcessorRefreshKeysResult;
|
||||
|
||||
// @alpha
|
||||
export const catalogServiceRef: ServiceRef<CatalogApi>;
|
||||
|
||||
// @public
|
||||
export type DeferredEntity = {
|
||||
entity: Entity;
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "^0.1.2-next.0",
|
||||
"@backstage/catalog-client": "^1.0.5-next.0",
|
||||
"@backstage/catalog-model": "^1.1.0",
|
||||
"@backstage/errors": "1.1.0",
|
||||
"@backstage/types": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.15.1-next.1",
|
||||
"@backstage/cli": "^0.19.0-next.1"
|
||||
"@backstage/cli": "^0.19.0-next.1",
|
||||
"@backstage/backend-test-utils": "^0.1.28-next.1"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2022 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 { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
createBackendModule,
|
||||
createServiceFactory,
|
||||
discoveryServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { catalogServiceRef } from './catalogService';
|
||||
|
||||
describe('catalogServiceRef', () => {
|
||||
it('should return a catalogClient', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const mockDiscoveryFactory = createServiceFactory({
|
||||
service: discoveryServiceRef,
|
||||
deps: {},
|
||||
factory: async ({}) => {
|
||||
return async () => jest.fn() as unknown as PluginEndpointDiscovery;
|
||||
},
|
||||
});
|
||||
|
||||
const testModule = createBackendModule({
|
||||
moduleId: 'test.module',
|
||||
pluginId: 'test',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
catalog: catalogServiceRef,
|
||||
},
|
||||
async init({ catalog }) {
|
||||
expect(catalog).toBeInstanceOf(CatalogClient);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
services: [mockDiscoveryFactory],
|
||||
features: [testModule({})],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
discoveryServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
|
||||
|
||||
/**
|
||||
* The catalogService provides the catalog API.
|
||||
* @alpha
|
||||
*/
|
||||
export const catalogServiceRef = createServiceRef<CatalogApi>({
|
||||
id: 'catalog-client',
|
||||
defaultFactory: async service =>
|
||||
createServiceFactory({
|
||||
service,
|
||||
deps: {
|
||||
discoveryFactory: discoveryServiceRef,
|
||||
},
|
||||
factory: async ({ discoveryFactory }) => {
|
||||
const discoveryApi = await discoveryFactory('root');
|
||||
const catalogClient = new CatalogClient({ discoveryApi });
|
||||
return async _pluginId => {
|
||||
return catalogClient;
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -22,5 +22,6 @@
|
||||
|
||||
export type { CatalogProcessingExtensionPoint } from './extensions';
|
||||
export { catalogProcessingExtensionPoint } from './extensions';
|
||||
export { catalogServiceRef } from './catalogService';
|
||||
export * from './api';
|
||||
export * from './processing';
|
||||
|
||||
@@ -569,6 +569,19 @@ export class ScaffolderEntitiesProcessor implements CatalogProcessor {
|
||||
validateEntityKind(entity: Entity): Promise<boolean>;
|
||||
}
|
||||
|
||||
// @alpha
|
||||
export const scaffolderPlugin: (
|
||||
options: ScaffolderPluginOptions,
|
||||
) => BackendFeature;
|
||||
|
||||
// @alpha
|
||||
export type ScaffolderPluginOptions = {
|
||||
actions?: TemplateAction<any>[];
|
||||
taskWorkers?: number;
|
||||
taskBroker?: TaskBroker;
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type SerializedTask = {
|
||||
id: string;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
configServiceRef,
|
||||
createBackendPlugin,
|
||||
databaseServiceRef,
|
||||
loggerServiceRef,
|
||||
loggerToWinstonLogger,
|
||||
permissionsServiceRef,
|
||||
urlReaderServiceRef,
|
||||
httpRouterServiceRef,
|
||||
createExtensionPoint,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { catalogServiceRef } from '@backstage/plugin-catalog-node';
|
||||
import { TemplateFilter } from './lib';
|
||||
import { createBuiltinActions, TaskBroker, TemplateAction } from './scaffolder';
|
||||
import { createRouter } from './service/router';
|
||||
|
||||
/**
|
||||
* Catalog plugin options
|
||||
* @alpha
|
||||
*/
|
||||
export type ScaffolderPluginOptions = {
|
||||
actions?: TemplateAction<any>[];
|
||||
taskWorkers?: number;
|
||||
taskBroker?: TaskBroker;
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* TODO: MOVE to scaffolder-node.
|
||||
*/
|
||||
interface ScaffolderActionsExtensionPoint {
|
||||
addActions(...actions: TemplateAction<any>[]): void;
|
||||
}
|
||||
|
||||
class ScaffolderActionsExtensionPointImpl
|
||||
implements ScaffolderActionsExtensionPoint
|
||||
{
|
||||
#actions = new Array<TemplateAction<any>>();
|
||||
addActions(...actions: TemplateAction<any>[]): void {
|
||||
this.#actions.push(...actions);
|
||||
}
|
||||
get actions() {
|
||||
return this.#actions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* TODO: MOVE to scaffolder-node.
|
||||
*/
|
||||
export const scaffolderActionsExtensionPoint =
|
||||
createExtensionPoint<ScaffolderActionsExtensionPoint>({
|
||||
id: 'scaffolder.actions',
|
||||
});
|
||||
|
||||
/**
|
||||
* Catalog plugin
|
||||
* @alpha
|
||||
*/
|
||||
export const scaffolderPlugin = createBackendPlugin({
|
||||
id: 'scaffolder',
|
||||
register(env, options: ScaffolderPluginOptions) {
|
||||
const actionsExtensions = new ScaffolderActionsExtensionPointImpl();
|
||||
env.registerExtensionPoint(
|
||||
scaffolderActionsExtensionPoint,
|
||||
actionsExtensions,
|
||||
);
|
||||
|
||||
env.registerInit({
|
||||
deps: {
|
||||
logger: loggerServiceRef,
|
||||
config: configServiceRef,
|
||||
reader: urlReaderServiceRef,
|
||||
permissions: permissionsServiceRef,
|
||||
database: databaseServiceRef,
|
||||
httpRouter: httpRouterServiceRef,
|
||||
catalogClient: catalogServiceRef,
|
||||
},
|
||||
async init({
|
||||
logger,
|
||||
config,
|
||||
reader,
|
||||
database,
|
||||
httpRouter,
|
||||
catalogClient,
|
||||
}) {
|
||||
const { additionalTemplateFilters, taskBroker, taskWorkers } = options;
|
||||
const log = loggerToWinstonLogger(logger);
|
||||
|
||||
const actions = options.actions || [
|
||||
...actionsExtensions.actions,
|
||||
...createBuiltinActions({
|
||||
integrations: ScmIntegrations.fromConfig(config),
|
||||
catalogClient,
|
||||
reader,
|
||||
config,
|
||||
additionalTemplateFilters,
|
||||
}),
|
||||
];
|
||||
|
||||
const actionIds = actions.map(action => action.id).join(', ');
|
||||
log.info(
|
||||
`Starting scaffolder with the following actions enabled ${actionIds}`,
|
||||
);
|
||||
|
||||
const router = await createRouter({
|
||||
logger: log,
|
||||
config,
|
||||
database,
|
||||
catalogClient,
|
||||
reader,
|
||||
actions,
|
||||
taskBroker,
|
||||
taskWorkers,
|
||||
additionalTemplateFilters,
|
||||
});
|
||||
httpRouter.use(router);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -25,3 +25,5 @@ export * from './service/router';
|
||||
export * from './lib';
|
||||
export * from './processor';
|
||||
export * from './extension';
|
||||
export { scaffolderPlugin } from './ScaffolderPlugin';
|
||||
export type { ScaffolderPluginOptions } from './ScaffolderPlugin';
|
||||
|
||||
Reference in New Issue
Block a user