Skip to content

Commit c59af97

Browse files
committed
feat(backend): add service factories by default
This change explicitely adds the default service factories to the backend statically to prevent dynamic plugins from being able to override them by default. It's possible to override each statically added service factory via an environment variable derived from the service factory ID. So for example to add a "core.rootHttpService" service factory configuration from a dynamic plugin, set ENABLE_CORE_ROOTHTTPSERVICE_OVERRIDE to "true". This change also adds a logger to the backend main. Finally, a unit test has been added that checks the installed backend-defaults value for the defaultServiceFactories list against what this change adds to catch future regressions. Signed-off-by: Stan Lewis <[email protected]>
1 parent 2b0c74c commit c59af97

File tree

7 files changed

+193
-2
lines changed

7 files changed

+193
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Overriding Core Backend Service Configuration
2+
3+
## Overview
4+
5+
The Backstage backend platform consists of a number of core services that are well encapsulated. The configuration of these core services is normally done by directly customizing the backend source code and rebuilding. However the dynamic plugin functionality adds the ability for core service customization via installing it as a `BackendFeature`. The Developer Hub backend normally installs all of these default core services statically during initialization. Environment variables can configure the backend to avoid statically installing a given default core service, allowing for dynamic plugin installation.
6+
7+
## An Example
8+
9+
Some use cases may be easier solved at a lower level service than what's available in the Backstage backend plugin API. Adding a middleware function to handle all incoming requests can be done by installing a custom `configure` function for the root HTTP router backend service, which allows access to the underlying Express app.
10+
11+
```typescript
12+
// Create the BackendFeature
13+
export const customRootHttpServerFactory: BackendFeature =
14+
rootHttpRouterServiceFactory({
15+
configure: ({ app, routes, middleware, logger }) => {
16+
logger.info(
17+
'Using custom root HttpRouterServiceFactory configure function',
18+
);
19+
app.use(middleware.helmet());
20+
app.use(middleware.cors());
21+
app.use(middleware.compression());
22+
app.use(middleware.logging());
23+
// Add a the custom middleware function before all
24+
// of the route handlers
25+
app.use(addTestHeaderMiddleware({ logger }));
26+
app.use(routes);
27+
app.use(middleware.notFound());
28+
app.use(middleware.error());
29+
},
30+
});
31+
32+
// Export the BackendFeature as the default entrypoint
33+
export default customRootHttpServerFactory;
34+
```
35+
36+
This `BackendFeature` overrides the default HTTP router service factory. Because this is overriding the default implementation of a core service, the above example would need the `ENABLE_CORE_ROOTHTTPROUTER_OVERRIDE` environment variable set to `true` so that the Developer Hub does not install the default implementation automatically.
37+
38+
## Override Environment Variables
39+
40+
To allow a dynamic plugin to load a core service override, start the Developer Hub backend with the environment variable set that corresponds with the core service ID to be overridden. Here is a list of the available environment variables and core service IDs:
41+
42+
43+
- `ENABLE_CORE_AUTH_OVERRIDE` - allow overriding the `core.auth` service
44+
- `ENABLE_CORE_CACHE_OVERRIDE` - allow overriding the `core.cache` service
45+
- `ENABLE_CORE_ROOTCONFIG_OVERRIDE` - allow overriding the `core.rootConfig` service
46+
- `ENABLE_CORE_DATABASE_OVERRIDE` - allow overriding the `core.database` service
47+
- `ENABLE_CORE_DISCOVERY_OVERRIDE` - allow overriding the `core.discovery` service
48+
- `ENABLE_CORE_HTTPAUTH_OVERRIDE` - allow overriding the `core.httpAuth` service
49+
- `ENABLE_CORE_HTTPROUTER_OVERRIDE` - allow overriding the `core.httpRouter` service
50+
- `ENABLE_CORE_LIFECYCLE_OVERRIDE` - allow overriding the `core.lifecycle` service
51+
- `ENABLE_CORE_LOGGER_OVERRIDE` - allow overriding the `core.logger` service
52+
- `ENABLE_CORE_PERMISSIONS_OVERRIDE` - allow overriding the `core.permissions` service
53+
- `ENABLE_CORE_ROOTHEALTH_OVERRIDE` - allow overriding the `core.rootHealth` service
54+
- `ENABLE_CORE_ROOTHTTPROUTER_OVERRIDE` - allow overriding the `core.rootHttpRouter` service
55+
- `ENABLE_CORE_ROOTLIFECYCLE_OVERRIDE` - allow overriding the `core.rootLifecycle` service
56+
- `ENABLE_CORE_SCHEDULER_OVERRIDE` - allow overriding the `core.scheduler` service
57+
- `ENABLE_CORE_USERINFO_OVERRIDE` - allow overriding the `core.userInfo` service
58+
- `ENABLE_CORE_URLREADER_OVERRIDE` - allow overriding the `core.urlReader` service
59+
- `ENABLE_EVENTS_SERVICE_OVERRIDE` - allow overriding the `events.service` service

