app-backend,cli: add support for injecting full config based on serialized schema

This commit is contained in:
Patrik Oldsberg
2020-11-09 10:28:11 +01:00
parent 2e4de99338
commit 7f95e40fbb
8 changed files with 79 additions and 40 deletions
+1
View File
@@ -22,6 +22,7 @@
"dependencies": {
"@backstage/backend-common": "^0.2.0",
"@backstage/config-loader": "^0.2.0",
"@backstage/config": "^0.1.1",
"@types/express": "^4.17.6",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
+12 -14
View File
@@ -17,7 +17,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { getVoidLogger } from '@backstage/backend-common';
import { injectEnvConfig } from './config';
import { injectConfig } from './config';
jest.mock('fs-extra');
@@ -29,12 +29,12 @@ const readFileMock = (fsMock.readFile as unknown) as jest.MockedFunction<
const MOCK_DIR = 'mock-dir';
const baseOptions = {
env: {},
appConfigs: [],
staticDir: MOCK_DIR,
logger: getVoidLogger(),
};
describe('injectEnvConfig', () => {
describe('injectConfig', () => {
beforeEach(() => {
fsMock.readdir.mockResolvedValue(['main.js']);
});
@@ -44,7 +44,7 @@ describe('injectEnvConfig', () => {
});
it('should not inject without config', async () => {
await injectEnvConfig(baseOptions);
await injectConfig(baseOptions);
expect(fsMock.readdir).toHaveBeenCalledTimes(0);
expect(fsMock.readFile).toHaveBeenCalledTimes(0);
expect(fsMock.writeFile).toHaveBeenCalledTimes(0);
@@ -64,7 +64,10 @@ describe('injectEnvConfig', () => {
return 'NO_PLACEHOLDER_HERE';
});
await injectEnvConfig({ ...baseOptions, env: { APP_CONFIG_x: '0' } });
await injectConfig({
...baseOptions,
appConfigs: [{ data: { x: 0 }, context: 'test' }],
});
expect(fsMock.readFile).toHaveBeenCalledTimes(2);
expect(fsMock.readFile).toHaveBeenNthCalledWith(
1,
@@ -96,11 +99,9 @@ describe('injectEnvConfig', () => {
'JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__")',
);
await injectEnvConfig({
await injectConfig({
...baseOptions,
env: {
APP_CONFIG_x: '0',
},
appConfigs: [{ data: { x: 0 }, context: 'test' }],
});
expect(fsMock.writeFile).toHaveBeenCalledTimes(1);
@@ -115,12 +116,9 @@ describe('injectEnvConfig', () => {
readFileMock.mockResolvedValue(fsMock.writeFile.mock.calls[0][1]);
await injectEnvConfig({
await injectConfig({
...baseOptions,
env: {
APP_CONFIG_x: '1',
APP_CONFIG_y: '2',
},
appConfigs: [{ data: { x: 1, y: 2 }, context: 'test' }],
});
expect(fsMock.writeFile).toHaveBeenCalledTimes(2);
+33 -13
View File
@@ -16,12 +16,12 @@
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { readEnvConfig } from '@backstage/config-loader';
import { Logger } from 'winston';
import { AppConfig, Config, JsonObject } from '@backstage/config';
import { loadConfigSchema, readEnvConfig } from '@backstage/config-loader';
type Options = {
// Environment to read config from
env: { [name: string]: string | undefined };
type InjectOptions = {
appConfigs: AppConfig[];
// Directory of the static JS files to search for file to inject
staticDir: string;
logger: Logger;
@@ -31,19 +31,13 @@ type Options = {
* Injects config from APP_CONFIG_ env vars, replacing existing
* injected config if it has already been injected.
*/
export async function injectEnvConfig(options: Options) {
const { env, staticDir, logger } = options;
const envConfig = readEnvConfig(env);
if (envConfig.length === 0) {
return;
}
export async function injectConfig(options: InjectOptions) {
const { staticDir, logger, appConfigs } = options;
const files = await fs.readdir(staticDir);
const jsFiles = files.filter(file => file.endsWith('.js'));
const [{ data }] = envConfig;
const escapedData = JSON.stringify(data).replace(/("|'|\\)/g, '\\$1');
const escapedData = JSON.stringify(appConfigs).replace(/("|'|\\)/g, '\\$1');
const injected = `/*__APP_INJECTED_CONFIG_MARKER__*/"${escapedData}"/*__INJECTED_END__*/`;
for (const jsFile of jsFiles) {
@@ -72,3 +66,29 @@ export async function injectEnvConfig(options: Options) {
}
logger.info('Env config not injected');
}
type ReadOptions = {
env: { [name: string]: string | undefined };
appDistDir: string;
config: Config;
};
export async function readConfigs(options: ReadOptions): Promise<AppConfig[]> {
const { env, appDistDir, config } = options;
const appConfigs = readEnvConfig(env);
const schemaPath = resolvePath(appDistDir, '.config-schema.json');
if (await fs.pathExists(schemaPath)) {
const serializedSchema = await fs.readJson(schemaPath);
const schema = await loadConfigSchema({ serialized: serializedSchema });
const frontendConfigs = await schema.process(
[{ data: config.get() as JsonObject, context: 'app' }],
{ visibilities: ['frontend'] },
);
appConfigs.push(...frontendConfigs);
}
return appConfigs;
}
@@ -16,10 +16,10 @@
import { resolve as resolvePath } from 'path';
import { getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import Router from 'express-promise-router';
import request from 'supertest';
import { createRouter } from './router';
jest.mock('../lib/config', () => ({ injectEnvConfig: jest.fn() }));
@@ -35,6 +35,7 @@ describe('createRouter', () => {
beforeAll(async () => {
const router = await createRouter({
logger: getVoidLogger(),
config: new ConfigReader({}),
appPackageName: 'example-app',
});
app = express().use(router);
@@ -76,6 +77,7 @@ describe('createRouter with static fallback handler', () => {
const router = await createRouter({
logger: getVoidLogger(),
config: new ConfigReader({}),
appPackageName: 'example-app',
staticFallbackHandler,
});
+16 -9
View File
@@ -15,13 +15,15 @@
*/
import { resolve as resolvePath } from 'path';
import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { injectEnvConfig } from '../lib/config';
import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { injectConfig, readConfigs } from '../lib/config';
export interface RouterOptions {
config: Config;
logger: Logger;
appPackageName: string;
staticFallbackHandler?: express.Handler;
@@ -30,22 +32,27 @@ export interface RouterOptions {
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const appDistDir = resolvePackagePath(options.appPackageName, 'dist');
options.logger.info(`Serving static app content from ${appDistDir}`);
const { config, logger, appPackageName, staticFallbackHandler } = options;
await injectEnvConfig({
const appDistDir = resolvePackagePath(appPackageName, 'dist');
logger.info(`Serving static app content from ${appDistDir}`);
const staticDir = resolvePath(appDistDir, 'static');
const appConfigs = await readConfigs({
config,
appDistDir,
env: process.env,
logger: options.logger,
staticDir: resolvePath(appDistDir, 'static'),
});
await injectConfig({ appConfigs, logger, staticDir });
const router = Router();
// Use a separate router for static content so that a fallback can be provided by backend
const staticRouter = Router();
staticRouter.use(express.static(resolvePath(appDistDir, 'static')));
if (options.staticFallbackHandler) {
staticRouter.use(options.staticFallbackHandler);
if (staticFallbackHandler) {
staticRouter.use(staticFallbackHandler);
}
staticRouter.use(notFoundHandler());
@@ -14,14 +14,16 @@
* limitations under the License.
*/
import { createServiceBuilder } from '@backstage/backend-common';
import { Server } from 'http';
import { Logger } from 'winston';
import { createServiceBuilder } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { createRouter } from './router';
export interface ServerOptions {
port: number;
enableCors: boolean;
config: Config;
logger: Logger;
}
@@ -32,6 +34,7 @@ export async function startStandaloneServer(
logger.debug('Starting application server...');
const router = await createRouter({
logger,
config: options.config,
appPackageName: 'example-app',
});