plugins/app-backend: add env config injection
This commit is contained in:
@@ -22,9 +22,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-common": "^0.1.1-alpha.18",
|
||||
"@backstage/config-loader": "^0.1.1-alpha.18",
|
||||
"@types/express": "^4.17.6",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^3.0.3",
|
||||
"fs-extra": "^9.0.0",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { injectEnvConfig } from './config';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
|
||||
const fsMock = fs as jest.Mocked<typeof fs>;
|
||||
const readFileMock = (fsMock.readFile as unknown) as jest.MockedFunction<
|
||||
(name: string) => Promise<string>
|
||||
>;
|
||||
|
||||
const MOCK_DIR = 'mock-dir';
|
||||
|
||||
const baseOptions = {
|
||||
env: {},
|
||||
staticDir: MOCK_DIR,
|
||||
logger: getVoidLogger(),
|
||||
};
|
||||
|
||||
describe('injectEnvConfig', () => {
|
||||
beforeEach(() => {
|
||||
fsMock.readdir.mockResolvedValue(['main.js']);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should not inject without config', async () => {
|
||||
await injectEnvConfig(baseOptions);
|
||||
expect(fsMock.readdir).toHaveBeenCalledTimes(0);
|
||||
expect(fsMock.readFile).toHaveBeenCalledTimes(0);
|
||||
expect(fsMock.writeFile).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should find the correct file to inject', async () => {
|
||||
fsMock.readdir.mockResolvedValue([
|
||||
'before.js',
|
||||
'not-js.txt',
|
||||
'main.js',
|
||||
'after.js',
|
||||
]);
|
||||
readFileMock.mockImplementation(async (file: string) => {
|
||||
if (file.endsWith('main.js')) {
|
||||
return '"__APP_INJECTED_RUNTIME_CONFIG__"';
|
||||
}
|
||||
return 'NO_PLACEHOLDER_HERE';
|
||||
});
|
||||
|
||||
await injectEnvConfig({ ...baseOptions, env: { APP_CONFIG_x: '0' } });
|
||||
expect(fsMock.readFile).toHaveBeenCalledTimes(2);
|
||||
expect(fsMock.readFile).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
resolvePath(MOCK_DIR, 'before.js'),
|
||||
'utf8',
|
||||
);
|
||||
expect(fsMock.readFile).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
resolvePath(MOCK_DIR, 'main.js'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
expect(fsMock.writeFile).toHaveBeenCalledTimes(1);
|
||||
expect(fsMock.writeFile).toHaveBeenCalledWith(
|
||||
resolvePath(MOCK_DIR, 'main.js'),
|
||||
'/*__APP_INJECTED_CONFIG_MARKER__*/"{\\"x\\":0}"/*__INJECTED_END__*/',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
expect(JSON.parse(eval(fsMock.writeFile.mock.calls[0][1]))).toEqual({
|
||||
x: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should re-inject config', async () => {
|
||||
fsMock.readdir.mockResolvedValue(['main.js']);
|
||||
readFileMock.mockResolvedValue(
|
||||
'JSON.parse("__APP_INJECTED_RUNTIME_CONFIG__")',
|
||||
);
|
||||
|
||||
await injectEnvConfig({
|
||||
...baseOptions,
|
||||
env: {
|
||||
APP_CONFIG_x: '0',
|
||||
},
|
||||
});
|
||||
|
||||
expect(fsMock.writeFile).toHaveBeenCalledTimes(1);
|
||||
expect(fsMock.writeFile).toHaveBeenCalledWith(
|
||||
resolvePath(MOCK_DIR, 'main.js'),
|
||||
'JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"{\\"x\\":0}"/*__INJECTED_END__*/)',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
expect(eval(fsMock.writeFile.mock.calls[0][1])).toEqual({ x: 0 });
|
||||
|
||||
readFileMock.mockResolvedValue(fsMock.writeFile.mock.calls[0][1]);
|
||||
|
||||
await injectEnvConfig({
|
||||
...baseOptions,
|
||||
env: {
|
||||
APP_CONFIG_x: '1',
|
||||
APP_CONFIG_y: '2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(fsMock.writeFile).toHaveBeenCalledTimes(2);
|
||||
expect(fsMock.writeFile).toHaveBeenLastCalledWith(
|
||||
resolvePath(MOCK_DIR, 'main.js'),
|
||||
'JSON.parse(/*__APP_INJECTED_CONFIG_MARKER__*/"{\\"x\\":1,\\"y\\":2}"/*__INJECTED_END__*/)',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
expect(eval(fsMock.writeFile.mock.calls[1][1])).toEqual({ x: 1, y: 2 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { readEnvConfig } from '@backstage/config-loader';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
type Options = {
|
||||
// Environment to read config from
|
||||
env: { [name: string]: string | undefined };
|
||||
// Directory of the static JS files to search for file to inject
|
||||
staticDir: string;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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 injected = `/*__APP_INJECTED_CONFIG_MARKER__*/"${escapedData}"/*__INJECTED_END__*/`;
|
||||
|
||||
for (const jsFile of jsFiles) {
|
||||
const path = resolvePath(staticDir, jsFile);
|
||||
|
||||
const content = await fs.readFile(path, 'utf8');
|
||||
if (content.includes('__APP_INJECTED_RUNTIME_CONFIG__')) {
|
||||
logger.info(`Injecting env config into ${jsFile}`);
|
||||
|
||||
const newContent = content.replace(
|
||||
'"__APP_INJECTED_RUNTIME_CONFIG__"',
|
||||
injected,
|
||||
);
|
||||
await fs.writeFile(path, newContent, 'utf8');
|
||||
return;
|
||||
} else if (content.includes('__APP_INJECTED_CONFIG_MARKER__')) {
|
||||
logger.info(`Replacing injected env config in ${jsFile}`);
|
||||
|
||||
const newContent = content.replace(
|
||||
/\/\*__APP_INJECTED_CONFIG_MARKER__\*\/.*\/\*__INJECTED_END__\*\//,
|
||||
injected,
|
||||
);
|
||||
await fs.writeFile(path, newContent, 'utf8');
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.info('Env config not injected');
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import request from 'supertest';
|
||||
|
||||
import { createRouter } from './router';
|
||||
|
||||
jest.mock('../lib/config', () => ({ injectEnvConfig: jest.fn() }));
|
||||
|
||||
global.__non_webpack_require__ = {
|
||||
resolve: () => resolvePath(__dirname, '__fixtures__/app-dir/package.json'),
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import { notFoundHandler } from '@backstage/backend-common';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { injectEnvConfig } from '../lib/config';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
@@ -37,6 +38,12 @@ export async function createRouter(
|
||||
);
|
||||
options.logger.info(`Serving static app content from ${appDistDir}`);
|
||||
|
||||
await injectEnvConfig({
|
||||
env: process.env,
|
||||
logger: options.logger,
|
||||
staticDir: resolvePath(appDistDir, 'static'),
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Use a separate router for static content so that a fallback can be provided by backend
|
||||
|
||||
Reference in New Issue
Block a user