core-api,app: switch to auto DI with ApiResolver and ApiFactories

This commit is contained in:
Patrik Oldsberg
2020-09-04 16:08:04 +02:00
parent 4e390d6e9d
commit c8faa6a73d
4 changed files with 235 additions and 184 deletions
+160 -133
View File
@@ -15,11 +15,9 @@
*/
import {
ApiRegistry,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ConfigApi,
ErrorApiForwarder,
ErrorAlerter,
featureFlagsApiRef,
@@ -44,6 +42,8 @@ import {
microsoftAuthApiRef,
storageApiRef,
WebStorage,
createApiFactory,
configApiRef,
} from '@backstage/core';
import {
@@ -82,135 +82,162 @@ import {
githubPullRequestsApiRef,
} from '@roadiehq/backstage-plugin-github-pull-requests';
export const apis = (config: ConfigApi) => {
// eslint-disable-next-line no-console
console.log(`Creating APIs for ${config.getString('app.title')}`);
const backendUrl = config.getString('backend.baseUrl');
const techdocsUrl = config.getString('techdocs.storageUrl');
const builder = ApiRegistry.builder();
const discoveryApi = builder.add(
discoveryApiRef,
UrlPatternDiscovery.compile(`${backendUrl}/{{ pluginId }}`),
);
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
const errorApi = builder.add(
errorApiRef,
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
);
builder.add(storageApiRef, WebStorage.create({ errorApi }));
builder.add(GCPApiRef, new GCPClient());
builder.add(
circleCIApiRef,
new CircleCIApi(`${backendUrl}/proxy/circleci/api`),
);
builder.add(jenkinsApiRef, new JenkinsApi(`${backendUrl}/proxy/jenkins/api`));
builder.add(githubActionsApiRef, new GithubActionsClient());
builder.add(featureFlagsApiRef, new FeatureFlags());
builder.add(lighthouseApiRef, LighthouseRestApi.fromConfig(config));
builder.add(travisCIApiRef, new TravisCIApi());
builder.add(githubPullRequestsApiRef, new GithubPullRequestsClient());
const oauthRequestApi = builder.add(
oauthRequestApiRef,
new OAuthRequestManager(),
);
builder.add(
googleAuthApiRef,
GoogleAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
microsoftAuthApiRef,
MicrosoftAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
const githubAuthApi = builder.add(
githubAuthApiRef,
GithubAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
oktaAuthApiRef,
OktaAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
auth0AuthApiRef,
Auth0Auth.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(
oauth2ApiRef,
OAuth2.create({
discoveryApi,
oauthRequestApi,
}),
);
builder.add(catalogApiRef, new CatalogClient({ discoveryApi }));
builder.add(scaffolderApiRef, new ScaffolderApi({ discoveryApi }));
builder.add(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008'));
builder.add(
graphQlBrowseApiRef,
GraphQLEndpoints.from([
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com/api/graphql',
export const apis = [
createApiFactory(alertApiRef, new AlertApiForwarder()),
createApiFactory({
implements: errorApiRef,
deps: { alertApi: alertApiRef },
factory: ({ alertApi }) =>
new ErrorAlerter(alertApi, new ErrorApiForwarder()),
}),
createApiFactory({
implements: storageApiRef,
deps: { errorApi: errorApiRef },
factory: ({ errorApi }) => WebStorage.create({ errorApi }),
}),
createApiFactory({
implements: discoveryApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
UrlPatternDiscovery.compile(
`${configApi.getString('backend.baseUrl')}/{{ pluginId }}`,
),
}),
createApiFactory(GCPApiRef, new GCPClient()),
createApiFactory({
implements: circleCIApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
new CircleCIApi(
`${configApi.getString('backend.baseUrl')}/proxy/circleci/api`,
),
}),
createApiFactory({
implements: jenkinsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
new JenkinsApi(
`${configApi.getString('backend.baseUrl')}/proxy/jenkins/api`,
),
}),
createApiFactory(githubActionsApiRef, new GithubActionsClient()),
createApiFactory(featureFlagsApiRef, new FeatureFlags()),
createApiFactory({
implements: lighthouseApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => LighthouseRestApi.fromConfig(configApi),
}),
createApiFactory(travisCIApiRef, new TravisCIApi()),
createApiFactory(githubPullRequestsApiRef, new GithubPullRequestsClient()),
createApiFactory(oauthRequestApiRef, new OAuthRequestManager()),
createApiFactory({
implements: techRadarApiRef,
deps: {},
factory: () => new TechRadar({ width: 1500, height: 800 }),
}),
createApiFactory({
implements: googleAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
GoogleAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: microsoftAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
MicrosoftAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: githubAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
GithubAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: oktaAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
OktaAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: gitlabAuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
GitlabAuth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: auth0AuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
Auth0Auth.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: oauth2ApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
},
factory: ({ discoveryApi, oauthRequestApi }) =>
OAuth2.create({ discoveryApi, oauthRequestApi }),
}),
createApiFactory({
implements: catalogApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new CatalogClient({ discoveryApi }),
}),
createApiFactory({
implements: scaffolderApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new ScaffolderApi({ discoveryApi }),
}),
createApiFactory(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')),
createApiFactory({
implements: graphQlBrowseApiRef,
deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef },
factory: ({ errorApi, githubAuthApi }) =>
GraphQLEndpoints.from([
GraphQLEndpoints.create({
id: 'gitlab',
title: 'GitLab',
url: 'https://gitlab.com/api/graphql',
}),
GraphQLEndpoints.github({
id: 'github',
title: 'GitHub',
errorApi,
githubAuthApi,
}),
]),
}),
createApiFactory({
implements: rollbarApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }),
}),
createApiFactory({
implements: techdocsStorageApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) =>
new TechDocsStorageApi({
apiOrigin: configApi.getString('techdocs.storageUrl'),
}),
GraphQLEndpoints.github({
id: 'github',
title: 'GitHub',
errorApi,
githubAuthApi,
}),
]),
);
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
builder.add(
techdocsStorageApiRef,
new TechDocsStorageApi({
apiOrigin: techdocsUrl,
}),
);
return builder.build();
};
}),
];
+68 -35
View File
@@ -26,7 +26,6 @@ import {
BackstageApp,
AppComponents,
AppConfigLoader,
Apis,
SignInResult,
SignInPageProps,
} from './types';
@@ -42,7 +41,6 @@ import { AppThemeProvider } from './AppThemeProvider';
import { IconComponent, SystemIcons, SystemIconKey } from '../icons';
import {
ApiHolder,
ApiProvider,
ApiRegistry,
AppTheme,
@@ -51,18 +49,22 @@ import {
configApiRef,
ConfigReader,
useApi,
AnyApiFactory,
ApiHolder,
} from '../apis';
import { ApiAggregator } from '../apis/ApiAggregator';
import { useAsync } from 'react-use';
import { AppIdentity } from './AppIdentity';
import { ApiFactoryRegistry } from '../apis/ApiFactoryRegistry';
import { ApiResolver } from '../apis/ApiResolver';
type FullAppOptions = {
apis: Apis;
apis: Iterable<AnyApiFactory>;
icons: SystemIcons;
plugins: BackstagePlugin[];
components: AppComponents;
themes: AppTheme[];
configLoader?: AppConfigLoader;
defaultApiFactories: Iterable<AnyApiFactory>;
};
function useConfigLoader(
@@ -101,31 +103,27 @@ function useConfigLoader(
}
export class PrivateAppImpl implements BackstageApp {
private apis?: ApiHolder = undefined;
private apiHolder?: ApiHolder;
private configApi?: ConfigApi;
private readonly apis: Iterable<AnyApiFactory>;
private readonly icons: SystemIcons;
private readonly plugins: BackstagePlugin[];
private readonly components: AppComponents;
private readonly themes: AppTheme[];
private readonly configLoader?: AppConfigLoader;
private readonly defaultApiFactories: Iterable<AnyApiFactory>;
private readonly identityApi = new AppIdentity();
private apisOrFactory: Apis;
constructor(options: FullAppOptions) {
this.apisOrFactory = options.apis;
this.apis = options.apis;
this.icons = options.icons;
this.plugins = options.plugins;
this.components = options.components;
this.themes = options.themes;
this.configLoader = options.configLoader;
}
getApis(): ApiHolder {
if (!this.apis) {
throw new Error('Tried to access APIs before app was loaded');
}
return this.apis;
this.defaultApiFactories = options.defaultApiFactories;
}
getPlugins(): BackstagePlugin[] {
@@ -186,9 +184,9 @@ export class PrivateAppImpl implements BackstageApp {
}
}
const FeatureFlags = this.apis && this.apis.get(featureFlagsApiRef);
if (FeatureFlags) {
FeatureFlags.registeredFeatureFlags = registeredFeatureFlags;
const featureFlags = this.getApiHolder().get(featureFlagsApiRef);
if (featureFlags) {
featureFlags.registeredFeatureFlags = registeredFeatureFlags;
}
routes.push(<Route path="/*" element={<NotFoundErrorPage />} />);
@@ -210,28 +208,14 @@ export class PrivateAppImpl implements BackstageApp {
);
if ('node' in loadedConfig) {
// Loading or error
return loadedConfig.node;
}
const configApi = loadedConfig.api;
const appApis = ApiRegistry.from([
[appThemeApiRef, appThemeApi],
[configApiRef, configApi],
[identityApiRef, this.identityApi],
]);
if (!this.apis) {
if ('get' in this.apisOrFactory) {
this.apis = this.apisOrFactory;
} else {
this.apis = this.apisOrFactory(configApi);
}
}
const apis = new ApiAggregator(this.apis, appApis);
this.configApi = loadedConfig.api;
return (
<ApiProvider apis={apis}>
<ApiProvider apis={this.getApiHolder()}>
<AppContextProvider app={this}>
<AppThemeProvider>{children}</AppThemeProvider>
</AppContextProvider>
@@ -306,6 +290,55 @@ export class PrivateAppImpl implements BackstageApp {
return AppRouter;
}
private getApiHolder(): ApiHolder {
if (this.apiHolder) {
return this.apiHolder;
}
const registry = new ApiFactoryRegistry();
registry.register('static', {
implements: appThemeApiRef,
deps: {},
factory: () => AppThemeSelector.createWithStorage(this.themes),
});
registry.register('static', {
implements: configApiRef,
deps: {},
factory: () => {
if (!this.configApi) {
throw new Error(
'Tried to access config API before config was loaded',
);
}
return this.configApi;
},
});
registry.register('static', {
implements: identityApiRef,
deps: {},
factory: () => this.identityApi,
});
for (const factory of this.defaultApiFactories) {
registry.register('default', factory);
}
for (const factory of this.apis) {
if (!registry.register('app', factory)) {
throw new Error(
`Duplicate or forbidden API factory for ${factory.implements} in app`,
);
}
}
ApiResolver.validateFactories(registry, registry.getAllApis());
this.apiHolder = new ApiResolver(registry);
return this.apiHolder;
}
verify() {
const pluginIds = new Set<string>();
+5 -14
View File
@@ -17,8 +17,8 @@
import { ComponentType } from 'react';
import { IconComponent, SystemIconKey, SystemIcons } from '../icons';
import { BackstagePlugin } from '../plugin';
import { ApiHolder } from '../apis';
import { AppTheme, ConfigApi, ProfileInfo } from '../apis/definitions';
import { AnyApiFactory } from '../apis';
import { AppTheme, ProfileInfo } from '../apis/definitions';
import { AppConfig } from '@backstage/config';
export type BootErrorPageProps = {
@@ -77,16 +77,12 @@ export type AppComponents = {
*/
export type AppConfigLoader = () => Promise<AppConfig[]>;
// TODO(Rugvip): Temporary workaround for accessing config when instantiating APIs, we might want to do this differently
export type Apis = ApiHolder | ((config: ConfigApi) => ApiHolder);
export type AppOptions = {
/**
* A holder of all APIs available in the app.
*
* Use for example ApiRegistry or ApiTestRegistry.
* A collection of ApiFactories to register in the application to either
* add add new ones, or override factories provided by default or by plugins.
*/
apis?: Apis;
apis?: Iterable<AnyApiFactory>;
/**
* Supply icons to override the default ones.
@@ -138,11 +134,6 @@ export type AppOptions = {
};
export type BackstageApp = {
/**
* Get the holder for all APIs available in the app.
*/
getApis(): ApiHolder;
/**
* Returns all plugins registered for the app.
*/
+2 -2
View File
@@ -17,7 +17,6 @@
import React, { FC } from 'react';
import privateExports, {
AppOptions,
ApiRegistry,
defaultSystemIcons,
BootErrorPageProps,
AppConfigLoader,
@@ -94,7 +93,7 @@ export function createApp(options?: AppOptions) {
);
};
const apis = options?.apis ?? ApiRegistry.from([]);
const apis = options?.apis ?? [];
const icons = { ...defaultSystemIcons, ...options?.icons };
const plugins = options?.plugins ?? [];
const components = {
@@ -127,6 +126,7 @@ export function createApp(options?: AppOptions) {
components,
themes,
configLoader,
defaultApiFactories: [],
});
app.verify();