From 9b8cec063177df147c9031945e713961ef4b63b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 17:12:37 +0200 Subject: [PATCH 1/5] config-loader: add config file watching support Signed-off-by: Patrik Oldsberg --- .changeset/thin-bugs-compete.md | 5 ++ packages/config-loader/package.json | 1 + packages/config-loader/src/loader.test.ts | 83 ++++++++++++++++++++++- packages/config-loader/src/loader.ts | 64 +++++++++++++++-- 4 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 .changeset/thin-bugs-compete.md diff --git a/.changeset/thin-bugs-compete.md b/.changeset/thin-bugs-compete.md new file mode 100644 index 0000000000..8b9805249c --- /dev/null +++ b/.changeset/thin-bugs-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Add support for config file watching through a new group of `watch` options to `loadConfig`. diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 33b8acc3b9..d4c737d2c8 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -34,6 +34,7 @@ "@backstage/config": "^0.1.6", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", + "chokidar": "^3.5.2", "fs-extra": "9.1.0", "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index b0e236f9c9..3f51e76936 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; import mockFs from 'mock-fs'; +import fs from 'fs-extra'; describe('loadConfig', () => { - beforeAll(() => { + beforeEach(() => { process.env.MY_SECRET = 'is-secret'; process.env.SUBSTITUTE_ME = 'substituted'; @@ -63,7 +65,7 @@ describe('loadConfig', () => { }); }); - afterAll(() => { + afterEach(() => { mockFs.restore(); }); @@ -170,4 +172,81 @@ describe('loadConfig', () => { }, ]); }); + + it('watches config files', async () => { + const onChange = defer(); + const stopSignal = defer(); + + await expect( + loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: onChange.resolve, + stopSignal: stopSignal.promise, + }, + }), + ).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await expect(onChange.promise).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'New Title', + }, + }, + }, + ]); + + stopSignal.resolve(); + }); + + it('stops watching config files', async () => { + const stopSignal = defer(); + + await loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: () => { + expect('not').toBe('called'); + }, + stopSignal: stopSignal.promise, + }, + }); + + stopSignal.resolve(); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + + function defer() { + let resolve: (value: T) => void; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve: resolve! }; + } }); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 3846d6474b..4dbfc007a7 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import yaml from 'yaml'; +import chokidar from 'chokidar'; import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path'; import { AppConfig } from '@backstage/config'; import { @@ -42,13 +43,27 @@ export type LoadConfigOptions = { * @experimental This API is not stable and may change at any point */ experimentalEnvFunc?: EnvFunc; + + /** + * 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; + }; }; export async function loadConfig( options: LoadConfigOptions, ): Promise { - const configs = []; - const { configRoot, experimentalEnvFunc: envFunc } = options; + const { configRoot, experimentalEnvFunc: envFunc, watch } = options; const configPaths = options.configPaths.slice(); // If no paths are provided, we default to reading @@ -64,7 +79,9 @@ export async function loadConfig( const env = envFunc ?? (async (name: string) => process.env[name]); - try { + const loadConfigFiles = async () => { + const configs = []; + for (const configPath of configPaths) { if (!isAbsolute(configPath)) { throw new Error(`Config load path is not absolute: '${configPath}'`); @@ -83,13 +100,50 @@ export async function loadConfig( configs.push({ data, context: basename(configPath) }); } + + return configs; + }; + + let fileConfigs; + try { + fileConfigs = await loadConfigFiles(); } catch (error) { throw new Error( `Failed to read static configuration file, ${error.message}`, ); } - configs.push(...readEnvConfig(process.env)); + const envConfigs = await readEnvConfig(process.env); - return configs; + // Set up config file watching if requested by the caller + if (watch) { + let currentSerializedConfig = JSON.stringify(fileConfigs); + + const watcher = chokidar.watch(configPaths, { + usePolling: process.env.NODE_ENV === 'test', + }); + watcher.on('change', async () => { + try { + const newConfigs = await loadConfigFiles(); + const newSerializedConfig = JSON.stringify(newConfigs); + + if (currentSerializedConfig === newSerializedConfig) { + return; + } + currentSerializedConfig = newSerializedConfig; + + watch.onChange([...newConfigs, ...envConfigs]); + } catch (error) { + console.error(`Failed to reload configuration files, ${error}`); + } + }); + + if (watch.stopSignal) { + watch.stopSignal.then(() => { + watcher.close(); + }); + } + } + + return [...fileConfigs, ...envConfigs]; } From 9b4604b38a4ddf3e48f70d38626ddd921439b3db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 17:21:25 +0200 Subject: [PATCH 2/5] backend-common: add ObservableConfig and config file watching Signed-off-by: Patrik Oldsberg --- .changeset/tricky-starfishes-cheer.md | 5 + packages/backend-common/src/config.ts | 128 ++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 .changeset/tricky-starfishes-cheer.md diff --git a/.changeset/tricky-starfishes-cheer.md b/.changeset/tricky-starfishes-cheer.md new file mode 100644 index 0000000000..455c8f42eb --- /dev/null +++ b/.changeset/tricky-starfishes-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add support for watching configuration through a new `ObservableConfig` type that is now returned by `loadBackendConfig`. diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 1316642ae1..d955aae71e 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,32 +18,148 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader } from '@backstage/config'; +import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; +export interface ObservableConfig extends Config { + subscribe(onChange: () => void): { unsubscribe: () => void }; +} + +class ObservableConfigProxy implements ObservableConfig { + private config: Config = new ConfigReader({}); + + private readonly subscribers: (() => void)[] = []; + + constructor(private readonly logger: Logger) {} + + setConfig(config: Config) { + this.config = config; + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + this.logger.error(`ObservableConfig subscriber threw error, ${error}`); + } + } + } + + subscribe(onChange: () => void): { unsubscribe: () => void } { + this.subscribers.push(onChange); + return { + unsubscribe: () => { + const index = this.subscribers.indexOf(onChange); + if (index >= 0) { + this.subscribers.splice(index, 1); + } + }, + }; + } + + has(key: string): boolean { + return this.config.has(key); + } + keys(): string[] { + return this.config.keys(); + } + get(key?: string): T { + return this.config.get(key); + } + getOptional(key?: string): T | undefined { + return this.config.getOptional(key); + } + getConfig(key: string): Config { + return this.config.getConfig(key); + } + getOptionalConfig(key: string): Config | undefined { + return this.config.getOptionalConfig(key); + } + getConfigArray(key: string): Config[] { + return this.config.getConfigArray(key); + } + getOptionalConfigArray(key: string): Config[] | undefined { + return this.config.getOptionalConfigArray(key); + } + getNumber(key: string): number { + return this.config.getNumber(key); + } + getOptionalNumber(key: string): number | undefined { + return this.config.getOptionalNumber(key); + } + getBoolean(key: string): boolean { + return this.config.getBoolean(key); + } + getOptionalBoolean(key: string): boolean | undefined { + return this.config.getOptionalBoolean(key); + } + getString(key: string): string { + return this.config.getString(key); + } + getOptionalString(key: string): string | undefined { + return this.config.getOptionalString(key); + } + getStringArray(key: string): string[] { + return this.config.getStringArray(key); + } + getOptionalStringArray(key: string): string[] | undefined { + return this.config.getOptionalStringArray(key); + } +} + type Options = { logger: Logger; // process.argv or any other overrides argv: string[]; }; +// A global used to ensure that only a single file watcher is active at a time. +let currentCancelFunc: () => void; + /** - * Load configuration for a Backend + * Load configuration for a Backend. + * + * This function should only be called once, during the initialization of the backend. */ -export async function loadBackendConfig(options: Options): Promise { +export async function loadBackendConfig( + options: Options, +): Promise { const args = parseArgs(options.argv); - const configOpts: string[] = [args.config ?? []].flat(); + const configPaths: string[] = [args.config ?? []].flat(); + + const config = new ObservableConfigProxy(options.logger); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); + const configs = await loadConfig({ configRoot: paths.targetRoot, - configPaths: configOpts.map(opt => resolvePath(opt)), + configPaths: configPaths.map(opt => resolvePath(opt)), + watch: { + onChange(newConfigs) { + options.logger.info( + `Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`, + ); + + config.setConfig(ConfigReader.fromConfigs(newConfigs)); + }, + stopSignal: new Promise(resolve => { + if (currentCancelFunc) { + currentCancelFunc(); + } + currentCancelFunc = resolve; + + // For reloads of this module we need to use a dispose handler rather than the global. + if (module.hot) { + module.hot.addDisposeHandler(resolve); + } + }), + }, }); options.logger.info( `Loaded config from ${configs.map(c => c.context).join(', ')}`, ); - return ConfigReader.fromConfigs(configs); + config.setConfig(ConfigReader.fromConfigs(configs)); + + return config; } From 03bb05af6d6a26611764adc7fadd39deb29fe6c9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 17:35:43 +0200 Subject: [PATCH 3/5] catalog-backend: enable config location live reloads Signed-off-by: Patrik Oldsberg --- .changeset/great-lemons-mix.md | 5 ++ .../next/ConfigLocationEntityProvider.test.ts | 65 ++++++++++++++++++- .../src/next/ConfigLocationEntityProvider.ts | 37 ++++++++--- .../src/next/NextCatalogBuilder.ts | 3 +- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 .changeset/great-lemons-mix.md diff --git a/.changeset/great-lemons-mix.md b/.changeset/great-lemons-mix.md new file mode 100644 index 0000000000..2faeea4317 --- /dev/null +++ b/.changeset/great-lemons-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Enabled live reload of locations configured in `catalog.locations`. diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts index 32ce28dc1b..e97de4eea4 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { resolvePackagePath } from '@backstage/backend-common'; +import { + resolvePackagePath, + ObservableConfig, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; @@ -70,4 +73,64 @@ describe('ConfigLocationEntityProvider', () => { ]), }); }); + + it('should be able to observe the config', async () => { + // Grab the subscriber function and use mutable config data to mock a config file change + let subscriber: () => void; + const mutableConfigData = { + catalog: { + locations: [{ type: 'url', target: 'https://github.com/a/a' }], + }, + }; + + const mockConfig: ObservableConfig = Object.assign( + new ConfigReader(mutableConfigData), + { + subscribe: (s: () => void) => { + subscriber = s; + return { unsubscribe: () => {} }; + }, + }, + ); + + const mockConnection = { + applyMutation: jest.fn(), + } as unknown as EntityProviderConnection; + const locationProvider = new ConfigLocationEntityProvider(mockConfig); + + await locationProvider.connect(mockConnection); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + spec: { + target: 'https://github.com/a/a', + type: 'url', + }, + }), + locationKey: 'url:https://github.com/a/a', + }, + ], + }); + + mutableConfigData.catalog.locations[0].target = 'https://github.com/b/b'; + subscriber!(); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + spec: { + target: 'https://github.com/b/b', + type: 'url', + }, + }), + locationKey: 'url:https://github.com/b/b', + }, + ], + }); + }); }); diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts index 71ec84dae7..3c148e808d 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts @@ -15,27 +15,49 @@ */ import { Config } from '@backstage/config'; +import { ObservableConfig } from '@backstage/backend-common'; import path from 'path'; import { getEntityLocationRef } from './processing/util'; import { EntityProvider, EntityProviderConnection } from './types'; import { locationSpecToLocationEntity } from './util'; export class ConfigLocationEntityProvider implements EntityProvider { - private connection: EntityProviderConnection | undefined; - - constructor(private readonly config: Config) {} + constructor(private readonly config: Config | ObservableConfig) {} getProviderName(): string { return 'ConfigLocationProvider'; } async connect(connection: EntityProviderConnection): Promise { - this.connection = connection; + const entities = this.getEntitiesFromConfig(); + await connection.applyMutation({ + type: 'full', + entities, + }); + if ('subscribe' in this.config) { + let currentKey = JSON.stringify(entities); + + this.config.subscribe(() => { + const newEntities = this.getEntitiesFromConfig(); + const newKey = JSON.stringify(newEntities); + + if (currentKey !== newKey) { + currentKey = newKey; + connection.applyMutation({ + type: 'full', + entities: newEntities, + }); + } + }); + } + } + + private getEntitiesFromConfig() { const locationConfigs = this.config.getOptionalConfigArray('catalog.locations') ?? []; - const entities = locationConfigs.map(location => { + return locationConfigs.map(location => { const type = location.getString('type'); const target = location.getString('target'); const entity = locationSpecToLocationEntity({ @@ -45,10 +67,5 @@ export class ConfigLocationEntityProvider implements EntityProvider { const locationKey = getEntityLocationRef(entity); return { entity, locationKey }; }); - - await this.connection.applyMutation({ - type: 'full', - entities, - }); } } diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index fb12077508..8d796d442e 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -15,6 +15,7 @@ */ import { + ObservableConfig, PluginDatabaseManager, resolvePackagePath, UrlReader, @@ -79,7 +80,7 @@ import { Stitcher } from './stitching/Stitcher'; export type CatalogEnvironment = { logger: Logger; database: PluginDatabaseManager; - config: Config; + config: Config | ObservableConfig; reader: UrlReader; }; From b89f04d9f7247e3331e09066f318b643a160e99a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 18:23:18 +0200 Subject: [PATCH 4/5] config-loader,backend-common,catalog-backend: update API reports Signed-off-by: Patrik Oldsberg --- packages/backend-common/api-report.md | 14 +++++++++++++- packages/config-loader/api-report.md | 4 ++++ plugins/catalog-backend/api-report.md | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2652dad122..a97d50c10f 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -386,13 +386,25 @@ export { isChildPath }; // Warning: (ae-missing-release-tag) "loadBackendConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function loadBackendConfig(options: Options): Promise; +export function loadBackendConfig(options: Options): Promise; // Warning: (ae-missing-release-tag) "notFoundHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function notFoundHandler(): RequestHandler; +// Warning: (ae-missing-release-tag) "ObservableConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ObservableConfig extends Config { + // (undocumented) + subscribe( + onChange: () => void, + ): { + unsubscribe: () => void; + }; +} + // Warning: (ae-missing-release-tag) "PluginCacheManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 47bc22c60f..38aa0b1ebb 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -37,6 +37,10 @@ export type LoadConfigOptions = { configPaths: string[]; env?: string; experimentalEnvFunc?: EnvFunc; + watch?: { + onChange: (configs: AppConfig[]) => void; + stopSignal?: Promise; + }; }; // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 7286855d4e..688780039b 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -21,6 +21,7 @@ import { Knex } from 'knex'; import { Location as Location_2 } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; +import { ObservableConfig } from '@backstage/backend-common'; import { Organizations } from 'aws-sdk'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; From 90f25476ac68f20b2ac151ee5d167da0c3e11c08 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Aug 2021 12:49:45 +0200 Subject: [PATCH 5/5] config,backend-common: move config subscribe method to the base config interface Signed-off-by: Patrik Oldsberg --- .changeset/modern-ladybugs-promise.md | 5 +++++ .changeset/tricky-starfishes-cheer.md | 2 +- packages/backend-common/api-report.md | 14 +------------- packages/backend-common/src/config.ts | 12 +++--------- packages/config/api-report.md | 3 +++ packages/config/src/types.ts | 11 +++++++++++ plugins/catalog-backend/api-report.md | 1 - .../next/ConfigLocationEntityProvider.test.ts | 18 ++++++------------ .../src/next/ConfigLocationEntityProvider.ts | 5 ++--- .../src/next/NextCatalogBuilder.ts | 3 +-- 10 files changed, 33 insertions(+), 41 deletions(-) create mode 100644 .changeset/modern-ladybugs-promise.md diff --git a/.changeset/modern-ladybugs-promise.md b/.changeset/modern-ladybugs-promise.md new file mode 100644 index 0000000000..8ad48d42f4 --- /dev/null +++ b/.changeset/modern-ladybugs-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/config': patch +--- + +Extended the `Config` interface to have an optional `subscribe` method that can be used be notified of updates to the configuration. diff --git a/.changeset/tricky-starfishes-cheer.md b/.changeset/tricky-starfishes-cheer.md index 455c8f42eb..bc2fb6a91b 100644 --- a/.changeset/tricky-starfishes-cheer.md +++ b/.changeset/tricky-starfishes-cheer.md @@ -2,4 +2,4 @@ '@backstage/backend-common': patch --- -Add support for watching configuration through a new `ObservableConfig` type that is now returned by `loadBackendConfig`. +Add support for watching configuration by implementing the `subscribe` method in the configuration returned by `loadBackendConfig`. diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index a97d50c10f..2652dad122 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -386,25 +386,13 @@ export { isChildPath }; // Warning: (ae-missing-release-tag) "loadBackendConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function loadBackendConfig(options: Options): Promise; +export function loadBackendConfig(options: Options): Promise; // Warning: (ae-missing-release-tag) "notFoundHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function notFoundHandler(): RequestHandler; -// Warning: (ae-missing-release-tag) "ObservableConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface ObservableConfig extends Config { - // (undocumented) - subscribe( - onChange: () => void, - ): { - unsubscribe: () => void; - }; -} - // Warning: (ae-missing-release-tag) "PluginCacheManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index d955aae71e..7c660ee4e6 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -21,11 +21,7 @@ import { findPaths } from '@backstage/cli-common'; import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; -export interface ObservableConfig extends Config { - subscribe(onChange: () => void): { unsubscribe: () => void }; -} - -class ObservableConfigProxy implements ObservableConfig { +class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); private readonly subscribers: (() => void)[] = []; @@ -38,7 +34,7 @@ class ObservableConfigProxy implements ObservableConfig { try { subscriber(); } catch (error) { - this.logger.error(`ObservableConfig subscriber threw error, ${error}`); + this.logger.error(`Config subscriber threw error, ${error}`); } } } @@ -119,9 +115,7 @@ let currentCancelFunc: () => void; * * This function should only be called once, during the initialization of the backend. */ -export async function loadBackendConfig( - options: Options, -): Promise { +export async function loadBackendConfig(options: Options): Promise { const args = parseArgs(options.argv); const configPaths: string[] = [args.config ?? []].flat(); diff --git a/packages/config/api-report.md b/packages/config/api-report.md index eff4236026..91f37894c0 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -16,6 +16,9 @@ export type AppConfig = { // // @public (undocumented) export type Config = { + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; has(key: string): boolean; keys(): string[]; get(key?: string): T; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 1135e7d554..84de5dae70 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -26,6 +26,17 @@ export type AppConfig = { }; export type Config = { + /** + * Subscribes to the configuration object in order to receive a notification + * whenever any value within the configuration has changed. + * + * This method is optional to implement, and consumers need to check if it is + * implemented before invoking it. + */ + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; + has(key: string): boolean; keys(): string[]; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 688780039b..7286855d4e 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -21,7 +21,6 @@ import { Knex } from 'knex'; import { Location as Location_2 } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; -import { ObservableConfig } from '@backstage/backend-common'; import { Organizations } from 'aws-sdk'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts index e97de4eea4..ef3e47eb8b 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - resolvePackagePath, - ObservableConfig, -} from '@backstage/backend-common'; +import { resolvePackagePath } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; @@ -83,15 +80,12 @@ describe('ConfigLocationEntityProvider', () => { }, }; - const mockConfig: ObservableConfig = Object.assign( - new ConfigReader(mutableConfigData), - { - subscribe: (s: () => void) => { - subscriber = s; - return { unsubscribe: () => {} }; - }, + const mockConfig = Object.assign(new ConfigReader(mutableConfigData), { + subscribe: (s: () => void) => { + subscriber = s; + return { unsubscribe: () => {} }; }, - ); + }); const mockConnection = { applyMutation: jest.fn(), diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts index 3c148e808d..5deca09944 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts @@ -15,14 +15,13 @@ */ import { Config } from '@backstage/config'; -import { ObservableConfig } from '@backstage/backend-common'; import path from 'path'; import { getEntityLocationRef } from './processing/util'; import { EntityProvider, EntityProviderConnection } from './types'; import { locationSpecToLocationEntity } from './util'; export class ConfigLocationEntityProvider implements EntityProvider { - constructor(private readonly config: Config | ObservableConfig) {} + constructor(private readonly config: Config) {} getProviderName(): string { return 'ConfigLocationProvider'; @@ -35,7 +34,7 @@ export class ConfigLocationEntityProvider implements EntityProvider { entities, }); - if ('subscribe' in this.config) { + if (this.config.subscribe) { let currentKey = JSON.stringify(entities); this.config.subscribe(() => { diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index 8d796d442e..fb12077508 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -15,7 +15,6 @@ */ import { - ObservableConfig, PluginDatabaseManager, resolvePackagePath, UrlReader, @@ -80,7 +79,7 @@ import { Stitcher } from './stitching/Stitcher'; export type CatalogEnvironment = { logger: Logger; database: PluginDatabaseManager; - config: Config | ObservableConfig; + config: Config; reader: UrlReader; };