diff --git a/.changeset/curly-steaks-mate.md b/.changeset/curly-steaks-mate.md new file mode 100644 index 0000000000..6f32f27a6d --- /dev/null +++ b/.changeset/curly-steaks-mate.md @@ -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. diff --git a/.changeset/spotty-cougars-wink.md b/.changeset/spotty-cougars-wink.md index 4e8b97a164..ca3effe724 100644 --- a/.changeset/spotty-cougars-wink.md +++ b/.changeset/spotty-cougars-wink.md @@ -1,7 +1,6 @@ --- '@backstage/backend-app-api': minor '@backstage/config-loader': minor -'@backstage/types': minor --- Introduces the ability to merge configs and JsonObjects diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index ed84730fc0..6ef023333b 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -5,6 +5,7 @@ ```ts /// +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; }>; diff --git a/packages/backend-app-api/src/config/ObservableConfigProxy.ts b/packages/backend-app-api/src/config/ObservableConfigProxy.ts index 485dc60901..8e0ad218e9 100644 --- a/packages/backend-app-api/src/config/ObservableConfigProxy.ts +++ b/packages/backend-app-api/src/config/ObservableConfigProxy.ts @@ -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; diff --git a/packages/backend-app-api/src/config/config.ts b/packages/backend-app-api/src/config/config.ts index 50da0d78b6..668226e8b2 100644 --- a/packages/backend-app-api/src/config/config.ts +++ b/packages/backend-app-api/src/config/config.ts @@ -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 }; } diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index ebdd7315c9..a37b5874a3 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -7,6 +7,8 @@ /// 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; diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index dd96a6b46a..47316cfd6e 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -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 { const secretEnumerator = await createConfigSecretEnumerator({ diff --git a/packages/types/api-report.md b/packages/types/api-report.md index e0b34203cf..4b43f5daa6 100644 --- a/packages/types/api-report.md +++ b/packages/types/api-report.md @@ -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 = { [Symbol.observable](): Observable; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 52b0b03622..0b5c8689b3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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'; diff --git a/packages/types/src/json.test.ts b/packages/types/src/json.test.ts index 1210d0a773..7da0ef8a3e 100644 --- a/packages/types/src/json.test.ts +++ b/packages/types/src/json.test.ts @@ -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', - }); - }); -}); diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 46b5cf9499..bcbaaa1030 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -41,62 +41,3 @@ 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 - * @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; -};