From ce17a1693141436b3c4ca9cc32b95dab5a0c22e6 Mon Sep 17 00:00:00 2001 From: Roy Jacobs Date: Wed, 25 Aug 2021 14:43:06 +0200 Subject: [PATCH] Allow the entities indexed by DefaultCatalogCollator to be filtered by kind Signed-off-by: Roy Jacobs --- .changeset/happy-cups-tap.md | 6 ++ packages/backend/src/plugins/search.ts | 2 +- plugins/catalog-backend/api-report.md | 11 ++++ plugins/catalog-backend/config.d.ts | 12 ++++ .../src/search/DefaultCatalogCollator.test.ts | 60 +++++++++++++++---- .../src/search/DefaultCatalogCollator.ts | 21 ++++++- 6 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 .changeset/happy-cups-tap.md diff --git a/.changeset/happy-cups-tap.md b/.changeset/happy-cups-tap.md new file mode 100644 index 0000000000..307b6fb4fc --- /dev/null +++ b/.changeset/happy-cups-tap.md @@ -0,0 +1,6 @@ +--- +'example-backend': patch +'@backstage/plugin-catalog-backend': patch +--- + +Allow the catalog search collator to filter the entities that it indexes diff --git a/packages/backend/src/plugins/search.ts b/packages/backend/src/plugins/search.ts index 3470110408..2da108608e 100644 --- a/packages/backend/src/plugins/search.ts +++ b/packages/backend/src/plugins/search.ts @@ -68,7 +68,7 @@ export default async function createPlugin({ // particular collator gathers entities from the software catalog. indexBuilder.addCollator({ defaultRefreshIntervalSeconds: 600, - collator: new DefaultCatalogCollator({ discovery }), + collator: DefaultCatalogCollator.fromConfig(config, { discovery }), }); indexBuilder.addCollator({ diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index a7a100db65..b46f860207 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -581,9 +581,11 @@ export class DefaultCatalogCollator implements DocumentCollator { constructor({ discovery, locationTemplate, + allow, }: { discovery: PluginEndpointDiscovery; locationTemplate?: string; + allow?: string[]; }); // (undocumented) protected applyArgsToFormat( @@ -595,6 +597,15 @@ export class DefaultCatalogCollator implements DocumentCollator { // (undocumented) execute(): Promise; // (undocumented) + protected filterUrl: string; + // (undocumented) + static fromConfig( + config: Config, + options: { + discovery: PluginEndpointDiscovery; + }, + ): DefaultCatalogCollator; + // (undocumented) protected locationTemplate: string; // (undocumented) readonly type: string; diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 7da9cad139..5a9291541c 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -179,5 +179,17 @@ export interface Config { }; }; }; + + /** + * Configuration for search collation + */ + search?: { + /** + * Request search to only index these particular kinds. + * + * E.g. ["Component", "API", "Template", "Location"] + */ + allow: Array; + }; }; } diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts index 2b9745298c..c19d2d21a6 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts @@ -17,6 +17,11 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { DefaultCatalogCollator } from './DefaultCatalogCollator'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; +import { ConfigReader } from '@backstage/config'; + +const server = setupServer(); const expectedEntities: Entity[] = [ { @@ -34,27 +39,37 @@ const expectedEntities: Entity[] = [ }, ]; -jest.mock('cross-fetch', () => ({ - __esModule: true, - default: async () => { - return { - json: async () => { - return expectedEntities; - }, - }; - }, -})); - describe('DefaultCatalogCollator', () => { let mockDiscoveryApi: jest.Mocked; let collator: DefaultCatalogCollator; - beforeEach(() => { + beforeAll(() => { mockDiscoveryApi = { - getBaseUrl: jest.fn().mockResolvedValueOnce('http://localhost:7000'), + getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000'), getExternalBaseUrl: jest.fn(), }; collator = new DefaultCatalogCollator({ discovery: mockDiscoveryApi }); + server.listen(); + }); + beforeEach(() => { + server.use( + rest.get('http://localhost:7000/entities', (req, res, ctx) => { + if (req.url.searchParams.has('filter')) { + const filter = req.url.searchParams.get('filter'); + if (filter === 'kind=Foo,Bar') { + // When filtering on the 'Foo,Bar' kinds we simply return no items, to simulate a filter + return res(ctx.json([])); + } + throw new Error('Unexpected filter parameter'); + } + return res(ctx.json(expectedEntities)); + }), + ); + }); + afterEach(() => server.resetHandlers()); + afterAll(() => { + server.close(); + jest.useRealTimers(); }); it('fetches from the configured catalog service', async () => { @@ -88,4 +103,23 @@ describe('DefaultCatalogCollator', () => { location: '/software/test-entity', }); }); + + it('allows filtering of the retrieved catalog entities', async () => { + const config = new ConfigReader({ + catalog: { + search: { + allow: ['Foo', 'Bar'], + }, + }, + }); + + // Provide an alternate location template. + collator = DefaultCatalogCollator.fromConfig(config, { + discovery: mockDiscoveryApi, + }); + + const documents = await collator.execute(); + // The simulated 'Foo,Bar' filter should return in an empty list + expect(documents).toHaveLength(0); + }); }); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 1f9c68e549..4043cf1c1d 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -18,6 +18,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { Entity } from '@backstage/catalog-model'; import { IndexableDocument, DocumentCollator } from '@backstage/search-common'; import fetch from 'cross-fetch'; +import { Config } from '@backstage/config'; export interface CatalogEntityDocument extends IndexableDocument { componentType: string; @@ -30,18 +31,36 @@ export interface CatalogEntityDocument extends IndexableDocument { export class DefaultCatalogCollator implements DocumentCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; + protected filterUrl: string; public readonly type: string = 'software-catalog'; + static fromConfig( + config: Config, + options: { discovery: PluginEndpointDiscovery }, + ) { + return new DefaultCatalogCollator({ + ...options, + allow: config.getOptionalStringArray('catalog.search.allow'), + }); + } + constructor({ discovery, locationTemplate, + allow, }: { discovery: PluginEndpointDiscovery; locationTemplate?: string; + allow?: string[]; }) { this.discovery = discovery; this.locationTemplate = locationTemplate || '/catalog/:namespace/:kind/:name'; + if (allow && allow.length) { + this.filterUrl = `?filter=kind=${allow.join(',')}`; + } else { + this.filterUrl = ''; + } } protected applyArgsToFormat( @@ -57,7 +76,7 @@ export class DefaultCatalogCollator implements DocumentCollator { async execute() { const baseUrl = await this.discovery.getBaseUrl('catalog'); - const res = await fetch(`${baseUrl}/entities`); + const res = await fetch(`${baseUrl}/entities${this.filterUrl}`); const entities: Entity[] = await res.json(); return entities.map((entity: Entity): CatalogEntityDocument => { return {