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