plugins/app-backend: add env config injection

This commit is contained in:
Patrik Oldsberg
2020-08-17 11:51:13 +02:00
parent c13de0b115
commit 35e1af6efa
5 changed files with 221 additions and 0 deletions
+136
View File
@@ -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 });
});
});
+74
View File
@@ -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