Read config from remote config server
Signed-off-by: Matto <muhamadto@gmail.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/cli': patch
|
||||
'@backstage/config-loader': patch
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Reading app config from a remote server
|
||||
@@ -19,7 +19,8 @@ import parseArgs from 'minimist';
|
||||
import { Logger } from 'winston';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import { Config, ConfigReader, JsonValue } from '@backstage/config';
|
||||
import { loadConfig } from '@backstage/config-loader';
|
||||
import { ConfigTarget, loadConfig } from '@backstage/config-loader';
|
||||
import { isValidUrl } from '@backstage/integration';
|
||||
|
||||
class ObservableConfigProxy implements Config {
|
||||
private config: Config = new ConfigReader({});
|
||||
@@ -117,7 +118,10 @@ export async function loadBackendConfig(options: {
|
||||
argv: string[];
|
||||
}): Promise<Config> {
|
||||
const args = parseArgs(options.argv);
|
||||
const configPaths: string[] = [args.config ?? []].flat();
|
||||
|
||||
const configTargets: ConfigTarget[] = [args.config ?? []]
|
||||
.flat()
|
||||
.map(arg => (isValidUrl(arg) ? { url: arg } : { path: resolvePath(arg) }));
|
||||
|
||||
const config = new ObservableConfigProxy(options.logger);
|
||||
|
||||
@@ -126,7 +130,7 @@ export async function loadBackendConfig(options: {
|
||||
|
||||
const configs = await loadConfig({
|
||||
configRoot: paths.targetRoot,
|
||||
configPaths: configPaths.map(opt => resolvePath(opt)),
|
||||
configTargets: configTargets,
|
||||
watch: {
|
||||
onChange(newConfigs) {
|
||||
options.logger.info(
|
||||
|
||||
@@ -31,8 +31,10 @@
|
||||
"@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.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",
|
||||
@@ -117,7 +119,6 @@
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "^0.9.4",
|
||||
"@backstage/config": "^0.1.10",
|
||||
"@backstage/core-components": "^0.5.0",
|
||||
"@backstage/core-plugin-api": "^0.1.8",
|
||||
|
||||
@@ -14,9 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { loadConfig, loadConfigSchema } from '@backstage/config-loader';
|
||||
import {
|
||||
ConfigTarget,
|
||||
loadConfig,
|
||||
loadConfigSchema,
|
||||
} from '@backstage/config-loader';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { paths } from './paths';
|
||||
import { isValidUrl } from '@backstage/integration';
|
||||
|
||||
type Options = {
|
||||
args: string[];
|
||||
@@ -26,7 +31,12 @@ type Options = {
|
||||
};
|
||||
|
||||
export async function loadCliConfig(options: Options) {
|
||||
const configPaths = options.args.map(arg => paths.resolveTarget(arg));
|
||||
const configTargets: ConfigTarget[] = [];
|
||||
options.args.forEach(arg => {
|
||||
if (!isValidUrl(arg)) {
|
||||
configTargets.push({ path: paths.resolveTarget(arg) });
|
||||
}
|
||||
});
|
||||
|
||||
// Consider all packages in the monorepo when loading in config
|
||||
const { Project } = require('@lerna/project');
|
||||
@@ -46,7 +56,7 @@ export async function loadCliConfig(options: Options) {
|
||||
? async name => process.env[name] || 'x'
|
||||
: undefined,
|
||||
configRoot: paths.targetRoot,
|
||||
configPaths,
|
||||
configTargets: configTargets,
|
||||
});
|
||||
|
||||
// printing to stderr to not clobber stdout in case the cli command
|
||||
|
||||
@@ -23,6 +23,17 @@ export type ConfigSchemaProcessingOptions = {
|
||||
withFilteredKeys?: boolean;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "ConfigTarget" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type ConfigTarget =
|
||||
| {
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
url: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ConfigVisibility = 'frontend' | 'backend' | 'secret';
|
||||
|
||||
@@ -35,13 +46,11 @@ export function loadConfig(options: LoadConfigOptions): Promise<AppConfig[]>;
|
||||
// @public (undocumented)
|
||||
export type LoadConfigOptions = {
|
||||
configRoot: string;
|
||||
configPaths: string[];
|
||||
configTargets: ConfigTarget[];
|
||||
env?: string;
|
||||
experimentalEnvFunc?: EnvFunc;
|
||||
watch?: {
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
remote?: Remote;
|
||||
watch?: Watch;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -66,6 +75,13 @@ export function readEnvConfig(env: {
|
||||
[name: string]: string | undefined;
|
||||
}): AppConfig[];
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Remote" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type Remote = {
|
||||
reloadIntervalSeconds: number;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TransformFunc<T extends number | string | boolean> = (
|
||||
value: T,
|
||||
@@ -73,4 +89,12 @@ export type TransformFunc<T extends number | string | boolean> = (
|
||||
visibility: ConfigVisibility;
|
||||
},
|
||||
) => T | undefined;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Watch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export type Watch = {
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -30,8 +30,10 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/integration": "^0.6.5",
|
||||
"@backstage/cli-common": "^0.1.3",
|
||||
"@backstage/config": "^0.1.9",
|
||||
"@backstage/backend-common": "^0.9.4",
|
||||
"@types/json-schema": "^7.0.6",
|
||||
"ajv": "^7.0.3",
|
||||
"chokidar": "^3.5.2",
|
||||
@@ -39,8 +41,10 @@
|
||||
"json-schema": "^0.3.0",
|
||||
"json-schema-merge-allof": "^0.8.1",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"uuid": "^8.3.2",
|
||||
"yaml": "^1.9.2",
|
||||
"yup": "^0.29.3"
|
||||
"yup": "^0.29.3",
|
||||
"node-fetch": "2.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.7",
|
||||
@@ -48,7 +52,9 @@
|
||||
"@types/mock-fs": "^4.10.0",
|
||||
"@types/node": "^14.14.32",
|
||||
"@types/yup": "^0.29.8",
|
||||
"mock-fs": "^5.1.0"
|
||||
"mock-fs": "^5.1.0",
|
||||
"fetch-mock-jest": "1.5.1",
|
||||
"fetch-mock": "^9.11.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -30,4 +30,4 @@ export type {
|
||||
TransformFunc,
|
||||
} from './lib';
|
||||
export { loadConfig } from './loader';
|
||||
export type { LoadConfigOptions } from './loader';
|
||||
export type { ConfigTarget, LoadConfigOptions, Watch, Remote } from './loader';
|
||||
|
||||
@@ -18,12 +18,33 @@ 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;
|
||||
|
||||
describe('loadConfig', () => {
|
||||
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:
|
||||
@@ -66,6 +87,7 @@ describe('loadConfig', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
@@ -73,7 +95,7 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
env: 'production',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -90,11 +112,37 @@ describe('loadConfig', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('load config from remote path', async () => {
|
||||
const configUrl = 'https://some.domain.io/app-config.yaml';
|
||||
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configTargets: [{ url: configUrl }],
|
||||
env: 'production',
|
||||
remote: {
|
||||
reloadIntervalSeconds: 30,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
context: configUrl,
|
||||
data: {
|
||||
app: {
|
||||
title: 'Remote Example App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads config with secrets', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config.yaml'],
|
||||
configTargets: [{ path: '/root/app-config.yaml' }],
|
||||
env: 'production',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -115,9 +163,9 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [
|
||||
'/root/app-config.yaml',
|
||||
'/root/app-config.development.yaml',
|
||||
configTargets: [
|
||||
{ path: '/root/app-config.yaml' },
|
||||
{ path: '/root/app-config.development.yaml' },
|
||||
],
|
||||
env: 'development',
|
||||
}),
|
||||
@@ -155,7 +203,7 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: ['/root/app-config.substitute.yaml'],
|
||||
configTargets: [{ path: '/root/app-config.substitute.yaml' }],
|
||||
env: 'development',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -180,7 +228,7 @@ describe('loadConfig', () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
stopSignal: stopSignal.promise,
|
||||
@@ -218,12 +266,73 @@ describe('loadConfig', () => {
|
||||
stopSignal.resolve();
|
||||
});
|
||||
|
||||
it('watches remote config urls', async () => {
|
||||
const onChange = defer<AppConfig[]>();
|
||||
const stopSignal = defer<void>();
|
||||
|
||||
const configUrl = 'https://some.domain.io/app-config.yaml';
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configTargets: [{ url: configUrl }],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
stopSignal: stopSignal.promise,
|
||||
},
|
||||
remote: {
|
||||
reloadIntervalSeconds: 1,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
context: configUrl,
|
||||
data: {
|
||||
app: {
|
||||
title: 'Remote Example App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
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 },
|
||||
);
|
||||
|
||||
await expect(onChange.promise).resolves.toEqual([
|
||||
{
|
||||
context: configUrl,
|
||||
data: {
|
||||
app: {
|
||||
title: 'NEW ReMOTe ExaMPLe App',
|
||||
sessionKey: 'abc123',
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
stopSignal.resolve();
|
||||
});
|
||||
|
||||
it('stops watching config files', async () => {
|
||||
const stopSignal = defer<void>();
|
||||
|
||||
await loadConfig({
|
||||
configRoot: '/root',
|
||||
configPaths: [],
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: () => {
|
||||
expect('not').toBe('called');
|
||||
|
||||
@@ -17,23 +17,67 @@
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
import chokidar from 'chokidar';
|
||||
import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path';
|
||||
import { basename, dirname, isAbsolute, resolve as resolvePath } from 'path';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import {
|
||||
applyConfigTransforms,
|
||||
readEnvConfig,
|
||||
createIncludeTransform,
|
||||
createSubstitutionTransform,
|
||||
EnvFunc,
|
||||
readEnvConfig,
|
||||
} from './lib';
|
||||
import { EnvFunc } from './lib/transform/types';
|
||||
import fetch from 'node-fetch';
|
||||
import { isValidUrl } from '@backstage/integration';
|
||||
|
||||
export type ConfigTarget = { path: string } | { url: string };
|
||||
|
||||
export type Watch = {
|
||||
/**
|
||||
* A listener that is called when a config file is changed.
|
||||
*/
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
|
||||
/**
|
||||
* An optional signal that stops the watcher once the promise resolves.
|
||||
*/
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
|
||||
export type Remote = {
|
||||
/**
|
||||
* An optional remote config reloading period, in seconds
|
||||
*/
|
||||
reloadIntervalSeconds: number;
|
||||
};
|
||||
|
||||
export type RemoteConfigProp = {
|
||||
/**
|
||||
* URL of the remote config
|
||||
*/
|
||||
url: string;
|
||||
|
||||
/**
|
||||
* Contents of the remote config
|
||||
*/
|
||||
content: string | null;
|
||||
|
||||
/**
|
||||
* An optional new ETag header value. Used when checking for updated config.
|
||||
*/
|
||||
newETag?: string;
|
||||
|
||||
/**
|
||||
* An optional old ETag header value. Used when checking for updated config
|
||||
*/
|
||||
oldETag?: string;
|
||||
};
|
||||
/** @public */
|
||||
export type LoadConfigOptions = {
|
||||
// The root directory of the config loading context. Used to find default configs.
|
||||
configRoot: string;
|
||||
|
||||
// Absolute paths to load config files from. Configs from earlier paths have lower priority.
|
||||
configPaths: string[];
|
||||
// Paths to load config files from. Configs from earlier paths have lower priority.
|
||||
configTargets: ConfigTarget[];
|
||||
|
||||
/** @deprecated This option has been removed */
|
||||
env?: string;
|
||||
@@ -45,22 +89,19 @@ export type LoadConfigOptions = {
|
||||
*/
|
||||
experimentalEnvFunc?: EnvFunc;
|
||||
|
||||
/**
|
||||
* An optional remote config
|
||||
*/
|
||||
remote?: Remote;
|
||||
|
||||
/**
|
||||
* An optional configuration that enables watching of config files.
|
||||
*/
|
||||
watch?: {
|
||||
/**
|
||||
* A listener that is called when a config file is changed.
|
||||
*/
|
||||
onChange: (configs: AppConfig[]) => void;
|
||||
|
||||
/**
|
||||
* An optional signal that stops the watcher once the promise resolves.
|
||||
*/
|
||||
stopSignal?: Promise<void>;
|
||||
};
|
||||
watch?: Watch;
|
||||
};
|
||||
|
||||
const HTTP_RESPONSE_HEADER_ETAG = 'ETag';
|
||||
|
||||
/**
|
||||
* Load configuration data.
|
||||
*
|
||||
@@ -69,12 +110,30 @@ export type LoadConfigOptions = {
|
||||
export async function loadConfig(
|
||||
options: LoadConfigOptions,
|
||||
): Promise<AppConfig[]> {
|
||||
const { configRoot, experimentalEnvFunc: envFunc, watch } = options;
|
||||
const configPaths = options.configPaths.slice();
|
||||
const { configRoot, experimentalEnvFunc: envFunc, watch, remote } = options;
|
||||
|
||||
const configPaths: string[] = options.configTargets
|
||||
.slice()
|
||||
.filter((e): e is { path: string } => e.hasOwnProperty('path'))
|
||||
.map(configTarget => configTarget.path);
|
||||
|
||||
let 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(
|
||||
`Remote config detected, however, this feature is turned off. Remote config will be ignored.`,
|
||||
);
|
||||
configUrls = [];
|
||||
}
|
||||
|
||||
// If no paths are provided, we default to reading
|
||||
// `app-config.yaml` and, if it exists, `app-config.local.yaml`
|
||||
if (configPaths.length === 0) {
|
||||
if (configPaths.length === 0 && configUrls.length === 0) {
|
||||
configPaths.push(resolvePath(configRoot, 'app-config.yaml'));
|
||||
|
||||
const localConfig = resolvePath(configRoot, 'app-config.local.yaml');
|
||||
@@ -99,6 +158,7 @@ export async function loadConfig(
|
||||
|
||||
const input = yaml.parse(await readFile(configPath));
|
||||
const substitutionTransform = createSubstitutionTransform(env);
|
||||
|
||||
const data = await applyConfigTransforms(dir, input, [
|
||||
createIncludeTransform(env, readFile, substitutionTransform),
|
||||
substitutionTransform,
|
||||
@@ -110,7 +170,56 @@ export async function loadConfig(
|
||||
return configs;
|
||||
};
|
||||
|
||||
let fileConfigs;
|
||||
const loadRemoteConfigFiles = async () => {
|
||||
const configs: AppConfig[] = [];
|
||||
|
||||
const readConfigFromUrl = async (remoteConfigProp: RemoteConfigProp) => {
|
||||
const response = await fetch(remoteConfigProp.url);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Could not read config file at ${remoteConfigProp.url}`,
|
||||
);
|
||||
}
|
||||
|
||||
remoteConfigProp.oldETag = remoteConfigProp.newETag ?? undefined;
|
||||
remoteConfigProp.newETag =
|
||||
response.headers.get(HTTP_RESPONSE_HEADER_ETAG) ?? undefined;
|
||||
remoteConfigProp.content = await response.text();
|
||||
|
||||
return remoteConfigProp;
|
||||
};
|
||||
|
||||
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 dir = configRoot;
|
||||
if (!remoteConfigProp.content) {
|
||||
throw new Error(`Config is not valid`);
|
||||
}
|
||||
const input = yaml.parse(remoteConfigProp.content);
|
||||
const substitutionTransform = createSubstitutionTransform(env);
|
||||
const data = await applyConfigTransforms(dir, input, [
|
||||
substitutionTransform,
|
||||
]);
|
||||
|
||||
configs.push({ data, context: remoteConfigProp.url });
|
||||
|
||||
remoteConfigProps.push(remoteConfigProp);
|
||||
}
|
||||
|
||||
return configs;
|
||||
};
|
||||
|
||||
let fileConfigs: AppConfig[];
|
||||
try {
|
||||
fileConfigs = await loadConfigFiles();
|
||||
} catch (error) {
|
||||
@@ -119,15 +228,23 @@ export async function loadConfig(
|
||||
);
|
||||
}
|
||||
|
||||
let remoteConfigs: AppConfig[] = [];
|
||||
if (remote) {
|
||||
try {
|
||||
remoteConfigs = await loadRemoteConfigFiles();
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read remote configuration file, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const envConfigs = await readEnvConfig(process.env);
|
||||
|
||||
// Set up config file watching if requested by the caller
|
||||
if (watch) {
|
||||
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
||||
|
||||
const watchConfigFile = (watchProp: Watch) => {
|
||||
const watcher = chokidar.watch(configPaths, {
|
||||
usePolling: process.env.NODE_ENV === 'test',
|
||||
});
|
||||
|
||||
let currentSerializedConfig = JSON.stringify(fileConfigs);
|
||||
watcher.on('change', async () => {
|
||||
try {
|
||||
const newConfigs = await loadConfigFiles();
|
||||
@@ -138,18 +255,77 @@ export async function loadConfig(
|
||||
}
|
||||
currentSerializedConfig = newSerializedConfig;
|
||||
|
||||
watch.onChange([...newConfigs, ...envConfigs]);
|
||||
watchProp.onChange([...remoteConfigs, ...newConfigs, ...envConfigs]);
|
||||
} catch (error) {
|
||||
console.error(`Failed to reload configuration files, ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (watch.stopSignal) {
|
||||
watch.stopSignal.then(() => {
|
||||
if (watchProp.stopSignal) {
|
||||
watchProp.stopSignal.then(() => {
|
||||
watcher.close();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
remoteConfigProp.oldETag !== undefined &&
|
||||
remoteConfigProp.newETag !== undefined &&
|
||||
remoteConfigProp.oldETag !== remoteConfigProp.newETag
|
||||
);
|
||||
};
|
||||
|
||||
let handle: NodeJS.Timeout | undefined;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}, remoteProp.reloadIntervalSeconds * 1000);
|
||||
} catch (error) {
|
||||
console.error(`Failed to reload configuration files, ${error}`);
|
||||
}
|
||||
|
||||
if (watchProp.stopSignal) {
|
||||
watchProp.stopSignal.then(() => {
|
||||
if (handle !== undefined) {
|
||||
console.info(`Stopping remote config watch`);
|
||||
clearInterval(handle);
|
||||
handle = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Set up config file watching if requested by the caller
|
||||
if (watch) {
|
||||
watchConfigFile(watch);
|
||||
}
|
||||
|
||||
return [...fileConfigs, ...envConfigs];
|
||||
if (watch && remote) {
|
||||
watchRemoteConfig(watch, remote);
|
||||
}
|
||||
|
||||
return remote
|
||||
? [...remoteConfigs, ...fileConfigs, ...envConfigs]
|
||||
: [...fileConfigs, ...envConfigs];
|
||||
}
|
||||
|
||||
@@ -347,6 +347,11 @@ export type GoogleGcsIntegrationConfig = {
|
||||
privateKey?: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isValidUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export function isValidUrl(url: string): boolean;
|
||||
|
||||
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
|
||||
// Warning: (ae-missing-release-tag) "readAwsS3IntegrationConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
||||
@@ -27,6 +27,7 @@ export * from './gitlab';
|
||||
export * from './googleGcs';
|
||||
export * from './awsS3';
|
||||
export { defaultScmResolveUrl } from './helpers';
|
||||
export { isValidUrl } from './helpers';
|
||||
export { ScmIntegrations } from './ScmIntegrations';
|
||||
export type { ScmIntegration, ScmIntegrationsGroup } from './types';
|
||||
export type { ScmIntegrationRegistry } from './registry';
|
||||
|
||||
@@ -11336,6 +11336,11 @@ core-js@^2.5.0:
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-js@^3.0.0:
|
||||
version "3.18.3"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz#86a0bba2d8ec3df860fefcc07a8d119779f01509"
|
||||
integrity sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw==
|
||||
|
||||
core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044"
|
||||
@@ -14037,6 +14042,29 @@ 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"
|
||||
integrity sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==
|
||||
dependencies:
|
||||
"@babel/core" "^7.0.0"
|
||||
"@babel/runtime" "^7.0.0"
|
||||
core-js "^3.0.0"
|
||||
debug "^4.1.1"
|
||||
glob-to-regexp "^0.4.0"
|
||||
is-subset "^0.1.1"
|
||||
lodash.isequal "^4.5.0"
|
||||
path-to-regexp "^2.2.1"
|
||||
querystring "^0.2.0"
|
||||
whatwg-url "^6.5.0"
|
||||
|
||||
fetch-readablestream@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795"
|
||||
@@ -14867,7 +14895,7 @@ glob-to-regexp@^0.3.0:
|
||||
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
|
||||
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
|
||||
|
||||
glob-to-regexp@^0.4.1:
|
||||
glob-to-regexp@^0.4.0, glob-to-regexp@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
@@ -16851,6 +16879,11 @@ is-subdir@^1.1.1:
|
||||
dependencies:
|
||||
better-path-resolve "1.0.0"
|
||||
|
||||
is-subset@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
|
||||
integrity sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=
|
||||
|
||||
is-svg@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
|
||||
@@ -18719,7 +18752,7 @@ lodash.isempty@^4.4.0:
|
||||
resolved "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e"
|
||||
integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=
|
||||
|
||||
lodash.isequal@^4.0.0:
|
||||
lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
|
||||
@@ -20192,6 +20225,13 @@ node-fetch@2.6.1, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1:
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
node-fetch@2.6.5:
|
||||
version "2.6.5"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
|
||||
integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-forge@^0.10.0:
|
||||
version "0.10.0"
|
||||
resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
@@ -21526,6 +21566,11 @@ path-to-regexp@^1.7.0:
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-to-regexp@^2.2.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704"
|
||||
integrity sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==
|
||||
|
||||
path-type@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
||||
@@ -26576,6 +26621,11 @@ tr46@^2.0.2:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
|
||||
|
||||
"traverse@>=0.3.0 <0.4":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
|
||||
@@ -27702,6 +27752,11 @@ web-namespaces@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
||||
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
@@ -27954,7 +28009,15 @@ whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0, whatwg-mimetype@^2.3.0:
|
||||
resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||
|
||||
whatwg-url@^6.4.1:
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
whatwg-url@^6.4.1, whatwg-url@^6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
|
||||
integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==
|
||||
|
||||
Reference in New Issue
Block a user