allow an additional config to be provided to loadBackendConfig during runtime

Signed-off-by: Oleg S <97077423+RobotSail@users.noreply.github.com>
This commit is contained in:
Oleg S
2023-03-08 10:07:01 -05:00
parent 1fbb7e6807
commit 5c7ce58582
11 changed files with 24 additions and 206 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/backend-app-api': minor
'@backstage/backend-common': minor
'@backstage/config-loader': minor
---
Allow an additionalConfig to be provided to loadBackendConfig that fetches config values during runtime.
-1
View File
@@ -1,7 +1,6 @@
---
'@backstage/backend-app-api': minor
'@backstage/config-loader': minor
'@backstage/types': minor
---
Introduces the ability to merge configs and JsonObjects
+2 -1
View File
@@ -5,6 +5,7 @@
```ts
/// <reference types="node" />
import type { AppConfig } from '@backstage/config';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { CacheClient } from '@backstage/backend-common';
import { Config } from '@backstage/config';
@@ -178,7 +179,7 @@ export const lifecycleServiceFactory: () => ServiceFactory<
export function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
argv: string[];
config?: JsonObject;
additionalConfig?: AppConfig;
}): Promise<{
config: Config;
}>;
@@ -16,8 +16,7 @@
import { ConfigService } from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
import type { JsonObject, JsonValue } from '@backstage/types';
import { mergeJson } from '@backstage/types';
import type { JsonValue } from '@backstage/types';
export class ObservableConfigProxy implements ConfigService {
private config: ConfigService = new ConfigReader({});
@@ -62,12 +61,6 @@ export class ObservableConfigProxy implements ConfigService {
},
};
}
mergeConfig(configData: JsonObject) {
if (this.parent) {
throw new Error('immutable');
}
this.setConfig(new ConfigReader(mergeJson(this.config.get(), configData)));
}
private select(required: true): ConfigService;
private select(required: false): ConfigService | undefined;
@@ -24,11 +24,11 @@ import {
ConfigTarget,
LoadConfigOptionsRemote,
} from '@backstage/config-loader';
import { type Config, ConfigReader } from '@backstage/config';
import { ConfigReader } from '@backstage/config';
import type { Config, AppConfig } from '@backstage/config';
import { getPackages } from '@manypkg/get-packages';
import { ObservableConfigProxy } from './ObservableConfigProxy';
import { isValidUrl } from '../lib/urls';
import type { JsonObject } from '@backstage/types';
/** @public */
export async function createConfigSecretEnumerator(options: {
@@ -71,7 +71,7 @@ export async function createConfigSecretEnumerator(options: {
export async function loadBackendConfig(options: {
remote?: LoadConfigOptionsRemote;
argv: string[];
config?: JsonObject;
additionalConfig?: AppConfig;
}): Promise<{ config: Config }> {
const args = parseArgs(options.argv);
@@ -115,8 +115,12 @@ export async function loadBackendConfig(options: {
`Loaded config from ${appConfigs.map(c => c.context).join(', ')}`,
);
// add the additional config if provided
if (options.additionalConfig) {
appConfigs.push(options.additionalConfig);
}
config.setConfig(ConfigReader.fromConfigs(appConfigs));
config.mergeConfig(options.config ?? {});
return { config };
}
+3
View File
@@ -7,6 +7,8 @@
/// <reference types="webpack-env" />
import { AwsCredentialsManager } from '@backstage/integration-aws-node';
import type { AppConfig } from '@backstage/config';
import aws from 'aws-sdk';
import { AwsS3Integration } from '@backstage/integration';
import { AzureIntegration } from '@backstage/integration';
import { BackendFeature } from '@backstage/backend-plugin-api';
@@ -532,6 +534,7 @@ export const legacyPlugin: (
export function loadBackendConfig(options: {
logger: LoggerService;
remote?: LoadConfigOptionsRemote;
additionalConfig?: AppConfig;
argv: string[];
}): Promise<Config>;
+2 -1
View File
@@ -19,7 +19,7 @@ import {
loadBackendConfig as newLoadBackendConfig,
} from '@backstage/backend-app-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import type { AppConfig, Config } from '@backstage/config';
import { LoadConfigOptionsRemote } from '@backstage/config-loader';
import { setRootLoggerRedactionList } from './logging/createRootLogger';
@@ -34,6 +34,7 @@ export async function loadBackendConfig(options: {
logger: LoggerService;
// process.argv or any other overrides
remote?: LoadConfigOptionsRemote;
additionalConfig?: AppConfig;
argv: string[];
}): Promise<Config> {
const secretEnumerator = await createConfigSecretEnumerator({
-3
View File
@@ -29,9 +29,6 @@ export type JsonPrimitive = number | string | boolean | null;
// @public
export type JsonValue = JsonObject | JsonArray | JsonPrimitive;
// @public
export const mergeJson: (a: JsonObject, b: JsonObject) => JsonObject;
// @public
export type Observable<T> = {
[Symbol.observable](): Observable<T>;
-1
View File
@@ -21,6 +21,5 @@
*/
export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './json';
export { mergeJson } from './json';
export type { Observable, Observer, Subscription } from './observable';
export type { HumanDuration } from './time';
+1 -128
View File
@@ -14,13 +14,7 @@
* limitations under the License.
*/
import {
JsonPrimitive,
JsonArray,
JsonObject,
JsonValue,
mergeJson,
} from './json';
import { JsonPrimitive, JsonArray, JsonObject, JsonValue } from './json';
describe('json', () => {
it('JsonPrimitive', () => {
@@ -85,124 +79,3 @@ describe('json', () => {
expect(true).toBe(true);
});
});
describe('jsonMerge', () => {
it('should merge two objects', () => {
const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { b: 4, c: 5, d: 6 };
const merged = mergeJson(obj1, obj2);
expect(merged).toEqual({ a: 1, b: 4, c: 5, d: 6 });
});
it('should always prefer to merge the values of the second parameter', () => {
const obj1 = {
a: 1,
b: [1, 2, 3],
c: {
z: 1,
y: 2,
x: 3,
},
};
const obj2 = {
a: 2,
b: [2, 4, 6],
c: {
z: 2,
y: 4,
x: 6,
},
};
const merged = mergeJson(obj1, obj2);
expect(merged).toEqual(obj2);
});
it('should prefer the second argument whenever keys collide', () => {
const obj1 = {
a: 1,
b: [1, 2, 3],
c: {
z: 1,
y: 2,
x: 3,
},
};
const obj2 = {
a: 2,
c: {
y: 4,
},
};
const merged = mergeJson(obj1, obj2);
expect(merged).toEqual({
a: 2,
b: [1, 2, 3],
c: {
z: 1,
y: 4,
x: 3,
},
});
});
it('should merge recursively', () => {
const obj1 = {
backend: {
database: {
provider: 'sqlite3',
},
},
};
const obj2 = {
backend: {
database: {
password: 'password123',
},
},
};
const merged = mergeJson(obj1, obj2);
expect(merged).toEqual({
backend: {
database: {
provider: 'sqlite3',
password: 'password123',
},
},
});
});
it("should overwrite the array with the second argument's array", () => {
const obj1 = {
array: ['a', 'b', 'c'],
};
const obj2 = {
array: [1, 2, 3],
};
const merged = mergeJson(obj1, obj2);
expect(merged).toEqual({
array: [1, 2, 3],
});
const merged2 = mergeJson(obj2, obj1);
expect(merged2).toEqual({
array: ['a', 'b', 'c'],
});
});
it('should take only the defined value in the case of a collision', () => {
const obj1 = {
sqlite: undefined,
};
const obj2 = {
sqlite: 'sqlite3',
};
const merged = mergeJson(obj1, obj2);
expect(merged).toEqual({
sqlite: 'sqlite3',
});
const merged2 = mergeJson(obj2, obj1);
expect(merged2).toEqual({
sqlite: 'sqlite3',
});
});
});
-59
View File
@@ -41,62 +41,3 @@ export interface JsonArray extends Array<JsonValue> {}
* @public
*/
export type JsonValue = JsonObject | JsonArray | JsonPrimitive;
/**
* Attempts to merge two JsonObjects together. In the case of collisions, this function
* prefers values from b, unless the value is an object, in which case it recursively
* merges the values.
*
* @param a - The base object
* @param b - The object to merge into a
* @returns The merged object
* @public
*/
export const mergeJson = (a: JsonObject, b: JsonObject): JsonObject => {
const final: JsonObject = {};
const bKeys = new Set(Object.keys(b));
const aKeys = new Set(Object.keys(a));
const intersectingKeys = new Set([...aKeys].filter(x => bKeys.has(x)));
// add all mutually exclusive keys to the final object
for (const key of aKeys.values()) {
if (!intersectingKeys.has(key)) {
final[key] = a[key];
continue;
}
}
for (const key of bKeys.values()) {
if (!intersectingKeys.has(key)) {
final[key] = b[key];
continue;
}
}
// values now are all overlapping and are either primitives, arrays, or objects.
// for all primitives and arrays, we want to assign the value from b
// for all objects, we want to recursively merge the values
for (const key of intersectingKeys.values()) {
// check if either value is undefined and default to the defined one
const aValue = a[key];
const bValue = b[key];
if (!aValue) {
final[key] = bValue;
continue;
}
if (!bValue) {
final[key] = aValue;
continue;
}
// check if value is an array or primitive
const value = bValue;
if (Array.isArray(value) || typeof value !== 'object') {
final[key] = value;
continue;
}
// recursively merge the values
final[key] = mergeJson(aValue as JsonObject, bValue as JsonObject);
}
return final;
};