Allow the entities indexed by DefaultCatalogCollator to be filtered by kind

Signed-off-by: Roy Jacobs <roy.jacobs@gmail.com>
This commit is contained in:
Roy Jacobs
2021-08-25 14:43:06 +02:00
parent 7823e77f0e
commit ce17a16931
6 changed files with 97 additions and 15 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'example-backend': patch
'@backstage/plugin-catalog-backend': patch
---
Allow the catalog search collator to filter the entities that it indexes
+1 -1
View File
@@ -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({
+11
View File
@@ -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<CatalogEntityDocument[]>;
// (undocumented)
protected filterUrl: string;
// (undocumented)
static fromConfig(
config: Config,
options: {
discovery: PluginEndpointDiscovery;
},
): DefaultCatalogCollator;
// (undocumented)
protected locationTemplate: string;
// (undocumented)
readonly type: string;
+12
View File
@@ -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<string>;
};
};
}
@@ -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<PluginEndpointDiscovery>;
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);
});
});
@@ -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 {