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:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'example-backend': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Allow the catalog search collator to filter the entities that it indexes
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+12
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user