diff --git a/packages/backend-app-api/src/config/ObservableConfigProxy.ts b/packages/backend-app-api/src/config/ObservableConfigProxy.ts index dd3334c9a5..485dc60901 100644 --- a/packages/backend-app-api/src/config/ObservableConfigProxy.ts +++ b/packages/backend-app-api/src/config/ObservableConfigProxy.ts @@ -16,7 +16,8 @@ import { ConfigService } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; -import { JsonValue } from '@backstage/types'; +import type { JsonObject, JsonValue } from '@backstage/types'; +import { mergeJson } from '@backstage/types'; export class ObservableConfigProxy implements ConfigService { private config: ConfigService = new ConfigReader({}); @@ -61,6 +62,12 @@ 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; diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-app-api/src/config/config.ts index d3349b41ef..50da0d78b6 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-app-api/src/config/config.ts @@ -24,10 +24,11 @@ import { ConfigTarget, LoadConfigOptionsRemote, } from '@backstage/config-loader'; -import { Config, ConfigReader } from '@backstage/config'; +import { type Config, ConfigReader } 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: { @@ -70,6 +71,7 @@ export async function createConfigSecretEnumerator(options: { export async function loadBackendConfig(options: { remote?: LoadConfigOptionsRemote; argv: string[]; + config?: JsonObject; }): Promise<{ config: Config }> { const args = parseArgs(options.argv); @@ -109,12 +111,12 @@ export async function loadBackendConfig(options: { }), }, }); - console.info( `Loaded config from ${appConfigs.map(c => c.context).join(', ')}`, ); config.setConfig(ConfigReader.fromConfigs(appConfigs)); + config.mergeConfig(options.config ?? {}); return { config }; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0b5c8689b3..52b0b03622 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -21,5 +21,6 @@ */ export type { JsonArray, JsonObject, JsonPrimitive, JsonValue } from './json'; +export { mergeJson } from './json'; export type { Observable, Observer, Subscription } from './observable'; export type { HumanDuration } from './time'; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index bcbaaa1030..1bfe0c543f 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -41,3 +41,61 @@ export interface JsonArray extends Array {} * @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 + */ +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 value is an array or primitive + const value = b[key]; + if (Array.isArray(value) || typeof value !== 'object') { + final[key] = value; + continue; + } + + // 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; + } + + // recursively merge the values + final[key] = mergeJson(aValue as JsonObject, bValue as JsonObject); + } + return final; +};