Merge pull request #6955 from bolcom/default-catalog-collator-filter

Allow the entities indexed by DefaultCatalogCollator to be filtered by kind
This commit is contained in:
Fredrik Adelöw
2021-09-02 16:44:47 +02:00
committed by GitHub
8 changed files with 125 additions and 37 deletions
+23
View File
@@ -0,0 +1,23 @@
---
'@backstage/create-app': patch
---
Updated the search configuration class to use the static `fromConfig`-based constructor for the `DefaultCatalogCollator`.
To apply this change to an existing app, replace the following line in `search.ts`:
```diff
-collator: new DefaultCatalogCollator({ discovery })
+collator: DefaultCatalogCollator.fromConfig(config, { discovery })
```
The `config` parameter was not needed before, so make sure you also add that in the signature of `createPlugin`
in `search.ts`:
```diff
export default async function createPlugin({
logger,
discovery,
+ config,
}: PluginEnvironment) {
```
+5
View File
@@ -0,0 +1,5 @@
---
'@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({
@@ -10,6 +10,7 @@ import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
export default async function createPlugin({
logger,
discovery,
config,
}: PluginEnvironment) {
// Initialize a connection to a search engine.
const searchEngine = new LunrSearchEngine({ logger });
@@ -19,7 +20,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 }),
});
// The scheduler controls when documents are gathered from collators and sent
+20 -16
View File
@@ -7,6 +7,8 @@
import { Account } from 'aws-sdk/clients/organizations';
import { BitbucketIntegration } from '@backstage/integration';
import { CatalogApi } from '@backstage/catalog-client';
import { CatalogEntitiesRequest } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { DocumentCollator } from '@backstage/search-common';
import { Entity } from '@backstage/catalog-model';
@@ -204,22 +206,6 @@ export class CatalogBuilder {
): CatalogBuilder;
}
// Warning: (ae-missing-release-tag) "CatalogEntityDocument" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface CatalogEntityDocument extends IndexableDocument {
// (undocumented)
componentType: string;
// (undocumented)
kind: string;
// (undocumented)
lifecycle: string;
// (undocumented)
namespace: string;
// (undocumented)
owner: string;
}
// Warning: (ae-missing-release-tag) "CatalogProcessingOrchestrator" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -581,9 +567,13 @@ export class DefaultCatalogCollator implements DocumentCollator {
constructor({
discovery,
locationTemplate,
filter,
catalogClient,
}: {
discovery: PluginEndpointDiscovery;
locationTemplate?: string;
filter?: CatalogEntitiesRequest['filter'];
catalogClient?: CatalogApi;
});
// (undocumented)
protected applyArgsToFormat(
@@ -591,10 +581,24 @@ export class DefaultCatalogCollator implements DocumentCollator {
args: Record<string, string>,
): string;
// (undocumented)
protected readonly catalogClient: CatalogApi;
// (undocumented)
protected discovery: PluginEndpointDiscovery;
// Warning: (ae-forgotten-export) The symbol "CatalogEntityDocument" needs to be exported by the entry point index.d.ts
//
// (undocumented)
execute(): Promise<CatalogEntityDocument[]>;
// (undocumented)
protected filter?: CatalogEntitiesRequest['filter'];
// (undocumented)
static fromConfig(
_config: Config,
options: {
discovery: PluginEndpointDiscovery;
filter?: CatalogEntitiesRequest['filter'];
},
): DefaultCatalogCollator;
// (undocumented)
protected locationTemplate: string;
// (undocumented)
readonly type: 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,kind=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,18 @@ describe('DefaultCatalogCollator', () => {
location: '/software/test-entity',
});
});
it('allows filtering of the retrieved catalog entities', async () => {
// Provide an alternate location template.
collator = DefaultCatalogCollator.fromConfig(new ConfigReader({}), {
discovery: mockDiscoveryApi,
filter: {
kind: ['Foo', 'Bar'],
},
});
const documents = await collator.execute();
// The simulated 'Foo,Bar' filter should return in an empty list
expect(documents).toHaveLength(0);
});
});
@@ -17,7 +17,12 @@
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';
import {
CatalogApi,
CatalogClient,
CatalogEntitiesRequest,
} from '@backstage/catalog-client';
export interface CatalogEntityDocument extends IndexableDocument {
componentType: string;
@@ -30,18 +35,39 @@ export interface CatalogEntityDocument extends IndexableDocument {
export class DefaultCatalogCollator implements DocumentCollator {
protected discovery: PluginEndpointDiscovery;
protected locationTemplate: string;
protected filter?: CatalogEntitiesRequest['filter'];
protected readonly catalogClient: CatalogApi;
public readonly type: string = 'software-catalog';
static fromConfig(
_config: Config,
options: {
discovery: PluginEndpointDiscovery;
filter?: CatalogEntitiesRequest['filter'];
},
) {
return new DefaultCatalogCollator({
...options,
});
}
constructor({
discovery,
locationTemplate,
filter,
catalogClient,
}: {
discovery: PluginEndpointDiscovery;
locationTemplate?: string;
filter?: CatalogEntitiesRequest['filter'];
catalogClient?: CatalogApi;
}) {
this.discovery = discovery;
this.locationTemplate =
locationTemplate || '/catalog/:namespace/:kind/:name';
this.filter = filter;
this.catalogClient =
catalogClient || new CatalogClient({ discoveryApi: discovery });
}
protected applyArgsToFormat(
@@ -56,10 +82,10 @@ export class DefaultCatalogCollator implements DocumentCollator {
}
async execute() {
const baseUrl = await this.discovery.getBaseUrl('catalog');
const res = await fetch(`${baseUrl}/entities`);
const entities: Entity[] = await res.json();
return entities.map((entity: Entity): CatalogEntityDocument => {
const response = await this.catalogClient.getEntities({
filter: this.filter,
});
return response.items.map((entity: Entity): CatalogEntityDocument => {
return {
title: entity.metadata.name,
location: this.applyArgsToFormat(this.locationTemplate, {
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './DefaultCatalogCollator';
export { DefaultCatalogCollator } from './DefaultCatalogCollator';