app-backend,cli: add support for injecting full config based on serialized schema
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user