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;
-};