packages/backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@backstage/plugin-catalog-backend-module-openapi": "0.2.3",
4141
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.1",
4242
"@backstage/plugin-events-backend": "0.3.15",
43+
"@backstage/plugin-events-node": "^0.4.4",
4344
"@backstage/plugin-proxy-backend": "0.5.7",
4445
"@backstage/plugin-scaffolder-backend": "1.26.2",
4546
"@backstage/plugin-search-backend": "1.6.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ServiceFactory } from '@backstage/backend-plugin-api';
2+
3+
import { defaultServiceFactories } from './defaultServiceFactories';
4+
5+
// explicitly check this against the module inside the installed package
6+
const {
7+
defaultServiceFactories: upstreamDefaultServiceFactories,
8+
// eslint-disable-next-line
9+
} = require('../../../node_modules/@backstage/backend-defaults/dist/CreateBackend.cjs.js');
10+
11+
function findDifference(a1: string[], a2: string[]) {
12+
const set = new Set(a2);
13+
return a1.filter(i => !set.has(i));
14+
}
15+
16+
function findSymmetricDifference(a1: string[], a2: string[]) {
17+
return [...new Set([...findDifference(a1, a2), ...findDifference(a2, a1)])];
18+
}
19+
20+
/**
21+
* Validate that the installed backend-defaults package contains the expected
22+
* list of default service factories. A failure in this test indicates that
23+
* either the export was removed, the list was moved, or the list in
24+
* "defaultServiceFactories" should be updated.
25+
*/
26+
describe('Default service factory list comparison', () => {
27+
it('Should produce an expected difference of service factories as compared to the upstream implementation', () => {
28+
const upstreamServiceFactoryIds = upstreamDefaultServiceFactories.map(
29+
(serviceFactory: ServiceFactory) => serviceFactory.service.id,
30+
);
31+
const serviceFactoryIds = defaultServiceFactories.map(
32+
(serviceFactory: ServiceFactory) => serviceFactory.service.id,
33+
);
34+
expect(
35+
findSymmetricDifference(upstreamServiceFactoryIds, serviceFactoryIds),
36+
).toEqual(['core.rootLogger']);
37+
});
38+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { authServiceFactory } from '@backstage/backend-defaults/auth';
2+
import { cacheServiceFactory } from '@backstage/backend-defaults/cache';
3+
import { databaseServiceFactory } from '@backstage/backend-defaults/database';
4+
import { discoveryServiceFactory } from '@backstage/backend-defaults/discovery';
5+
import { httpAuthServiceFactory } from '@backstage/backend-defaults/httpAuth';
6+
import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter';
7+
import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle';
8+
import { loggerServiceFactory } from '@backstage/backend-defaults/logger';
9+
import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions';
10+
import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig';
11+
import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth';
12+
import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter';
13+
import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle';
14+
import { WinstonLogger } from '@backstage/backend-defaults/rootLogger';
15+
import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler';
16+
import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader';
17+
import { userInfoServiceFactory } from '@backstage/backend-defaults/userInfo';
18+
import { eventsServiceFactory } from '@backstage/plugin-events-node';
19+
20+
/**
21+
* Service factories that are added to the backend statically by default. This
22+
* should be kept up to date with the upstream package code, which is currently
23+
* not exported.
24+
*/
25+
export const defaultServiceFactories = [
26+
authServiceFactory,
27+
cacheServiceFactory,
28+
rootConfigServiceFactory,
29+
databaseServiceFactory,
30+
discoveryServiceFactory,
31+
httpAuthServiceFactory,
32+
httpRouterServiceFactory,
33+
lifecycleServiceFactory,
34+
loggerServiceFactory,
35+
permissionsServiceFactory,
36+
rootHealthServiceFactory,
37+
rootHttpRouterServiceFactory,
38+
rootLifecycleServiceFactory,
39+
// rootLoggerServiceFactory,
40+
schedulerServiceFactory,
41+
userInfoServiceFactory,
42+
urlReaderServiceFactory,
43+
eventsServiceFactory,
44+
];
45+
46+
export const getDefaultServiceFactories = ({
47+
logger,
48+
}: {
49+
logger: WinstonLogger;
50+
}) => {
51+
return defaultServiceFactories.filter(serviceFactory => {
52+
const envName = `ENABLE_${serviceFactory.service.id.toLocaleUpperCase().replace('.', '_')}_OVERRIDE`;
53+
if ((process.env[envName] || '').toLocaleLowerCase() !== 'true') {
54+
logger.debug(
55+
`Adding service factory "${serviceFactory.service.id}", to override set "${envName}" to "true"`,
56+
);
57+
return true;
58+
}
59+
logger.warn(
60+
`Allowing override for service factory "${serviceFactory.service.id}"`,
61+
);
62+
return false;
63+
});
64+
};

packages/backend/src/index.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,31 @@ import { PackageRoles } from '@backstage/cli-node';
55
import * as path from 'path';
66

77
import { configureCorporateProxyAgent } from './corporate-proxy';
8+
import { getDefaultServiceFactories } from './defaultServiceFactories';
89
import { CommonJSModuleLoader } from './loader';
9-
import { transports } from './logger';
10+
import { createStaticLogger, transports } from './logger';
1011
import {
1112
healthCheckPlugin,
1213
pluginIDProviderService,
1314
rbacDynamicPluginsProvider,
1415
} from './modules';
1516

17+
// Create a logger to cover logging static initialization tasks
18+
const staticLogger = createStaticLogger({ service: 'developer-hub-init' });
19+
staticLogger.info('Starting Developer Hub backend');
20+
1621
// RHIDP-2217: adds support for corporate proxy
1722
configureCorporateProxyAgent();
1823

1924
const backend = createBackend();
2025

26+
const defaultServiceFactories = getDefaultServiceFactories({
27+
logger: staticLogger,
28+
});
29+
defaultServiceFactories.forEach(serviceFactory => {
30+
backend.add(serviceFactory);
31+
});
32+
2133
backend.add(
2234
dynamicPluginsFeatureLoader({
2335
schemaLocator(pluginPackage) {

packages/backend/src/logger/customLogger.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { WinstonLogger } from '@backstage/backend-defaults/rootLogger';
12
import type { Config } from '@backstage/config';
23

34
import * as winston from 'winston';
@@ -73,3 +74,18 @@ export const transports = {
7374
];
7475
},
7576
};
77+
78+
export const createStaticLogger = ({ service }: { service: string }) => {
79+
const logger = WinstonLogger.create({
80+
meta: {
81+
service,
82+
},
83+
level: process.env.LOG_LEVEL || 'info',
84+
format:
85+
process.env.NODE_ENV === 'production'
86+
? defaultFormat
87+
: WinstonLogger.colorFormat(),
88+
transports: transports.log,
89+
});
90+
return logger;
91+
};

yarn.lock

+2-1
Original file line numberDiff line numberDiff line change
@@ -6764,7 +6764,7 @@ __metadata:
67646764
languageName: node
67656765
linkType: hard
67666766

6767-
"@backstage/plugin-events-node@npm:^0.4.1":
6767+
"@backstage/plugin-events-node@npm:^0.4.1, @backstage/plugin-events-node@npm:^0.4.4":
67686768
version: 0.4.4
67696769
resolution: "@backstage/plugin-events-node@npm:0.4.4"
67706770
dependencies:
@@ -22368,6 +22368,7 @@ __metadata:
2236822368
"@backstage/plugin-catalog-backend-module-openapi": 0.2.3
2236922369
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": 0.2.1
2237022370
"@backstage/plugin-events-backend": 0.3.15
22371+
"@backstage/plugin-events-node": ^0.4.4
2237122372
"@backstage/plugin-proxy-backend": 0.5.7
2237222373
"@backstage/plugin-scaffolder-backend": 1.26.2
2237322374
"@backstage/plugin-search-backend": 1.6.1

0 commit comments

Comments
 (0)