Initial work on the backend/next setup for the catalog

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: blam <ben@blam.sh>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2022-06-15 14:27:23 +02:00
committed by Patrik Oldsberg
parent 4ad817262c
commit 39b24c78fe
19 changed files with 697 additions and 34 deletions
+15
View File
@@ -0,0 +1,15 @@
## API Report File for "@backstage/backend-app-api"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AnyServiceFactory } from '@backstage/backend-plugin-api';
import { BackendRegistrable } from '@backstage/backend-plugin-api';
// Warning: (ae-forgotten-export) The symbol "CreateBackendOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Backend" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "createBackend" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function createBackend(options?: CreateBackendOptions): Backend;
```
+3 -1
View File
@@ -33,7 +33,9 @@
"clean": "backstage-cli package clean",
"start": "backstage-cli package start"
},
"dependencies": {},
"dependencies": {
"@backstage/backend-plugin-api": "^0.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.17.2-next.0"
},
+1 -1
View File
@@ -20,4 +20,4 @@
* @packageDocumentation
*/
export {};
export { createBackend } from './wiring/types';
@@ -0,0 +1,153 @@
/*
* 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 { BackendRegistrable, ServiceRef } from '@backstage/backend-plugin-api';
import { BackendRegisterInit, ApiHolder } from './types';
export class BackendInitializer {
#started = false;
#extensions = new Map<BackendRegistrable, unknown>();
// #stops = [];
#registerInits = new Array<BackendRegisterInit>();
#apis = new Map<ServiceRef<unknown>, unknown>();
#apiHolder: ApiHolder;
constructor(apiHolder: ApiHolder) {
this.#apiHolder = apiHolder;
}
async #getInitDeps(
deps: { [name: string]: ServiceRef<unknown> },
pluginId: string,
) {
return Object.fromEntries(
await Promise.all(
Object.entries(deps).map(async ([name, apiRef]) => [
name,
this.#apis.get(apiRef) ||
(await this.#apiHolder.get(apiRef)!(pluginId)),
]),
),
);
}
add<TOptions>(extension: BackendRegistrable, options?: TOptions) {
if (this.#started) {
throw new Error(
'extension can not be added after the backend has started',
);
}
this.#extensions.set(extension, options);
}
async start(): Promise<void> {
console.log(`Starting backend`);
if (this.#started) {
throw new Error('Backend has already started');
}
this.#started = true;
for (const [extension] of this.#extensions) {
const provides = new Set<ServiceRef<unknown>>();
let registerInit: BackendRegisterInit | undefined = undefined;
console.log('Registering', extension.id);
extension.register({
registerExtensionPoint: (api, impl) => {
if (registerInit) {
throw new Error('registerInitApi called after registerInit');
}
if (this.#apis.has(api)) {
throw new Error(`API ${api.id} already registered`);
}
this.#apis.set(api, impl);
provides.add(api);
},
registerInit: registerOptions => {
if (registerInit) {
throw new Error('registerInit must only be called once');
}
registerInit = {
id: extension.id,
provides,
consumes: new Set(Object.values(registerOptions.deps)),
deps: registerOptions.deps,
init: registerOptions.init as BackendRegisterInit['init'],
};
},
});
if (!registerInit) {
throw new Error(
`registerInit was not called by register in ${extension.id}`,
);
}
this.#registerInits.push(registerInit);
}
this.validateSetup();
const orderedRegisterResults = this.#resolveInitOrder(this.#registerInits);
for (const registerInit of orderedRegisterResults) {
// TODO: DI
const deps = await this.#getInitDeps(registerInit.deps, registerInit.id);
await registerInit.init(deps);
// Maybe return stop? or lifecycle API
// this.#stops.push();
}
}
// async stop(): Promise<void> {
// for (const stop of this.#stops) {
// await stop.stop();
// }
// }
private validateSetup() {}
#resolveInitOrder(registerInits: Array<BackendRegisterInit>) {
let registerInitsToOrder = registerInits.slice();
const orderedRegisterInits = new Array<BackendRegisterInit>();
// TODO: Validate duplicates
while (registerInitsToOrder.length > 0) {
const toRemove = new Set<unknown>();
for (const registerInit of registerInitsToOrder) {
const unInitializedDependents = Array.from(
registerInit.provides,
// eslint-disable-next-line no-loop-func
).filter(r =>
registerInitsToOrder.some(
init => init !== registerInit && init.consumes.has(r),
),
);
if (unInitializedDependents.length === 0) {
orderedRegisterInits.push(registerInit);
toRemove.add(registerInit);
}
}
registerInitsToOrder = registerInitsToOrder.filter(r => !toRemove.has(r));
}
return orderedRegisterInits;
}
}
@@ -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 {
BackendRegistrable,
AnyServiceFactory,
} from '@backstage/backend-plugin-api';
import { BackendInitializer } from './BackendInitializer';
import { CoreApiRegistry } from './CoreApiRegistry';
import { Backend } from './types';
export class BackstageBackend implements Backend {
#coreApis: CoreApiRegistry;
#initializer: BackendInitializer;
constructor(apiFactories: AnyServiceFactory[]) {
this.#coreApis = new CoreApiRegistry(apiFactories);
this.#initializer = new BackendInitializer(this.#coreApis);
}
add(extension: BackendRegistrable): void {
this.#initializer.add(extension);
}
async start(): Promise<void> {
await this.#initializer.start();
}
// async stop(): Promise<void> {
// await this.#initializer.stop();
// }
}
@@ -0,0 +1,65 @@
/*
* 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 {
AnyServiceFactory,
ServiceRef,
FactoryFunc,
} from '@backstage/backend-plugin-api';
export class CoreApiRegistry {
readonly #implementations: Map<string, Map<string, unknown>>;
readonly #factories: Map<string, AnyServiceFactory>;
constructor(factories: AnyServiceFactory[]) {
this.#factories = new Map(factories.map(f => [f.service.id, f]));
this.#implementations = new Map();
}
get<T>(ref: ServiceRef<T>): FactoryFunc<T> | undefined {
const factory = this.#factories.get(ref.id);
if (!factory) {
return undefined;
}
return async (pluginId: string): Promise<T> => {
if (this.#implementations.has(ref.id)) {
if (this.#implementations.get(ref.id)!.has(pluginId)) {
return this.#implementations.get(ref.id)!.get(pluginId) as T;
}
this.#implementations.set(ref.id, new Map<string, unknown>());
} else {
this.#implementations.set(ref.id, new Map());
}
const factoryDeps = Object.fromEntries(
Object.entries(factory.deps).map(([name, apiRef]) => [
name,
this.get(apiRef)!, // TODO: throw
]),
);
const factoryFunc = await factory.factory(factoryDeps);
const implementation = await factoryFunc(pluginId);
this.#implementations.set(
ref.id,
this.#implementations.get(ref.id)!.set(pluginId, implementation),
);
return implementation as T;
};
}
}
+23 -2
View File
@@ -14,14 +14,35 @@
* limitations under the License.
*/
export interface BackendApp {
add(registrable: BackendRegistrable): void;
import {
BackendRegistrable,
AnyServiceFactory,
ServiceRef,
FactoryFunc,
} from '@backstage/backend-plugin-api';
import { BackstageBackend } from './BackstageBackend';
export interface Backend {
add(extension: BackendRegistrable): void;
start(): Promise<void>;
}
export interface BackendRegisterInit {
id: string;
consumes: Set<ServiceRef<unknown>>;
provides: Set<ServiceRef<unknown>>;
deps: { [name: string]: ServiceRef<unknown> };
init: (deps: { [name: string]: unknown }) => Promise<void>;
}
interface CreateBackendOptions {
apis: AnyServiceFactory[];
}
export type ApiHolder = {
get<T>(api: ServiceRef<T>): FactoryFunc<T> | undefined;
};
export function createBackend(options?: CreateBackendOptions): Backend {
return new BackstageBackend(options?.apis ?? []);
}
+3 -1
View File
@@ -33,7 +33,9 @@
"clean": "backstage-cli package clean",
"start": "backstage-cli package start"
},
"dependencies": {},
"dependencies": {
"@backstage/config": "^1.0.1"
},
"devDependencies": {
"@backstage/cli": "^0.17.2-next.0"
},
+2
View File
@@ -21,3 +21,5 @@
*/
export * from './wiring';
export * from './services/system/types';
export * from './services/definitions';
@@ -0,0 +1,22 @@
/*
* 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 { Config } from '@backstage/config';
import { createServiceRef } from '../system/types';
export const configApiRef = createServiceRef<Config>({
id: 'core.config',
});
@@ -0,0 +1,25 @@
/*
* 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 { createServiceRef } from '../system/types';
export interface HttpRouterApi {
get(path: string): void;
}
export const httpRouterApiRef = createServiceRef<HttpRouterApi>({
id: 'core.httpRouter',
});
@@ -14,31 +14,6 @@
* limitations under the License.
*/
interface Logger {
log(message: string): void;
child(fields: { [name: string]: string }): Logger;
}
interface ConfigApi {
getString(key: string): string;
}
interface HttpRouterApi {
get(path: string): void;
}
export const loggerApiRef = createServiceRef<Logger>({
id: 'core.logger',
});
export const configApiRef = createServiceRef<ConfigApi>({
id: 'core.config',
});
export const httpRouterApiRef = createServiceRef<HttpRouterApi>({
id: 'core.apiRouter',
});
// export type PluginEnvironment = {
// logger: Logger;
// cache: PluginCacheManager;
@@ -50,3 +25,9 @@ export const httpRouterApiRef = createServiceRef<HttpRouterApi>({
// permissions: PermissionEvaluator | PermissionAuthorizer;
// scheduler: PluginTaskScheduler;
// };
export { configApiRef } from './configApiRef';
export { httpRouterApiRef } from './httpRouterApiRef';
export type { HttpRouterApi } from './httpRouterApiRef';
export { loggerApiRef } from './loggerApiRef';
export type { Logger } from './loggerApiRef';
@@ -0,0 +1,26 @@
/*
* 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 { createServiceRef } from '../system/types';
export interface Logger {
log(message: string): void;
child(fields: { [name: string]: string }): Logger;
}
export const loggerApiRef = createServiceRef<Logger>({
id: 'core.logger',
});
@@ -14,17 +14,56 @@
* limitations under the License.
*/
/**
* TODO
*
* @public
*/
export interface ServiceRef<T> {
id: string;
/**
* Utility for getting the type of the service, using `typeof serviceRef.T`.
* Attempting to actually read this value will result in an exception.
*/
T: T;
toString(): string;
$$ref: 'service';
}
type TypesToServiceRef<T> = { [key in keyof T]: ServiceRef<T[key]> };
type DepsToDepFactories<T> = {
[key in keyof T]: (pluginId: string) => Promise<T[key]>;
};
export type FactoryFunc<Impl> = (pluginId: string) => Promise<Impl>;
export type ServiceFactory<
TApi,
TImpl extends TApi,
TDeps extends { [name in string]: unknown },
> = {
service: ServiceRef<TApi>;
deps: TypesToServiceRef<TDeps>;
factory(deps: DepsToDepFactories<TDeps>): Promise<FactoryFunc<TImpl>>;
};
export type AnyServiceFactory = ServiceFactory<
unknown,
unknown,
{ [key in string]: unknown }
>;
export function createServiceRef<T>(options: { id: string }): ServiceRef<T> {
return {
id: options.id,
get T(): T {
throw Error('NO T');
throw new Error(`tried to read ServiceRef.T of ${this}`);
},
toString() {
return `serviceRef{${options.id}}`;
},
$$ref: 'service', // TODO: declare
};
@@ -14,4 +14,5 @@
* limitations under the License.
*/
export { BackendRegistrable } from './types';
export type { BackendRegistrable } from './types';
export * from './types';
@@ -14,9 +14,25 @@
* limitations under the License.
*/
import { ServiceRef } from '../services/system/types';
/**
* TODO
*
* @public
*/
export interface ExtensionPoint<T> {
id: string;
/**
* Utility for getting the type of the extension point, using `typeof
* extensionPoint.T`. Attempting to actually read this value will result in an
* exception.
*/
T: T;
toString(): string;
$$ref: 'extension-point';
}
@@ -26,7 +42,10 @@ export function createExtensionPoint<T>(options: {
return {
id: options.id,
get T(): T {
throw Error('NO T');
throw new Error(`tried to read ExtensionPoint.T of ${this}`);
},
toString() {
return `extensionPoint{${options.id}}`;
},
$$ref: 'extension-point', // TODO: declare
};
+2
View File
@@ -26,6 +26,8 @@
"build-image": "docker build ../.. -f Dockerfile --tag example-backend"
},
"dependencies": {
"@backstage/backend-plugin-api": "^0.0.0",
"@backstage/backend-app-api": "^0.0.0",
"@backstage/backend-common": "^0.14.1-next.2",
"@backstage/backend-tasks": "^0.3.3-next.2",
"@backstage/catalog-client": "^1.0.4-next.1",
+100
View File
@@ -0,0 +1,100 @@
/*
* 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 { createBackend } from '@backstage/backend-app-api';
import {
createBackendModule,
createBackendPlugin,
createServiceRef,
} from '@backstage/backend-plugin-api';
// import { catalogPlugin } from '@backstage/plugin-catalog-backend';
// import { scaffolderPlugin } from '@backstage/plugin-scaffolder-backend';
interface CatalogProcessor {
process(): void;
}
interface CatalogProcessingInitApi {
addProcessor(processor: CatalogProcessor): void;
}
export const catalogProcessingInitApiRef =
createServiceRef<CatalogProcessingInitApi>({
id: 'catalog.processing',
});
class CatalogExtensionPointImpl implements CatalogProcessingInitApi {
#processors = new Array<CatalogProcessor>();
addProcessor(processor: CatalogProcessor): void {
this.#processors.push(processor);
}
get processors() {
return this.#processors;
}
}
export const catalogPlugin = createBackendPlugin({
id: 'catalog',
register(env) {
const processingExtensions = new CatalogExtensionPointImpl();
// plugins depending on this API will be initialized before this plugins init method is executed.
env.registerExtensionPoint(
catalogProcessingInitApiRef,
processingExtensions,
);
env.registerInit({
deps: {
// logger: loggerApiRef,
},
async init() {
console.log('I HAZ', processingExtensions.processors[0].process());
console.log('I AM le CATALOG!');
// logger.log('HELLO!');
},
});
},
});
export const scaffolderCatalogExtension = createBackendModule({
moduleId: 'boop',
pluginId: 'catalog',
register(env) {
env.registerInit({
deps: {
catalogProcessingInitApi: catalogProcessingInitApiRef,
},
async init({ catalogProcessingInitApi }) {
catalogProcessingInitApi.addProcessor({
process() {
console.log('Running scaffolder processor');
},
});
},
});
},
});
const backend = createBackend({
apis: [],
});
// backend.add(scaffolderPlugin());
backend.add(catalogPlugin({}));
backend.add(scaffolderCatalogExtension({}));
backend.start();