Remove the dependency on Etag headers for reloading config

Signed-off-by: Matto <muhamadto@gmail.com>
This commit is contained in:
Matto
2021-10-21 18:20:36 +11:00
parent e51740f9c9
commit ce843364f0
6 changed files with 73 additions and 106 deletions
+3 -2
View File
@@ -1,6 +1,7 @@
---
id: writing title: Writing Backstage Configuration Files description: Documentation on Writing
Backstage Configuration Files
id: writing
title: Writing Backstage Configuration Files
description: Documentation on Writing Backstage Configuration Files
---
## File Format
-2
View File
@@ -31,14 +31,12 @@
"@backstage/cli-common": "^0.1.4",
"@babel/core": "^7.4.4",
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.3",
"@backstage/integration": "^0.6.5",
"@backstage/config": "^0.1.10",
"@backstage/config-loader": "^0.7.0",
"@backstage/errors": "^0.1.3",
"@backstage/types": "^0.1.1",
"@backstage/config-loader": "^0.6.8",
"@backstage/backend-common": "^0.9.4",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
+1 -3
View File
@@ -44,7 +44,6 @@
"json-schema-merge-allof": "^0.8.1",
"json-schema-traverse": "^1.0.0",
"typescript-json-schema": "^0.50.1",
"uuid": "^8.3.2",
"yaml": "^1.9.2",
"yup": "^0.32.9",
"node-fetch": "2.6.5"
@@ -56,8 +55,7 @@
"@types/node": "^14.14.32",
"@types/yup": "^0.29.13",
"mock-fs": "^5.1.0",
"fetch-mock-jest": "1.5.1",
"fetch-mock": "^9.11.0"
"msw": "^0.29.0"
},
"files": [
"dist"
+43 -36
View File
@@ -18,33 +18,47 @@ import { AppConfig } from '@backstage/config';
import { loadConfig } from './loader';
import mockFs from 'mock-fs';
import fs from 'fs-extra';
import { v4 as uuidv4 } from 'uuid';
const fetchMock = require('fetch-mock').sandbox();
const nodeFetch = require('node-fetch');
nodeFetch.default = fetchMock;
import { rest } from 'msw';
import { setupServer } from 'msw/node';
describe('loadConfig', () => {
const server = setupServer();
const initialLoaderHandler = rest.get(
`https://some.domain.io/app-config.yaml`,
(_req, res, ctx) => {
return res(
ctx.body(
`app:
title: Remote Example App
sessionKey: 'abc123'
escaped: \$\${Escaped}
`,
),
);
},
);
const reloadHandler = rest.get(
`https://some.domain.io/app-config.yaml`,
(_req, res, ctx) => {
return res(
ctx.body(
`app:
title: NEW ReMOTe ExaMPLe App
sessionKey: 'abc123'
escaped: \$\${Escaped}
`,
),
);
},
);
beforeAll(() => server.listen());
beforeEach(() => {
process.env.MY_SECRET = 'is-secret';
process.env.SUBSTITUTE_ME = 'substituted';
fetchMock.mock(
{
url: 'https://some.domain.io/app-config.yaml',
method: 'GET',
},
{
body: `app:
title: Remote Example App
sessionKey: 'abc123'
escaped: \$\${Escaped}
`,
headers: { ETag: uuidv4().toString() },
},
);
mockFs({
'/root/app-config.yaml': `
app:
@@ -87,10 +101,12 @@ describe('loadConfig', () => {
});
afterEach(() => {
fetchMock.restore();
mockFs.restore();
server.resetHandlers();
});
afterAll(() => server.close());
it('load config from default path', async () => {
await expect(
loadConfig({
@@ -113,6 +129,8 @@ describe('loadConfig', () => {
});
it('load config from remote path', async () => {
server.use(initialLoaderHandler);
const configUrl = 'https://some.domain.io/app-config.yaml';
await expect(
@@ -267,6 +285,8 @@ describe('loadConfig', () => {
});
it('watches remote config urls', async () => {
server.use(initialLoaderHandler);
const onChange = defer<AppConfig[]>();
const stopSignal = defer<void>();
@@ -296,20 +316,7 @@ describe('loadConfig', () => {
},
]);
fetchMock.mock(
{
url: 'https://some.domain.io/app-config.yaml',
},
{
body: `app:
title: NEW ReMOTe ExaMPLe App
sessionKey: 'abc123'
escaped: \$\${Escaped}
`,
headers: { ETag: uuidv4().toString() },
},
{ overwriteRoutes: true },
);
server.use(reloadHandler);
await expect(onChange.promise).resolves.toEqual([
{
+26 -56
View File
@@ -56,6 +56,7 @@ export type Remote = {
reloadIntervalSeconds: number;
};
/** @public */
export type RemoteConfigProp = {
/**
* URL of the remote config
@@ -111,8 +112,6 @@ export type LoadConfigOptions = {
watch?: Watch;
};
const HTTP_RESPONSE_HEADER_ETAG = 'ETag';
/**
* Load configuration data.
*
@@ -128,18 +127,15 @@ export async function loadConfig(
.filter((e): e is { path: string } => e.hasOwnProperty('path'))
.map(configTarget => configTarget.path);
let configUrls: string[] = options.configTargets
const configUrls: string[] = options.configTargets
.slice()
.filter((e): e is { url: string } => e.hasOwnProperty('url'))
.map(configTarget => configTarget.url);
const remoteConfigProps: RemoteConfigProp[] = [];
if (remote === undefined && configUrls.length > 0) {
console.warn(
throw new Error(
`Remote config detected, however, this feature is turned off. Remote config will be ignored.`,
);
configUrls = [];
}
// If no paths are provided, we default to reading
@@ -183,47 +179,32 @@ export async function loadConfig(
const loadRemoteConfigFiles = async () => {
const configs: AppConfig[] = [];
const readConfigFromUrl = async (remoteConfigProp: RemoteConfigProp) => {
const response = await fetch(remoteConfigProp.url);
const readConfigFromUrl = async (url: string) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Could not read config file at ${remoteConfigProp.url}`,
);
throw new Error(`Could not read config file at ${url}`);
}
remoteConfigProp.oldETag = remoteConfigProp.newETag ?? undefined;
remoteConfigProp.newETag =
response.headers.get(HTTP_RESPONSE_HEADER_ETAG) ?? undefined;
remoteConfigProp.content = await response.text();
return remoteConfigProp;
return await response.text();
};
for (let i = 0; i < configUrls.length; i++) {
const remoteConfigProp = await readConfigFromUrl({
url: configUrls[i],
content: null,
});
if (!isValidUrl(remoteConfigProp.url)) {
throw new Error(
`Config load path is not valid: '${remoteConfigProp.url}'`,
);
const configUrl = configUrls[i];
if (!isValidUrl(configUrl)) {
throw new Error(`Config load path is not valid: '${configUrl}'`);
}
const dir = configRoot;
if (!remoteConfigProp.content) {
const remoteConfigContent = await readConfigFromUrl(configUrl);
if (!remoteConfigContent) {
throw new Error(`Config is not valid`);
}
const input = yaml.parse(remoteConfigProp.content);
const configYaml = yaml.parse(remoteConfigContent);
const substitutionTransform = createSubstitutionTransform(env);
const data = await applyConfigTransforms(dir, input, [
const data = await applyConfigTransforms(configRoot, configYaml, [
substitutionTransform,
]);
configs.push({ data, context: remoteConfigProp.url });
remoteConfigProps.push(remoteConfigProp);
configs.push({ data, context: configUrl });
}
return configs;
@@ -277,17 +258,12 @@ export async function loadConfig(
};
const watchRemoteConfig = (watchProp: Watch, remoteProp: Remote) => {
const hasConfigChanged = async (remoteConfigProp: RemoteConfigProp) => {
const requestProps = { method: 'HEAD' };
const { headers } = await fetch(remoteConfigProp.url, requestProps);
remoteConfigProp.oldETag = remoteConfigProp.newETag ?? undefined;
remoteConfigProp.newETag =
headers.get(HTTP_RESPONSE_HEADER_ETAG) ?? undefined;
const hasConfigChanged = async (
oldRemoteConfigs: AppConfig[],
newRemoteConfigs: AppConfig[],
) => {
return (
remoteConfigProp.oldETag !== undefined &&
remoteConfigProp.newETag !== undefined &&
remoteConfigProp.oldETag !== remoteConfigProp.newETag
JSON.stringify(oldRemoteConfigs) !== JSON.stringify(newRemoteConfigs)
);
};
@@ -295,18 +271,12 @@ export async function loadConfig(
try {
handle = setInterval(async () => {
console.info(`Checking for config update`);
for (const remoteConfigProp of remoteConfigProps) {
if (await hasConfigChanged(remoteConfigProp)) {
console.info(`Remote config change, reloading config ...`);
const newRemoteConfigs = await loadRemoteConfigFiles();
watchProp.onChange([
...newRemoteConfigs,
...fileConfigs,
...envConfigs,
]);
console.info(`Remote config reloaded`);
break;
}
const newRemoteConfigs = await loadRemoteConfigFiles();
if (await hasConfigChanged(remoteConfigs, newRemoteConfigs)) {
remoteConfigs = newRemoteConfigs;
console.info(`Remote config change, reloading config ...`);
watchProp.onChange([...remoteConfigs, ...fileConfigs, ...envConfigs]);
console.info(`Remote config reloaded`);
}
}, remoteProp.reloadIntervalSeconds * 1000);
} catch (error) {
-7
View File
@@ -14248,13 +14248,6 @@ fetch-blob@2.1.2:
resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz#a7805db1361bd44c1ef62bb57fb5fe8ea173ef3c"
integrity sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==
fetch-mock-jest@1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/fetch-mock-jest/-/fetch-mock-jest-1.5.1.tgz#0e13df990d286d9239e284f12b279ed509bf53cd"
integrity sha512-+utwzP8C+Pax1GSka3nFXILWMY3Er2L+s090FOgqVNrNCPp0fDqgXnAHAJf12PLHi0z4PhcTaZNTz8e7K3fjqQ==
dependencies:
fetch-mock "^9.11.0"
fetch-mock@^9.11.0:
version "9.11.0"
resolved "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.11.0.tgz#371c6fb7d45584d2ae4a18ee6824e7ad4b637a3f"